Merge branch 'misskey-dev:develop' into develop

This commit is contained in:
老兄 2023-04-04 17:41:13 +08:00 committed by GitHub
commit c094f76d2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
529 changed files with 26704 additions and 6933 deletions

36
.github/workflows/api-misskey-js.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: API report (misskey.js)
on: [push, pull_request]
jobs:
report:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3.3.0
- run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@v3.6.0
with:
node-version: 18.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm i --frozen-lockfile
- name: Build
run: pnpm --filter misskey-js build
- name: Check files
run: ls packages/misskey-js/built
- name: API report
run: pnpm --filter misskey-js api-prod
- name: Show report
if: always()
run: cat packages/misskey-js/temp/misskey-js.api.md

View File

@ -36,6 +36,7 @@ jobs:
- backend - backend
- frontend - frontend
- sw - sw
- misskey-js
steps: steps:
- uses: actions/checkout@v3.3.0 - uses: actions/checkout@v3.3.0
with: with:
@ -61,6 +62,7 @@ jobs:
matrix: matrix:
workspace: workspace:
- backend - backend
- misskey-js
steps: steps:
- uses: actions/checkout@v3.3.0 - uses: actions/checkout@v3.3.0
with: with:

56
.github/workflows/storybook.yml vendored Normal file
View File

@ -0,0 +1,56 @@
name: Storybook
on:
push:
branches:
- master
- develop
pull_request_target:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.3.0
with:
fetch-depth: 0
submodules: true
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 7
run_install: false
- name: Use Node.js 18.x
uses: actions/setup-node@v3.6.0
with:
node-version: 18.x
cache: 'pnpm'
- run: corepack enable
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml
run: git diff --exit-code pnpm-lock.yaml
- name: Build misskey-js
run: pnpm --filter misskey-js build
- name: Build storybook
run: pnpm --filter frontend build-storybook
env:
NODE_OPTIONS: "--max_old_space_size=7168"
- name: Publish to Chromatic
id: chromatic
uses: chromaui/action@v1
with:
exitOnceUploaded: true
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
storybookBuildDir: storybook-static
workingDir: packages/frontend
- name: Compare on Chromatic
if: github.event_name == 'pull_request_target'
run: pnpm --filter frontend chromatic -d storybook-static --exit-once-uploaded --patch-build ${{ github.head_ref }}...${{ github.base_ref }}
env:
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: storybook
path: packages/frontend/storybook-static

52
.github/workflows/test-misskey-js.yml vendored Normal file
View File

@ -0,0 +1,52 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Test (misskey.js)
on:
push:
branches: [ develop ]
pull_request:
branches: [ develop ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- name: Checkout
uses: actions/checkout@v3.3.0
- run: corepack enable
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml
run: git diff --exit-code pnpm-lock.yaml
- name: Build
run: pnpm --filter misskey-js build
- name: Test
run: pnpm --filter misskey-js test
env:
CI: true
- name: Upload Coverage
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/misskey-js/coverage/coverage-final.json

2
.gitignore vendored
View File

@ -55,6 +55,8 @@ api-docs.json
.DS_Store .DS_Store
/files /files
ormconfig.json ormconfig.json
temp
/packages/frontend/src/**/*.stories.ts
# blender backups # blender backups
*.blend1 *.blend1

View File

@ -1,25 +1,129 @@
<!-- <!--
## 13.x.x (unreleased) ## 13.x.x (unreleased)
### Improvements ### General
- -
### Bugfixes ### Client
-
### Server
- -
You should also include the user name that made the change.
--> -->
## 13.x.x (unreleased) ## 13.x.x (unreleased)
### Improvements ### General
- チャンネルをお気に入りに登録できるように
- チャンネルにノートをピン留めできるように
### Client
- 検索ページでURLを入力した際に照会したときと同等の挙動をするように
- ノートのリアクションを大きく表示するオプションを追加
- オブジェクトストレージの設定画面を分かりやすく
- 「にゃああああああああああああああ!!!!!!!!!!!!」 (`isCat`) 有効時にアバターに表示される猫耳について挙動を変更
- 「UIにぼかし効果を使用」 (`useBlurEffect`) で次の挙動が有効になります
- 猫耳のアバター内部部分をぼかしでマスク表示してより猫耳っぽく見えるように
- 猫耳の色がアバター上部のピクセルから決定されます(無効化時はアバター全体の平均色)
- 左耳は上からおよそ 10%, 左からおよそ 20% の位置で決定します
- 右耳は上からおよそ 10%, 左からおよそ 80% の位置で決定します
- 「UIのアニメーションを減らす」 (`reduceAnimation`) で猫耳を撫でられなくなります
### Server
- ノート作成時のパフォーマンスを向上
- アンテナのタイムライン取得時のパフォーマンスを向上
- チャンネルのタイムライン取得時のパフォーマンスを向上
- 通知に関する全体的なパフォーマンスを向上
## 13.10.3
### Changes
- オブジェクトストレージのリージョン指定が必須になりました
- リージョンの指定の無いサービスは us-east-1 を設定してください
- 値が空の場合は設定ファイルまたは環境変数の使用を試みます
- e.g. ~/aws/config, AWS_REGION
### General
- コンディショナルロールの条件に「投稿数が~以下」「投稿数が~以上」を追加
- リアクション非対応AP実装からのLikeアクティビティの解釈を👍から♥に
### Client
- クリップボタンをノートアクションに追加できるように
- センシティブワードの一覧にピン留めユーザーのIDが表示される問題を修正
### Server
- リモートユーザーのチャート生成を無効にするオプションを追加
- リモートサーバーのチャート生成を無効にするオプションを追加
- ドライブのチャートはローカルユーザーのみ生成するように
- 空のアンテナが作成できるのを修正
## 13.10.2
### Server
- 絵文字を編集すると保存できないことがある問題を修正
### Client
- ドライブファイルのメニューが正常に動作しない問題を修正
## 13.10.1
### Client
- Misskey PlayのPlayボタンを押した時にエラーが発生する問題を修正
## 13.10.0
### General
- ユーザーごとにRenoteをミュートできるように - ユーザーごとにRenoteをミュートできるように
- ノートごとに絵文字リアクションを受け取るか設定できるように - ノートごとに絵文字リアクションを受け取るか設定できるように
- enhance(client): DM作成時にメンションも含むように - クリップをお気に入りに登録できるように
- enhance(client): フォロー申請のボタンのデザインを改善 - ノート検索の利用可否をロールで制御可能に(デフォルトでオフ)
- ロールの並び順を設定可能に
- カスタム絵文字にライセンス情報を付与できるように
- 指定した文字列を含む投稿の公開範囲をホームにできるように
- 使われてないアンテナは自動停止されるように
### Bugfixes ### Client
- 設定から自分のロールを確認できるように
- 広告一覧ページを追加
- ドライブクリーナーを追加
- DM作成時にメンションも含むように
- フォロー申請のボタンのデザインを改善
- 付箋ウィジェットの高さを設定可能に
- APオブジェクトを入力してフェッチする機能とユーザーやートの検索機能を分離
- ナビゲーションバーの項目に「プロフィール」を追加できるように
- ナビゲーションバーのカスタマイズをドラッグ&ドロップで行えるように
- ジョブキューの再試行をワンクリックでできるように
- AiScriptを0.13.1に更新
- oEmbedをサポートしているウェブサイトのプレビューができるように
- YouTubeをoEmbedでロードし、プレビューで共有ボタンを押すとOSの共有画面がでるように
- ([FirefoxでSpotifyのプレビューを開けるとフルサイズじゃなくプレビューサイズだけ再生できる問題](https://bugzilla.mozilla.org/show_bug.cgi?id=1792395)があります)
- (すでにブラウザーでキャッシュされたリンクに対しては以前のプレビュー行動が行われてます。その場合、ブラウザーのキャッシュをクリアしてまた試してください。)
- プロフィールで設定した情報が削除できない問題を修正
- ロールで広告を無効にするとadmin/adsでプレビューがでてこない問題を修正 - ロールで広告を無効にするとadmin/adsでプレビューがでてこない問題を修正
- /api-consoleページにアクセスすると404が出る問題を修正
- Safariでプラグインが複数ある場合に正常に読み込まれない問題を修正
- Bookwyrmのユーザーのプロフィールページで「リモートで表示」をタップしても反応がない問題を修正
- 非ログイン時の「Misskeyについて」の表示を修正
- PC版にて「設定」「コントロールパネル」のリンクを2度以上続けてクリックした際に空白のページが表示される問題を修正
### Server
- OpenAPIエンドポイントを復旧
- WebP/AVIF/JPEGのweb公開用画像は、サーバーサイドではJPEGではなくWebPに変換するように
- アニメーション画像のサムネイルを生成するように
- アクティブユーザー数チャートの記録上限値を拡張
- Playのソースコード上限文字数を2倍に拡張
- 配送先サーバーが410 Goneで応答してきた場合は自動で配送停止をするように
- avatarBlurHash/bannerBlurHashの型をstringに限定
- タイムライン取得時のパフォーマンスを改善
- SMTP Login id length is too short
- API上で`visibility`を`followers`に設定してrenoteすると連合や削除で不具合が発生する問題を修正
- AWS S3からのファイル削除でNoSuchKeyエラーが出ると進めらない状態になる問題を修正
- `disableCache: true`を設定している場合に絵文字管理操作でエラーが出る問題を修正
- リテンション分析が上手く機能しないことがあるのを修正
- 空のアンテナが作成できないように修正
- 特定の条件で通報が見れない問題を修正
- 絵文字の名前に任意の文字が使用できる問題を修正
## 13.9.2 (2023/03/06) ## 13.9.2 (2023/03/06)

View File

@ -203,6 +203,116 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド
vue-routerとの最大の違いは、niraxは複数のルーターが存在することを許可している点です。 vue-routerとの最大の違いは、niraxは複数のルーターが存在することを許可している点です。
これにより、アプリ内ウィンドウでブラウザとは個別にルーティングすることなどが可能になります。 これにより、アプリ内ウィンドウでブラウザとは個別にルーティングすることなどが可能になります。
## Storybook
Misskey uses [Storybook](https://storybook.js.org/) for UI development.
### Setup & Run
#### Universal
##### Setup
```bash
pnpm --filter misskey-js build
pnpm --filter frontend tsc -p .storybook && (node packages/frontend/.storybook/preload-locale.js & node packages/frontend/.storybook/preload-theme.js)
```
##### Run
```bash
node packages/frontend/.storybook/generate.js && pnpm --filter frontend storybook dev
```
#### macOS & Linux
##### Setup
```bash
pnpm --filter misskey-js build
```
##### Run
```bash
pnpm --filter frontend storybook-dev
```
### Usage
When you create a new component (in this example, `MyComponent.vue`), the story file (`MyComponent.stories.ts`) will be automatically generated by the `.storybook/generate.js` script.
You can override the default story by creating a impl story file (`MyComponent.stories.impl.ts`).
```ts
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable import/no-duplicates */
import { StoryObj } from '@storybook/vue3';
import MyComponent from './MyComponent.vue';
export const Default = {
render(args) {
return {
components: {
MyComponent,
},
setup() {
return {
args,
};
},
computed: {
props() {
return {
...this.args,
};
},
},
template: '<MyComponent v-bind="props" />',
};
},
args: {
foo: 'bar',
},
parameters: {
layout: 'centered',
},
} satisfies StoryObj<typeof MkAvatar>;
```
If you want to opt-out from the automatic generation, create a `MyComponent.stories.impl.ts` file and add the following line to the file.
```ts
import MyComponent from './MyComponent.vue';
void MyComponent;
```
You can override the component meta by creating a meta story file (`MyComponent.stories.meta.ts`).
```ts
export const argTypes = {
scale: {
control: {
type: 'range',
min: 1,
max: 4,
},
};
```
Also, you can use msw to mock API requests in the storybook. Creating a `MyComponent.stories.msw.ts` file to define the mock handlers.
```ts
import { rest } from 'msw';
export const handlers = [
rest.post('/api/notes/timeline', (req, res, ctx) => {
return res(
ctx.json([]),
);
}),
];
```
Don't forget to re-run the `.storybook/generate.js` script after adding, editing, or removing the above files.
## Notes ## Notes
### How to resolve conflictions occurred at pnpm-lock.yaml? ### How to resolve conflictions occurred at pnpm-lock.yaml?

View File

@ -23,6 +23,7 @@ COPY --link ["scripts", "./scripts"]
COPY --link ["packages/backend/package.json", "./packages/backend/"] COPY --link ["packages/backend/package.json", "./packages/backend/"]
COPY --link ["packages/frontend/package.json", "./packages/frontend/"] COPY --link ["packages/frontend/package.json", "./packages/frontend/"]
COPY --link ["packages/sw/package.json", "./packages/sw/"] COPY --link ["packages/sw/package.json", "./packages/sw/"]
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
pnpm i --frozen-lockfile --aggregate-output pnpm i --frozen-lockfile --aggregate-output

View File

@ -545,7 +545,6 @@ tokenRequested: "منح حق الوصول إلى الحساب"
pluginTokenRequestedDescription: "ستتمكن الإضافة من استخدام هذه الأذونات." pluginTokenRequestedDescription: "ستتمكن الإضافة من استخدام هذه الأذونات."
notificationType: "أنواع الإشعارات" notificationType: "أنواع الإشعارات"
edit: "التعديل" edit: "التعديل"
useStarForReactionFallback: "استخدم ★ كبديل إذا كان التفاعل مجهولًا"
emailServer: "خادم البريد الإلكتروني" emailServer: "خادم البريد الإلكتروني"
emailConfigInfo: "يستخدم لتأكيد عنوان بريدك الإلكتروني ولإعادة تعيين كلمة المرور إن نسيتها." emailConfigInfo: "يستخدم لتأكيد عنوان بريدك الإلكتروني ولإعادة تعيين كلمة المرور إن نسيتها."
email: "البريد الإلكتروني " email: "البريد الإلكتروني "
@ -1275,3 +1274,7 @@ _deck:
channel: "القنوات" channel: "القنوات"
mentions: "الإشارات" mentions: "الإشارات"
direct: "مباشرة" direct: "مباشرة"
_webhookSettings:
name: "الإسم"
active: "مفعّل"

View File

@ -562,7 +562,6 @@ tokenRequested: "অ্যাকাউন্টে অ্যাক্সেস
pluginTokenRequestedDescription: "এই প্লাগইনটি এখানে দেওয়া অনুমুতিসমূহ ব্যাবহার করবে" pluginTokenRequestedDescription: "এই প্লাগইনটি এখানে দেওয়া অনুমুতিসমূহ ব্যাবহার করবে"
notificationType: "বিজ্ঞপ্তির ধরন" notificationType: "বিজ্ঞপ্তির ধরন"
edit: "সম্পাদনা" edit: "সম্পাদনা"
useStarForReactionFallback: "রিঅ্যাকশনের ইমোজি না জানলে ★ ব্যবহার করুন"
emailServer: "ইমেইল সার্ভার" emailServer: "ইমেইল সার্ভার"
enableEmail: "ইমেইল বিতরণ চালু করুন" enableEmail: "ইমেইল বিতরণ চালু করুন"
emailConfigInfo: "আপনার ইমেল ঠিকানা নিশ্চিত করতে এবং আপনার পাসওয়ার্ড পুনরায় সেট করতে ব্যবহৃত হয়" emailConfigInfo: "আপনার ইমেল ঠিকানা নিশ্চিত করতে এবং আপনার পাসওয়ার্ড পুনরায় সেট করতে ব্যবহৃত হয়"
@ -1354,3 +1353,7 @@ _deck:
channel: "চ্যানেলগুলি" channel: "চ্যানেলগুলি"
mentions: "উল্লেখসমূহ" mentions: "উল্লেখসমূহ"
direct: "ডাইরেক্ট নোটগুলি" direct: "ডাইরেক্ট নোটগুলি"
_webhookSettings:
name: "নাম"
active: "চালু"

View File

@ -460,3 +460,4 @@ _deck:
list: "Llistes" list: "Llistes"
mentions: "Mencions" mentions: "Mencions"
direct: "Publicacions directes" direct: "Publicacions directes"

View File

@ -776,3 +776,7 @@ _deck:
list: "Seznamy" list: "Seznamy"
channel: "Kanály" channel: "Kanály"
mentions: "Zmínění" mentions: "Zmínění"
_webhookSettings:
name: "Jméno"
active: "Zapnuto"

View File

@ -1,2 +1,3 @@
--- ---
_lang_: "Dansk" _lang_: "Dansk"

View File

@ -122,6 +122,8 @@ unmarkAsSensitive: "Als nicht NSFW markieren"
enterFileName: "Dateinamen eingeben" enterFileName: "Dateinamen eingeben"
mute: "Stummschalten" mute: "Stummschalten"
unmute: "Stummschaltung aufheben" unmute: "Stummschaltung aufheben"
renoteMute: "Renotes stummschalten"
renoteUnmute: "Renote-Stummschaltung aufheben"
block: "Blockieren" block: "Blockieren"
unblock: "Blockierung aufheben" unblock: "Blockierung aufheben"
suspend: "Sperren" suspend: "Sperren"
@ -153,6 +155,7 @@ flagShowTimelineReplies: "Antworten in der Chronik anzeigen"
flagShowTimelineRepliesDescription: "Ist diese Option aktiviert, so werden Antworten von Benutzern auf die Notizen anderer Benutzer in der Chronik angezeigt." flagShowTimelineRepliesDescription: "Ist diese Option aktiviert, so werden Antworten von Benutzern auf die Notizen anderer Benutzer in der Chronik angezeigt."
autoAcceptFollowed: "Follow-Anfragen von Benutzern, denen du folgst, automatisch akzeptieren" autoAcceptFollowed: "Follow-Anfragen von Benutzern, denen du folgst, automatisch akzeptieren"
addAccount: "Benutzerkonto hinzufügen" addAccount: "Benutzerkonto hinzufügen"
reloadAccountsList: "Benutzerkontoliste aktualisieren"
loginFailed: "Anmeldung fehlgeschlagen" loginFailed: "Anmeldung fehlgeschlagen"
showOnRemote: "Auf Ursprungsinstanz ansehen" showOnRemote: "Auf Ursprungsinstanz ansehen"
general: "Allgemein" general: "Allgemein"
@ -457,7 +460,7 @@ aboutX: "Über {x}"
emojiStyle: "Emoji-Stil" emojiStyle: "Emoji-Stil"
native: "Nativ" native: "Nativ"
disableDrawer: "Keine ausfahrbaren Menüs verwenden" disableDrawer: "Keine ausfahrbaren Menüs verwenden"
showNoteActionsOnlyHover: "Aktionen für Notizen nur bei Mouseover anzeigen" showNoteActionsOnlyHover: "Notizmenü nur bei Mouseover anzeigen"
noHistory: "Kein Verlauf gefunden" noHistory: "Kein Verlauf gefunden"
signinHistory: "Anmeldungsverlauf" signinHistory: "Anmeldungsverlauf"
enableAdvancedMfm: "Erweitertes MFM aktivieren" enableAdvancedMfm: "Erweitertes MFM aktivieren"
@ -544,6 +547,10 @@ userSuspended: "Dieser Benutzer wurde gesperrt."
userSilenced: "Dieser Benutzer wurde instanzweit stummgeschaltet." userSilenced: "Dieser Benutzer wurde instanzweit stummgeschaltet."
yourAccountSuspendedTitle: "Dieses Benutzerkonto ist gesperrt" yourAccountSuspendedTitle: "Dieses Benutzerkonto ist gesperrt"
yourAccountSuspendedDescription: "Dieses Benutzerkonto wurde gesperrt, da es gegen die Nutzungsbedingungen dieses Servers verstoßen hat. Trete mit dem Betreiber in Kontakt, falls du weitere Details erfahren möchtest. Bitte erstelle kein neues Benutzerkonto." yourAccountSuspendedDescription: "Dieses Benutzerkonto wurde gesperrt, da es gegen die Nutzungsbedingungen dieses Servers verstoßen hat. Trete mit dem Betreiber in Kontakt, falls du weitere Details erfahren möchtest. Bitte erstelle kein neues Benutzerkonto."
tokenRevoked: "Ungültiger Token"
tokenRevokedDescription: "Der Token ist abgelaufen. Bitte melde dich erneut an."
accountDeleted: "Benutzerkonto wurde gelöscht"
accountDeletedDescription: "Dieses Konto wurde gelöscht."
menu: "Menü" menu: "Menü"
divider: "Trenner" divider: "Trenner"
addItem: "Element hinzufügen" addItem: "Element hinzufügen"
@ -587,7 +594,6 @@ tokenRequested: "Zugriff zum Benutzerkonto gewähren"
pluginTokenRequestedDescription: "Dieses Plugin wird die hier konfigurierten Berechtigungen verwenden können." pluginTokenRequestedDescription: "Dieses Plugin wird die hier konfigurierten Berechtigungen verwenden können."
notificationType: "Art der Benachrichtigung" notificationType: "Art der Benachrichtigung"
edit: "Bearbeiten" edit: "Bearbeiten"
useStarForReactionFallback: "Verwende ★ falls das Reaktions-Emoji unbekannt ist"
emailServer: "Email-Server" emailServer: "Email-Server"
enableEmail: "Email-Versand aktivieren" enableEmail: "Email-Versand aktivieren"
emailConfigInfo: "Zur Email-Bestätigung bei Registrierung oder zum Zurücksetzen des Passworts verwendet" emailConfigInfo: "Zur Email-Bestätigung bei Registrierung oder zum Zurücksetzen des Passworts verwendet"
@ -959,6 +965,24 @@ invitationRequiredToRegister: "Diese Instanz ist einladungsbasiert. Du musst ein
emailNotSupported: "Diese Instanz unterstützt das Versenden von Emails nicht" emailNotSupported: "Diese Instanz unterstützt das Versenden von Emails nicht"
postToTheChannel: "In Kanal senden" postToTheChannel: "In Kanal senden"
cannotBeChangedLater: "Kann später nicht mehr geändert werden." cannotBeChangedLater: "Kann später nicht mehr geändert werden."
reactionAcceptance: "Reaktionsannahme"
likeOnly: "Nur \"Gefällt mir\""
likeOnlyForRemote: "Nur \"Gefällt mir\" für fremde Instanzen"
rolesAssignedToMe: "Mir zugewiesene Rollen"
resetPasswordConfirm: "Wirklich Passwort zurücksetzen?"
sensitiveWords: "Sensible Wörter"
sensitiveWordsDescription: "Die Notizsichtbarkeit aller Notizen, die diese Wörter enthalten, wird automatisch auf \"Startseite\" gesetzt. Durch Zeilenumbrüche können mehrere konfiguriert werden."
notesSearchNotAvailable: "Die Notizsuche ist nicht verfügbar."
license: "Lizenz"
unfavoriteConfirm: "Wirklich aus Favoriten entfernen?"
myClips: "Meine Clips"
drivecleaner: "Drive-Reiniger"
retryAllQueuesNow: "Sofort Warteschlangen erneut ausführen"
retryAllQueuesConfirmTitle: "Wirklich erneut versuchen?"
retryAllQueuesConfirmText: "Dies wird zu einer temporären Erhöhung der Serverlast führen."
enableChartsForRemoteUser: "Diagramme für Nutzer fremder Instanzen erstellen"
enableChartsForFederatedInstances: "Diagramme für fremde Instanzen erstellen"
showClipButtonInNoteFooter: "\"Clip\" zum Notizmenu hinzufügen"
_achievements: _achievements:
earnedAt: "Freigeschaltet am" earnedAt: "Freigeschaltet am"
_types: _types:
@ -1106,7 +1130,7 @@ _achievements:
title: "Beliebt" title: "Beliebt"
description: "Die Anzahl deiner Follower hat 100 überschritten" description: "Die Anzahl deiner Follower hat 100 überschritten"
_followers300: _followers300:
title: "Stellt euch bitte in einer Reihe auf" title: "Eine geordnete Reihe, bitte!"
description: "Die Anzahl deiner Follower hat 300 überschritten" description: "Die Anzahl deiner Follower hat 300 überschritten"
_followers500: _followers500:
title: "Funkmast" title: "Funkmast"
@ -1218,6 +1242,8 @@ _role:
iconUrl: "Icon-URL" iconUrl: "Icon-URL"
asBadge: "Als Abzeichen anzeigen" asBadge: "Als Abzeichen anzeigen"
descriptionOfAsBadge: "Ist dies aktiviert, so wird das Icon dieser Rolle an der Seite der Namen von Benutzern mit dieser Rolle angezeigt." descriptionOfAsBadge: "Ist dies aktiviert, so wird das Icon dieser Rolle an der Seite der Namen von Benutzern mit dieser Rolle angezeigt."
displayOrder: "Position"
descriptionOfDisplayOrder: "Je höher die Nummer, desto höher die UI-Position."
canEditMembersByModerator: "Moderatoren können Benutzern diese Rolle zuweisen" canEditMembersByModerator: "Moderatoren können Benutzern diese Rolle zuweisen"
descriptionOfCanEditMembersByModerator: "Wenn aktiviert, so können Moderatoren und Adminstratoren anderen Benutzern diese Rolle zuweisen bzw. diese Zuweisung aufheben. Wenn deaktiviert, so ist es nur Administratoren möglich, Zuweisungen dieser Rolle zu verwalten." descriptionOfCanEditMembersByModerator: "Wenn aktiviert, so können Moderatoren und Adminstratoren anderen Benutzern diese Rolle zuweisen bzw. diese Zuweisung aufheben. Wenn deaktiviert, so ist es nur Administratoren möglich, Zuweisungen dieser Rolle zu verwalten."
priority: "Priorität" priority: "Priorität"
@ -1243,6 +1269,7 @@ _role:
rateLimitFactor: "Versuchsanzahl" rateLimitFactor: "Versuchsanzahl"
descriptionOfRateLimitFactor: "Je niedriger desto weniger restriktiv, je höher destro restriktiver." descriptionOfRateLimitFactor: "Je niedriger desto weniger restriktiv, je höher destro restriktiver."
canHideAds: "Kann Werbung ausblenden" canHideAds: "Kann Werbung ausblenden"
canSearchNotes: "Nutzung der Notizsuchfunktion"
_condition: _condition:
isLocal: "Lokaler Benutzer" isLocal: "Lokaler Benutzer"
isRemote: "Benutzer fremder Instanz" isRemote: "Benutzer fremder Instanz"
@ -1252,6 +1279,8 @@ _role:
followersMoreThanOrEq: "Hat X oder mehr Follower" followersMoreThanOrEq: "Hat X oder mehr Follower"
followingLessThanOrEq: "Folgt X oder weniger Benutzern" followingLessThanOrEq: "Folgt X oder weniger Benutzern"
followingMoreThanOrEq: "Folgt X oder mehr Benutzern" followingMoreThanOrEq: "Folgt X oder mehr Benutzern"
notesLessThanOrEq: "Beitragszahl ist kleiner-gleich"
notesMoreThanOrEq: "Beitragszahl ist größer-gleich"
and: "UND-Bedingung" and: "UND-Bedingung"
or: "ODER-Bedingung" or: "ODER-Bedingung"
not: "NICHT-Bedingung" not: "NICHT-Bedingung"
@ -1844,3 +1873,24 @@ _deck:
_dialog: _dialog:
charactersExceeded: "Maximallänge überschritten! Momentan {current} von {max}" charactersExceeded: "Maximallänge überschritten! Momentan {current} von {max}"
charactersBelow: "Minimallänge unterschritten! Momentan {current} von {min}" charactersBelow: "Minimallänge unterschritten! Momentan {current} von {min}"
_disabledTimeline:
title: "Chronik deaktiviert"
description: "Mit deinen jetzigen Rollen ist diese Chronik nicht verfügbar."
_drivecleaner:
orderBySizeDesc: "Absteigende Dateigrößen"
orderByCreatedAtAsc: "Aufsteigendes Erstelldatum"
_webhookSettings:
createWebhook: "Webhook erstellen"
name: "Name"
secret: "Secret"
events: "Webhook-Ereignisse"
active: "Aktiviert"
_events:
follow: "Wenn du jemandem folgst"
followed: "Wenn dir jemand folgt"
note: "Wenn du eine Notiz schickst"
reply: "Wenn du eine Antwort erhältst"
renote: "Wenn du ein Renote erhältst"
reaction: "Wenn du eine Reaktion erhältst"
mention: "Wenn du erwähnt wirst"

View File

@ -392,3 +392,6 @@ _deck:
antenna: "Αντένες" antenna: "Αντένες"
list: "Λίστα" list: "Λίστα"
mentions: "Επισημάνσεις" mentions: "Επισημάνσεις"
_webhookSettings:
name: "Όνομα"

View File

@ -67,7 +67,7 @@ import: "Import"
export: "Export" export: "Export"
files: "Files" files: "Files"
download: "Download" download: "Download"
driveFileDeleteConfirm: "Are you sure you want to delete the file \"{name}\"? Notes with this file attached will also be deleted." driveFileDeleteConfirm: "Are you sure you want to delete \"{name}\"? All notes with this file attached will also be deleted."
unfollowConfirm: "Are you sure you want to unfollow {name}?" unfollowConfirm: "Are you sure you want to unfollow {name}?"
exportRequested: "You've requested an export. This may take a while. It will be added to your Drive once completed." exportRequested: "You've requested an export. This may take a while. It will be added to your Drive once completed."
importRequested: "You've requested an import. This may take a while." importRequested: "You've requested an import. This may take a while."
@ -122,6 +122,8 @@ unmarkAsSensitive: "Unmark as NSFW"
enterFileName: "Enter filename" enterFileName: "Enter filename"
mute: "Mute" mute: "Mute"
unmute: "Unmute" unmute: "Unmute"
renoteMute: "Mute Renotes"
renoteUnmute: "Unmute Renotes"
block: "Block" block: "Block"
unblock: "Unblock" unblock: "Unblock"
suspend: "Suspend" suspend: "Suspend"
@ -153,6 +155,7 @@ flagShowTimelineReplies: "Show replies in timeline"
flagShowTimelineRepliesDescription: "Shows replies of users to notes of other users in the timeline if turned on." flagShowTimelineRepliesDescription: "Shows replies of users to notes of other users in the timeline if turned on."
autoAcceptFollowed: "Automatically approve follow requests from users you're following" autoAcceptFollowed: "Automatically approve follow requests from users you're following"
addAccount: "Add account" addAccount: "Add account"
reloadAccountsList: "Reload account list"
loginFailed: "Failed to sign in" loginFailed: "Failed to sign in"
showOnRemote: "View on remote instance" showOnRemote: "View on remote instance"
general: "General" general: "General"
@ -527,7 +530,7 @@ nothing: "There's nothing to see here"
installedDate: "Authorized at" installedDate: "Authorized at"
lastUsedDate: "Last used at" lastUsedDate: "Last used at"
state: "State" state: "State"
sort: "Sort" sort: "Sorting order"
ascendingOrder: "Ascending" ascendingOrder: "Ascending"
descendingOrder: "Descending" descendingOrder: "Descending"
scratchpad: "Scratchpad" scratchpad: "Scratchpad"
@ -544,6 +547,10 @@ userSuspended: "This user has been suspended."
userSilenced: "This user is being silenced." userSilenced: "This user is being silenced."
yourAccountSuspendedTitle: "This account is suspended" yourAccountSuspendedTitle: "This account is suspended"
yourAccountSuspendedDescription: "This account has been suspended due to breaking the server's terms of services or similar. Contact the administrator if you would like to know a more detailed reason. Please do not create a new account." yourAccountSuspendedDescription: "This account has been suspended due to breaking the server's terms of services or similar. Contact the administrator if you would like to know a more detailed reason. Please do not create a new account."
tokenRevoked: "Invalid token"
tokenRevokedDescription: "This token has expired. Please log in again."
accountDeleted: "Account deleted"
accountDeletedDescription: "This account has been deleted."
menu: "Menu" menu: "Menu"
divider: "Divider" divider: "Divider"
addItem: "Add Item" addItem: "Add Item"
@ -587,7 +594,6 @@ tokenRequested: "Grant access to account"
pluginTokenRequestedDescription: "This plugin will be able to use the permissions set here." pluginTokenRequestedDescription: "This plugin will be able to use the permissions set here."
notificationType: "Notification type" notificationType: "Notification type"
edit: "Edit" edit: "Edit"
useStarForReactionFallback: "Use ★ as fallback if the reaction emoji is unknown"
emailServer: "Email server" emailServer: "Email server"
enableEmail: "Enable email distribution" enableEmail: "Enable email distribution"
emailConfigInfo: "Used to confirm your email during sign-up or if you forget your password" emailConfigInfo: "Used to confirm your email during sign-up or if you forget your password"
@ -959,6 +965,24 @@ invitationRequiredToRegister: "This instance is invite-only. You must enter a va
emailNotSupported: "This instance does not support sending emails" emailNotSupported: "This instance does not support sending emails"
postToTheChannel: "Post to channel" postToTheChannel: "Post to channel"
cannotBeChangedLater: "This cannot be changed later." cannotBeChangedLater: "This cannot be changed later."
reactionAcceptance: "Reaction Acceptance"
likeOnly: "Only likes"
likeOnlyForRemote: "Only likes for remote instances"
rolesAssignedToMe: "Roles assigned to me"
resetPasswordConfirm: "Really reset your password?"
sensitiveWords: "Sensitive words"
sensitiveWordsDescription: "The visibility of all notes containing any of the configured words will be set to \"Home\" automatically. You can list multiple by separating them via line breaks."
notesSearchNotAvailable: "Note search is unavailable."
license: "License"
unfavoriteConfirm: "Really remove from favorites?"
myClips: "My clips"
drivecleaner: "Drive Cleaner"
retryAllQueuesNow: "Retry running all queues"
retryAllQueuesConfirmTitle: "Really retry all?"
retryAllQueuesConfirmText: "This will temporarily increase the server load."
enableChartsForRemoteUser: "Generate remote user data charts"
enableChartsForFederatedInstances: "Generate remote instance data charts"
showClipButtonInNoteFooter: "Add \"Clip\" to note action menu"
_achievements: _achievements:
earnedAt: "Unlocked at" earnedAt: "Unlocked at"
_types: _types:
@ -1218,6 +1242,8 @@ _role:
iconUrl: "Icon URL" iconUrl: "Icon URL"
asBadge: "Show as badge" asBadge: "Show as badge"
descriptionOfAsBadge: "This role's icon will be displayed next to the username of users with this role if turned on." descriptionOfAsBadge: "This role's icon will be displayed next to the username of users with this role if turned on."
displayOrder: "Position"
descriptionOfDisplayOrder: "The higher the number, the higher its UI position."
canEditMembersByModerator: "Allow moderators to edit the list of members for this role" canEditMembersByModerator: "Allow moderators to edit the list of members for this role"
descriptionOfCanEditMembersByModerator: "When turned on, moderators as well as administrators will be able to assign and unassign users to this role. When turned off, only administrators will be able to assign users." descriptionOfCanEditMembersByModerator: "When turned on, moderators as well as administrators will be able to assign and unassign users to this role. When turned off, only administrators will be able to assign users."
priority: "Priority" priority: "Priority"
@ -1243,6 +1269,7 @@ _role:
rateLimitFactor: "Rate limit" rateLimitFactor: "Rate limit"
descriptionOfRateLimitFactor: "Lower rate limits are less restrictive, higher ones more restrictive. " descriptionOfRateLimitFactor: "Lower rate limits are less restrictive, higher ones more restrictive. "
canHideAds: "Can hide ads" canHideAds: "Can hide ads"
canSearchNotes: "Usage of note search"
_condition: _condition:
isLocal: "Local user" isLocal: "Local user"
isRemote: "Remote user" isRemote: "Remote user"
@ -1252,6 +1279,8 @@ _role:
followersMoreThanOrEq: "Has X or more followers" followersMoreThanOrEq: "Has X or more followers"
followingLessThanOrEq: "Follows X or fewer accounts" followingLessThanOrEq: "Follows X or fewer accounts"
followingMoreThanOrEq: "Follows X or more accounts" followingMoreThanOrEq: "Follows X or more accounts"
notesLessThanOrEq: "Post count is less than/equal to"
notesMoreThanOrEq: "Post count is greater than/equal to"
and: "AND-Condition" and: "AND-Condition"
or: "OR-Condition" or: "OR-Condition"
not: "NOT-Condition" not: "NOT-Condition"
@ -1844,3 +1873,24 @@ _deck:
_dialog: _dialog:
charactersExceeded: "You've exceeded the maximum character limit! Currently at {current} of {max}." charactersExceeded: "You've exceeded the maximum character limit! Currently at {current} of {max}."
charactersBelow: "You're below the minimum character limit! Currently at {current} of {min}." charactersBelow: "You're below the minimum character limit! Currently at {current} of {min}."
_disabledTimeline:
title: "Timeline disabled"
description: "You cannot use this timeline under your current roles."
_drivecleaner:
orderBySizeDesc: "Descending Filesizes"
orderByCreatedAtAsc: "Ascending Dates"
_webhookSettings:
createWebhook: "Create Webhook"
name: "Name"
secret: "Secret"
events: "Webhook Events"
active: "Enabled"
_events:
follow: "When following a user"
followed: "When being followed"
note: "When posting a note"
reply: "When receiving a reply"
renote: "When renoted"
reaction: "When receiving a reaction"
mention: "When being mentioned"

View File

@ -122,6 +122,8 @@ unmarkAsSensitive: "Desmarcar como sensible"
enterFileName: "Ingrese el nombre del archivo" enterFileName: "Ingrese el nombre del archivo"
mute: "Silenciar" mute: "Silenciar"
unmute: "Dejar de silenciar" unmute: "Dejar de silenciar"
renoteMute: "Silenciar renota"
renoteUnmute: "Desilenciar renota"
block: "Bloquear" block: "Bloquear"
unblock: "Dejar de bloquear" unblock: "Dejar de bloquear"
suspend: "Suspender" suspend: "Suspender"
@ -153,6 +155,7 @@ flagShowTimelineReplies: "Mostrar respuestas a las notas en la biografía"
flagShowTimelineRepliesDescription: "Cuando se marca, la línea de tiempo muestra respuestas a otras notas además de las notas del usuario" flagShowTimelineRepliesDescription: "Cuando se marca, la línea de tiempo muestra respuestas a otras notas además de las notas del usuario"
autoAcceptFollowed: "Aceptar automáticamente las solicitudes de seguimiento de los usuarios que sigues" autoAcceptFollowed: "Aceptar automáticamente las solicitudes de seguimiento de los usuarios que sigues"
addAccount: "Agregar Cuenta" addAccount: "Agregar Cuenta"
reloadAccountsList: "Recargar lista de cuentas"
loginFailed: "Error al iniciar sesión." loginFailed: "Error al iniciar sesión."
showOnRemote: "Ver en una instancia remota" showOnRemote: "Ver en una instancia remota"
general: "General" general: "General"
@ -506,6 +509,7 @@ objectStorageSetPublicRead: "Seleccionar \"public-read\" al subir "
serverLogs: "Registros del servidor" serverLogs: "Registros del servidor"
deleteAll: "Eliminar todos" deleteAll: "Eliminar todos"
showFixedPostForm: "Mostrar el formulario de las entradas encima de la línea de tiempo" showFixedPostForm: "Mostrar el formulario de las entradas encima de la línea de tiempo"
showFixedPostFormInChannel: "Mostrar el formulario de publicación por encima de la cronología (Canales)"
newNoteRecived: "Tienes una nota nueva" newNoteRecived: "Tienes una nota nueva"
sounds: "Sonidos" sounds: "Sonidos"
sound: "Sonidos" sound: "Sonidos"
@ -543,6 +547,10 @@ userSuspended: "Este usuario ha sido suspendido."
userSilenced: "Este usuario ha sido silenciado." userSilenced: "Este usuario ha sido silenciado."
yourAccountSuspendedTitle: "Esta cuenta ha sido suspendida" yourAccountSuspendedTitle: "Esta cuenta ha sido suspendida"
yourAccountSuspendedDescription: "Esta cuenta ha sido suspendida debido a violaciones de los términos de servicio del servidor y otras razones. Para más información, póngase en contacto con el administrador. Por favor, no cree una nueva cuenta." yourAccountSuspendedDescription: "Esta cuenta ha sido suspendida debido a violaciones de los términos de servicio del servidor y otras razones. Para más información, póngase en contacto con el administrador. Por favor, no cree una nueva cuenta."
tokenRevoked: "Token inválido"
tokenRevokedDescription: "Este token expiró, vuelve a iniciar sesión."
accountDeleted: "Cuenta borrada"
accountDeletedDescription: "Esta cuenta ha sido borrada."
menu: "Menú" menu: "Menú"
divider: "Divisor" divider: "Divisor"
addItem: "Agregar elemento" addItem: "Agregar elemento"
@ -586,7 +594,6 @@ tokenRequested: "Permiso de acceso a la cuenta"
pluginTokenRequestedDescription: "Este plugin podrá usar los permisos descritos aquí" pluginTokenRequestedDescription: "Este plugin podrá usar los permisos descritos aquí"
notificationType: "Tipo de notificación" notificationType: "Tipo de notificación"
edit: "Editar" edit: "Editar"
useStarForReactionFallback: "En caso de que los emojis de reacciones no sean claros, usar en su lugar una estrella"
emailServer: "Servidor de correo" emailServer: "Servidor de correo"
enableEmail: "Activar el envío de correos electrónicos" enableEmail: "Activar el envío de correos electrónicos"
emailConfigInfo: "Usar en caso de validación de correo electrónico y pedido de contraseña" emailConfigInfo: "Usar en caso de validación de correo electrónico y pedido de contraseña"
@ -955,6 +962,24 @@ exploreOtherServers: "Buscar otra instancia"
letsLookAtTimeline: "Mirar la línea de tiempo local" letsLookAtTimeline: "Mirar la línea de tiempo local"
disableFederationWarn: "Esto desactivará la federación, pero las publicaciones segurán siendo públicas al menos que se configure diferente. Usualmente no necesitas usar esta configuración." disableFederationWarn: "Esto desactivará la federación, pero las publicaciones segurán siendo públicas al menos que se configure diferente. Usualmente no necesitas usar esta configuración."
invitationRequiredToRegister: "Esta instancia está configurada sólo por invitación, tienes que ingresar un código de invitación válido." invitationRequiredToRegister: "Esta instancia está configurada sólo por invitación, tienes que ingresar un código de invitación válido."
emailNotSupported: "Esta instancia no soporta el envío de correo electrónico"
postToTheChannel: "Publicar en el canal"
cannotBeChangedLater: "Esto no podrá ser cambiado después."
reactionAcceptance: "Aceptación de reacciones"
likeOnly: "Sólo 'me gusta'"
likeOnlyForRemote: "Sólo reacciones de instancias remotas"
rolesAssignedToMe: "Roles asignados a mí"
resetPasswordConfirm: "¿Realmente quieres cambiar la contraseña?"
sensitiveWords: "Palabras sensibles"
sensitiveWordsDescription: "La visibilidad de todas las notas que contienen cualquiera de las palabras configuradas serán puestas en \"Inicio\" automáticamente. Puedes enumerás varias separándolas con saltos de línea"
notesSearchNotAvailable: "No se puede buscar una nota"
license: "Licencia"
unfavoriteConfirm: "¿Desea quitar de favoritos?"
myClips: "Mis clips"
drivecleaner: "Limpiador del Drive"
retryAllQueuesNow: "Reintentar inmediatamente todas las colas"
retryAllQueuesConfirmTitle: "Desea ¿reintentar inmediatamente todas las colas?"
retryAllQueuesConfirmText: "La carga del servidor está incrementándose temporalmente "
_achievements: _achievements:
earnedAt: "Desbloqueado el" earnedAt: "Desbloqueado el"
_types: _types:
@ -1214,6 +1239,8 @@ _role:
iconUrl: "URL del ícono" iconUrl: "URL del ícono"
asBadge: "Mostrar como emblema" asBadge: "Mostrar como emblema"
descriptionOfAsBadge: "Este ícono de rol se mostrará a lado del nombre de usuario cuando este rol se encuentre activo." descriptionOfAsBadge: "Este ícono de rol se mostrará a lado del nombre de usuario cuando este rol se encuentre activo."
displayOrder: "Posición"
descriptionOfDisplayOrder: "Entre más alto el número, mayor es la posición en la interfaz."
canEditMembersByModerator: "Permitir a los moderadores editar los miembros" canEditMembersByModerator: "Permitir a los moderadores editar los miembros"
descriptionOfCanEditMembersByModerator: "Si se activa, los moderadores, al igual que los administradores, serán capaces de asignar/quitar usuarios a éste rol. Si se desactiva, sólo los administradores podrán hacerlo." descriptionOfCanEditMembersByModerator: "Si se activa, los moderadores, al igual que los administradores, serán capaces de asignar/quitar usuarios a éste rol. Si se desactiva, sólo los administradores podrán hacerlo."
priority: "Prioridad" priority: "Prioridad"
@ -1239,6 +1266,7 @@ _role:
rateLimitFactor: "Limitador" rateLimitFactor: "Limitador"
descriptionOfRateLimitFactor: "Límites más bajos son menos restrictivos, más altos menos restrictivos" descriptionOfRateLimitFactor: "Límites más bajos son menos restrictivos, más altos menos restrictivos"
canHideAds: "Puede ocultar anuncios" canHideAds: "Puede ocultar anuncios"
canSearchNotes: "Uso de la búsqueda de notas"
_condition: _condition:
isLocal: "Usuario local" isLocal: "Usuario local"
isRemote: "Usuario remoto" isRemote: "Usuario remoto"
@ -1840,3 +1868,13 @@ _deck:
_dialog: _dialog:
charactersExceeded: "¡Has excedido el límite de caracteres! Actualmente {current} de {max}." charactersExceeded: "¡Has excedido el límite de caracteres! Actualmente {current} de {max}."
charactersBelow: "¡Estás por debajo del límite de caracteres! Actualmente {current} de {min}." charactersBelow: "¡Estás por debajo del límite de caracteres! Actualmente {current} de {min}."
_disabledTimeline:
title: "Línea de tiempo deshabilitada"
description: "No puedes usar esta línea de tiempo con tus roles actuales."
_drivecleaner:
orderBySizeDesc: "Más grandes"
orderByCreatedAtAsc: "Más antiguos"
_webhookSettings:
name: "Nombre"
active: "Activado"

View File

@ -575,7 +575,6 @@ tokenRequested: "Autoriser l'accès au compte"
pluginTokenRequestedDescription: "Ce plugin pourra utiliser les autorisations définies ici." pluginTokenRequestedDescription: "Ce plugin pourra utiliser les autorisations définies ici."
notificationType: "Type de notifications" notificationType: "Type de notifications"
edit: "Editer" edit: "Editer"
useStarForReactionFallback: "Utiliser ★ comme alternative si lémoji de réaction est inconnu"
emailServer: "Serveur mail" emailServer: "Serveur mail"
enableEmail: "Activer la distribution de courriel" enableEmail: "Activer la distribution de courriel"
emailConfigInfo: "Utilisé pour confirmer votre adresse de courriel et la réinitialisation de votre mot de passe en cas doubli." emailConfigInfo: "Utilisé pour confirmer votre adresse de courriel et la réinitialisation de votre mot de passe en cas doubli."
@ -1468,3 +1467,7 @@ _deck:
channel: "Canaux" channel: "Canaux"
mentions: "Mentions" mentions: "Mentions"
direct: "Direct" direct: "Direct"
_webhookSettings:
name: "Nom"
active: "Activé"

View File

@ -1 +1,2 @@
--- ---

View File

@ -1 +1,2 @@
--- ---

View File

@ -579,7 +579,6 @@ tokenRequested: "Berikan ijin akses ke akun"
pluginTokenRequestedDescription: "Plugin ini dapat menggunakan setelan ijin disini." pluginTokenRequestedDescription: "Plugin ini dapat menggunakan setelan ijin disini."
notificationType: "Jenis pemberitahuan" notificationType: "Jenis pemberitahuan"
edit: "Sunting" edit: "Sunting"
useStarForReactionFallback: "Gunakan ★ sebagai fallback jika reaksi emoji tidak diketahui"
emailServer: "Peladen surel" emailServer: "Peladen surel"
enableEmail: "Nyalakan distribusi surel" enableEmail: "Nyalakan distribusi surel"
emailConfigInfo: "Digunakan untuk mengonfirmasi surel kamu disaat mendaftar dan lupa kata sandi" emailConfigInfo: "Digunakan untuk mengonfirmasi surel kamu disaat mendaftar dan lupa kata sandi"
@ -1804,3 +1803,7 @@ _deck:
channel: "Kanal" channel: "Kanal"
mentions: "Sebutan" mentions: "Sebutan"
direct: "Langsung" direct: "Langsung"
_webhookSettings:
name: "Nama"
active: "Aktif"

View File

@ -122,6 +122,8 @@ unmarkAsSensitive: "Segna come non sensibile"
enterFileName: "Nome del file" enterFileName: "Nome del file"
mute: "Silenzia" mute: "Silenzia"
unmute: "Riattiva l'audio" unmute: "Riattiva l'audio"
renoteMute: "Silenzia i Rinota"
renoteUnmute: "Non silenziare i Rinota"
block: "Blocca" block: "Blocca"
unblock: "Sblocca" unblock: "Sblocca"
suspend: "Sospendi" suspend: "Sospendi"
@ -153,6 +155,7 @@ flagShowTimelineReplies: "Mostra le risposte alle note sulla timeline."
flagShowTimelineRepliesDescription: "Se è attiva, la timeline mostra le risposte alle altre note dell'utente oltre a quelle dell'utente stesso." flagShowTimelineRepliesDescription: "Se è attiva, la timeline mostra le risposte alle altre note dell'utente oltre a quelle dell'utente stesso."
autoAcceptFollowed: "Accetta automaticamente le richieste di follow da utenti che già segui" autoAcceptFollowed: "Accetta automaticamente le richieste di follow da utenti che già segui"
addAccount: "Aggiungi profilo" addAccount: "Aggiungi profilo"
reloadAccountsList: "Ricarica l'elenco dei profili"
loginFailed: "Accesso non riuscito" loginFailed: "Accesso non riuscito"
showOnRemote: "Leggi sull'istanza remota" showOnRemote: "Leggi sull'istanza remota"
general: "Generali" general: "Generali"
@ -544,6 +547,10 @@ userSuspended: "L'utente è in sospensione"
userSilenced: "L'utente è silenziat@." userSilenced: "L'utente è silenziat@."
yourAccountSuspendedTitle: "Questo profilo è sospeso" yourAccountSuspendedTitle: "Questo profilo è sospeso"
yourAccountSuspendedDescription: "Questo profilo è stato sospeso a causa di una violazione del regolamento. Per informazioni, contattare l'amministrazione. Si prega di non creare un nuovo account." yourAccountSuspendedDescription: "Questo profilo è stato sospeso a causa di una violazione del regolamento. Per informazioni, contattare l'amministrazione. Si prega di non creare un nuovo account."
tokenRevoked: "Il token non è valido"
tokenRevokedDescription: "Il token di accesso è scaduto. Per favore, accedi nuovamente."
accountDeleted: "Profilo eliminato"
accountDeletedDescription: "Questo profilo è stato eliminato."
menu: "Menù" menu: "Menù"
divider: "Linea di separazione" divider: "Linea di separazione"
addItem: "Aggiungi elemento" addItem: "Aggiungi elemento"
@ -558,8 +565,8 @@ enableInfiniteScroll: "Abilita scorrimento infinito"
visibility: "Visibilità" visibility: "Visibilità"
poll: "Sondaggio" poll: "Sondaggio"
useCw: "Nascondere media" useCw: "Nascondere media"
enablePlayer: "Apri in lettore video" enablePlayer: "Visualizza"
disablePlayer: "Chiudi il lettore" disablePlayer: "Chiudi"
expandTweet: "Espandi tweet" expandTweet: "Espandi tweet"
themeEditor: "Editor di temi" themeEditor: "Editor di temi"
description: "Descrizione" description: "Descrizione"
@ -587,7 +594,6 @@ tokenRequested: "Autorizza accesso al profilo"
pluginTokenRequestedDescription: "Il plugin potrà utilizzare le autorizzazioni impostate qui." pluginTokenRequestedDescription: "Il plugin potrà utilizzare le autorizzazioni impostate qui."
notificationType: "Tipo di notifiche" notificationType: "Tipo di notifiche"
edit: "Modifica" edit: "Modifica"
useStarForReactionFallback: "Se è sconosciuto l'emoji di reazione, usare la ★ come alternativa."
emailServer: "Server email" emailServer: "Server email"
enableEmail: "Abilita consegna email" enableEmail: "Abilita consegna email"
emailConfigInfo: "Utilizzato per verificare il tuo indirizzo di posta elettronica e per reimpostare la tua password" emailConfigInfo: "Utilizzato per verificare il tuo indirizzo di posta elettronica e per reimpostare la tua password"
@ -959,6 +965,21 @@ invitationRequiredToRegister: "L'accesso a questo nodo è solo ad invito. Devi i
emailNotSupported: "L'istanza non supporta l'invio di email" emailNotSupported: "L'istanza non supporta l'invio di email"
postToTheChannel: "Pubblica sul canale" postToTheChannel: "Pubblica sul canale"
cannotBeChangedLater: "Non sarà più modificabile" cannotBeChangedLater: "Non sarà più modificabile"
reactionAcceptance: "Accettazione reazioni"
likeOnly: "Solo i Like"
likeOnlyForRemote: "Solo Like remoti"
rolesAssignedToMe: "I miei ruoli"
resetPasswordConfirm: "Vuoi reimpostare la password?"
sensitiveWords: "Parole sensibili"
sensitiveWordsDescription: "Imposta automaticamente \"Home\" alla visibilità delle Note che contengono una qualsiasi parola tra queste configurate. Puoi separarle per riga."
notesSearchNotAvailable: "Non è possibile cercare tra le Note."
license: "Licenza"
unfavoriteConfirm: "Vuoi davvero rimuovere la preferenza?"
myClips: "Le mie Clip"
drivecleaner: "Drive cleaner"
retryAllQueuesNow: "Ritenta di consumare tutte le code"
retryAllQueuesConfirmTitle: "Vuoi ritentare adesso?"
retryAllQueuesConfirmText: "Potrebbe sovraccaricare il server temporaneamente."
_achievements: _achievements:
earnedAt: "Data di conseguimento" earnedAt: "Data di conseguimento"
_types: _types:
@ -1218,6 +1239,8 @@ _role:
iconUrl: "URL dell'icona" iconUrl: "URL dell'icona"
asBadge: "Mostra come badge" asBadge: "Mostra come badge"
descriptionOfAsBadge: "Se indicato, accanto al nome utente viene visualizzata l'icona del ruolo." descriptionOfAsBadge: "Se indicato, accanto al nome utente viene visualizzata l'icona del ruolo."
displayOrder: "Ordine di visualizzazione"
descriptionOfDisplayOrder: "I valori più alti vengono visualizzati per primi"
canEditMembersByModerator: "Anche i Moderatori assegnano profili a questo ruolo" canEditMembersByModerator: "Anche i Moderatori assegnano profili a questo ruolo"
descriptionOfCanEditMembersByModerator: "Se disattivo, potranno farlo solamente gli Amministratori." descriptionOfCanEditMembersByModerator: "Se disattivo, potranno farlo solamente gli Amministratori."
priority: "Priorità" priority: "Priorità"
@ -1243,6 +1266,7 @@ _role:
rateLimitFactor: "Limite del rapporto" rateLimitFactor: "Limite del rapporto"
descriptionOfRateLimitFactor: "I rapporti più bassi sono meno restrittivi, quelli più alti lo sono di più." descriptionOfRateLimitFactor: "I rapporti più bassi sono meno restrittivi, quelli più alti lo sono di più."
canHideAds: "Può nascondere i banner" canHideAds: "Può nascondere i banner"
canSearchNotes: "Ricercare nelle Note"
_condition: _condition:
isLocal: "Profilo locale" isLocal: "Profilo locale"
isRemote: "Profilo remoto" isRemote: "Profilo remoto"
@ -1844,3 +1868,13 @@ _deck:
_dialog: _dialog:
charactersExceeded: "Hai superato il limite di {max} caratteri! ({corrente})" charactersExceeded: "Hai superato il limite di {max} caratteri! ({corrente})"
charactersBelow: "Sei al di sotto del minimo di {min} caratteri! ({corrente})" charactersBelow: "Sei al di sotto del minimo di {min} caratteri! ({corrente})"
_disabledTimeline:
title: "Timeline disabilitata"
description: "Il tuo ruolo non ha i permessi per accedere a questa timeline"
_drivecleaner:
orderBySizeDesc: "Dal più grande al più piccolo"
orderByCreatedAtAsc: "Dal più vecchio al più recente"
_webhookSettings:
name: "Nome"
active: "Attivo"

View File

@ -67,7 +67,7 @@ import: "インポート"
export: "エクスポート" export: "エクスポート"
files: "ファイル" files: "ファイル"
download: "ダウンロード" download: "ダウンロード"
driveFileDeleteConfirm: "ファイル「{name}」を削除しますか?このファイルを添付したノートも消えます。" driveFileDeleteConfirm: "ファイル「{name}」を削除しますか?このファイルを使用した全てのコンテンツからも削除されます。"
unfollowConfirm: "{name}のフォローを解除しますか?" unfollowConfirm: "{name}のフォローを解除しますか?"
exportRequested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、「ドライブ」に追加されます。" exportRequested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、「ドライブ」に追加されます。"
importRequested: "インポートをリクエストしました。これには時間がかかる場合があります。" importRequested: "インポートをリクエストしました。これには時間がかかる場合があります。"
@ -460,7 +460,7 @@ aboutX: "{x}について"
emojiStyle: "絵文字のスタイル" emojiStyle: "絵文字のスタイル"
native: "ネイティブ" native: "ネイティブ"
disableDrawer: "メニューをドロワーで表示しない" disableDrawer: "メニューをドロワーで表示しない"
showNoteActionsOnlyHover: "ノートの操作部をホバー時のみ表示する" showNoteActionsOnlyHover: "ノートのアクションをホバー時のみ表示する"
noHistory: "履歴はありません" noHistory: "履歴はありません"
signinHistory: "ログイン履歴" signinHistory: "ログイン履歴"
enableAdvancedMfm: "高度なMFMを有効にする" enableAdvancedMfm: "高度なMFMを有効にする"
@ -500,12 +500,13 @@ objectStoragePrefixDesc: "このprefixのディレクトリ下に格納されま
objectStorageEndpoint: "Endpoint" objectStorageEndpoint: "Endpoint"
objectStorageEndpointDesc: "S3の場合は空、それ以外の場合は各サービスのendpointを指定してください。'<host>'または'<host>:<port>'のように指定します。" objectStorageEndpointDesc: "S3の場合は空、それ以外の場合は各サービスのendpointを指定してください。'<host>'または'<host>:<port>'のように指定します。"
objectStorageRegion: "Region" objectStorageRegion: "Region"
objectStorageRegionDesc: "'xx-east-1'のようなregionを指定してください。使用サービスにregionの概念がない場合は、空または'us-east-1'にしてください。" objectStorageRegionDesc: "'xx-east-1'のようなregionを指定してください。使用サービスにregionの概念がない場合は'us-east-1'にしてください。AWS設定ファイルまたは環境変数を参照する場合は空にしてください。"
objectStorageUseSSL: "SSLを使用する" objectStorageUseSSL: "SSLを使用する"
objectStorageUseSSLDesc: "API接続にhttpsを使用しない場合はオフにしてください" objectStorageUseSSLDesc: "API接続にhttpsを使用しない場合はオフにしてください"
objectStorageUseProxy: "Proxyを利用する" objectStorageUseProxy: "Proxyを利用する"
objectStorageUseProxyDesc: "API接続にproxyを利用しない場合はオフにしてください" objectStorageUseProxyDesc: "API接続にproxyを利用しない場合はオフにしてください"
objectStorageSetPublicRead: "アップロード時に'public-read'を設定する" objectStorageSetPublicRead: "アップロード時に'public-read'を設定する"
s3ForcePathStyleDesc: "s3ForcePathStyleを有効にすると、バケット名をURLのホスト名ではなくパスの一部として指定することを強制します。セルフホストされたMinioなどの使用時に有効にする必要がある場合があります。"
serverLogs: "サーバーログ" serverLogs: "サーバーログ"
deleteAll: "全て削除" deleteAll: "全て削除"
showFixedPostForm: "タイムライン上部に投稿フォームを表示する" showFixedPostForm: "タイムライン上部に投稿フォームを表示する"
@ -594,7 +595,6 @@ tokenRequested: "アカウントへのアクセス許可"
pluginTokenRequestedDescription: "このプラグインはここで設定した権限を行使できるようになります。" pluginTokenRequestedDescription: "このプラグインはここで設定した権限を行使できるようになります。"
notificationType: "通知の種類" notificationType: "通知の種類"
edit: "編集" edit: "編集"
useStarForReactionFallback: "リアクション絵文字が不明な場合、代わりに★を使う"
emailServer: "メールサーバー" emailServer: "メールサーバー"
enableEmail: "メール配信機能を有効化する" enableEmail: "メール配信機能を有効化する"
emailConfigInfo: "メールアドレスの確認やパスワードリセットの際に使います" emailConfigInfo: "メールアドレスの確認やパスワードリセットの際に使います"
@ -969,6 +969,23 @@ cannotBeChangedLater: "後から変更できません。"
reactionAcceptance: "リアクションの受け入れ" reactionAcceptance: "リアクションの受け入れ"
likeOnly: "いいねのみ" likeOnly: "いいねのみ"
likeOnlyForRemote: "リモートからはいいねのみ" likeOnlyForRemote: "リモートからはいいねのみ"
rolesAssignedToMe: "自分に割り当てられたロール"
resetPasswordConfirm: "パスワードリセットしますか?"
sensitiveWords: "センシティブワード"
sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。"
notesSearchNotAvailable: "ノート検索は利用できません。"
license: "ライセンス"
unfavoriteConfirm: "お気に入り解除しますか?"
myClips: "自分のクリップ"
drivecleaner: "ドライブクリーナー"
retryAllQueuesNow: "すべてのキューを今すぐ再試行"
retryAllQueuesConfirmTitle: "今すぐ再試行しますか?"
retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大することがあります。"
enableChartsForRemoteUser: "リモートユーザーのチャートを生成"
enableChartsForFederatedInstances: "リモートサーバーのチャートを生成"
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
largeNoteReactions: "ノートのリアクションを大きく表示"
noteIdOrUrl: "ートIDまたはURL"
_achievements: _achievements:
earnedAt: "獲得日時" earnedAt: "獲得日時"
@ -1230,6 +1247,8 @@ _role:
iconUrl: "アイコン画像のURL" iconUrl: "アイコン画像のURL"
asBadge: "バッジとして表示" asBadge: "バッジとして表示"
descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。" descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。"
displayOrder: "表示順"
descriptionOfDisplayOrder: "数値が大きいほどUI上で先頭に表示されます。"
canEditMembersByModerator: "モデレーターのメンバー編集を許可" canEditMembersByModerator: "モデレーターのメンバー編集を許可"
descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。" descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。"
priority: "優先度" priority: "優先度"
@ -1255,6 +1274,7 @@ _role:
rateLimitFactor: "レートリミット" rateLimitFactor: "レートリミット"
descriptionOfRateLimitFactor: "小さいほど制限が緩和され、大きいほど制限が強化されます。" descriptionOfRateLimitFactor: "小さいほど制限が緩和され、大きいほど制限が強化されます。"
canHideAds: "広告の非表示" canHideAds: "広告の非表示"
canSearchNotes: "ノート検索の利用可否"
_condition: _condition:
isLocal: "ローカルユーザー" isLocal: "ローカルユーザー"
isRemote: "リモートユーザー" isRemote: "リモートユーザー"
@ -1264,6 +1284,8 @@ _role:
followersMoreThanOrEq: "フォロワー数が~以上" followersMoreThanOrEq: "フォロワー数が~以上"
followingLessThanOrEq: "フォロー数が~以下" followingLessThanOrEq: "フォロー数が~以下"
followingMoreThanOrEq: "フォロー数が~以上" followingMoreThanOrEq: "フォロー数が~以上"
notesLessThanOrEq: "投稿数が~以下"
notesMoreThanOrEq: "投稿数が~以上"
and: "~かつ~" and: "~かつ~"
or: "~または~" or: "~または~"
not: "~ではない" not: "~ではない"
@ -1907,3 +1929,27 @@ _deck:
_dialog: _dialog:
charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}" charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}"
charactersBelow: "最小文字数を下回っています! 現在 {current} / 制限 {min}" charactersBelow: "最小文字数を下回っています! 現在 {current} / 制限 {min}"
_disabledTimeline:
title: "無効化されたタイムライン"
description: "現在のロールでは、このタイムラインを使用することはできません。"
_drivecleaner:
orderBySizeDesc: "サイズが大きい順"
orderByCreatedAtAsc: "追加日が古い順"
_webhookSettings:
createWebhook: "Webhookを作成"
name: "名前"
secret: "シークレット"
events: "Webhookを実行するタイミング"
active: "有効"
_events:
follow: "フォローしたとき"
followed: "フォローされたとき"
note: "ノートを投稿したとき"
reply: "返信されたとき"
renote: "Renoteされたとき"
reaction: "リアクションがあったとき"
mention: "メンションされたとき"

View File

@ -2,7 +2,7 @@
_lang_: "日本語 (関西弁)" _lang_: "日本語 (関西弁)"
headlineMisskey: "ノートでつながるネットワーク" headlineMisskey: "ノートでつながるネットワーク"
introMisskey: "ようお越しMisskeyは、オープンソースの分散型マイクロブログサービスやねん。\n「ート」を作って、いま起こっとることを共有したり、あんたについて皆に発信しよう📡\n「リアクション」機能で、皆のートに素早く反応を追加したりもできるで✌\nほな新しい世界を探検しよか🚀" introMisskey: "ようお越しMisskeyは、オープンソースの分散型マイクロブログサービスやねん。\n「ート」を作って、いま起こっとることを共有したり、あんたについて皆に発信しよう📡\n「リアクション」機能で、皆のートに素早く反応を追加したりもできるで✌\nほな新しい世界を探検しよか🚀"
poweredByMisskeyDescription: "{name}は、オープンソースのプラットフォーム<b>Misskey</b>を使ったサービス(Misskeyインスタンスと呼ばれるやつや)のひとつやで。" poweredByMisskeyDescription: "{name}は、オープンソースのプラットフォーム<b>Misskey</b>のサーバーのひとつなんやで。"
monthAndDay: "{month}月 {day}日" monthAndDay: "{month}月 {day}日"
search: "探す" search: "探す"
notifications: "通知" notifications: "通知"
@ -15,13 +15,13 @@ gotIt: "ほい"
cancel: "やめとく" cancel: "やめとく"
noThankYou: "やめとく" noThankYou: "やめとく"
enterUsername: "ユーザー名を入れてや" enterUsername: "ユーザー名を入れてや"
renotedBy: "{user}がRenote" renotedBy: "{user}がRenoteしたで"
noNotes: "ノートはあらへん" noNotes: "ノートなんてあらへんで"
noNotifications: "通知はあらへん" noNotifications: "通知なんてあらへんで"
instance: "インスタンス" instance: "サーバー"
settings: "設定" settings: "設定"
basicSettings: "基本設定" basicSettings: "基本設定"
otherSettings: "その他の設定" otherSettings: "ほかの設定"
openInWindow: "ウィンドウで開くで" openInWindow: "ウィンドウで開くで"
profile: "プロフィール" profile: "プロフィール"
timeline: "タイムライン" timeline: "タイムライン"
@ -55,7 +55,7 @@ searchUser: "ユーザーを検索"
reply: "返事" reply: "返事"
loadMore: "まだまだあるで!" loadMore: "まだまだあるで!"
showMore: "まだまだあるで!" showMore: "まだまだあるで!"
showLess: "閉じる" showLess: "さいなら"
youGotNewFollower: "フォローされたで" youGotNewFollower: "フォローされたで"
receiveFollowRequest: "フォローリクエストされたで" receiveFollowRequest: "フォローリクエストされたで"
followRequestAccepted: "フォローが承認されたで" followRequestAccepted: "フォローが承認されたで"
@ -84,12 +84,12 @@ error: "エラー"
somethingHappened: "なんかアカンことが起こったで" somethingHappened: "なんかアカンことが起こったで"
retry: "もっぺんやる?" retry: "もっぺんやる?"
pageLoadError: "ページの読み込みに失敗してもうたわ…" pageLoadError: "ページの読み込みに失敗してもうたわ…"
pageLoadErrorDescription: "これは普通、ネットワークかブラウザキャッシュが原因やからね。キャッシュをクリアするか、もうちっとだけ待ってくれへんか" pageLoadErrorDescription: "これは普通ならネットワークかブラウザキャッシュが悪さしてるんよ。キャッシュをほかすか、もうちょっとだけ待ってくれへん"
serverIsDead: "サーバーからの応答がないで。もうちょい待ってから試してみてな。" serverIsDead: "サーバーからの応答がないで。もうちょい待ってから試してみてな。"
youShouldUpgradeClient: "このページを表示するには、リロードして新しいバージョンのクライアントを使ってなー。" youShouldUpgradeClient: "このページを表示するには、リロードして新しいバージョンのクライアントを使ってなー。"
enterListName: "リスト名を入れてや" enterListName: "リスト名を入れてや"
privacy: "プライバシー" privacy: "プライバシー"
makeFollowManuallyApprove: "他人のフォローは許可してからや!" makeFollowManuallyApprove: "ええって言わなフォローできへんようにする"
defaultNoteVisibility: "もとからの公開範囲" defaultNoteVisibility: "もとからの公開範囲"
follow: "フォロー" follow: "フォロー"
followRequest: "フォローを頼む" followRequest: "フォローを頼む"
@ -113,7 +113,7 @@ sensitive: "ちょっとアカンやつやで"
add: "増やす" add: "増やす"
reaction: "リアクション" reaction: "リアクション"
reactions: "リアクション" reactions: "リアクション"
reactionSetting: "Reaction that will be displayed in Picker. " reactionSetting: "ピッカーに出しとくリアクション"
reactionSettingDescription2: "ドラッグで並び替え、クリックで削除、+を押して追加やで。" reactionSettingDescription2: "ドラッグで並び替え、クリックで削除、+を押して追加やで。"
rememberNoteVisibility: "公開範囲覚えといて" rememberNoteVisibility: "公開範囲覚えといて"
attachCancel: "のっけるのやめる" attachCancel: "のっけるのやめる"
@ -122,6 +122,8 @@ unmarkAsSensitive: "そこまでアカンことないやろ"
enterFileName: "ファイル名を入れてや" enterFileName: "ファイル名を入れてや"
mute: "ミュート" mute: "ミュート"
unmute: "ミュートやめたる" unmute: "ミュートやめたる"
renoteMute: "リノートは見いひん"
renoteUnmute: "リノートもやっぱ見るわ"
block: "ブロック" block: "ブロック"
unblock: "ブロックやめたる" unblock: "ブロックやめたる"
suspend: "凍結" suspend: "凍結"
@ -145,20 +147,21 @@ addEmoji: "絵文字を追加"
settingGuide: "ええ感じの設定" settingGuide: "ええ感じの設定"
cacheRemoteFiles: "リモートのファイルをキャッシュする" cacheRemoteFiles: "リモートのファイルをキャッシュする"
cacheRemoteFilesDescription: "この設定を切っとくと、リモートファイルをキャッシュせず直リンクするようになるで。サーバーの容量は節約できるけど、サムネイルが作られんくなるから通信量が増えるで。" cacheRemoteFilesDescription: "この設定を切っとくと、リモートファイルをキャッシュせず直リンクするようになるで。サーバーの容量は節約できるけど、サムネイルが作られんくなるから通信量が増えるで。"
flagAsBot: "Botで" flagAsBot: "Botにするで"
flagAsBotDescription: "もしこのアカウントがプログラムによって運用されるんやったら、このフラグをオンにしてたのむで。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Misskeyのシステム上での扱いがBotに合ったもんになるんやで。" flagAsBotDescription: "もしこのアカウントをプログラム使うて運用するんやったら、このフラグをオンにしてや。オンにすれば、反応がバーッて連鎖するのを避けるために開発者が使うたり、Misskeyのシステム上での扱いがBotに合ったもんになるからな。"
flagAsCat: "Catやで" flagAsCat: "Catやで"
flagAsCatDescription: "ワレ、猫ちゃんならこのフラグをつけてみ?" flagAsCatDescription: "ワレ、猫ちゃんならこのフラグをつけてみ?"
flagShowTimelineReplies: "タイムラインにノートへの返信を表示するで" flagShowTimelineReplies: "タイムラインにノートへの返信を表示するで"
flagShowTimelineRepliesDescription: "オンにしたら、タイムラインにユーザーのノートの他にもそのユーザーの他のノートへの返信を表示するで。" flagShowTimelineRepliesDescription: "オンにしたら、タイムラインにユーザーのノートの他にもそのユーザーの他のノートへの返信を表示するで。"
autoAcceptFollowed: "フォローしとるユーザーからのフォローリクエストを勝手に許可しとく" autoAcceptFollowed: "フォローしとるユーザーからのフォローリクエストを勝手に許可しとく"
addAccount: "アカウントを追加" addAccount: "アカウントを追加"
reloadAccountsList: "アカウントリストの情報を更新"
loginFailed: "ログインに失敗してもうた…" loginFailed: "ログインに失敗してもうた…"
showOnRemote: "リモートで見る" showOnRemote: "リモートで見る"
general: "全般" general: "全般"
wallpaper: "壁紙" wallpaper: "壁紙"
setWallpaper: "壁紙を設定" setWallpaper: "壁紙を設定"
removeWallpaper: "壁紙を削除" removeWallpaper: "壁紙ほかす"
searchWith: "検索: {q}" searchWith: "検索: {q}"
youHaveNoLists: "リストがあらへんで?" youHaveNoLists: "リストがあらへんで?"
followConfirm: "{name}をフォローしてええか?" followConfirm: "{name}をフォローしてええか?"
@ -169,7 +172,7 @@ selectUser: "ユーザーを選ぶ"
recipient: "宛先" recipient: "宛先"
annotation: "注釈" annotation: "注釈"
federation: "連合" federation: "連合"
instances: "インスタンス" instances: "サーバー"
registeredAt: "初観測" registeredAt: "初観測"
latestRequestReceivedAt: "ちょっと前のリクエスト受信" latestRequestReceivedAt: "ちょっと前のリクエスト受信"
latestStatus: "ちょっと前のステータス" latestStatus: "ちょっと前のステータス"
@ -178,7 +181,7 @@ charts: "チャート"
perHour: "1時間ごと" perHour: "1時間ごと"
perDay: "1日ごと" perDay: "1日ごと"
stopActivityDelivery: "アクティビティの配送をやめる" stopActivityDelivery: "アクティビティの配送をやめる"
blockThisInstance: "このインスタンスをブロック" blockThisInstance: "このサーバーをブロックすんで"
operations: "操作" operations: "操作"
software: "ソフトウェア" software: "ソフトウェア"
version: "バージョン" version: "バージョン"
@ -189,28 +192,28 @@ jobQueue: "ジョブキュー"
cpuAndMemory: "CPUとメモリ" cpuAndMemory: "CPUとメモリ"
network: "ネットワーク" network: "ネットワーク"
disk: "ディスク" disk: "ディスク"
instanceInfo: "インスタンス情報" instanceInfo: "サーバー情報"
statistics: "統計" statistics: "統計"
clearQueue: "キューにさいなら" clearQueue: "キューにさいなら"
clearQueueConfirmTitle: "キューをクリアしまっか?" clearQueueConfirmTitle: "キューをクリアしまっか?"
clearQueueConfirmText: "未配達の投稿は配送されなくなるで。通常この操作を行う必要はあらへんや。" clearQueueConfirmText: "未配達の投稿は配送されなくなるで。ふつうこの操作を行う必要は無いんやけどな。"
clearCachedFiles: "キャッシュにさいなら" clearCachedFiles: "キャッシュにさいなら"
clearCachedFilesConfirm: "キャッシュされとるリモートファイルをみんなほかしてええか?" clearCachedFilesConfirm: "キャッシュされとるリモートファイルをみんなほかしてええか?"
blockedInstances: "インスタンスブロック" blockedInstances: "ブロックしたサーバー"
blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定してな。ブロックされてもうたインスタンスとはもう金輪際やり取りできひんくなるで。" blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定してな。ブロックされてもうたサーバーとはもう金輪際やり取りできひんくなるで。ついでにそのサブドメインもブロックするで。"
muteAndBlock: "ミュートとブロック" muteAndBlock: "ミュートとブロック"
mutedUsers: "ミュートしたユーザー" mutedUsers: "ミュートしたユーザー"
blockedUsers: "ブロックしたユーザー" blockedUsers: "ブロックしたユーザー"
noUsers: "ユーザーはおらん" noUsers: "ユーザーはおらん"
editProfile: "プロフィールをいじる" editProfile: "プロフィールをいじる"
noteDeleteConfirm: "このノートを削除しまっか?" noteDeleteConfirm: "このノートを削除しまっか?"
pinLimitExceeded: "これ以上ピン留めできひん" pinLimitExceeded: "これ以上ピン留めできひん"
intro: "Misskeyのインストールが完了してん!管理者アカウントを作ってや。" intro: "Misskeyのインストールが完了したで!管理者アカウントを作ってや。"
done: "でけた" done: "でけた"
processing: "処理しとる" processing: "処理しとる"
preview: "プレビュー" preview: "プレビュー"
default: "デフォルト" default: "デフォルト"
defaultValueIs: "デフォルト" defaultValueIs: "デフォルト: {value}"
noCustomEmojis: "絵文字はあらへん" noCustomEmojis: "絵文字はあらへん"
noJobs: "ジョブはあらへん" noJobs: "ジョブはあらへん"
federating: "連合しとる" federating: "連合しとる"
@ -220,17 +223,17 @@ all: "みんな"
subscribing: "購読しとる" subscribing: "購読しとる"
publishing: "配信しとる" publishing: "配信しとる"
notResponding: "応答してへんで" notResponding: "応答してへんで"
instanceFollowing: "インスタンスのフォロー" instanceFollowing: "サーバーのフォロー"
instanceFollowers: "インスタンスのフォロワー\n" instanceFollowers: "サーバーのフォロワー\n"
instanceUsers: "インスタンスのユーザー" instanceUsers: "サーバーのユーザー"
changePassword: "パスワード変える" changePassword: "パスワード変える"
security: "セキュリティ" security: "セキュリティ"
retypedNotMatch: "そやないねん。" retypedNotMatch: "入れたやつ同じになってないで。"
currentPassword: "今のパスワード" currentPassword: "今のパスワード"
newPassword: "今度のパスワード" newPassword: "のパスワード"
newPasswordRetype: "今度のパスワード(もっぺん入れて)" newPasswordRetype: "今度のパスワード(もっぺん入れて)"
attachFile: "ファイルのっける" attachFile: "ファイルのっける"
more: "他のやつ!" more: "他の"
featured: "ハイライト" featured: "ハイライト"
usernameOrUserId: "ユーザー名かユーザーID" usernameOrUserId: "ユーザー名かユーザーID"
noSuchUser: "ユーザーが見つからへんで" noSuchUser: "ユーザーが見つからへんで"
@ -238,15 +241,15 @@ lookup: "見てきて"
announcements: "お知らせ" announcements: "お知らせ"
imageUrl: "画像URL" imageUrl: "画像URL"
remove: "ほかす" remove: "ほかす"
removed: "削除したで!" removed: "ほかしたで!"
removeAreYouSure: "「{x}」はほかしてええか?" removeAreYouSure: "「{x}」はほかしてええか?"
deleteAreYouSure: "「{x}」はほかしてええか?" deleteAreYouSure: "「{x}」はほかしてええか?"
resetAreYouSure: "リセットしてええん?" resetAreYouSure: "リセットしてええん?"
saved: "保存したで!" saved: "保存したで!"
messaging: "チャット" messaging: "チャット"
upload: "アップロード" upload: "アップロード"
keepOriginalUploading: "オリジナル画像を保持するわ" keepOriginalUploading: "オリジナル画像のまんま"
keepOriginalUploadingDescription: "画像を上げるときにオリジナル版を保持するで。オフにしたら上げたときにブラウザでWeb公開用の画像を生成するで。 " keepOriginalUploadingDescription: "画像を上げるときにオリジナル版のまんまにするで。オフにしたら、上げたときにブラウザでWeb公開用の画像を生成するで。 "
fromDrive: "ドライブから" fromDrive: "ドライブから"
fromUrl: "URLから" fromUrl: "URLから"
uploadFromUrl: "URLアップロード" uploadFromUrl: "URLアップロード"
@ -272,8 +275,8 @@ yearsOld: "{age}歳"
registeredDate: "始めた日" registeredDate: "始めた日"
location: "場所" location: "場所"
theme: "テーマ" theme: "テーマ"
themeForLightMode: "ライトモードではこのテーマつこて" themeForLightMode: "ライトモードではこのテーマ使うて"
themeForDarkMode: "ダークモードではこのテーマつこて" themeForDarkMode: "ダークモードではこのテーマ使うて"
light: "ライト" light: "ライト"
dark: "ダーク" dark: "ダーク"
lightThemes: "デイゲーム" lightThemes: "デイゲーム"
@ -289,13 +292,13 @@ renameFile: "ファイル名をいらう"
folderName: "フォルダー名" folderName: "フォルダー名"
createFolder: "フォルダー作る" createFolder: "フォルダー作る"
renameFolder: "フォルダー名を変える" renameFolder: "フォルダー名を変える"
deleteFolder: "フォルダーを消してまう" deleteFolder: "フォルダーをほかす"
addFile: "ファイルを追加" addFile: "ファイルを追加"
emptyDrive: "ドライブにはなんも残っとらん" emptyDrive: "ドライブにはなんも残っとらん"
emptyFolder: "ふぉろだーにはなんも残っとらん" emptyFolder: "このフォルダーは空や"
unableToDelete: "消そうおもってんけどな、あかんかったわ" unableToDelete: "消そうおもってんけどな、あかんかったわ"
inputNewFileName: "今度のファイル名は何にするん?" inputNewFileName: "今度のファイル名は何にするん?"
inputNewDescription: "新しいキャプションを入力しましょ" inputNewDescription: "新しいキャプションを入れてや"
inputNewFolderName: "今度のフォルダ名は何にするん?" inputNewFolderName: "今度のフォルダ名は何にするん?"
circularReferenceFolder: "移動先のフォルダーは、移動するフォルダーのサブフォルダーや。" circularReferenceFolder: "移動先のフォルダーは、移動するフォルダーのサブフォルダーや。"
hasChildFilesOrFolders: "このフォルダ、まだなんか入っとるから消されへん" hasChildFilesOrFolders: "このフォルダ、まだなんか入っとるから消されへん"
@ -303,8 +306,8 @@ copyUrl: "URLをコピー"
rename: "名前を変えるで" rename: "名前を変えるで"
avatar: "アイコン" avatar: "アイコン"
banner: "バナー" banner: "バナー"
nsfw: "閲覧注意" nsfw: "見るんは気いつけてな"
whenServerDisconnected: "サーバーとの接続が切れたとき" whenServerDisconnected: "サーバーとの接続が失くなってしもうたとき"
disconnectedFromServer: "サーバーが機嫌悪いねん" disconnectedFromServer: "サーバーが機嫌悪いねん"
reload: "リロード" reload: "リロード"
doNothing: "何もせんとく" doNothing: "何もせんとく"
@ -314,10 +317,10 @@ unwatch: "ウォッチやめる"
accept: "ええで" accept: "ええで"
reject: "あかん" reject: "あかん"
normal: "ええ感じ" normal: "ええ感じ"
instanceName: "インスタンス名" instanceName: "サーバー名"
instanceDescription: "インスタンスの紹介" instanceDescription: "サーバーの紹介"
maintainerName: "管理者の名前" maintainerName: "管理者はんの名前"
maintainerEmail: "管理者のメールアドレス" maintainerEmail: "管理者はんのメールアドレス"
tosUrl: "利用規約のURL" tosUrl: "利用規約のURL"
thisYear: "今年" thisYear: "今年"
thisMonth: "今月" thisMonth: "今月"
@ -329,23 +332,23 @@ pages: "ページ"
integration: "連携" integration: "連携"
connectService: "つなげるで" connectService: "つなげるで"
disconnectService: "切るで" disconnectService: "切るで"
enableLocalTimeline: "ローカルタイムラインを使えるようにする" enableLocalTimeline: "ローカルタイムラインを使えるようにする"
enableGlobalTimeline: "グローバルタイムラインを使えるようにする" enableGlobalTimeline: "グローバルタイムラインを使えるようにする"
disablingTimelinesInfo: "ここらへんのタイムラインを使えんようにしてしもても、管理者とモデレーターは使えるままになってるで、そうやなかったら不便やからな。" disablingTimelinesInfo: "ここらへんのタイムラインを使えんようにしてしもても、管理者とモデレーターは使えるままになってるで、そうやなかったら不便やからな。"
registration: "登録" registration: "登録"
enableRegistration: "一見さんでも誰でもいらっしゃ~い" enableRegistration: "一見さんでも誰でもいらっしゃ~い"
invite: "来てや" invite: "来てや"
driveCapacityPerLocalAccount: "ローカルユーザーひとりあたりのドライブ容量" driveCapacityPerLocalAccount: "ローカルユーザーはんひとりあたりのドライブ容量"
driveCapacityPerRemoteAccount: "リモートユーザーひとりあたりのドライブ容量" driveCapacityPerRemoteAccount: "リモートユーザーはんひとりあたりのドライブ容量"
inMb: "メガバイト単位" inMb: "メガバイト単位"
iconUrl: "アイコン画像のURL" iconUrl: "アイコン画像のURL"
bannerUrl: "バナー画像のURL" bannerUrl: "バナー画像のURL"
backgroundImageUrl: "背景画像のURL" backgroundImageUrl: "背景画像のURL"
basicInfo: "基本情報" basicInfo: "基本情報"
pinnedUsers: "ピン留めしたユーザー" pinnedUsers: "ピン留めしたユーザー"
pinnedUsersDescription: "「みつける」ページとかにピン留めしたいユーザーをここに書けばええんやで。他ん人との名前は改行で区切ればええんやで。" pinnedUsersDescription: "「みつける」ページとかにピン留めしたいユーザーをここに書けばええんやで。ユーザー毎に改行してや。"
pinnedPages: "ピン留めページ" pinnedPages: "ピン留めページ"
pinnedPagesDescription: "インスタンスのいっちゃん上にピン留めしたいページのパスを改行で区切って記述してな" pinnedPagesDescription: "サーバーのいっちゃん上にピン留めしたいページのパスを改行で区切って記述してな"
pinnedClipId: "ピン留めするクリップのID" pinnedClipId: "ピン留めするクリップのID"
pinnedNotes: "ピン留めされとるノート" pinnedNotes: "ピン留めされとるノート"
hcaptcha: "hCaptchaキャプチャ" hcaptcha: "hCaptchaキャプチャ"
@ -370,7 +373,7 @@ antennaExcludeKeywords: "除外キーワード"
antennaKeywordsDescription: "スペースで区切ったるとAND指定で、改行で区切ったるとOR指定や" antennaKeywordsDescription: "スペースで区切ったるとAND指定で、改行で区切ったるとOR指定や"
notifyAntenna: "新しいノートを通知すんで" notifyAntenna: "新しいノートを通知すんで"
withFileAntenna: "なんか添付されたノートだけ" withFileAntenna: "なんか添付されたノートだけ"
enableServiceworker: "ServiceWorkerをつこて" enableServiceworker: "ブラウザにプッシュ通知が行くようにする"
antennaUsersDescription: "ユーザー名を改行で区切ったってな" antennaUsersDescription: "ユーザー名を改行で区切ったってな"
caseSensitive: "大文字と小文字は別もんや" caseSensitive: "大文字と小文字は別もんや"
withReplies: "返信も入れたって" withReplies: "返信も入れたって"
@ -395,23 +398,23 @@ administrator: "管理者"
token: "トークン" token: "トークン"
2fa: "二要素認証" 2fa: "二要素認証"
totp: "認証アプリ" totp: "認証アプリ"
totpDescription: "認証アプリ使てワンタイムパスワードを入れる" totpDescription: "認証アプリ使てワンタイムパスワードを入れる"
moderator: "モデレーター" moderator: "モデレーター"
moderation: "モデレーション" moderation: "モデレーション"
nUsersMentioned: "{n}人が投稿" nUsersMentioned: "{n}人が投稿"
securityKeyAndPasskey: "セキュリティキー・パスキー" securityKeyAndPasskey: "セキュリティキー・パスキー"
securityKey: "セキュリティキー" securityKey: "セキュリティキー"
lastUsed: "最後につこうた日" lastUsed: "最後につこうた日"
lastUsedAt: "最後に使たん: {t}" lastUsedAt: "最後に使たん: {t}"
unregister: "登録やめる" unregister: "登録やめる"
passwordLessLogin: "パスワード無くてもログインできるようにする" passwordLessLogin: "パスワード無くてもログインできるようにする"
passwordLessLoginDescription: "パスワードやなくて、セキュリティキーとかパスキーだけでログインするわ" passwordLessLoginDescription: "パスワードなんかいらん、セキュリティキーとかパスキーだけでログインするわ"
resetPassword: "パスワードをリセット" resetPassword: "パスワードをリセット"
newPasswordIs: "今度のパスワードは「{password}」や" newPasswordIs: "今度のパスワードは「{password}」や"
reduceUiAnimation: "UIの動きやアニメーションを減らす" reduceUiAnimation: "UIの動きやアニメーションを少なする"
share: "わけわけ" share: "わけわけ"
notFound: "見つからへんね" notFound: "見つからへんね"
notFoundDescription: "指定されたURLに該当するページはあらへんやった。" notFoundDescription: "言われたURLにはまるページはなかったで。"
uploadFolder: "とりあえずアップロードしたやつ置いとく所" uploadFolder: "とりあえずアップロードしたやつ置いとく所"
cacheClear: "キャッシュをほかす" cacheClear: "キャッシュをほかす"
markAsReadAllNotifications: "通知はもう全て読んだわっ" markAsReadAllNotifications: "通知はもう全て読んだわっ"
@ -419,37 +422,37 @@ markAsReadAllUnreadNotes: "投稿は全て読んだわっ"
markAsReadAllTalkMessages: "チャットはもうぜんぶ読んだわっ" markAsReadAllTalkMessages: "チャットはもうぜんぶ読んだわっ"
help: "ヘルプ" help: "ヘルプ"
inputMessageHere: "ここにメッセージ書いてや" inputMessageHere: "ここにメッセージ書いてや"
close: "閉じる" close: "さいなら"
invites: "来てや" invites: "来てや"
members: "メンバー" members: "メンバーはん"
transfer: "譲渡" transfer: "譲渡"
title: "タイトル" title: "タイトル"
text: "テキスト" text: "テキスト"
enable: "有効にするで" enable: "有効にするで"
next: "次" next: "次"
retype: "もっかい入力" retype: "もっかい入力"
noteOf: "{user}のノート" noteOf: "{user}はんのノート"
quoteAttached: "引用付いとるで" quoteAttached: "引用付いとるで"
quoteQuestion: "引用として添付してもええか?" quoteQuestion: "引用として添付してもええか?"
noMessagesYet: "まだチャットはあらへんで" noMessagesYet: "まだチャットはあらへんで"
newMessageExists: "新しいメッセージがきたで" newMessageExists: "新しいメッセージがきたで"
onlyOneFileCanBeAttached: "すまん、メッセージに添付できるファイルはひとつだけなんや。" onlyOneFileCanBeAttached: "ごめんな、メッセージに添付できるファイルはひとつだけなんよ。"
signinRequired: "ログインしてくれへん?" signinRequired: "ログインしてくれへん?"
invitations: "来てや" invitations: "来てや"
invitationCode: "招待コード" invitationCode: "招待コード"
checking: "確認しとるで" checking: "確認しとるで"
available: "利用できる\n" available: "使えるで"
unavailable: "利用できん" unavailable: "利用できん"
usernameInvalidFormat: "a~z、A~Z、0~9、_が使えるで" usernameInvalidFormat: "a~z、A~Z、0~9、_が使えるで"
tooShort: "短すぎやろ!" tooShort: "短すぎやろ!"
tooLong: "長すぎやろ!" tooLong: "長すぎやろ!"
weakPassword: "へぼいパスワード" weakPassword: "へぼいパスワード"
normalPassword: "普通のパスワード" normalPassword: "ぼちぼちのパスワード"
strongPassword: "ええ感じのパスワード" strongPassword: "ええ感じのパスワード"
passwordMatched: "よし!一致や!" passwordMatched: "よし!一致や!"
passwordNotMatched: "一致しとらんで?" passwordNotMatched: "一致しとらんで?"
signinWith: "{x}でログイン" signinWith: "{x}でログイン"
signinFailed: "ログインできんかったで。もっかいユーザー名とパスワードを確認してみて。" signinFailed: "ログインできんかったで。もっかいユーザー名とパスワードを確認してみて。"
or: "それか" or: "それか"
language: "言語" language: "言語"
uiLanguage: "UIの表示言語" uiLanguage: "UIの表示言語"
@ -458,7 +461,7 @@ emojiStyle: "絵文字のスタイル"
native: "ネイティブ" native: "ネイティブ"
disableDrawer: "メニューをドロワーで表示せぇへん" disableDrawer: "メニューをドロワーで表示せぇへん"
showNoteActionsOnlyHover: "ノートの操作部をホバー時のみ表示するで" showNoteActionsOnlyHover: "ノートの操作部をホバー時のみ表示するで"
noHistory: "履歴はあらへんねぇ。" noHistory: "履歴はないわ。"
signinHistory: "ログイン履歴" signinHistory: "ログイン履歴"
enableAdvancedMfm: "ややこしいMFMもありにする" enableAdvancedMfm: "ややこしいMFMもありにする"
enableAnimatedMfm: "動きがやかましいMFMも許したる" enableAnimatedMfm: "動きがやかましいMFMも許したる"
@ -466,12 +469,12 @@ doing: "やっとるがな"
category: "カテゴリ" category: "カテゴリ"
tags: "タグ" tags: "タグ"
docSource: "このドキュメントのソース" docSource: "このドキュメントのソース"
createAccount: "アカウントを作" createAccount: "アカウントを作るで"
existingAccount: "既存のアカウント" existingAccount: "前に作ったアカウント"
regenerate: "再生成" regenerate: "もっぺん生成するで"
fontSize: "フォントサイズ" fontSize: "字の大きさ"
noFollowRequests: "フォロー申請はあらへんで" noFollowRequests: "フォロー申請はあらへんで"
openImageInNewTab: "画像を新しいタブで開く" openImageInNewTab: "画像を新しいタブで開く"
dashboard: "ダッシュボード" dashboard: "ダッシュボード"
local: "ローカル" local: "ローカル"
remote: "リモート" remote: "リモート"
@ -504,7 +507,7 @@ objectStorageUseProxy: "Proxyを使う"
objectStorageUseProxyDesc: "API接続にproxy使わんのやったら切ってくれへん" objectStorageUseProxyDesc: "API接続にproxy使わんのやったら切ってくれへん"
objectStorageSetPublicRead: "アップロードした時に'public-read'を設定してや" objectStorageSetPublicRead: "アップロードした時に'public-read'を設定してや"
serverLogs: "サーバーログ" serverLogs: "サーバーログ"
deleteAll: "全て削除してや" deleteAll: "全部ほかす"
showFixedPostForm: "タイムラインの上の方で投稿できるようにやってくれへん?" showFixedPostForm: "タイムラインの上の方で投稿できるようにやってくれへん?"
showFixedPostFormInChannel: "タイムラインの上の方で投稿できるようにするわ(チャンネル)" showFixedPostFormInChannel: "タイムラインの上の方で投稿できるようにするわ(チャンネル)"
newNoteRecived: "新しいノートがあるで" newNoteRecived: "新しいノートがあるで"
@ -514,11 +517,11 @@ listen: "聴く"
none: "なし" none: "なし"
showInPage: "ページで表示" showInPage: "ページで表示"
popout: "ポップアウト" popout: "ポップアウト"
volume: "音量" volume: "やかましさ"
masterVolume: "全体の音量" masterVolume: "全体のやかましさ"
details: "もっと" details: "もっと"
chooseEmoji: "絵文字を選ぶ" chooseEmoji: "絵文字を選ぶ"
unableToProcess: "なんか作業が止まってしまったようやね" unableToProcess: "なんか奥の方で詰まってもうた"
recentUsed: "最近使ったやつ" recentUsed: "最近使ったやつ"
install: "インストール" install: "インストール"
uninstall: "アンインストール" uninstall: "アンインストール"
@ -536,14 +539,18 @@ output: "出力"
script: "スクリプト" script: "スクリプト"
disablePagesScript: "Pagesのスクリプトを無効にしてや" disablePagesScript: "Pagesのスクリプトを無効にしてや"
updateRemoteUser: "リモートユーザー情報の更新してくれん?" updateRemoteUser: "リモートユーザー情報の更新してくれん?"
deleteAllFiles: "すべてのファイルを削除" deleteAllFiles: "ファイルを全部ほかす"
deleteAllFilesConfirm: "ホンマにすべてのファイルを削除するん?消したもんはもう戻ってこんのやで?" deleteAllFilesConfirm: "ホンマにファイル全部ほかすんか?消したもんはもう戻ってこんのやで?"
removeAllFollowing: "フォローを全解除" removeAllFollowing: "フォローを全解除"
removeAllFollowingDescription: "{host}からのフォローをすべて解除するで。そのインスタンスが消えて無くなった時とかには便利な機能やで。" removeAllFollowingDescription: "{host}からのフォローをすべて解除するで。そのインスタンスが消えて無くなった時とかには便利な機能やで。"
userSuspended: "このユーザーは...凍結されとる。" userSuspended: "このユーザーは...凍結されとる。"
userSilenced: "このユーザーは...サイレンスされとる。" userSilenced: "このユーザーは...サイレンスされとる。"
yourAccountSuspendedTitle: "あんたのアカウント凍結されとるで" yourAccountSuspendedTitle: "あんたのアカウント凍結されとるで"
yourAccountSuspendedDescription: "あんたのアカウントは、サーバーの利用規約に違反したとかの理由で、凍結されとるで。細かいことは管理者までお問い合わせたってなー。絶対に新しいアカウント作ったらあかんで。絶対やで。" yourAccountSuspendedDescription: "あんたのアカウントは、サーバーの利用規約に違反したとかの理由で、凍結されとるで。細かいことは管理者までお問い合わせたってなー。絶対に新しいアカウント作ったらあかんで。絶対やで。"
tokenRevoked: "トークンが無効やで"
tokenRevokedDescription: "ログイントークンが失効しとるで。もっかいログインしてもろてもええか?"
accountDeleted: "アカウントは削除されとるで"
accountDeletedDescription: "このアカウントは削除されとるで。"
menu: "メニュー" menu: "メニュー"
divider: "分割線" divider: "分割線"
addItem: "項目を追加" addItem: "項目を追加"
@ -566,7 +573,7 @@ description: "説明"
describeFile: "キャプションを付ける" describeFile: "キャプションを付ける"
enterFileDescription: "キャプションを入力" enterFileDescription: "キャプションを入力"
author: "作者" author: "作者"
leaveConfirm: "未保存の変更があるで!ほかしてええか?" leaveConfirm: "あんた、いじったのにまだ保存してないで!ほかしてええか?"
manage: "管理" manage: "管理"
plugins: "プラグイン" plugins: "プラグイン"
preferencesBackups: "設定のバックアップ" preferencesBackups: "設定のバックアップ"
@ -587,7 +594,6 @@ tokenRequested: "アカウントへのアクセス許してやったらどうや
pluginTokenRequestedDescription: "このプラグインはここで設定した権限を使えるようになるで。" pluginTokenRequestedDescription: "このプラグインはここで設定した権限を使えるようになるで。"
notificationType: "通知の種類" notificationType: "通知の種類"
edit: "編集" edit: "編集"
useStarForReactionFallback: "リアクションがようわからん場合、★を使う"
emailServer: "メールサーバー" emailServer: "メールサーバー"
enableEmail: "メール配信を受け取る" enableEmail: "メール配信を受け取る"
emailConfigInfo: "メールアドレスの確認とかパスワードリセットの時に使うで" emailConfigInfo: "メールアドレスの確認とかパスワードリセットの時に使うで"
@ -600,12 +606,12 @@ smtpUser: "ユーザー名"
smtpPass: "パスワード" smtpPass: "パスワード"
emptyToDisableSmtpAuth: "ユーザー名とパスワードになんも入れんかったら、SMTP認証を無効化するで" emptyToDisableSmtpAuth: "ユーザー名とパスワードになんも入れんかったら、SMTP認証を無効化するで"
smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する" smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する"
smtpSecureInfo: "STARTTLS使っとる時はオフにするで。" smtpSecureInfo: "STARTTLS使っとる時はオフにしてや。"
testEmail: "配信テスト" testEmail: "配信テスト"
wordMute: "ワードミュート" wordMute: "ワードミュート"
regexpError: "正規表現エラー" regexpError: "正規表現エラー"
regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが出てきたで:" regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが出てきたで:"
instanceMute: "インスタンスミュート" instanceMute: "サーバーミュート"
userSaysSomething: "{name}が何か言うとるわ" userSaysSomething: "{name}が何か言うとるわ"
makeActive: "使うで" makeActive: "使うで"
display: "表示" display: "表示"
@ -624,7 +630,7 @@ useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使
other: "その他" other: "その他"
regenerateLoginToken: "ログイントークンを再生成" regenerateLoginToken: "ログイントークンを再生成"
regenerateLoginTokenDescription: "ログインに使われる内部トークンをもっかい作るで。いつもならこれをやる必要はないで。もっかい作ると、全部のデバイスでログアウトされるで気ぃつけてなー。" regenerateLoginTokenDescription: "ログインに使われる内部トークンをもっかい作るで。いつもならこれをやる必要はないで。もっかい作ると、全部のデバイスでログアウトされるで気ぃつけてなー。"
setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できるで。" setMultipleBySeparatingWithSpace: "スペースで区切って何個でも設定できるで。"
fileIdOrUrl: "ファイルIDかURL" fileIdOrUrl: "ファイルIDかURL"
behavior: "動作" behavior: "動作"
sample: "サンプル" sample: "サンプル"
@ -636,7 +642,7 @@ abuseReported: "無事内容が送信されたみたいやで。おおきに〜
reporter: "通報者" reporter: "通報者"
reporteeOrigin: "通報先" reporteeOrigin: "通報先"
reporterOrigin: "通報元" reporterOrigin: "通報元"
forwardReport: "リモートインスタンスに通報を転送するで" forwardReport: "リモートサーバーに通報を転送するで"
forwardReportIsAnonymous: "リモートインスタンスからはあんたの情報は見れへんくって、匿名のシステムアカウントとして表示されるで。" forwardReportIsAnonymous: "リモートインスタンスからはあんたの情報は見れへんくって、匿名のシステムアカウントとして表示されるで。"
send: "送信" send: "送信"
abuseMarkAsResolved: "対応したで" abuseMarkAsResolved: "対応したで"
@ -644,7 +650,7 @@ openInNewTab: "新しいタブで開く"
openInSideView: "サイドビューで開く" openInSideView: "サイドビューで開く"
defaultNavigationBehaviour: "デフォルトのナビゲーション" defaultNavigationBehaviour: "デフォルトのナビゲーション"
editTheseSettingsMayBreakAccount: "このへんの設定をようわからんままイジるとアカウントが壊れて使えんくなるかも知れへんで?" editTheseSettingsMayBreakAccount: "このへんの設定をようわからんままイジるとアカウントが壊れて使えんくなるかも知れへんで?"
instanceTicker: "ノートのインスタンス情報" instanceTicker: "ノートのサーバー情報"
waitingFor: "{x}を待っとるで" waitingFor: "{x}を待っとるで"
random: "ランダム" random: "ランダム"
system: "システム" system: "システム"
@ -655,7 +661,7 @@ createNew: "新しく作るで"
optional: "任意" optional: "任意"
createNewClip: "新しいクリップを作るで" createNewClip: "新しいクリップを作るで"
unclip: "クリップ解除するで" unclip: "クリップ解除するで"
confirmToUnclipAlreadyClippedNote: "このノートはすでにクリップ「{name}」に含まれとるで。ノートをこのクリップから除外したる" confirmToUnclipAlreadyClippedNote: "このノートはすでにクリップ「{name}」に含まれとるで。ノートをこのクリップから除外しよか"
public: "パブリック" public: "パブリック"
i18nInfo: "Misskeyは有志によっていろんな言語に翻訳されとるで。{link}で翻訳に協力したってやー。" i18nInfo: "Misskeyは有志によっていろんな言語に翻訳されとるで。{link}で翻訳に協力したってやー。"
manageAccessTokens: "アクセストークンの管理" manageAccessTokens: "アクセストークンの管理"
@ -672,15 +678,15 @@ receivedReactionsCount: "リアクションされた数"
pollVotesCount: "アンケートに投票した数" pollVotesCount: "アンケートに投票した数"
pollVotedCount: "アンケートに投票された数" pollVotedCount: "アンケートに投票された数"
yes: "ええで" yes: "ええで"
no: "あかん" no: "あかん"
driveFilesCount: "ドライブのファイル数" driveFilesCount: "ドライブのファイル数"
driveUsage: "ドライブ使用量やで" driveUsage: "ドライブ使用量やで"
noCrawle: "クローラーによるインデックスを拒否するで" noCrawle: "クローラーによるインデックスを拒否するで"
noCrawleDescription: "検索エンジンにあんたのユーザーページ、ート、Pagesとかのコンテンツを登録(インデックス)せぇへんように頼むで。" noCrawleDescription: "検索エンジンにあんたのユーザーページ、ート、Pagesとかのコンテンツを登録(インデックス)せんように頼むで。邪魔すんねんやったら帰って〜。"
lockedAccountInfo: "フォローを承認制にしとっても、ノートの公開範囲を「フォロワー」にせぇへん限り、誰でもあんたのノートを見れるで。" lockedAccountInfo: "フォローを承認制にしとっても、ノートの公開範囲を「フォロワー」にせぇへん限り、誰でもあんたのノートを見れるで。"
alwaysMarkSensitive: "デフォルトでメディアを閲覧注意にするで" alwaysMarkSensitive: "デフォルトでメディアを閲覧注意にするで"
loadRawImages: "添付画像のサムネイルをオリジナル画質にするで" loadRawImages: "添付画像のサムネイルをオリジナル画質にするで"
disableShowingAnimatedImages: "アニメーション画像を再生しやへんで" disableShowingAnimatedImages: "アニメーション画像を再生せんとくで"
verificationEmailSent: "無事確認のメールを送れたで。メールに書いてあるリンクにアクセスして、設定を完了してなー。" verificationEmailSent: "無事確認のメールを送れたで。メールに書いてあるリンクにアクセスして、設定を完了してなー。"
notSet: "未設定" notSet: "未設定"
emailVerified: "メールアドレスは確認されたで" emailVerified: "メールアドレスは確認されたで"
@ -690,14 +696,14 @@ pageLikedCount: "Pageにええやんと思ってくれた数"
contact: "連絡先" contact: "連絡先"
useSystemFont: "システムのデフォルトのフォントを使うで" useSystemFont: "システムのデフォルトのフォントを使うで"
clips: "クリップ" clips: "クリップ"
experimentalFeatures: "実験的機能やで" experimentalFeatures: "おためし機能やで"
developer: "開発者やで" developer: "開発者やで"
makeExplorable: "アカウントを見つけやすくするで" makeExplorable: "アカウントを見つけやすくするで"
makeExplorableDescription: "オフにすると、「みつける」にアカウントが載らんくなるで。" makeExplorableDescription: "オフにすると、「みつける」にアカウントが載らんくなるで。"
showGapBetweenNotesInTimeline: "タイムラインのノートを離して表示するで" showGapBetweenNotesInTimeline: "タイムラインのノートを離して表示するで"
duplicate: "複製" duplicate: "複製"
left: "左" left: "左"
center: "" center: "真ん中"
wide: "広い" wide: "広い"
narrow: "狭い" narrow: "狭い"
reloadToApplySetting: "設定はページリロード後に反映されるで。今リロードしとくか?" reloadToApplySetting: "設定はページリロード後に反映されるで。今リロードしとくか?"
@ -708,7 +714,7 @@ onlineUsersCount: "{n}人が起きとるで"
nUsers: "{n}ユーザー" nUsers: "{n}ユーザー"
nNotes: "{n}ノート" nNotes: "{n}ノート"
sendErrorReports: "エラーリポートを送る" sendErrorReports: "エラーリポートを送る"
sendErrorReportsDescription: "オンにしたら、なんか変なことが起きたときにエラーの詳細がMisskeyに共有されて、ソフトウェアの品質向上に役立てられるんや。エラー情報には、OSのバージョン、ブラウザの種類、行動履歴などが含まれるで。" sendErrorReportsDescription: "オンにしたら、変なことが起きたときにエラーの詳細がMisskeyに送られて、ソフトウェアの品質向上に使えるようになるで。エラー情報には、OSのバージョン、ブラウザの種類、行動履歴なんかが含まれるで。"
myTheme: "マイテーマ" myTheme: "マイテーマ"
backgroundColor: "背景" backgroundColor: "背景"
accentColor: "アクセント" accentColor: "アクセント"
@ -877,7 +883,7 @@ isSystemAccount: "システムが自動で作成・管理しとるアカウン
typeToConfirm: "この操作をやるんなら {x} と入力してなー" typeToConfirm: "この操作をやるんなら {x} と入力してなー"
deleteAccount: "アカウント削除するで" deleteAccount: "アカウント削除するで"
document: "ドキュメント" document: "ドキュメント"
numberOfPageCache: "ページキャッシュ数やで" numberOfPageCache: "ページ、どんだけキャッシュすんの?"
numberOfPageCacheDescription: "増やすと使いやすくなる、負荷とメモリ使用量が増えてくで。一長一短やな。" numberOfPageCacheDescription: "増やすと使いやすくなる、負荷とメモリ使用量が増えてくで。一長一短やな。"
logoutConfirm: "ログアウトしまっか?" logoutConfirm: "ログアウトしまっか?"
lastActiveDate: "最後に使った日時" lastActiveDate: "最後に使った日時"
@ -947,7 +953,7 @@ thisPostMayBeAnnoying: "この投稿は迷惑かもしらんで。"
thisPostMayBeAnnoyingHome: "ホームに投稿" thisPostMayBeAnnoyingHome: "ホームに投稿"
thisPostMayBeAnnoyingCancel: "やめとく" thisPostMayBeAnnoyingCancel: "やめとく"
thisPostMayBeAnnoyingIgnore: "このまま投稿" thisPostMayBeAnnoyingIgnore: "このまま投稿"
collapseRenotes: "見たことあるRenoteは省略やで" collapseRenotes: "見たことあるRenoteは飛ばして表示するで"
internalServerError: "サーバー内部エラー" internalServerError: "サーバー内部エラー"
internalServerErrorDescription: "サーバー内部でよう分からんエラーやわ" internalServerErrorDescription: "サーバー内部でよう分からんエラーやわ"
copyErrorInfo: "エラー情報をコピー" copyErrorInfo: "エラー情報をコピー"
@ -959,6 +965,24 @@ invitationRequiredToRegister: "今このサーバー招待制になってもう
emailNotSupported: "このサーバーはメール配信がサポートされてへんみたいやわ" emailNotSupported: "このサーバーはメール配信がサポートされてへんみたいやわ"
postToTheChannel: "チャンネルに投稿" postToTheChannel: "チャンネルに投稿"
cannotBeChangedLater: "後からは変えられへんで。" cannotBeChangedLater: "後からは変えられへんで。"
reactionAcceptance: "リアクションの受け入れ"
likeOnly: "いいねだけ"
likeOnlyForRemote: "リモートからはいいねだけな"
rolesAssignedToMe: "自分に割り当てられたロール"
resetPasswordConfirm: "パスワード作り直すんでええな?"
sensitiveWords: "けったいな単語"
sensitiveWordsDescription: "設定した単語が入っとるノートの公開範囲をホームにしたるわ。改行で区切ったら複数設定できるで。"
notesSearchNotAvailable: "ノート検索は使われへんで。"
license: "ライセンス"
unfavoriteConfirm: "ほんまに気に入らんの?"
myClips: "自分のクリップ"
drivecleaner: "ドライブキレイキレイ"
retryAllQueuesNow: "キューを全部もっかいやり直す"
retryAllQueuesConfirmTitle: "もっかいやってみるか?"
retryAllQueuesConfirmText: "一時的にサーバー重なるかもしれへんで。"
enableChartsForRemoteUser: "リモートユーザーのチャートを作る"
enableChartsForFederatedInstances: "リモートサーバーのチャートを作る"
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
_achievements: _achievements:
earnedAt: "貰った日ぃ" earnedAt: "貰った日ぃ"
_types: _types:
@ -976,7 +1000,7 @@ _achievements:
title: "ノートの生駒山" title: "ノートの生駒山"
description: "ートを500回投稿した" description: "ートを500回投稿した"
_notes1000: _notes1000:
title: "ノートの山" title: "ノートの六甲山"
description: "ートを1,000回投稿した" description: "ートを1,000回投稿した"
_notes5000: _notes5000:
title: "箕面の滝からノート" title: "箕面の滝からノート"
@ -1218,6 +1242,8 @@ _role:
iconUrl: "アイコン画像のURL" iconUrl: "アイコン画像のURL"
asBadge: "バッジとして見せる" asBadge: "バッジとして見せる"
descriptionOfAsBadge: "オンにすると、ユーザー名の横んとこにロールのアイコンが表示されるで。" descriptionOfAsBadge: "オンにすると、ユーザー名の横んとこにロールのアイコンが表示されるで。"
displayOrder: "表示順"
descriptionOfDisplayOrder: "数がでかいほど、UI上で先に表示されるで。"
canEditMembersByModerator: "モデレーターのメンバー編集を許可" canEditMembersByModerator: "モデレーターのメンバー編集を許可"
descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになるで。オフにすると管理者のみが行えるで。" descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになるで。オフにすると管理者のみが行えるで。"
priority: "優先度" priority: "優先度"
@ -1243,6 +1269,7 @@ _role:
rateLimitFactor: "レートリミット" rateLimitFactor: "レートリミット"
descriptionOfRateLimitFactor: "ちっちゃいほど制限が緩くなって、大きいほど制限されるで。" descriptionOfRateLimitFactor: "ちっちゃいほど制限が緩くなって、大きいほど制限されるで。"
canHideAds: "広告を表示させへん" canHideAds: "広告を表示させへん"
canSearchNotes: "ノート検索を使わすかどうか"
_condition: _condition:
isLocal: "ローカルユーザー" isLocal: "ローカルユーザー"
isRemote: "リモートユーザー" isRemote: "リモートユーザー"
@ -1252,6 +1279,8 @@ _role:
followersMoreThanOrEq: "フォロワー数が~以上" followersMoreThanOrEq: "フォロワー数が~以上"
followingLessThanOrEq: "フォロー数が~以下" followingLessThanOrEq: "フォロー数が~以下"
followingMoreThanOrEq: "フォロー数が~以上" followingMoreThanOrEq: "フォロー数が~以上"
notesLessThanOrEq: "投稿数が~以下しかない"
notesMoreThanOrEq: "投稿を~以上しとる"
and: "~かつ~" and: "~かつ~"
or: "~または~" or: "~または~"
not: "~ではない" not: "~ではない"
@ -1377,7 +1406,7 @@ _wordMute:
mutedNotes: "ミュートされたノート" mutedNotes: "ミュートされたノート"
_instanceMute: _instanceMute:
instanceMuteDescription: "ミュートしたインスタンスのユーザーへの返信を含めて、設定したインスタンスの全てのートとRenoteをミュートにするで。" instanceMuteDescription: "ミュートしたインスタンスのユーザーへの返信を含めて、設定したインスタンスの全てのートとRenoteをミュートにするで。"
instanceMuteDescription2: "改行で区切って設定するで" instanceMuteDescription2: "改行で区切って設定するんやで"
title: "設定したインスタンスのノートを隠すで。" title: "設定したインスタンスのノートを隠すで。"
heading: "ミュートするインスタンス" heading: "ミュートするインスタンス"
_theme: _theme:
@ -1464,7 +1493,7 @@ _sfx:
channel: "チャンネル通知" channel: "チャンネル通知"
_ago: _ago:
future: "未来" future: "未来"
justNow: "たった今" justNow: "ついさっき"
secondsAgo: "{n}秒前" secondsAgo: "{n}秒前"
minutesAgo: "{n}分前" minutesAgo: "{n}分前"
hoursAgo: "{n}時間前" hoursAgo: "{n}時間前"
@ -1587,7 +1616,7 @@ _weekday:
saturday: "土曜日" saturday: "土曜日"
_widgets: _widgets:
profile: "プロフィール" profile: "プロフィール"
instanceInfo: "インスタンス情報" instanceInfo: "サーバー情報"
memo: "付箋" memo: "付箋"
notifications: "通知" notifications: "通知"
timeline: "タイムライン" timeline: "タイムライン"
@ -1690,7 +1719,7 @@ _charts:
apRequest: "リクエスト" apRequest: "リクエスト"
usersIncDec: "ユーザーの増減" usersIncDec: "ユーザーの増減"
usersTotal: "ユーザーの合計" usersTotal: "ユーザーの合計"
activeUsers: "アクティブユーザー数" activeUsers: "いまおるユーザー数"
notesIncDec: "ノートの増減" notesIncDec: "ノートの増減"
localNotesIncDec: "ローカルのノートの増減" localNotesIncDec: "ローカルのノートの増減"
remoteNotesIncDec: "リモートのノートの増減" remoteNotesIncDec: "リモートのノートの増減"
@ -1844,3 +1873,24 @@ _deck:
_dialog: _dialog:
charactersExceeded: "最大の文字数を上回っとるで!今は {current} / 最大でも {max}" charactersExceeded: "最大の文字数を上回っとるで!今は {current} / 最大でも {max}"
charactersBelow: "最小の文字数を下回っとるで!今は {current} / 最低でも {min}" charactersBelow: "最小の文字数を下回っとるで!今は {current} / 最低でも {min}"
_disabledTimeline:
title: "使われへんタイムライン"
description: "あんたの今のロールやったら、このタイムラインは使われへんで。"
_drivecleaner:
orderBySizeDesc: "サイズのでかい順"
orderByCreatedAtAsc: "追加日の古い順"
_webhookSettings:
createWebhook: "Webhookをつくる"
name: "名前"
secret: "シークレット"
events: "Webhookを投げるタイミング"
active: "有効"
_events:
follow: "フォローしたとき~!"
followed: "フォローもらったとき~!"
note: "ノートを投稿したとき~!"
reply: "返信があるとき~!"
renote: "Renoteされるとき"
reaction: "リアクションがあるとき~!"
mention: "メンションがあるとき~!"

View File

@ -1 +1,2 @@
--- ---

View File

@ -103,3 +103,4 @@ _deck:
_columns: _columns:
notifications: "Ilɣuyen" notifications: "Ilɣuyen"
list: "Tibdarin" list: "Tibdarin"

View File

@ -83,3 +83,4 @@ _deck:
notifications: "ಅಧಿಸೂಚನೆಗಳು" notifications: "ಅಧಿಸೂಚನೆಗಳು"
tl: "ಸಮಯಸಾಲು" tl: "ಸಮಯಸಾಲು"
mentions: "ಹೆಸರಿಸಿದ" mentions: "ಹೆಸರಿಸಿದ"

View File

@ -122,6 +122,8 @@ unmarkAsSensitive: "열람주의 해제"
enterFileName: "파일명을 입력" enterFileName: "파일명을 입력"
mute: "뮤트" mute: "뮤트"
unmute: "뮤트 해제" unmute: "뮤트 해제"
renoteMute: "리노트를 뮤트"
renoteUnmute: "리노트 뮤트 해제"
block: "차단" block: "차단"
unblock: "차단 해제" unblock: "차단 해제"
suspend: "정지" suspend: "정지"
@ -153,6 +155,7 @@ flagShowTimelineReplies: "타임라인에 노트의 답글을 표시하기"
flagShowTimelineRepliesDescription: "이 설정을 활성화하면 타임라인에 다른 유저 간의 답글을 표시합니다." flagShowTimelineRepliesDescription: "이 설정을 활성화하면 타임라인에 다른 유저 간의 답글을 표시합니다."
autoAcceptFollowed: "팔로우 중인 유저로부터의 팔로우 요청을 자동 수락" autoAcceptFollowed: "팔로우 중인 유저로부터의 팔로우 요청을 자동 수락"
addAccount: "계정 추가" addAccount: "계정 추가"
reloadAccountsList: "계정 리스트 정보 갱신"
loginFailed: "로그인에 실패했습니다" loginFailed: "로그인에 실패했습니다"
showOnRemote: "리모트에서 보기" showOnRemote: "리모트에서 보기"
general: "일반" general: "일반"
@ -506,6 +509,7 @@ objectStorageSetPublicRead: "업로드할 때 'public-read'를 설정하기"
serverLogs: "서버 로그" serverLogs: "서버 로그"
deleteAll: "모두 삭제" deleteAll: "모두 삭제"
showFixedPostForm: "타임라인 상단에 글 작성란을 표시" showFixedPostForm: "타임라인 상단에 글 작성란을 표시"
showFixedPostFormInChannel: "채널 타임라인 상단에 글 작성란을 표시"
newNoteRecived: "새 노트가 있습니다" newNoteRecived: "새 노트가 있습니다"
sounds: "소리" sounds: "소리"
sound: "소리" sound: "소리"
@ -543,6 +547,8 @@ userSuspended: "이 계정은 정지된 상태입니다."
userSilenced: "이 계정은 사일런스된 상태입니다." userSilenced: "이 계정은 사일런스된 상태입니다."
yourAccountSuspendedTitle: "계정이 정지되었습니다" yourAccountSuspendedTitle: "계정이 정지되었습니다"
yourAccountSuspendedDescription: "이 계정은 서버의 이용 약관을 위반하거나, 기타 다른 이유로 인해 정지되었습니다. 자세한 사항은 관리자에게 문의해 주십시오. 계정을 새로 생성하지 마십시오." yourAccountSuspendedDescription: "이 계정은 서버의 이용 약관을 위반하거나, 기타 다른 이유로 인해 정지되었습니다. 자세한 사항은 관리자에게 문의해 주십시오. 계정을 새로 생성하지 마십시오."
accountDeleted: "계정이 정지되었습니다"
accountDeletedDescription: "이 계정이 삭제되었습니다."
menu: "메뉴" menu: "메뉴"
divider: "구분선" divider: "구분선"
addItem: "항목 추가" addItem: "항목 추가"
@ -586,7 +592,6 @@ tokenRequested: "계정 접근 허용"
pluginTokenRequestedDescription: "이 플러그인은 여기서 설정한 권한을 사용할 수 있게 됩니다." pluginTokenRequestedDescription: "이 플러그인은 여기서 설정한 권한을 사용할 수 있게 됩니다."
notificationType: "알림 유형" notificationType: "알림 유형"
edit: "편집" edit: "편집"
useStarForReactionFallback: "알 수 없는 리액션 이모지 대신 ★ 사용"
emailServer: "메일 서버" emailServer: "메일 서버"
enableEmail: "이메일 송신 기능 활성화" enableEmail: "이메일 송신 기능 활성화"
emailConfigInfo: "가입 시 메일 주소 확인이나 비밀번호 초기화 시에 사용합니다." emailConfigInfo: "가입 시 메일 주소 확인이나 비밀번호 초기화 시에 사용합니다."
@ -955,6 +960,9 @@ exploreOtherServers: "다른 서버 둘러보기"
letsLookAtTimeline: "타임라인 구경하기" letsLookAtTimeline: "타임라인 구경하기"
disableFederationWarn: "연합이 비활성화됩니다. 비활성화해도 게시물이 비공개가 되지는 않습니다. 대부분의 경우 이 옵션을 활성화할 필요가 없습니다." disableFederationWarn: "연합이 비활성화됩니다. 비활성화해도 게시물이 비공개가 되지는 않습니다. 대부분의 경우 이 옵션을 활성화할 필요가 없습니다."
invitationRequiredToRegister: "현재 이 서버는 비공개입니다. 회원가입을 하시려면 초대 코드가 필요합니다." invitationRequiredToRegister: "현재 이 서버는 비공개입니다. 회원가입을 하시려면 초대 코드가 필요합니다."
emailNotSupported: "이 서버에서는 메일 전송을 지원하지 않습니다"
postToTheChannel: "채널에 게시하기"
cannotBeChangedLater: "나중에 변경할 수 없습니다."
_achievements: _achievements:
earnedAt: "달성 일시" earnedAt: "달성 일시"
_types: _types:
@ -1840,3 +1848,7 @@ _deck:
_dialog: _dialog:
charactersExceeded: "최대 글자수를 초과하였습니다! 현재 {current} / 최대 {min}" charactersExceeded: "최대 글자수를 초과하였습니다! 현재 {current} / 최대 {min}"
charactersBelow: "최소 글자수 미만입니다! 현재 {current} / 최소 {min}" charactersBelow: "최소 글자수 미만입니다! 현재 {current} / 최소 {min}"
_webhookSettings:
name: "이름"
active: "활성화"

View File

@ -368,3 +368,4 @@ _deck:
list: "ລາຍການ" list: "ລາຍການ"
channel: "ຊ່ອງ" channel: "ຊ່ອງ"
mentions: "ກ່າວເຖິງ" mentions: "ກ່າວເຖິງ"

View File

@ -483,3 +483,6 @@ _deck:
antenna: "Antennes" antenna: "Antennes"
list: "Lijsten" list: "Lijsten"
mentions: "Vermeldingen" mentions: "Vermeldingen"
_webhookSettings:
name: "Naam"

View File

@ -1,2 +1,3 @@
--- ---
_lang_: "Norsk Bokmål" _lang_: "Norsk Bokmål"

View File

@ -129,6 +129,7 @@ unblockConfirm: "Czy na pewno chcesz odblokować to konto?"
suspendConfirm: "Czy na pewno chcesz zawiesić to konto?" suspendConfirm: "Czy na pewno chcesz zawiesić to konto?"
unsuspendConfirm: "Czy na pewno chcesz cofnąć zawieszenie tego konta?" unsuspendConfirm: "Czy na pewno chcesz cofnąć zawieszenie tego konta?"
selectList: "Wybierz listę" selectList: "Wybierz listę"
selectChannel: "Wybierz kanał"
selectAntenna: "Wybierz Antennę" selectAntenna: "Wybierz Antennę"
selectWidget: "Wybierz widżet" selectWidget: "Wybierz widżet"
editWidgets: "Edytuj widżety" editWidgets: "Edytuj widżety"
@ -149,6 +150,7 @@ flagAsCatDescription: "Przełącz tę opcję, aby konto było oznaczone jako kot
flagShowTimelineReplies: "Pokazuj odpowiedzi na osi czasu" flagShowTimelineReplies: "Pokazuj odpowiedzi na osi czasu"
autoAcceptFollowed: "Automatycznie przyjmuj prośby o możliwość obserwacji od użytkowników, których obserwujesz" autoAcceptFollowed: "Automatycznie przyjmuj prośby o możliwość obserwacji od użytkowników, których obserwujesz"
addAccount: "Dodaj konto" addAccount: "Dodaj konto"
reloadAccountsList: "Odśwież listę kont"
loginFailed: "Nie udało się zalogować" loginFailed: "Nie udało się zalogować"
showOnRemote: "Zobacz na zdalnej instancji" showOnRemote: "Zobacz na zdalnej instancji"
general: "Ogólne" general: "Ogólne"
@ -159,6 +161,7 @@ searchWith: "Szukaj: {q}"
youHaveNoLists: "Nie masz żadnej listy" youHaveNoLists: "Nie masz żadnej listy"
followConfirm: "Czy na pewno chcesz zaobserwować {name}?" followConfirm: "Czy na pewno chcesz zaobserwować {name}?"
proxyAccount: "Konto proxy" proxyAccount: "Konto proxy"
proxyAccountDescription: "Opis konta pełnomocniczego"
host: "Host" host: "Host"
selectUser: "Wybierz użytkownika" selectUser: "Wybierz użytkownika"
recipient: "Odbiorca" recipient: "Odbiorca"
@ -253,6 +256,7 @@ noMoreHistory: "Nie ma dalszej historii"
startMessaging: "Rozpocznij czat" startMessaging: "Rozpocznij czat"
nUsersRead: "przeczytano przez {n}" nUsersRead: "przeczytano przez {n}"
agreeTo: "Wyrażam zgodę na {0}" agreeTo: "Wyrażam zgodę na {0}"
agreeBelow: "Zaakceptuj poniżej"
tos: "Regulamin" tos: "Regulamin"
start: "Rozpocznij" start: "Rozpocznij"
home: "Strona główna" home: "Strona główna"
@ -385,13 +389,19 @@ about: "Informacje"
aboutMisskey: "O Misskey" aboutMisskey: "O Misskey"
administrator: "Admin" administrator: "Admin"
token: "Token" token: "Token"
2fa: "Klucz 2FA "
totp: "Klucz aplikacji uwierzytelniającej (totp)"
totpDescription: "Opis klucza czasowego"
moderator: "Moderator" moderator: "Moderator"
moderation: "Moderacja" moderation: "Moderacja"
nUsersMentioned: "{n} wspomnianych użytkowników" nUsersMentioned: "{n} wspomnianych użytkowników"
securityKeyAndPasskey: "Klucz bezpieczeństwa i klucze Passkey"
securityKey: "Klucz bezpieczeństwa" securityKey: "Klucz bezpieczeństwa"
lastUsed: "Ostatnio używane" lastUsed: "Ostatnio używane"
lastUsedAt: "Ostatnio używane w"
unregister: "Cofnij rejestrację" unregister: "Cofnij rejestrację"
passwordLessLogin: "Skonfiguruj logowanie bez użycia hasła" passwordLessLogin: "Skonfiguruj logowanie bez użycia hasła"
passwordLessLoginDescription: "Opis logowania bez użycia hasła"
resetPassword: "Zresetuj hasło" resetPassword: "Zresetuj hasło"
newPasswordIs: "Nowe hasło to „{password}”" newPasswordIs: "Nowe hasło to „{password}”"
reduceUiAnimation: "Ogranicz animacje w UI" reduceUiAnimation: "Ogranicz animacje w UI"
@ -518,11 +528,16 @@ disablePagesScript: "Wyłącz AiScript na Stronach"
updateRemoteUser: "Aktualizuj zdalne dane o użytkowniku" updateRemoteUser: "Aktualizuj zdalne dane o użytkowniku"
deleteAllFiles: "Usuń wszystkie pliki" deleteAllFiles: "Usuń wszystkie pliki"
deleteAllFilesConfirm: "Czy na pewno chcesz usunąć wszystkie pliki?" deleteAllFilesConfirm: "Czy na pewno chcesz usunąć wszystkie pliki?"
removeAllFollowing: "Przestań obserwować"
removeAllFollowingDescription: "Przestań obserwować wszystkie konta z {host}. Wykonaj to, jeżeli instancja już nie istnieje." removeAllFollowingDescription: "Przestań obserwować wszystkie konta z {host}. Wykonaj to, jeżeli instancja już nie istnieje."
userSuspended: "To konto zostało zawieszone." userSuspended: "To konto zostało zawieszone."
userSilenced: "Ten użytkownik został wyciszony." userSilenced: "Ten użytkownik został wyciszony."
yourAccountSuspendedTitle: "To konto jest zawieszone" yourAccountSuspendedTitle: "To konto jest zawieszone"
yourAccountSuspendedDescription: "To konto zostało zawieszone z powodu złamania regulaminu serwera lub innych podobnych. Skontaktuj się z administratorem, jeśli chciałbyś poznać bardziej szczegółowy powód. Proszę nie zakładać nowego konta." yourAccountSuspendedDescription: "To konto zostało zawieszone z powodu złamania regulaminu serwera lub innych podobnych. Skontaktuj się z administratorem, jeśli chciałbyś poznać bardziej szczegółowy powód. Proszę nie zakładać nowego konta."
tokenRevoked: "Token odrzucony"
tokenRevokedDescription: "Opis odrzuconego tokena"
accountDeleted: "Konto usunięte"
accountDeletedDescription: "Opis konta usuniętego"
menu: "Menu" menu: "Menu"
divider: "Rozdzielacz" divider: "Rozdzielacz"
addItem: "Dodaj element" addItem: "Dodaj element"
@ -548,7 +563,9 @@ author: "Autor"
leaveConfirm: "Są niezapisane zmiany. Czy chcesz je odrzucić?" leaveConfirm: "Są niezapisane zmiany. Czy chcesz je odrzucić?"
manage: "Zarządzanie" manage: "Zarządzanie"
plugins: "Wtyczki" plugins: "Wtyczki"
preferencesBackups: "Kopia zapasowa ustawień"
deck: "Tablica" deck: "Tablica"
undeck: "oddkouj"
useBlurEffectForModal: "Używaj efektu rozmycia w modalach" useBlurEffectForModal: "Używaj efektu rozmycia w modalach"
useFullReactionPicker: "Używaj pełnowymiarowego wybornika reakcji" useFullReactionPicker: "Używaj pełnowymiarowego wybornika reakcji"
width: "Szerokość" width: "Szerokość"
@ -564,7 +581,6 @@ tokenRequested: "Przydziel dostęp do konta"
pluginTokenRequestedDescription: "Ta wtyczka będzie mogła korzystać z ustawionych tu uprawnień." pluginTokenRequestedDescription: "Ta wtyczka będzie mogła korzystać z ustawionych tu uprawnień."
notificationType: "Rodzaj powiadomień" notificationType: "Rodzaj powiadomień"
edit: "Edytuj" edit: "Edytuj"
useStarForReactionFallback: "Użyj ★ jako zapasowego emoji, gdy emoji reakcji jest nieznane"
emailServer: "Serwer poczty e-mail" emailServer: "Serwer poczty e-mail"
enableEmail: "Włącz dostarczanie wiadomości e-mail" enableEmail: "Włącz dostarczanie wiadomości e-mail"
emailConfigInfo: "Wykorzystywany do potwierdzenia adresu e-mail w trakcie rejestracji, lub gdy zapomnisz hasła" emailConfigInfo: "Wykorzystywany do potwierdzenia adresu e-mail w trakcie rejestracji, lub gdy zapomnisz hasła"
@ -816,6 +832,8 @@ tenMinutes: "10 minut"
oneHour: "1 godzina" oneHour: "1 godzina"
oneDay: "1 dzień" oneDay: "1 dzień"
oneWeek: "1 tydzień" oneWeek: "1 tydzień"
oneMonth: "jeden miesiąc"
failedToFetchAccountInformation: "Nie udało się uzyskać informacji o koncie"
file: "Pliki" file: "Pliki"
recommended: "Zalecane" recommended: "Zalecane"
check: "Zweryfikuj" check: "Zweryfikuj"
@ -1358,3 +1376,7 @@ _deck:
channel: "Kanały" channel: "Kanały"
mentions: "Wspomnienia" mentions: "Wspomnienia"
direct: "Bezpośredni" direct: "Bezpośredni"
_webhookSettings:
name: "Nazwa"
active: "Właczono"

View File

@ -555,3 +555,6 @@ _deck:
list: "Listas" list: "Listas"
mentions: "Menções" mentions: "Menções"
direct: "Notas diretas" direct: "Notas diretas"
_webhookSettings:
name: "Nome"

View File

@ -561,7 +561,6 @@ tokenRequested: "Acordă acces la cont"
pluginTokenRequestedDescription: "Acest plugin va putea să folosească permisiunile setate aici." pluginTokenRequestedDescription: "Acest plugin va putea să folosească permisiunile setate aici."
notificationType: "Tipul notificării" notificationType: "Tipul notificării"
edit: "Editează" edit: "Editează"
useStarForReactionFallback: "Folosește ★ ca fallback dacă emoji-ul este necunoscut"
emailServer: "Server email" emailServer: "Server email"
enableEmail: "Activează distribuția de emailuri" enableEmail: "Activează distribuția de emailuri"
emailConfigInfo: "Folosit pentru a confirma emailul tău în timpul logări dacă îți uiți parola" emailConfigInfo: "Folosit pentru a confirma emailul tău în timpul logări dacă îți uiți parola"
@ -702,3 +701,6 @@ _deck:
list: "Liste" list: "Liste"
channel: "Canale" channel: "Canale"
mentions: "Mențiuni" mentions: "Mențiuni"
_webhookSettings:
name: "Nume"

View File

@ -585,7 +585,6 @@ tokenRequested: "Открыть доступ к учётной записи"
pluginTokenRequestedDescription: "Это расширение сможет пользоваться разрешениями, установленными здесь." pluginTokenRequestedDescription: "Это расширение сможет пользоваться разрешениями, установленными здесь."
notificationType: "Тип уведомления" notificationType: "Тип уведомления"
edit: "Изменить" edit: "Изменить"
useStarForReactionFallback: "Ставить ★ в качестве реакции вместо неизвестного эмодзи"
emailServer: "Сервер электронной почты" emailServer: "Сервер электронной почты"
enableEmail: "Включить обмен электронной почтой" enableEmail: "Включить обмен электронной почтой"
emailConfigInfo: "Используется для подтверждения адреса электронной почты и сброса пароля." emailConfigInfo: "Используется для подтверждения адреса электронной почты и сброса пароля."
@ -1837,3 +1836,7 @@ _deck:
_dialog: _dialog:
charactersExceeded: "Превышено максимальное количество символов! У вас {current} / из {max}" charactersExceeded: "Превышено максимальное количество символов! У вас {current} / из {max}"
charactersBelow: "Это ниже минимального количества символов! У вас {current} / из {min}" charactersBelow: "Это ниже минимального количества символов! У вас {current} / из {min}"
_webhookSettings:
name: "Название"
active: "Вкл."

View File

@ -1 +1,2 @@
--- ---

View File

@ -586,7 +586,6 @@ tokenRequested: "Povoliť prístup k účtu"
pluginTokenRequestedDescription: "Tento plugin bude môcť používať oprávnenia nastavené tu." pluginTokenRequestedDescription: "Tento plugin bude môcť používať oprávnenia nastavené tu."
notificationType: "Typ oznámenia" notificationType: "Typ oznámenia"
edit: "Upraviť" edit: "Upraviť"
useStarForReactionFallback: "Použiť ★ keď emoji reakcie nie je známe"
emailServer: "Email server" emailServer: "Email server"
enableEmail: "Zapnúť email" enableEmail: "Zapnúť email"
emailConfigInfo: "Používa sa na overenie emaily pri registrácii alebo pri zabudnutí hesla" emailConfigInfo: "Používa sa na overenie emaily pri registrácii alebo pri zabudnutí hesla"
@ -1475,3 +1474,7 @@ _deck:
channel: "Kanály" channel: "Kanály"
mentions: "Zmienky" mentions: "Zmienky"
direct: "Priame poznámky" direct: "Priame poznámky"
_webhookSettings:
name: "Názov"
active: "Zapnuté"

View File

@ -442,3 +442,6 @@ _deck:
antenna: "Antenner" antenna: "Antenner"
list: "Listor" list: "Listor"
mentions: "Omnämningar" mentions: "Omnämningar"
_webhookSettings:
active: "Aktiverad"

View File

@ -544,6 +544,8 @@ userSuspended: "ผู้ใช้รายนี้ถูกระงับก
userSilenced: "ผู้ใช้รายนี้กำลังถูกปิดกั้น" userSilenced: "ผู้ใช้รายนี้กำลังถูกปิดกั้น"
yourAccountSuspendedTitle: "บัญชีนี้นั้นถูกระงับ" yourAccountSuspendedTitle: "บัญชีนี้นั้นถูกระงับ"
yourAccountSuspendedDescription: "บัญชีนี้ถูกระงับ เนื่องจากละเมิดข้อกำหนดในการให้บริการของเซิร์ฟเวอร์หรืออาจจะละเมิดหลักเกณฑ์ชุมชน หรือ อาจจะโดนร้องเรียนเรื่องการละเมิดลิขสิทธิ์และอื่นๆอย่างต่อเนื่องซ้ำๆ หากคุณคิดว่าไม่ได้ทำผิดจริงๆหรือตัดสินผิดพลาด ได้โปรดกรุณาติดต่อผู้ดูแลระบบหากคุณต้องการทราบเหตุผลโดยละเอียดเพิ่มเติม และขอความกรุณาอย่าสร้างบัญชีใหม่" yourAccountSuspendedDescription: "บัญชีนี้ถูกระงับ เนื่องจากละเมิดข้อกำหนดในการให้บริการของเซิร์ฟเวอร์หรืออาจจะละเมิดหลักเกณฑ์ชุมชน หรือ อาจจะโดนร้องเรียนเรื่องการละเมิดลิขสิทธิ์และอื่นๆอย่างต่อเนื่องซ้ำๆ หากคุณคิดว่าไม่ได้ทำผิดจริงๆหรือตัดสินผิดพลาด ได้โปรดกรุณาติดต่อผู้ดูแลระบบหากคุณต้องการทราบเหตุผลโดยละเอียดเพิ่มเติม และขอความกรุณาอย่าสร้างบัญชีใหม่"
tokenRevoked: "โทเค็นไม่ถูกต้อง"
accountDeleted: "ลบบัญชีแล้ว"
menu: "เมนู" menu: "เมนู"
divider: "ตัวแบ่ง" divider: "ตัวแบ่ง"
addItem: "เพิ่มรายการ" addItem: "เพิ่มรายการ"
@ -587,7 +589,6 @@ tokenRequested: "ให้สิทธิ์การเข้าถึงบั
pluginTokenRequestedDescription: "ปลั๊กอินนี้จะสามารถใช้การอนุญาตที่ตั้งค่าไว้ที่นี่นะ" pluginTokenRequestedDescription: "ปลั๊กอินนี้จะสามารถใช้การอนุญาตที่ตั้งค่าไว้ที่นี่นะ"
notificationType: "ประเภทการแจ้งเตือน" notificationType: "ประเภทการแจ้งเตือน"
edit: "แก้ไข" edit: "แก้ไข"
useStarForReactionFallback: "ใช้ ★ เป็นทางเลือกแทนถ้าหากไม่ทราบอิโมจิ"
emailServer: "อีเมล์เซิร์ฟเวอร์" emailServer: "อีเมล์เซิร์ฟเวอร์"
enableEmail: "เปิดใช้งานการกระจายอีเมล" enableEmail: "เปิดใช้งานการกระจายอีเมล"
emailConfigInfo: "ใช้เพื่อยืนยันอีเมลของคุณระหว่างการสมัครหรือถ้าหากคุณลืมรหัสผ่าน" emailConfigInfo: "ใช้เพื่อยืนยันอีเมลของคุณระหว่างการสมัครหรือถ้าหากคุณลืมรหัสผ่าน"
@ -959,6 +960,18 @@ invitationRequiredToRegister: "อินสแตนซ์นี้เป็น
emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมลนะค่ะ" emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมลนะค่ะ"
postToTheChannel: "โพสต์ลงช่อง" postToTheChannel: "โพสต์ลงช่อง"
cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนแปลงได้ในภายหลังนะ" cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนแปลงได้ในภายหลังนะ"
likeOnly: "ที่ชอบเท่านั้น"
resetPasswordConfirm: "รีเซ็ตรหัสผ่านของคุณจริงๆหรอ?"
sensitiveWords: "คำที่ละเอียดอ่อน"
sensitiveWordsDescription: "การเปิดเผยโน้ตทั้งหมดที่มีคำที่กำหนดค่าไว้จะถูกตั้งค่าเป็น \"หน้าแรก\" โดยอัตโนมัติ คุณยังสามารถแสดงหลายรายการได้โดยแยกรายการโดยใช้ตัวแบ่งบรรทัดได้นะ"
notesSearchNotAvailable: "การค้นหาโน้ตไม่พร้อมใช้งานนะค่ะ"
license: "ใบอนุญาต"
unfavoriteConfirm: "ลบออกจากรายการโปรดแน่ใจหรอ?"
myClips: "คลิปของฉัน"
drivecleaner: "ทำความสะอาดไดรฟ์"
retryAllQueuesNow: "ลองเรียกใช้คิวทั้งหมดอีกครั้ง"
retryAllQueuesConfirmTitle: "ลองใหม่ทั้งหมดจริงๆหรอแน่ใจนะ?"
retryAllQueuesConfirmText: "สิ่งนี้จะเพิ่มการโหลดเซิร์ฟเวอร์ชั่วคราวนะ"
_achievements: _achievements:
earnedAt: "ได้รับเมื่อ" earnedAt: "ได้รับเมื่อ"
_types: _types:
@ -1218,6 +1231,8 @@ _role:
iconUrl: "ไอคอน URL" iconUrl: "ไอคอน URL"
asBadge: "แสดงเป็นตรา" asBadge: "แสดงเป็นตรา"
descriptionOfAsBadge: "ไอคอนของบทบาทนี้จะปรากฏถัดจากชื่อผู้ใช้ของผู้ใช้งานด้วยบทบาทนี้ถ้าหากเปิดใช้งาน" descriptionOfAsBadge: "ไอคอนของบทบาทนี้จะปรากฏถัดจากชื่อผู้ใช้ของผู้ใช้งานด้วยบทบาทนี้ถ้าหากเปิดใช้งาน"
displayOrder: "ตำแหน่ง"
descriptionOfDisplayOrder: "ยิ่งตัวเลขสูง ตำแหน่ง UI ก็ยิ่งสูงขึ้นนะ"
canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก" canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก"
descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ" descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ"
priority: "ลำดับความสำคัญ" priority: "ลำดับความสำคัญ"
@ -1243,6 +1258,7 @@ _role:
rateLimitFactor: "ขีดจำกัดอัตรา" rateLimitFactor: "ขีดจำกัดอัตรา"
descriptionOfRateLimitFactor: "ขีดจํากัดอัตราที่ต่ำกว่ามีข้อจํากัดน้อยกว่าข้อจํากัดที่สูงกว่า" descriptionOfRateLimitFactor: "ขีดจํากัดอัตราที่ต่ำกว่ามีข้อจํากัดน้อยกว่าข้อจํากัดที่สูงกว่า"
canHideAds: "ซ่อนโฆษณา" canHideAds: "ซ่อนโฆษณา"
canSearchNotes: "การใช้การค้นหาโน้ต"
_condition: _condition:
isLocal: "ผู้ใช้ภายใน" isLocal: "ผู้ใช้ภายใน"
isRemote: "ผู้ใช้ระยะไกล" isRemote: "ผู้ใช้ระยะไกล"
@ -1844,3 +1860,13 @@ _deck:
_dialog: _dialog:
charactersExceeded: "คุณกำลังมีตัวอักขระเกินขีดจำกัดสูงสุดแล้วนะ! ปัจจุบันอยู่ที่ {current} จาก {max}" charactersExceeded: "คุณกำลังมีตัวอักขระเกินขีดจำกัดสูงสุดแล้วนะ! ปัจจุบันอยู่ที่ {current} จาก {max}"
charactersBelow: "คุณกำลังใช้อักขระต่ำกว่าขีดจำกัดขั้นต่ำเลยนะ! ปัจจุบันอยู่ที่ {current} จาก {min}" charactersBelow: "คุณกำลังใช้อักขระต่ำกว่าขีดจำกัดขั้นต่ำเลยนะ! ปัจจุบันอยู่ที่ {current} จาก {min}"
_disabledTimeline:
title: "ปิดใช้งานไทม์ไลน์"
description: "คุณไม่สามารถใช้ไทม์ไลน์นี้ภายใต้บทบาทปัจจุบันของคุณได้"
_drivecleaner:
orderBySizeDesc: "ขนาดไฟล์จากมากไปหาน้อย"
orderByCreatedAtAsc: "วันที่จากน้อยไปหามาก"
_webhookSettings:
name: "ชื่อ"
active: "เปิดใช้งาน"

View File

@ -60,3 +60,4 @@ _deck:
_columns: _columns:
notifications: "Bildirim" notifications: "Bildirim"
tl: "Zaman çizelgesi" tl: "Zaman çizelgesi"

View File

@ -2,3 +2,4 @@
_lang_: "ياپونچە" _lang_: "ياپونچە"
search: "ئىزدەش" search: "ئىزدەش"
searchByGoogle: "ئىزدەش" searchByGoogle: "ئىزدەش"

View File

@ -576,7 +576,6 @@ tokenRequested: "Надати доступ до акаунту"
pluginTokenRequestedDescription: "Цей плагін зможе використовувати дозволи які тут вказані." pluginTokenRequestedDescription: "Цей плагін зможе використовувати дозволи які тут вказані."
notificationType: "Тип сповіщення" notificationType: "Тип сповіщення"
edit: "Редагувати" edit: "Редагувати"
useStarForReactionFallback: "Використовувати ★ як запасний варіант, якщо емодзі реакції невідомий"
emailServer: "Email сервер" emailServer: "Email сервер"
enableEmail: "Увімкнути функцію доставки пошти" enableEmail: "Увімкнути функцію доставки пошти"
emailConfigInfo: "Використовується для підтвердження електронної пошти підчас реєстрації, а також для відновлення паролю." emailConfigInfo: "Використовується для підтвердження електронної пошти підчас реєстрації, а також для відновлення паролю."
@ -1639,3 +1638,7 @@ _deck:
channel: "Канали" channel: "Канали"
mentions: "Згадки" mentions: "Згадки"
direct: "Особисте" direct: "Особисте"
_webhookSettings:
name: "Ім'я"
active: "Увімкнено"

View File

@ -585,7 +585,6 @@ tokenRequested: "Cấp quyền truy cập vào tài khoản"
pluginTokenRequestedDescription: "Plugin này sẽ có thể sử dụng các quyền được đặt ở đây." pluginTokenRequestedDescription: "Plugin này sẽ có thể sử dụng các quyền được đặt ở đây."
notificationType: "Loại thông báo" notificationType: "Loại thông báo"
edit: "Sửa" edit: "Sửa"
useStarForReactionFallback: "Dùng ★ nếu emoji biểu cảm không có"
emailServer: "Email máy chủ" emailServer: "Email máy chủ"
enableEmail: "Bật phân phối email" enableEmail: "Bật phân phối email"
emailConfigInfo: "Được dùng để xác minh email của bạn lúc đăng ký hoặc nếu bạn quên mật khẩu của mình" emailConfigInfo: "Được dùng để xác minh email của bạn lúc đăng ký hoặc nếu bạn quên mật khẩu của mình"
@ -1705,3 +1704,7 @@ _deck:
_dialog: _dialog:
charactersExceeded: "Bạn nhắn quá giới hạn ký tự!! Hiện nay {current} / giới hạn {max}" charactersExceeded: "Bạn nhắn quá giới hạn ký tự!! Hiện nay {current} / giới hạn {max}"
charactersBelow: "Bạn nhắn quá ít tối thiểu ký tự!! Hiện nay {current} / Tối thiểu {min}" charactersBelow: "Bạn nhắn quá ít tối thiểu ký tự!! Hiện nay {current} / Tối thiểu {min}"
_webhookSettings:
name: "Tên"
active: "Đã bật"

View File

@ -2,7 +2,7 @@
_lang_: "中文(简体)" _lang_: "中文(简体)"
headlineMisskey: "通过帖子连接在一起的网络" headlineMisskey: "通过帖子连接在一起的网络"
introMisskey: "欢迎Misskey是一个开源的、去中心化的“微博客”服务。\n通过编写「帖文」来和大家分享你的以及你周围的事情吧📡\n通过「回应」功能可以让你快速地对大家的帖文表达反馈👍\n来探索新的世界吧🚀" introMisskey: "欢迎Misskey是一个开源的、去中心化的“微博客”服务。\n通过编写「帖文」来和大家分享你的以及你周围的事情吧📡\n通过「回应」功能可以让你快速地对大家的帖文表达反馈👍\n来探索新的世界吧🚀"
poweredByMisskeyDescription: "{name} 由开源平台 <b>Misskey</b> 驱动(也被称为 Misskey 服务器)" poweredByMisskeyDescription: "{name} 是开源平台 <b>Misskey</b> 的服务器之一。"
monthAndDay: "{month}月 {day}日" monthAndDay: "{month}月 {day}日"
search: "搜索" search: "搜索"
notifications: "通知" notifications: "通知"
@ -122,6 +122,8 @@ unmarkAsSensitive: "取消标记为敏感内容"
enterFileName: "请输入文件名" enterFileName: "请输入文件名"
mute: "屏蔽" mute: "屏蔽"
unmute: "解除屏蔽" unmute: "解除屏蔽"
renoteMute: "屏蔽转帖"
renoteUnmute: "解除屏蔽转帖"
block: "拉黑" block: "拉黑"
unblock: "取消拉黑" unblock: "取消拉黑"
suspend: "冻结" suspend: "冻结"
@ -153,6 +155,7 @@ flagShowTimelineReplies: "在时间线上显示帖子的回复"
flagShowTimelineRepliesDescription: "启用时,时间线除了显示用户的帖子外,还会显示其他用户对帖子的回复。" flagShowTimelineRepliesDescription: "启用时,时间线除了显示用户的帖子外,还会显示其他用户对帖子的回复。"
autoAcceptFollowed: "自动允许关注者的关注" autoAcceptFollowed: "自动允许关注者的关注"
addAccount: "添加账户" addAccount: "添加账户"
reloadAccountsList: "更新账户列表"
loginFailed: "登录失败" loginFailed: "登录失败"
showOnRemote: "转到所在服务器显示" showOnRemote: "转到所在服务器显示"
general: "常规设置" general: "常规设置"
@ -355,7 +358,7 @@ hcaptchaSecretKey: "hCaptcha 密钥(SecretKey)"
recaptcha: "reCAPTCHA" recaptcha: "reCAPTCHA"
enableRecaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)" enableRecaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)"
recaptchaSiteKey: "网站密钥" recaptchaSiteKey: "网站密钥"
recaptchaSecretKey: "reCAPTCHA 密钥" recaptchaSecretKey: "reCAPTCHA 密钥(SecretKey)"
turnstile: "Turnstile" turnstile: "Turnstile"
enableTurnstile: "启用Turnstile" enableTurnstile: "启用Turnstile"
turnstileSiteKey: "网站密钥" turnstileSiteKey: "网站密钥"
@ -489,7 +492,7 @@ showFeaturedNotesInTimeline: "在时间线上显示热门推荐"
objectStorage: "对象存储" objectStorage: "对象存储"
useObjectStorage: "使用对象存储" useObjectStorage: "使用对象存储"
objectStorageBaseUrl: "Base URL" objectStorageBaseUrl: "Base URL"
objectStorageBaseUrlDesc: "用于引用的URL。如果您正在使用CDN或反向代理请指定其URL例如S3“https://<bucket>.s3.amazonaws.com”GCS“https://storage.googleapis.com/<bucket>”" objectStorageBaseUrlDesc: "这里是用于参考的URL如果您正在使用CDN或反向代理请指定其URL例如S3“https://<bucket>.s3.amazonaws.com”GCS“https://storage.googleapis.com/<bucket>”"
objectStorageBucket: "存储桶" objectStorageBucket: "存储桶"
objectStorageBucketDesc: "请指定使用的对象存储服务的存储桶名称。" objectStorageBucketDesc: "请指定使用的对象存储服务的存储桶名称。"
objectStoragePrefix: "前缀" objectStoragePrefix: "前缀"
@ -544,6 +547,10 @@ userSuspended: "该用户已被冻结。"
userSilenced: "该用户已被禁言。" userSilenced: "该用户已被禁言。"
yourAccountSuspendedTitle: "账户已被冻结" yourAccountSuspendedTitle: "账户已被冻结"
yourAccountSuspendedDescription: "由于违反了服务器的服务条款或其他原因,该账户已被冻结。 您可以与管理员联系以了解更多信息。 请不要创建一个新的账户。" yourAccountSuspendedDescription: "由于违反了服务器的服务条款或其他原因,该账户已被冻结。 您可以与管理员联系以了解更多信息。 请不要创建一个新的账户。"
tokenRevoked: "令牌无效"
tokenRevokedDescription: "登录令牌已经失效。请重新登录。"
accountDeleted: "帐户已删除"
accountDeletedDescription: "此帐户已经被删除。"
menu: "菜单" menu: "菜单"
divider: "分割线" divider: "分割线"
addItem: "添加项目" addItem: "添加项目"
@ -587,7 +594,6 @@ tokenRequested: "允许访问账户"
pluginTokenRequestedDescription: "此插件将能够拥有此处设置的权限" pluginTokenRequestedDescription: "此插件将能够拥有此处设置的权限"
notificationType: "通知类型" notificationType: "通知类型"
edit: "编辑" edit: "编辑"
useStarForReactionFallback: "如果回应的是未知表情符号,则使用★作为代替"
emailServer: "邮件服务器" emailServer: "邮件服务器"
enableEmail: "启用发送邮件功能" enableEmail: "启用发送邮件功能"
emailConfigInfo: "用于确认电子邮件和密码重置" emailConfigInfo: "用于确认电子邮件和密码重置"
@ -959,6 +965,24 @@ invitationRequiredToRegister: "此服务器目前只允许拥有邀请码的人
emailNotSupported: "此服务器不支持发送邮件" emailNotSupported: "此服务器不支持发送邮件"
postToTheChannel: "发布到频道" postToTheChannel: "发布到频道"
cannotBeChangedLater: "之后不能再更改。" cannotBeChangedLater: "之后不能再更改。"
reactionAcceptance: "接受表情回应"
likeOnly: "仅点赞"
likeOnlyForRemote: "远程仅点赞"
rolesAssignedToMe: "指派给自己的角色"
resetPasswordConfirm: "确定重置密码?"
sensitiveWords: "敏感词"
sensitiveWordsDescription: "将包含设置词的帖子的可见范围设置为首页。可以通过用换行符分隔来设置多个。"
notesSearchNotAvailable: "帖子检索不可用"
license: "许可信息"
unfavoriteConfirm: "确定要取消收藏吗?"
myClips: "我的便签"
drivecleaner: "网盘整理"
retryAllQueuesNow: "立刻重试所有队列"
retryAllQueuesConfirmTitle: "要再尝试一次吗?"
retryAllQueuesConfirmText: "可能会使服务器负荷在一定时间内增加"
enableChartsForRemoteUser: "生成远程用户的图表"
enableChartsForFederatedInstances: "生成远程服务器的图表"
showClipButtonInNoteFooter: "在贴文下方显示便签按钮"
_achievements: _achievements:
earnedAt: "达成时间" earnedAt: "达成时间"
_types: _types:
@ -1218,6 +1242,8 @@ _role:
iconUrl: "图标URL" iconUrl: "图标URL"
asBadge: "作为徽章显示" asBadge: "作为徽章显示"
descriptionOfAsBadge: "开启后,用户名旁边将会出现角色图标。" descriptionOfAsBadge: "开启后,用户名旁边将会出现角色图标。"
displayOrder: "显示顺序"
descriptionOfDisplayOrder: "数字越大,显示位置越靠前。"
canEditMembersByModerator: "允许监察者编辑成员" canEditMembersByModerator: "允许监察者编辑成员"
descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。" descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。"
priority: "优先级" priority: "优先级"
@ -1243,6 +1269,7 @@ _role:
rateLimitFactor: "速率限制" rateLimitFactor: "速率限制"
descriptionOfRateLimitFactor: "值越小限制越少,值越大限制越多。" descriptionOfRateLimitFactor: "值越小限制越少,值越大限制越多。"
canHideAds: "可以隐藏广告" canHideAds: "可以隐藏广告"
canSearchNotes: "是否可以搜索帖子"
_condition: _condition:
isLocal: "是本地用户" isLocal: "是本地用户"
isRemote: "是远程用户" isRemote: "是远程用户"
@ -1252,6 +1279,8 @@ _role:
followersMoreThanOrEq: "关注者不少于" followersMoreThanOrEq: "关注者不少于"
followingLessThanOrEq: "关注中不多于" followingLessThanOrEq: "关注中不多于"
followingMoreThanOrEq: "关注中不少于" followingMoreThanOrEq: "关注中不少于"
notesLessThanOrEq: "帖子数在~以下"
notesMoreThanOrEq: "帖子数在~以上"
and: "符合以下全部条件" and: "符合以下全部条件"
or: "符合以下任一条件" or: "符合以下任一条件"
not: "不符合以下任何条件" not: "不符合以下任何条件"
@ -1517,7 +1546,7 @@ _2fa:
step4: "从现在开始,任何登录操作都将要求您提供动态口令。" step4: "从现在开始,任何登录操作都将要求您提供动态口令。"
securityKeyNotSupported: "您的浏览器不支持安全密钥。" securityKeyNotSupported: "您的浏览器不支持安全密钥。"
registerTOTPBeforeKey: "要注册安全密钥或Passkey请先设置验证器应用程序。" registerTOTPBeforeKey: "要注册安全密钥或Passkey请先设置验证器应用程序。"
securityKeyInfo: "您可以设置使用支持FIDO2的硬件安全密钥、设备上的指纹或PIN来保护您的登录过程。" securityKeyInfo: "注册兼容 WebAuthn 的密钥,例如支持 FIDO2 的硬件安全密钥、设备上的生物识别功能、PIN 码以及 Passkey 等。"
chromePasskeyNotSupported: "目前不支持 Chrome 的Passkey。" chromePasskeyNotSupported: "目前不支持 Chrome 的Passkey。"
registerSecurityKey: "注册安全密钥或Passkey" registerSecurityKey: "注册安全密钥或Passkey"
securityKeyName: "输入密钥名称" securityKeyName: "输入密钥名称"
@ -1844,3 +1873,24 @@ _deck:
_dialog: _dialog:
charactersExceeded: "已经超过了最大字符数! 当前字符数 {current} / 限制字符数 {max}" charactersExceeded: "已经超过了最大字符数! 当前字符数 {current} / 限制字符数 {max}"
charactersBelow: "低于最小字符数!当前字符数 {current} / 限制字符数 {min}" charactersBelow: "低于最小字符数!当前字符数 {current} / 限制字符数 {min}"
_disabledTimeline:
title: "时间线已禁用"
description: "您不能在当前角色使用时间线。"
_drivecleaner:
orderBySizeDesc: "按大小降序排列"
orderByCreatedAtAsc: "按添加日期降序排列"
_webhookSettings:
createWebhook: "创建 Webhook"
name: "名称"
secret: "密钥"
events: "何时运行Webhook"
active: "已启用"
_events:
follow: "关注时"
followed: "被关注时"
note: "发布贴文时"
reply: "收到回复时"
renote: "被转发时"
reaction: "被回应时"
mention: "被提及时"

View File

@ -15,7 +15,7 @@ gotIt: "知道了"
cancel: "取消" cancel: "取消"
noThankYou: "現在不要" noThankYou: "現在不要"
enterUsername: "輸入使用者名稱" enterUsername: "輸入使用者名稱"
renotedBy: "{user} 轉了" renotedBy: "{user} 轉了"
noNotes: "無貼文。" noNotes: "無貼文。"
noNotifications: "沒有通知" noNotifications: "沒有通知"
instance: "實例" instance: "實例"
@ -99,9 +99,9 @@ followRequestPending: "追隨許可批准中"
enterEmoji: "輸入表情符號" enterEmoji: "輸入表情符號"
renote: "轉發" renote: "轉發"
unrenote: "取消轉發" unrenote: "取消轉發"
renoted: "轉成功" renoted: "轉成功"
cantRenote: "無法轉發此貼文。" cantRenote: "無法轉發此貼文。"
cantReRenote: "無法轉傳之前已經轉傳過的內容。" cantReRenote: "無法轉發之前已經轉發過的內容。"
quote: "引用" quote: "引用"
inChannelRenote: "在頻道內轉發" inChannelRenote: "在頻道內轉發"
inChannelQuote: "在頻道內引用" inChannelQuote: "在頻道內引用"
@ -122,14 +122,16 @@ unmarkAsSensitive: "取消標記為敏感內容"
enterFileName: "請輸入檔案名稱" enterFileName: "請輸入檔案名稱"
mute: "靜音" mute: "靜音"
unmute: "解除靜音" unmute: "解除靜音"
renoteMute: "將轉發貼文靜音"
renoteUnmute: "解除轉發貼文的靜音"
block: "封鎖" block: "封鎖"
unblock: "解除封鎖" unblock: "解除封鎖"
suspend: "凍結" suspend: "凍結"
unsuspend: "解除凍結" unsuspend: "解除凍結"
blockConfirm: "確定要封鎖此用戶?" blockConfirm: "確定要封鎖此用戶?"
unblockConfirm: "確定解除封鎖此用戶?" unblockConfirm: "確定解除封鎖此用戶?"
suspendConfirm: "確定凍結此帳" suspendConfirm: "確定凍結此帳"
unsuspendConfirm: "確定解凍此帳" unsuspendConfirm: "確定解凍此帳"
selectList: "選擇清單" selectList: "選擇清單"
selectChannel: "選擇頻道" selectChannel: "選擇頻道"
selectAntenna: "選擇天線" selectAntenna: "選擇天線"
@ -153,6 +155,7 @@ flagShowTimelineReplies: "在時間軸上顯示貼文的回覆"
flagShowTimelineRepliesDescription: "啟用時,時間線除了顯示用戶的貼文以外,還會顯示用戶對其他貼文的回覆。" flagShowTimelineRepliesDescription: "啟用時,時間線除了顯示用戶的貼文以外,還會顯示用戶對其他貼文的回覆。"
autoAcceptFollowed: "自動追隨中使用者的追隨請求" autoAcceptFollowed: "自動追隨中使用者的追隨請求"
addAccount: "添加帳戶" addAccount: "添加帳戶"
reloadAccountsList: "更新帳戶清單的資訊"
loginFailed: "登入失敗" loginFailed: "登入失敗"
showOnRemote: "轉到所在實例顯示" showOnRemote: "轉到所在實例顯示"
general: "一般" general: "一般"
@ -169,7 +172,7 @@ selectUser: "選取使用者"
recipient: "收件人" recipient: "收件人"
annotation: "註解" annotation: "註解"
federation: "站台聯邦" federation: "站台聯邦"
instances: "實例" instances: "伺服器"
registeredAt: "初次觀測" registeredAt: "初次觀測"
latestRequestReceivedAt: "上次收到的請求" latestRequestReceivedAt: "上次收到的請求"
latestStatus: "最後狀態" latestStatus: "最後狀態"
@ -403,7 +406,7 @@ securityKeyAndPasskey: "安全金鑰・Passkey"
securityKey: "安全金鑰" securityKey: "安全金鑰"
lastUsed: "上次使用" lastUsed: "上次使用"
lastUsedAt: "最後使用:{t}" lastUsedAt: "最後使用:{t}"
unregister: "註銷帳" unregister: "註銷帳"
passwordLessLogin: "設置無密碼登入" passwordLessLogin: "設置無密碼登入"
passwordLessLoginDescription: "不使用密碼,以安全金鑰或 Passkey 登入" passwordLessLoginDescription: "不使用密碼,以安全金鑰或 Passkey 登入"
resetPassword: "重置密碼" resetPassword: "重置密碼"
@ -528,8 +531,8 @@ installedDate: "安裝時間"
lastUsedDate: "最後上線日期" lastUsedDate: "最後上線日期"
state: "狀態" state: "狀態"
sort: "排序" sort: "排序"
ascendingOrder: "遞增" ascendingOrder: "昇冪"
descendingOrder: "遞減" descendingOrder: "降冪"
scratchpad: "暫存記憶體" scratchpad: "暫存記憶體"
scratchpadDescription: "AiScript控制台為AiScript提供了實驗環境。您可以在此編寫、執行和確認代碼與Misskey互動的结果。" scratchpadDescription: "AiScript控制台為AiScript提供了實驗環境。您可以在此編寫、執行和確認代碼與Misskey互動的结果。"
output: "輸出" output: "輸出"
@ -544,6 +547,10 @@ userSuspended: "該使用者已被停用"
userSilenced: "該用戶已被禁言。" userSilenced: "該用戶已被禁言。"
yourAccountSuspendedTitle: "帳戶已被凍結" yourAccountSuspendedTitle: "帳戶已被凍結"
yourAccountSuspendedDescription: "由於違反了伺服器的服務條款或其他原因,該帳戶已被凍結。 您可以與管理員連繫以了解更多訊息。 請不要創建一個新的帳戶。" yourAccountSuspendedDescription: "由於違反了伺服器的服務條款或其他原因,該帳戶已被凍結。 您可以與管理員連繫以了解更多訊息。 請不要創建一個新的帳戶。"
tokenRevoked: "權杖無效"
tokenRevokedDescription: "登入權杖失效,請重新登入。"
accountDeleted: "帳戶已被刪除"
accountDeletedDescription: "這個帳戶已被刪除。"
menu: "選單" menu: "選單"
divider: "分割線" divider: "分割線"
addItem: "新增項目" addItem: "新增項目"
@ -587,7 +594,6 @@ tokenRequested: "允許存取帳戶"
pluginTokenRequestedDescription: "此外掛將擁有在此設定的權限。" pluginTokenRequestedDescription: "此外掛將擁有在此設定的權限。"
notificationType: "通知形式" notificationType: "通知形式"
edit: "編輯" edit: "編輯"
useStarForReactionFallback: "以★代替未知的表情符號"
emailServer: "電郵伺服器" emailServer: "電郵伺服器"
enableEmail: "啟用發送電郵功能" enableEmail: "啟用發送電郵功能"
emailConfigInfo: "用於確認電郵地址及密碼重置" emailConfigInfo: "用於確認電郵地址及密碼重置"
@ -671,8 +677,8 @@ sentReactionsCount: "反應發送次數"
receivedReactionsCount: "收到反應次數" receivedReactionsCount: "收到反應次數"
pollVotesCount: "已統計的投票數" pollVotesCount: "已統計的投票數"
pollVotedCount: "已投票數" pollVotedCount: "已投票數"
yes: "確定" yes: ""
no: "取消" no: ""
driveFilesCount: "雲端硬碟檔案數量" driveFilesCount: "雲端硬碟檔案數量"
driveUsage: "雲端硬碟使用量" driveUsage: "雲端硬碟使用量"
noCrawle: "拒絕搜尋引擎索引" noCrawle: "拒絕搜尋引擎索引"
@ -872,10 +878,10 @@ recommended: "推薦"
check: "檢查" check: "檢查"
driveCapOverrideLabel: "更改這個使用者的雲端硬碟容量上限" driveCapOverrideLabel: "更改這個使用者的雲端硬碟容量上限"
driveCapOverrideCaption: "如果指定0以下的值就會被取消。" driveCapOverrideCaption: "如果指定0以下的值就會被取消。"
requireAdminForView: "必須以管理員帳登入才可以檢視。" requireAdminForView: "必須以管理員帳登入才可以檢視。"
isSystemAccount: "由系統自動建立與管理的帳。" isSystemAccount: "由系統自動建立與管理的帳。"
typeToConfirm: "要執行這項操作,請輸入 {x} " typeToConfirm: "要執行這項操作,請輸入 {x} "
deleteAccount: "刪除帳" deleteAccount: "刪除帳"
document: "文件" document: "文件"
numberOfPageCache: "快取頁面數" numberOfPageCache: "快取頁面數"
numberOfPageCacheDescription: "增加數量會提高便利性,但也會增加負荷與記憶體使用量。" numberOfPageCacheDescription: "增加數量會提高便利性,但也會增加負荷與記憶體使用量。"
@ -915,7 +921,7 @@ sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」通
windowMaximize: "最大化" windowMaximize: "最大化"
windowRestore: "復原" windowRestore: "復原"
caption: "標題" caption: "標題"
loggedInAsBot: "以機器人帳登入中" loggedInAsBot: "以機器人帳登入中"
tools: "工具" tools: "工具"
cannotLoad: "無法載入" cannotLoad: "無法載入"
numberOfProfileView: "個人檔案檢視次數" numberOfProfileView: "個人檔案檢視次數"
@ -958,6 +964,25 @@ disableFederationWarn: "聯邦被停用了。即使停用也不會讓您的貼
invitationRequiredToRegister: "目前這個伺服器為邀請制,必須擁有邀請碼才能註冊。" invitationRequiredToRegister: "目前這個伺服器為邀請制,必須擁有邀請碼才能註冊。"
emailNotSupported: "這個伺服器不支援寄送郵件" emailNotSupported: "這個伺服器不支援寄送郵件"
postToTheChannel: "發布到頻道" postToTheChannel: "發布到頻道"
cannotBeChangedLater: "之後不能變更。"
reactionAcceptance: "接受表情反應"
likeOnly: "僅限讚"
likeOnlyForRemote: "遠端僅限讚"
rolesAssignedToMe: "指派給自己的角色"
resetPasswordConfirm: "重設密碼?"
sensitiveWords: "敏感詞"
sensitiveWordsDescription: "將含有設定詞彙的貼文可見性設為發送至首頁。可以用換行來進行複數的設定。"
notesSearchNotAvailable: "無法使用搜尋貼文功能。"
license: "授權"
unfavoriteConfirm: "要取消收錄我的最愛嗎?"
myClips: "我的摘錄"
drivecleaner: "雲端硬碟清掃器"
retryAllQueuesNow: "立刻重試所有佇列"
retryAllQueuesConfirmTitle: "要現在重試嗎?"
retryAllQueuesConfirmText: "伺服器的負荷可能會暫時增加。"
enableChartsForRemoteUser: "生成遠端用戶的圖表"
enableChartsForFederatedInstances: "生成遠端伺服器的圖表"
showClipButtonInNoteFooter: "將摘錄添加至貼文"
_achievements: _achievements:
earnedAt: "獲得日期" earnedAt: "獲得日期"
_types: _types:
@ -1074,7 +1099,7 @@ _achievements:
title: "有備而來" title: "有備而來"
description: "設定了個人檔案" description: "設定了個人檔案"
_markedAsCat: _markedAsCat:
title: "我是貓" title: "吾輩乃貓是也"
description: "已將帳戶設定為貓" description: "已將帳戶設定為貓"
flavor: "還沒有名字。" flavor: "還沒有名字。"
_following1: _following1:
@ -1217,6 +1242,8 @@ _role:
iconUrl: "圖示的URL" iconUrl: "圖示的URL"
asBadge: "顯示為徽章" asBadge: "顯示為徽章"
descriptionOfAsBadge: "開啟的話,角色圖示會顯示在用戶名旁邊。" descriptionOfAsBadge: "開啟的話,角色圖示會顯示在用戶名旁邊。"
displayOrder: "顯示順序"
descriptionOfDisplayOrder: "數字越大顯示在UI上的越上面。"
canEditMembersByModerator: "允許編輯審查員的成員" canEditMembersByModerator: "允許編輯審查員的成員"
descriptionOfCanEditMembersByModerator: "如果開啟,管理員與審查員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。" descriptionOfCanEditMembersByModerator: "如果開啟,管理員與審查員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。"
priority: "優先級" priority: "優先級"
@ -1242,6 +1269,7 @@ _role:
rateLimitFactor: "速率限制" rateLimitFactor: "速率限制"
descriptionOfRateLimitFactor: "值越小限制越少,值越大限制越多。" descriptionOfRateLimitFactor: "值越小限制越少,值越大限制越多。"
canHideAds: "不顯示廣告" canHideAds: "不顯示廣告"
canSearchNotes: "可否搜尋貼文"
_condition: _condition:
isLocal: "本地使用者" isLocal: "本地使用者"
isRemote: "遠端使用者" isRemote: "遠端使用者"
@ -1251,6 +1279,8 @@ _role:
followersMoreThanOrEq: "追隨者人數在~以上" followersMoreThanOrEq: "追隨者人數在~以上"
followingLessThanOrEq: "追隨人數在~以下" followingLessThanOrEq: "追隨人數在~以下"
followingMoreThanOrEq: "追隨人數在~以上" followingMoreThanOrEq: "追隨人數在~以上"
notesLessThanOrEq: "發布數在~以下"
notesMoreThanOrEq: "發布數在~以上"
and: "~和~" and: "~和~"
or: "~或~" or: "~或~"
not: "~否" not: "~否"
@ -1480,7 +1510,7 @@ _time:
_tutorial: _tutorial:
title: "Misskey使用方法" title: "Misskey使用方法"
step1_1: "歡迎!" step1_1: "歡迎!"
step1_2: "此為「時間軸」頁面,它會按照時間順序顯示你「追隨」的人發出的「貼文」" step1_2: "此為「時間軸」頁面,它會按照時間順序顯示你「追隨」的人發出的「貼文」"
step1_3: "由於你沒有發佈任何貼文,也沒有追隨任何人,所以你的時間軸目前是空的。" step1_3: "由於你沒有發佈任何貼文,也沒有追隨任何人,所以你的時間軸目前是空的。"
step2_1: "在發文或追隨其他人之前先讓我們設定一下個人資料吧。" step2_1: "在發文或追隨其他人之前先讓我們設定一下個人資料吧。"
step2_2: "提供一些關於自己的資訊來讓其他人更有追隨你的意願。" step2_2: "提供一些關於自己的資訊來讓其他人更有追隨你的意願。"
@ -1713,7 +1743,7 @@ _instanceCharts:
_timelines: _timelines:
home: "首頁" home: "首頁"
local: "本地" local: "本地"
social: "社" social: "社"
global: "公開" global: "公開"
_play: _play:
new: "新增Play" new: "新增Play"
@ -1843,3 +1873,24 @@ _deck:
_dialog: _dialog:
charactersExceeded: "已超過最大字數!現在 {current} / 限制 {max}" charactersExceeded: "已超過最大字數!現在 {current} / 限制 {max}"
charactersBelow: "低於最少字數!現在 {current} / 限制 {max}" charactersBelow: "低於最少字數!現在 {current} / 限制 {max}"
_disabledTimeline:
title: "停用的時間軸"
description: "目前的角色無法使用這個時間軸。"
_drivecleaner:
orderBySizeDesc: "檔案由大到小"
orderByCreatedAtAsc: "依照加入的日期順序"
_webhookSettings:
createWebhook: "建立 Webhook"
name: "名稱"
secret: "秘密"
events: "什麼時候運行Webhook"
active: "已啟用"
_events:
follow: "當你追隨時"
followed: "當被追隨時"
note: "當發布貼文時"
reply: "當收到回覆時"
renote: "當被轉發時"
reaction: "當獲得反應時"
mention: "當被提到時"

View File

@ -1,12 +1,12 @@
{ {
"name": "misskey", "name": "misskey",
"version": "13.9.2", "version": "13.10.3",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/misskey-dev/misskey.git" "url": "https://github.com/misskey-dev/misskey.git"
}, },
"packageManager": "pnpm@7.27.0", "packageManager": "pnpm@8.1.0",
"workspaces": [ "workspaces": [
"packages/frontend", "packages/frontend",
"packages/backend", "packages/backend",
@ -50,17 +50,17 @@
"gulp-replace": "1.1.4", "gulp-replace": "1.1.4",
"gulp-terser": "2.1.0", "gulp-terser": "2.1.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"typescript": "4.9.5" "typescript": "5.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/gulp": "4.0.10", "@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1", "@types/gulp-rename": "2.0.1",
"@typescript-eslint/eslint-plugin": "5.53.0", "@typescript-eslint/eslint-plugin": "5.57.0",
"@typescript-eslint/parser": "5.53.0", "@typescript-eslint/parser": "5.57.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "12.7.0", "cypress": "12.9.0",
"eslint": "8.35.0", "eslint": "8.37.0",
"start-server-and-test": "1.15.4" "start-server-and-test": "2.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@tensorflow/tfjs-core": "4.2.0" "@tensorflow/tfjs-core": "4.2.0"

View File

@ -19,6 +19,6 @@
</head> </head>
<body> <body>
<redoc spec-url="/api.json" expand-responses="200" expand-single-schema-field="true"></redoc> <redoc spec-url="/api.json" expand-responses="200" expand-single-schema-field="true"></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@2.0.0-rc.50/bundles/redoc.standalone.js" integrity="sha256-WJbngBWN9vp6vkEuzeoSj5tE5saW9Hfj6/SinkzhL2s=" crossorigin="anonymous"></script> <script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,15 @@
export class fixforeignkeyreports1675053125067 {
name = 'fixforeignkeyreports1675053125067'
async up(queryRunner) {
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_a9021cc2e1feb5f72d3db6e9f5" ON "abuse_user_report" ("targetUserId")`);
await queryRunner.query(`DELETE FROM "abuse_user_report" WHERE "targetUserId" NOT IN (SELECT "id" FROM "user")`);
await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT IF EXISTS "FK_a9021cc2e1feb5f72d3db6e9f5f"`);
await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_a9021cc2e1feb5f72d3db6e9f5"`);
await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f"`);
}
}

View File

@ -0,0 +1,68 @@
export class tweakVarcharLength1678426061773 {
name = 'tweakVarcharLength1678426061773'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "name" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "maintainerName" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "maintainerEmail" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "langs" TYPE character varying(1024) array`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "pinnedUsers" TYPE character varying(1024) array`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "hiddenTags" TYPE character varying(1024) array`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "blockedHosts" TYPE character varying(1024) array`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "themeColor" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "mascotImageUrl" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "bannerUrl" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "backgroundImageUrl" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "logoImageUrl" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "errorImageUrl" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "iconUrl" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "hcaptchaSiteKey" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "hcaptchaSecretKey" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "recaptchaSiteKey" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "recaptchaSecretKey" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "turnstileSiteKey" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "turnstileSecretKey" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "summalyProxy" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "email" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "smtpHost" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "smtpUser" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "smtpPass" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "swPublicKey" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "swPrivateKey" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "deeplAuthKey" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" RENAME COLUMN "ToSUrl" TO "termsOfServiceUrl"`);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "termsOfServiceUrl" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "feedbackUrl" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "objectStorageBucket" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "objectStoragePrefix" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "objectStorageBaseUrl" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "objectStorageEndpoint" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "objectStorageRegion" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "objectStorageAccessKey" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "objectStorageSecretKey" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "flash" ALTER COLUMN "script" TYPE character varying(65536)`, undefined);
await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___readWrite" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___read" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___write" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___registeredWithinWeek" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___registeredWithinMonth" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___registeredWithinYear" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___registeredOutsideWeek" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___registeredOutsideMonth" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___registeredOutsideYear" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___readWrite" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___read" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___write" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___registeredWithinWeek" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___registeredWithinMonth" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___registeredWithinYear" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___registeredOutsideWeek" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___registeredOutsideMonth" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___registeredOutsideYear" TYPE integer`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" RENAME COLUMN "termsOfServiceUrl" TO "ToSUrl"`);
}
}

View File

@ -0,0 +1,13 @@
export class removeUnused1678427401214 {
name = 'removeUnused1678427401214'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "pinnedPages"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "pinnedClipId"`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "pinnedClipId" character varying(32)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "pinnedPages" character varying(512) array NOT NULL DEFAULT '{/featured,/channels,/explore,/pages,/about-misskey}'`);
}
}

View File

@ -0,0 +1,11 @@
export class roleDisplayOrder1678602320354 {
name = 'roleDisplayOrder1678602320354'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "role" ADD "displayOrder" integer NOT NULL DEFAULT '0'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "displayOrder"`);
}
}

View File

@ -0,0 +1,11 @@
export class sensitiveWords1678694614599 {
name = 'sensitiveWords1678694614599'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "sensitiveWords" character varying(1024) array NOT NULL DEFAULT '{}'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "sensitiveWords"`);
}
}

View File

@ -0,0 +1,14 @@
export class retentionDateKey1678869617549 {
name = 'retentionDateKey1678869617549'
async up(queryRunner) {
await queryRunner.query(`TRUNCATE TABLE "retention_aggregation"`, undefined);
await queryRunner.query(`ALTER TABLE "retention_aggregation" ADD "dateKey" character varying(512) NOT NULL`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_f7c3576b37bd2eec966ae24477" ON "retention_aggregation" ("dateKey") `);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_f7c3576b37bd2eec966ae24477"`);
await queryRunner.query(`ALTER TABLE "retention_aggregation" DROP COLUMN "dateKey"`);
}
}

View File

@ -0,0 +1,11 @@
export class addPropsForCustomEmoji1678945242650 {
name = 'addPropsForCustomEmoji1678945242650'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "emoji" ADD "license" character varying(1024)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "license"`);
}
}

View File

@ -0,0 +1,23 @@
export class clipFavorite1678953978856 {
name = 'clipFavorite1678953978856'
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "clip_favorite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "clipId" character varying(32) NOT NULL, CONSTRAINT "PK_1b539f43906f05ebcabe752a977" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_25a31662b0b0cc9af6549a9d71" ON "clip_favorite" ("userId") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_b1754a39d0b281e07ed7c078ec" ON "clip_favorite" ("userId", "clipId") `);
await queryRunner.query(`ALTER TABLE "clip" ADD "lastClippedAt" TIMESTAMP WITH TIME ZONE`);
await queryRunner.query(`CREATE INDEX "IDX_a3eac04ae2aa9e221e7596114a" ON "clip" ("lastClippedAt") `);
await queryRunner.query(`ALTER TABLE "clip_favorite" ADD CONSTRAINT "FK_25a31662b0b0cc9af6549a9d711" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "clip_favorite" ADD CONSTRAINT "FK_fce61c7986cee54393e79f1d849" FOREIGN KEY ("clipId") REFERENCES "clip"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "clip_favorite" DROP CONSTRAINT "FK_fce61c7986cee54393e79f1d849"`);
await queryRunner.query(`ALTER TABLE "clip_favorite" DROP CONSTRAINT "FK_25a31662b0b0cc9af6549a9d711"`);
await queryRunner.query(`DROP INDEX "public"."IDX_a3eac04ae2aa9e221e7596114a"`);
await queryRunner.query(`ALTER TABLE "clip" DROP COLUMN "lastClippedAt"`);
await queryRunner.query(`DROP INDEX "public"."IDX_b1754a39d0b281e07ed7c078ec"`);
await queryRunner.query(`DROP INDEX "public"."IDX_25a31662b0b0cc9af6549a9d71"`);
await queryRunner.query(`DROP TABLE "clip_favorite"`);
}
}

View File

@ -0,0 +1,17 @@
export class antennaActive1679309757174 {
name = 'antennaActive1679309757174'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "antenna" ADD "lastUsedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT 'now'`);
await queryRunner.query(`ALTER TABLE "antenna" ADD "isActive" boolean NOT NULL DEFAULT true`);
await queryRunner.query(`CREATE INDEX "IDX_084c2abb8948ef59a37dce6ac1" ON "antenna" ("lastUsedAt") `);
await queryRunner.query(`CREATE INDEX "IDX_36ef5192a1ce55ed0e40aa4db5" ON "antenna" ("isActive") `);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_36ef5192a1ce55ed0e40aa4db5"`);
await queryRunner.query(`DROP INDEX "public"."IDX_084c2abb8948ef59a37dce6ac1"`);
await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "isActive"`);
await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "lastUsedAt"`);
}
}

View File

@ -0,0 +1,11 @@
export class enableChartsForRemoteUser1679639483253 {
name = 'enableChartsForRemoteUser1679639483253'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "enableChartsForRemoteUser" boolean NOT NULL DEFAULT true`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableChartsForRemoteUser"`);
}
}

View File

@ -0,0 +1,11 @@
export class cleanup1679651580149 {
name = 'cleanup1679651580149'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "useStarForReactionFallback"`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "useStarForReactionFallback" boolean NOT NULL DEFAULT false`);
}
}

View File

@ -0,0 +1,11 @@
export class enableChartsForFederatedInstances1679652081809 {
name = 'enableChartsForFederatedInstances1679652081809'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "enableChartsForFederatedInstances" boolean NOT NULL DEFAULT true`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableChartsForFederatedInstances"`);
}
}

View File

@ -0,0 +1,21 @@
export class channelFavorite1680228513388 {
name = 'channelFavorite1680228513388'
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "channel_favorite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "channelId" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "PK_59bddfd54d48689a298d41af00c" PRIMARY KEY ("id")); COMMENT ON COLUMN "channel_favorite"."createdAt" IS 'The created date of the ChannelFavorite.'`);
await queryRunner.query(`CREATE INDEX "IDX_735a5544f9249d412255f47f95" ON "channel_favorite" ("createdAt") `);
await queryRunner.query(`CREATE INDEX "IDX_d3ca0db011b75ac2a940a2337d" ON "channel_favorite" ("channelId") `);
await queryRunner.query(`CREATE INDEX "IDX_8302bd27226605ece14842fb25" ON "channel_favorite" ("userId") `);
await queryRunner.query(`ALTER TABLE "channel_favorite" ADD CONSTRAINT "FK_d3ca0db011b75ac2a940a2337d2" FOREIGN KEY ("channelId") REFERENCES "channel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "channel_favorite" ADD CONSTRAINT "FK_8302bd27226605ece14842fb25a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "channel_favorite" DROP CONSTRAINT "FK_8302bd27226605ece14842fb25a"`);
await queryRunner.query(`ALTER TABLE "channel_favorite" DROP CONSTRAINT "FK_d3ca0db011b75ac2a940a2337d2"`);
await queryRunner.query(`DROP INDEX "public"."IDX_8302bd27226605ece14842fb25"`);
await queryRunner.query(`DROP INDEX "public"."IDX_d3ca0db011b75ac2a940a2337d"`);
await queryRunner.query(`DROP INDEX "public"."IDX_735a5544f9249d412255f47f95"`);
await queryRunner.query(`DROP TABLE "channel_favorite"`);
}
}

View File

@ -0,0 +1,11 @@
export class channelNotePining1680238118084 {
name = 'channelNotePining1680238118084'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "channel" ADD "pinnedNoteIds" character varying(128) array NOT NULL DEFAULT '{}'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "channel" DROP COLUMN "pinnedNoteIds"`);
}
}

View File

@ -0,0 +1,10 @@
export class cleanup1680491187535 {
name = 'cleanup1680491187535'
async up(queryRunner) {
await queryRunner.query(`DROP TABLE "antenna_note" `);
}
async down(queryRunner) {
}
}

View File

@ -0,0 +1,11 @@
export class cleanup1680582195041 {
name = 'cleanup1680582195041'
async up(queryRunner) {
await queryRunner.query(`DROP TABLE "notification" `);
}
async down(queryRunner) {
}
}

View File

@ -23,43 +23,45 @@
}, },
"optionalDependencies": { "optionalDependencies": {
"@swc/core-android-arm64": "^1.3.11", "@swc/core-android-arm64": "^1.3.11",
"@swc/core-darwin-arm64": "^1.3.36", "@swc/core-darwin-arm64": "^1.3.42",
"@swc/core-darwin-x64": "^1.3.36", "@swc/core-darwin-x64": "^1.3.42",
"@swc/core-linux-arm-gnueabihf": "^1.3.36", "@swc/core-linux-arm-gnueabihf": "^1.3.42",
"@swc/core-linux-arm64-gnu": "^1.3.36", "@swc/core-linux-arm64-gnu": "^1.3.42",
"@swc/core-linux-arm64-musl": "^1.3.36", "@swc/core-linux-arm64-musl": "^1.3.42",
"@swc/core-linux-x64-gnu": "^1.3.36", "@swc/core-linux-x64-gnu": "^1.3.42",
"@swc/core-linux-x64-musl": "^1.3.36", "@swc/core-linux-x64-musl": "^1.3.42",
"@swc/core-win32-arm64-msvc": "^1.3.36", "@swc/core-win32-arm64-msvc": "^1.3.42",
"@swc/core-win32-ia32-msvc": "^1.3.36", "@swc/core-win32-ia32-msvc": "^1.3.42",
"@swc/core-win32-x64-msvc": "^1.3.36", "@swc/core-win32-x64-msvc": "^1.3.42",
"@tensorflow/tfjs": "4.2.0", "@tensorflow/tfjs": "4.2.0",
"@tensorflow/tfjs-node": "4.2.0" "@tensorflow/tfjs-node": "4.2.0"
}, },
"dependencies": { "dependencies": {
"@bull-board/api": "4.12.1", "@aws-sdk/client-s3": "3.301.0",
"@bull-board/fastify": "4.12.1", "@aws-sdk/lib-storage": "3.301.0",
"@bull-board/ui": "4.12.1", "@aws-sdk/node-http-handler": "3.296.0",
"@discordapp/twemoji": "14.0.2", "@bull-board/api": "5.0.0",
"@bull-board/fastify": "5.0.0",
"@bull-board/ui": "5.0.0",
"@discordapp/twemoji": "14.1.2",
"@fastify/accepts": "4.1.0", "@fastify/accepts": "4.1.0",
"@fastify/cookie": "8.3.0", "@fastify/cookie": "8.3.0",
"@fastify/cors": "8.2.0", "@fastify/cors": "8.2.1",
"@fastify/http-proxy": "8.4.0", "@fastify/http-proxy": "9.0.0",
"@fastify/multipart": "7.4.1", "@fastify/multipart": "7.5.0",
"@fastify/static": "6.9.0", "@fastify/static": "6.9.0",
"@fastify/view": "7.4.1", "@fastify/view": "7.4.1",
"@nestjs/common": "9.3.9", "@nestjs/common": "9.3.12",
"@nestjs/core": "9.3.9", "@nestjs/core": "9.3.12",
"@nestjs/testing": "9.3.9", "@nestjs/testing": "9.3.12",
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.0.2", "@sinonjs/fake-timers": "10.0.2",
"@swc/cli": "0.1.62", "@swc/cli": "0.1.62",
"@swc/core": "1.3.36", "@swc/core": "1.3.42",
"accepts": "1.3.8", "accepts": "1.3.8",
"ajv": "8.12.0", "ajv": "8.12.0",
"archiver": "5.3.1", "archiver": "5.3.1",
"autwh": "0.1.0", "autwh": "0.1.0",
"aws-sdk": "2.1318.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"bull": "4.10.4", "bull": "4.10.4",
@ -74,35 +76,35 @@
"date-fns": "2.29.3", "date-fns": "2.29.3",
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"escape-regexp": "0.0.1", "escape-regexp": "0.0.1",
"fastify": "4.13.0", "fastify": "4.15.0",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "18.2.1", "file-type": "18.2.1",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
"form-data": "4.0.0", "form-data": "4.0.0",
"got": "12.5.3", "got": "12.6.0",
"happy-dom": "8.9.0", "happy-dom": "8.9.0",
"hpagent": "1.2.0", "hpagent": "1.2.0",
"ioredis": "4.28.5", "ioredis": "4.28.5",
"ip-cidr": "3.1.0", "ip-cidr": "3.1.0",
"is-svg": "4.3.2", "is-svg": "4.3.2",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsdom": "21.1.0", "jsdom": "21.1.1",
"json5": "2.2.3", "json5": "2.2.3",
"jsonld": "8.1.1", "jsonld": "8.1.1",
"jsrsasign": "10.6.1", "jsrsasign": "10.7.0",
"mfm-js": "0.23.3", "mfm-js": "0.23.3",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"misskey-js": "0.0.15", "misskey-js": "workspace:*",
"ms": "3.0.0-canary.1", "ms": "3.0.0-canary.1",
"nested-property": "4.0.0", "nested-property": "4.0.0",
"node-fetch": "3.3.0", "node-fetch": "3.3.1",
"nodemailer": "6.9.1", "nodemailer": "6.9.1",
"nsfwjs": "2.4.2", "nsfwjs": "2.4.2",
"oauth": "0.10.0", "oauth": "0.10.0",
"os-utils": "0.0.14", "os-utils": "0.0.14",
"otpauth": "^9.0.2", "otpauth": "9.1.1",
"parse5": "7.1.2", "parse5": "7.1.2",
"pg": "8.9.0", "pg": "8.10.0",
"private-ip": "3.0.0", "private-ip": "3.0.0",
"probe-image-size": "7.2.3", "probe-image-size": "7.2.3",
"promise-limit": "2.7.0", "promise-limit": "2.7.0",
@ -123,33 +125,33 @@
"sanitize-html": "2.10.0", "sanitize-html": "2.10.0",
"seedrandom": "3.0.5", "seedrandom": "3.0.5",
"semver": "7.3.8", "semver": "7.3.8",
"sharp": "0.31.3", "sharp": "0.32.0",
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp", "sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"summaly": "github:misskey-dev/summaly", "summaly": "github:misskey-dev/summaly",
"systeminformation": "5.17.10", "systeminformation": "5.17.12",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tmp": "0.2.1", "tmp": "0.2.1",
"tsc-alias": "1.8.2", "tsc-alias": "1.8.5",
"tsconfig-paths": "4.1.2", "tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0", "twemoji-parser": "14.0.0",
"typeorm": "0.3.11", "typeorm": "0.3.11",
"typescript": "4.9.5", "typescript": "5.0.2",
"ulid": "2.3.0", "ulid": "2.3.0",
"unzipper": "0.10.11", "unzipper": "0.10.11",
"uuid": "9.0.0", "uuid": "9.0.0",
"vary": "1.1.2", "vary": "1.1.2",
"web-push": "3.5.0", "web-push": "3.5.0",
"websocket": "1.0.34", "websocket": "1.0.34",
"ws": "8.12.1", "ws": "8.13.0",
"xev": "3.0.2" "xev": "3.0.2"
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "29.4.3", "@jest/globals": "29.5.0",
"@swc/jest": "0.2.24", "@swc/jest": "0.2.24",
"@types/accepts": "1.3.5", "@types/accepts": "1.3.5",
"@types/archiver": "5.3.1", "@types/archiver": "5.3.2",
"@types/bcryptjs": "2.4.2", "@types/bcryptjs": "2.4.2",
"@types/bull": "4.10.0", "@types/bull": "4.10.0",
"@types/cbor": "6.0.0", "@types/cbor": "6.0.0",
@ -158,13 +160,13 @@
"@types/escape-regexp": "0.0.1", "@types/escape-regexp": "0.0.1",
"@types/fluent-ffmpeg": "2.1.21", "@types/fluent-ffmpeg": "2.1.21",
"@types/ioredis": "4.28.10", "@types/ioredis": "4.28.10",
"@types/jest": "29.4.0", "@types/jest": "29.5.0",
"@types/js-yaml": "4.0.5", "@types/js-yaml": "4.0.5",
"@types/jsdom": "21.1.0", "@types/jsdom": "21.1.1",
"@types/jsonld": "1.5.8", "@types/jsonld": "1.5.8",
"@types/jsrsasign": "10.5.5", "@types/jsrsasign": "10.5.8",
"@types/mime-types": "2.1.1", "@types/mime-types": "2.1.1",
"@types/node": "18.14.1", "@types/node": "18.15.11",
"@types/node-fetch": "3.0.3", "@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.7", "@types/nodemailer": "6.4.7",
"@types/oauth": "0.9.1", "@types/oauth": "0.9.1",
@ -176,7 +178,7 @@
"@types/ratelimiter": "3.4.4", "@types/ratelimiter": "3.4.4",
"@types/redis": "4.0.11", "@types/redis": "4.0.11",
"@types/rename": "1.0.4", "@types/rename": "1.0.4",
"@types/sanitize-html": "2.8.0", "@types/sanitize-html": "2.9.0",
"@types/semver": "7.3.13", "@types/semver": "7.3.13",
"@types/sharp": "0.31.1", "@types/sharp": "0.31.1",
"@types/sinonjs__fake-timers": "8.1.2", "@types/sinonjs__fake-timers": "8.1.2",
@ -188,13 +190,14 @@
"@types/web-push": "3.3.2", "@types/web-push": "3.3.2",
"@types/websocket": "1.0.5", "@types/websocket": "1.0.5",
"@types/ws": "8.5.4", "@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.52.0", "@typescript-eslint/eslint-plugin": "5.57.0",
"@typescript-eslint/parser": "5.53.0", "@typescript-eslint/parser": "5.57.0",
"aws-sdk-client-mock": "^2.1.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.35.0", "eslint": "8.37.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
"execa": "6.1.0", "execa": "6.1.0",
"jest": "29.4.3", "jest": "29.5.0",
"jest-mock": "29.4.3" "jest-mock": "29.5.0"
} }
} }

View File

@ -3,7 +3,7 @@ import type { UserProfilesRepository, UsersRepository } from '@/models/index.js'
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { CreateNotificationService } from '@/core/CreateNotificationService.js'; import { NotificationService } from '@/core/NotificationService.js';
export const ACHIEVEMENT_TYPES = [ export const ACHIEVEMENT_TYPES = [
'notes1', 'notes1',
@ -90,7 +90,7 @@ export class AchievementService {
@Inject(DI.userProfilesRepository) @Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository, private userProfilesRepository: UserProfilesRepository,
private createNotificationService: CreateNotificationService, private notificationService: NotificationService,
) { ) {
} }
@ -114,7 +114,7 @@ export class AchievementService {
}], }],
}); });
this.createNotificationService.createNotification(userId, 'achievementEarned', { this.notificationService.createNotification(userId, 'achievementEarned', {
achievement: type, achievement: type,
}); });
} }

View File

@ -10,9 +10,9 @@ import { isUserRelated } from '@/misc/is-user-related.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { PushNotificationService } from '@/core/PushNotificationService.js'; import { PushNotificationService } from '@/core/PushNotificationService.js';
import * as Acct from '@/misc/acct.js'; import * as Acct from '@/misc/acct.js';
import type { Packed } from '@/misc/schema.js'; import type { Packed } from '@/misc/json-schema.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { MutingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserListJoiningsRepository } from '@/models/index.js'; import type { MutingsRepository, NotesRepository, AntennasRepository, UserListJoiningsRepository } from '@/models/index.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { StreamMessages } from '@/server/api/stream/types.js'; import { StreamMessages } from '@/server/api/stream/types.js';
@ -24,6 +24,9 @@ export class AntennaService implements OnApplicationShutdown {
private antennas: Antenna[]; private antennas: Antenna[];
constructor( constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.redisSubscriber) @Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis, private redisSubscriber: Redis.Redis,
@ -33,9 +36,6 @@ export class AntennaService implements OnApplicationShutdown {
@Inject(DI.notesRepository) @Inject(DI.notesRepository)
private notesRepository: NotesRepository, private notesRepository: NotesRepository,
@Inject(DI.antennaNotesRepository)
private antennaNotesRepository: AntennaNotesRepository,
@Inject(DI.antennasRepository) @Inject(DI.antennasRepository)
private antennasRepository: AntennasRepository, private antennasRepository: AntennasRepository,
@ -71,12 +71,14 @@ export class AntennaService implements OnApplicationShutdown {
this.antennas.push({ this.antennas.push({
...body, ...body,
createdAt: new Date(body.createdAt), createdAt: new Date(body.createdAt),
lastUsedAt: new Date(body.lastUsedAt),
}); });
break; break;
case 'antennaUpdated': case 'antennaUpdated':
this.antennas[this.antennas.findIndex(a => a.id === body.id)] = { this.antennas[this.antennas.findIndex(a => a.id === body.id)] = {
...body, ...body,
createdAt: new Date(body.createdAt), createdAt: new Date(body.createdAt),
lastUsedAt: new Date(body.lastUsedAt),
}; };
break; break;
case 'antennaDeleted': case 'antennaDeleted':
@ -90,54 +92,13 @@ export class AntennaService implements OnApplicationShutdown {
@bindThis @bindThis
public async addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }): Promise<void> { public async addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }): Promise<void> {
// 通知しない設定になっているか、自分自身の投稿なら既読にする this.redisClient.xadd(
const read = !antenna.notify || (antenna.userId === noteUser.id); `antennaTimeline:${antenna.id}`,
'MAXLEN', '~', '200',
this.antennaNotesRepository.insert({ `${this.idService.parse(note.id).date.getTime()}-*`,
id: this.idService.genId(), 'note', note.id);
antennaId: antenna.id,
noteId: note.id,
read: read,
});
this.globalEventService.publishAntennaStream(antenna.id, 'note', note); this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
if (!read) {
const mutings = await this.mutingsRepository.find({
where: {
muterId: antenna.userId,
},
select: ['muteeId'],
});
// Copy
const _note: Note = {
...note,
};
if (note.replyId != null) {
_note.reply = await this.notesRepository.findOneByOrFail({ id: note.replyId });
}
if (note.renoteId != null) {
_note.renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId });
}
if (isUserRelated(_note, new Set<string>(mutings.map(x => x.muteeId)))) {
return;
}
// 2秒経っても既読にならなかったら通知
setTimeout(async () => {
const unread = await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false });
if (unread) {
this.globalEventService.publishMainStream(antenna.userId, 'unreadAntenna', antenna);
this.pushNotificationService.pushNotification(antenna.userId, 'unreadAntennaNote', {
antenna: { id: antenna.id, name: antenna.name },
note: await this.noteEntityService.pack(note),
});
}
}, 2000);
}
} }
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている // NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
@ -217,7 +178,9 @@ export class AntennaService implements OnApplicationShutdown {
@bindThis @bindThis
public async getAntennas() { public async getAntennas() {
if (!this.antennasFetched) { if (!this.antennasFetched) {
this.antennas = await this.antennasRepository.find(); this.antennas = await this.antennasRepository.findBy({
isActive: true,
});
this.antennasFetched = true; this.antennasFetched = true;
} }

View File

@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis'; import Redis from 'ioredis';
import type { UsersRepository } from '@/models/index.js'; import type { UserProfile, UsersRepository } from '@/models/index.js';
import { Cache } from '@/misc/cache.js'; import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
import type { LocalUser, User } from '@/models/entities/User.js'; import type { LocalUser, User } from '@/models/entities/User.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
@ -10,13 +10,18 @@ import { StreamMessages } from '@/server/api/stream/types.js';
import type { OnApplicationShutdown } from '@nestjs/common'; import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable() @Injectable()
export class UserCacheService implements OnApplicationShutdown { export class CacheService implements OnApplicationShutdown {
public userByIdCache: Cache<User>; public userByIdCache: MemoryKVCache<User>;
public localUserByNativeTokenCache: Cache<LocalUser | null>; public localUserByNativeTokenCache: MemoryKVCache<LocalUser | null>;
public localUserByIdCache: Cache<LocalUser>; public localUserByIdCache: MemoryKVCache<LocalUser>;
public uriPersonCache: Cache<User | null>; public uriPersonCache: MemoryKVCache<User | null>;
public userProfileCache: RedisKVCache<UserProfile>;
public userMutingsCache: RedisKVCache<string[]>;
constructor( constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.redisSubscriber) @Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis, private redisSubscriber: Redis.Redis,
@ -27,10 +32,12 @@ export class UserCacheService implements OnApplicationShutdown {
) { ) {
//this.onMessage = this.onMessage.bind(this); //this.onMessage = this.onMessage.bind(this);
this.userByIdCache = new Cache<User>(Infinity); this.userByIdCache = new MemoryKVCache<User>(Infinity);
this.localUserByNativeTokenCache = new Cache<LocalUser | null>(Infinity); this.localUserByNativeTokenCache = new MemoryKVCache<LocalUser | null>(Infinity);
this.localUserByIdCache = new Cache<LocalUser>(Infinity); this.localUserByIdCache = new MemoryKVCache<LocalUser>(Infinity);
this.uriPersonCache = new Cache<User | null>(Infinity); this.uriPersonCache = new MemoryKVCache<User | null>(Infinity);
this.userProfileCache = new RedisKVCache<UserProfile>(this.redisClient, 'userProfile', 1000 * 60 * 60 * 24, 1000 * 60);
this.userMutingsCache = new RedisKVCache<string[]>(this.redisClient, 'userMutings', 1000 * 60 * 60 * 24, 1000 * 60);
this.redisSubscriber.on('message', this.onMessage); this.redisSubscriber.on('message', this.onMessage);
} }
@ -52,7 +59,7 @@ export class UserCacheService implements OnApplicationShutdown {
} }
} }
if (this.userEntityService.isLocalUser(user)) { if (this.userEntityService.isLocalUser(user)) {
this.localUserByNativeTokenCache.set(user.token, user); this.localUserByNativeTokenCache.set(user.token!, user);
this.localUserByIdCache.set(user.id, user); this.localUserByIdCache.set(user.id, user);
} }
break; break;
@ -77,7 +84,7 @@ export class UserCacheService implements OnApplicationShutdown {
} }
@bindThis @bindThis
public findById(userId: User['id']) { public findUserById(userId: User['id']) {
return this.userByIdCache.fetch(userId, () => this.usersRepository.findOneByOrFail({ id: userId })); return this.userByIdCache.fetch(userId, () => this.usersRepository.findOneByOrFail({ id: userId }));
} }

View File

@ -5,7 +5,6 @@ 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 { CaptchaService } from './CaptchaService.js'; import { CaptchaService } from './CaptchaService.js';
import { CreateNotificationService } from './CreateNotificationService.js';
import { CreateSystemUserService } from './CreateSystemUserService.js'; import { CreateSystemUserService } from './CreateSystemUserService.js';
import { CustomEmojiService } from './CustomEmojiService.js'; import { CustomEmojiService } from './CustomEmojiService.js';
import { DeleteAccountService } from './DeleteAccountService.js'; import { DeleteAccountService } from './DeleteAccountService.js';
@ -39,7 +38,7 @@ import { S3Service } from './S3Service.js';
import { SignupService } from './SignupService.js'; import { SignupService } from './SignupService.js';
import { TwoFactorAuthenticationService } from './TwoFactorAuthenticationService.js'; import { TwoFactorAuthenticationService } from './TwoFactorAuthenticationService.js';
import { UserBlockingService } from './UserBlockingService.js'; import { UserBlockingService } from './UserBlockingService.js';
import { UserCacheService } from './UserCacheService.js'; import { CacheService } from './CacheService.js';
import { UserFollowingService } from './UserFollowingService.js'; import { UserFollowingService } from './UserFollowingService.js';
import { UserKeypairStoreService } from './UserKeypairStoreService.js'; import { UserKeypairStoreService } from './UserKeypairStoreService.js';
import { UserListService } from './UserListService.js'; import { UserListService } from './UserListService.js';
@ -126,7 +125,6 @@ const $AntennaService: Provider = { provide: 'AntennaService', useExisting: Ante
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 $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService }; const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService };
const $CreateNotificationService: Provider = { provide: 'CreateNotificationService', useExisting: CreateNotificationService };
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 };
const $DeleteAccountService: Provider = { provide: 'DeleteAccountService', useExisting: DeleteAccountService }; const $DeleteAccountService: Provider = { provide: 'DeleteAccountService', useExisting: DeleteAccountService };
@ -161,7 +159,7 @@ const $S3Service: Provider = { provide: 'S3Service', useExisting: S3Service };
const $SignupService: Provider = { provide: 'SignupService', useExisting: SignupService }; const $SignupService: Provider = { provide: 'SignupService', useExisting: SignupService };
const $TwoFactorAuthenticationService: Provider = { provide: 'TwoFactorAuthenticationService', useExisting: TwoFactorAuthenticationService }; const $TwoFactorAuthenticationService: Provider = { provide: 'TwoFactorAuthenticationService', useExisting: TwoFactorAuthenticationService };
const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService }; const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService };
const $UserCacheService: Provider = { provide: 'UserCacheService', useExisting: UserCacheService }; const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService };
const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService }; const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService };
const $UserKeypairStoreService: Provider = { provide: 'UserKeypairStoreService', useExisting: UserKeypairStoreService }; const $UserKeypairStoreService: Provider = { provide: 'UserKeypairStoreService', useExisting: UserKeypairStoreService };
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService }; const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
@ -250,7 +248,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AppLockService, AppLockService,
AchievementService, AchievementService,
CaptchaService, CaptchaService,
CreateNotificationService,
CreateSystemUserService, CreateSystemUserService,
CustomEmojiService, CustomEmojiService,
DeleteAccountService, DeleteAccountService,
@ -285,7 +282,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
SignupService, SignupService,
TwoFactorAuthenticationService, TwoFactorAuthenticationService,
UserBlockingService, UserBlockingService,
UserCacheService, CacheService,
UserFollowingService, UserFollowingService,
UserKeypairStoreService, UserKeypairStoreService,
UserListService, UserListService,
@ -368,7 +365,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AppLockService, $AppLockService,
$AchievementService, $AchievementService,
$CaptchaService, $CaptchaService,
$CreateNotificationService,
$CreateSystemUserService, $CreateSystemUserService,
$CustomEmojiService, $CustomEmojiService,
$DeleteAccountService, $DeleteAccountService,
@ -403,7 +399,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$SignupService, $SignupService,
$TwoFactorAuthenticationService, $TwoFactorAuthenticationService,
$UserBlockingService, $UserBlockingService,
$UserCacheService, $CacheService,
$UserFollowingService, $UserFollowingService,
$UserKeypairStoreService, $UserKeypairStoreService,
$UserListService, $UserListService,
@ -487,7 +483,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AppLockService, AppLockService,
AchievementService, AchievementService,
CaptchaService, CaptchaService,
CreateNotificationService,
CreateSystemUserService, CreateSystemUserService,
CustomEmojiService, CustomEmojiService,
DeleteAccountService, DeleteAccountService,
@ -522,7 +517,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
SignupService, SignupService,
TwoFactorAuthenticationService, TwoFactorAuthenticationService,
UserBlockingService, UserBlockingService,
UserCacheService, CacheService,
UserFollowingService, UserFollowingService,
UserKeypairStoreService, UserKeypairStoreService,
UserListService, UserListService,
@ -604,7 +599,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AppLockService, $AppLockService,
$AchievementService, $AchievementService,
$CaptchaService, $CaptchaService,
$CreateNotificationService,
$CreateSystemUserService, $CreateSystemUserService,
$CustomEmojiService, $CustomEmojiService,
$DeleteAccountService, $DeleteAccountService,
@ -639,7 +633,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$SignupService, $SignupService,
$TwoFactorAuthenticationService, $TwoFactorAuthenticationService,
$UserBlockingService, $UserBlockingService,
$UserCacheService, $CacheService,
$UserFollowingService, $UserFollowingService,
$UserKeypairStoreService, $UserKeypairStoreService,
$UserListService, $UserListService,
@ -714,4 +708,4 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
//#endregion //#endregion
], ],
}) })
export class CoreModule {} export class CoreModule { }

View File

@ -1,125 +0,0 @@
import { setTimeout } from 'node:timers/promises';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import type { MutingsRepository, NotificationsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
import type { User } from '@/models/entities/User.js';
import type { Notification } from '@/models/entities/Notification.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js';
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
import { PushNotificationService } from '@/core/PushNotificationService.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class CreateNotificationService implements OnApplicationShutdown {
#shutdownController = new AbortController();
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
@Inject(DI.notificationsRepository)
private notificationsRepository: NotificationsRepository,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
private notificationEntityService: NotificationEntityService,
private idService: IdService,
private globalEventService: GlobalEventService,
private pushNotificationService: PushNotificationService,
) {
}
@bindThis
public async createNotification(
notifieeId: User['id'],
type: Notification['type'],
data: Partial<Notification>,
): Promise<Notification | null> {
if (data.notifierId && (notifieeId === data.notifierId)) {
return null;
}
const profile = await this.userProfilesRepository.findOneBy({ userId: notifieeId });
const isMuted = profile?.mutingNotificationTypes.includes(type);
// Create notification
const notification = await this.notificationsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
notifieeId: notifieeId,
type: type,
// 相手がこの通知をミュートしているようなら、既読を予めつけておく
isRead: isMuted,
...data,
} as Partial<Notification>)
.then(x => this.notificationsRepository.findOneByOrFail(x.identifiers[0]));
const packed = await this.notificationEntityService.pack(notification, {});
// Publish notification event
this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
const fresh = await this.notificationsRepository.findOneBy({ id: notification.id });
if (fresh == null) return; // 既に削除されているかもしれない
if (fresh.isRead) return;
//#region ただしミュートしているユーザーからの通知なら無視
const mutings = await this.mutingsRepository.findBy({
muterId: notifieeId,
});
if (data.notifierId && mutings.map(m => m.muteeId).includes(data.notifierId)) {
return;
}
//#endregion
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! }));
if (type === 'receiveFollowRequest') this.emailNotificationReceiveFollowRequest(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! }));
}, () => { /* aborted, ignore it */ });
return notification;
}
// TODO
//const locales = await import('../../../../locales/index.js');
// TODO: locale ファイルをクライアント用とサーバー用で分けたい
@bindThis
private async emailNotificationFollow(userId: User['id'], follower: User) {
/*
const userProfile = await UserProfiles.findOneByOrFail({ userId: userId });
if (!userProfile.email || !userProfile.emailNotificationTypes.includes('follow')) return;
const locale = locales[userProfile.lang ?? 'ja-JP'];
const i18n = new I18n(locale);
// TODO: render user information html
sendEmail(userProfile.email, i18n.t('_email._follow.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`);
*/
}
@bindThis
private async emailNotificationReceiveFollowRequest(userId: User['id'], follower: User) {
/*
const userProfile = await UserProfiles.findOneByOrFail({ userId: userId });
if (!userProfile.email || !userProfile.emailNotificationTypes.includes('receiveFollowRequest')) return;
const locale = locales[userProfile.lang ?? 'ja-JP'];
const i18n = new I18n(locale);
// TODO: render user information html
sendEmail(userProfile.email, i18n.t('_email._receiveFollowRequest.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`);
*/
}
onApplicationShutdown(signal?: string | undefined): void {
this.#shutdownController.abort();
}
}

View File

@ -8,7 +8,7 @@ import type { DriveFile } from '@/models/entities/DriveFile.js';
import type { Emoji } from '@/models/entities/Emoji.js'; import type { Emoji } from '@/models/entities/Emoji.js';
import type { EmojisRepository, Note } from '@/models/index.js'; import type { EmojisRepository, Note } from '@/models/index.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { Cache } from '@/misc/cache.js'; import { MemoryKVCache } from '@/misc/cache.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { ReactionService } from '@/core/ReactionService.js'; import { ReactionService } from '@/core/ReactionService.js';
@ -16,7 +16,7 @@ import { query } from '@/misc/prelude/url.js';
@Injectable() @Injectable()
export class CustomEmojiService { export class CustomEmojiService {
private cache: Cache<Emoji | null>; private cache: MemoryKVCache<Emoji | null>;
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
@ -34,7 +34,7 @@ export class CustomEmojiService {
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private reactionService: ReactionService, private reactionService: ReactionService,
) { ) {
this.cache = new Cache<Emoji | null>(1000 * 60 * 60 * 12); this.cache = new MemoryKVCache<Emoji | null>(1000 * 60 * 60 * 12);
} }
@bindThis @bindThis
@ -44,6 +44,7 @@ export class CustomEmojiService {
category: string | null; category: string | null;
aliases: string[]; aliases: string[];
host: string | null; host: string | null;
license: string | null;
}): Promise<Emoji> { }): Promise<Emoji> {
const emoji = await this.emojisRepository.insert({ const emoji = await this.emojisRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
@ -55,10 +56,11 @@ export class CustomEmojiService {
originalUrl: data.driveFile.url, originalUrl: data.driveFile.url,
publicUrl: data.driveFile.webpublicUrl ?? data.driveFile.url, publicUrl: data.driveFile.webpublicUrl ?? data.driveFile.url,
type: data.driveFile.webpublicType ?? data.driveFile.type, type: data.driveFile.webpublicType ?? data.driveFile.type,
license: data.license,
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
if (data.host == null) { if (data.host == null) {
await this.db.queryResultCache!.remove(['meta_emojis']); await this.db.queryResultCache?.remove(['meta_emojis']);
this.globalEventService.publishBroadcastStream('emojiAdded', { this.globalEventService.publishBroadcastStream('emojiAdded', {
emoji: await this.emojiEntityService.packDetailed(emoji.id), emoji: await this.emojiEntityService.packDetailed(emoji.id),

View File

@ -2,7 +2,9 @@ import * as fs from 'node:fs';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import sharp from 'sharp'; import sharp from 'sharp';
import { sharpBmp } from 'sharp-read-bmp';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/index.js'; import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/index.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
@ -33,8 +35,8 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { FileInfoService } from '@/core/FileInfoService.js'; import { FileInfoService } from '@/core/FileInfoService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import type S3 from 'aws-sdk/clients/s3.js';
import { correctFilename } from '@/misc/correct-filename.js'; import { correctFilename } from '@/misc/correct-filename.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
type AddFileArgs = { type AddFileArgs = {
/** User who wish to add file */ /** User who wish to add file */
@ -79,6 +81,7 @@ type UploadFromUrlArgs = {
export class DriveService { export class DriveService {
private registerLogger: Logger; private registerLogger: Logger;
private downloaderLogger: Logger; private downloaderLogger: Logger;
private deleteLogger: Logger;
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
@ -116,6 +119,7 @@ export class DriveService {
const logger = new Logger('drive', 'blue'); const logger = new Logger('drive', 'blue');
this.registerLogger = logger.createSubLogger('register', 'yellow'); this.registerLogger = logger.createSubLogger('register', 'yellow');
this.downloaderLogger = logger.createSubLogger('downloader'); this.downloaderLogger = logger.createSubLogger('downloader');
this.deleteLogger = logger.createSubLogger('delete');
} }
/*** /***
@ -274,8 +278,8 @@ export class DriveService {
} }
} }
if (!['image/jpeg', 'image/png', 'image/webp', 'image/avif', 'image/svg+xml'].includes(type)) { if (!isMimeImage(type, 'sharp-convertible-image-with-bmp')) {
this.registerLogger.debug('web image and thumbnail not created (not an required file)'); this.registerLogger.debug('web image and thumbnail not created (cannot convert by sharp)');
return { return {
webpublic: null, webpublic: null,
thumbnail: null, thumbnail: null,
@ -284,22 +288,16 @@ export class DriveService {
let img: sharp.Sharp | null = null; let img: sharp.Sharp | null = null;
let satisfyWebpublic: boolean; let satisfyWebpublic: boolean;
let isAnimated: boolean;
try { try {
img = sharp(path); img = await sharpBmp(path, type);
const metadata = await img.metadata(); const metadata = await img.metadata();
const isAnimated = metadata.pages && metadata.pages > 1; isAnimated = !!(metadata.pages && metadata.pages > 1);
// skip animated
if (isAnimated) {
return {
webpublic: null,
thumbnail: null,
};
}
satisfyWebpublic = !!( satisfyWebpublic = !!(
type !== 'image/svg+xml' && type !== 'image/webp' && type !== 'image/avif' && type !== 'image/svg+xml' && // security reason
type !== 'image/avif' && // not supported by Mastodon and MS Edge
!(metadata.exif ?? metadata.iptc ?? metadata.xmp ?? metadata.tifftagPhotoshop) && !(metadata.exif ?? metadata.iptc ?? metadata.xmp ?? metadata.tifftagPhotoshop) &&
metadata.width && metadata.width <= 2048 && metadata.width && metadata.width <= 2048 &&
metadata.height && metadata.height <= 2048 metadata.height && metadata.height <= 2048
@ -315,15 +313,13 @@ export class DriveService {
// #region webpublic // #region webpublic
let webpublic: IImage | null = null; let webpublic: IImage | null = null;
if (generateWeb && !satisfyWebpublic) { if (generateWeb && !satisfyWebpublic && !isAnimated) {
this.registerLogger.info('creating web image'); this.registerLogger.info('creating web image');
try { try {
if (['image/jpeg', 'image/webp', 'image/avif'].includes(type)) { if (['image/jpeg', 'image/webp', 'image/avif'].includes(type)) {
webpublic = await this.imageProcessingService.convertSharpToJpeg(img, 2048, 2048); webpublic = await this.imageProcessingService.convertSharpToWebp(img, 2048, 2048);
} else if (['image/png'].includes(type)) { } else if (['image/png', 'image/bmp', 'image/svg+xml'].includes(type)) {
webpublic = await this.imageProcessingService.convertSharpToPng(img, 2048, 2048);
} else if (['image/svg+xml'].includes(type)) {
webpublic = await this.imageProcessingService.convertSharpToPng(img, 2048, 2048); webpublic = await this.imageProcessingService.convertSharpToPng(img, 2048, 2048);
} else { } else {
this.registerLogger.debug('web image not created (not an required image)'); this.registerLogger.debug('web image not created (not an required image)');
@ -333,6 +329,7 @@ export class DriveService {
} }
} else { } else {
if (satisfyWebpublic) this.registerLogger.info('web image not created (original satisfies webpublic)'); if (satisfyWebpublic) this.registerLogger.info('web image not created (original satisfies webpublic)');
else if (isAnimated) this.registerLogger.info('web image not created (animated image)');
else this.registerLogger.info('web image not created (from remote)'); else this.registerLogger.info('web image not created (from remote)');
} }
// #endregion webpublic // #endregion webpublic
@ -341,10 +338,10 @@ export class DriveService {
let thumbnail: IImage | null = null; let thumbnail: IImage | null = null;
try { try {
if (['image/jpeg', 'image/webp', 'image/avif', 'image/png', 'image/svg+xml'].includes(type)) { if (isAnimated) {
thumbnail = await this.imageProcessingService.convertSharpToWebp(img, 498, 280); thumbnail = await this.imageProcessingService.convertSharpToWebp(sharp(path, { animated: true }), 374, 317, { alphaQuality: 70 });
} else { } else {
this.registerLogger.debug('thumbnail not created (not an required file)'); thumbnail = await this.imageProcessingService.convertSharpToWebp(img, 498, 422);
} }
} catch (err) { } catch (err) {
this.registerLogger.warn('thumbnail not created (an error occured)', err as Error); this.registerLogger.warn('thumbnail not created (an error occured)', err as Error);
@ -373,7 +370,7 @@ export class DriveService {
Body: stream, Body: stream,
ContentType: type, ContentType: type,
CacheControl: 'max-age=31536000, immutable', CacheControl: 'max-age=31536000, immutable',
} as S3.PutObjectRequest; } as PutObjectCommandInput;
if (filename) params.ContentDisposition = contentDisposition( if (filename) params.ContentDisposition = contentDisposition(
'inline', 'inline',
@ -383,21 +380,16 @@ export class DriveService {
); );
if (meta.objectStorageSetPublicRead) params.ACL = 'public-read'; if (meta.objectStorageSetPublicRead) params.ACL = 'public-read';
const s3 = this.s3Service.getS3(meta); await this.s3Service.upload(meta, params)
const upload = s3.upload(params, {
partSize: s3.endpoint.hostname === 'storage.googleapis.com' ? 500 * 1024 * 1024 : 8 * 1024 * 1024,
});
await upload.promise()
.then( .then(
result => { result => {
if (result) { if ('Bucket' in result) { // CompleteMultipartUploadCommandOutput
this.registerLogger.debug(`Uploaded: ${result.Bucket}/${result.Key} => ${result.Location}`); this.registerLogger.debug(`Uploaded: ${result.Bucket}/${result.Key} => ${result.Location}`);
} else { } else { // AbortMultipartUploadCommandOutput
this.registerLogger.error(`Upload Result Empty: key = ${key}, filename = ${filename}`); this.registerLogger.error(`Upload Result Aborted: key = ${key}, filename = ${filename}`);
} }
}, })
.catch(
err => { err => {
this.registerLogger.error(`Upload Failed: key = ${key}, filename = ${filename}`, err); this.registerLogger.error(`Upload Failed: key = ${key}, filename = ${filename}`, err);
}, },
@ -476,7 +468,7 @@ export class DriveService {
// DriveFile.nameは256文字, validateFileNameは200文字制限であるため、 // DriveFile.nameは256文字, validateFileNameは200文字制限であるため、
// extを付加してデータベースの文字数制限に当たることはまずない // extを付加してデータベースの文字数制限に当たることはまずない
(name && this.driveFileEntityService.validateFileName(name)) ? name : 'untitled', (name && this.driveFileEntityService.validateFileName(name)) ? name : 'untitled',
info.type.ext info.type.ext,
); );
if (user && !force) { if (user && !force) {
@ -533,10 +525,10 @@ export class DriveService {
}; };
const properties: { const properties: {
width?: number; width?: number;
height?: number; height?: number;
orientation?: number; orientation?: number;
} = {}; } = {};
if (info.width) { if (info.width) {
properties['width'] = info.width; properties['width'] = info.width;
@ -621,17 +613,20 @@ export class DriveService {
if (user) { if (user) {
this.driveFileEntityService.pack(file, { self: true }).then(packedFile => { this.driveFileEntityService.pack(file, { self: true }).then(packedFile => {
// Publish driveFileCreated event // Publish driveFileCreated event
this.globalEventService.publishMainStream(user.id, 'driveFileCreated', packedFile); this.globalEventService.publishMainStream(user.id, 'driveFileCreated', packedFile);
this.globalEventService.publishDriveStream(user.id, 'fileCreated', packedFile); this.globalEventService.publishDriveStream(user.id, 'fileCreated', packedFile);
}); });
} }
// 統計を更新
this.driveChart.update(file, true); this.driveChart.update(file, true);
this.perUserDriveChart.update(file, true); if (file.userHost == null) {
if (file.userHost !== null) { // ローカルユーザーのみ
this.instanceChart.updateDrive(file, true); this.perUserDriveChart.update(file, true);
} else {
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateDrive(file, true);
}
} }
return file; return file;
@ -697,7 +692,7 @@ export class DriveService {
@bindThis @bindThis
private async deletePostProcess(file: DriveFile, isExpired = false) { private async deletePostProcess(file: DriveFile, isExpired = false) {
// リモートファイル期限切れ削除後は直リンクにする // リモートファイル期限切れ削除後は直リンクにする
if (isExpired && file.userHost !== null && file.uri != null) { if (isExpired && file.userHost !== null && file.uri != null) {
this.driveFilesRepository.update(file.id, { this.driveFilesRepository.update(file.id, {
isLink: true, isLink: true,
@ -714,24 +709,37 @@ export class DriveService {
this.driveFilesRepository.delete(file.id); this.driveFilesRepository.delete(file.id);
} }
// 統計を更新
this.driveChart.update(file, false); this.driveChart.update(file, false);
this.perUserDriveChart.update(file, false); if (file.userHost == null) {
if (file.userHost !== null) { // ローカルユーザーのみ
this.instanceChart.updateDrive(file, false); this.perUserDriveChart.update(file, false);
} else {
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateDrive(file, false);
}
} }
} }
@bindThis @bindThis
public async deleteObjectStorageFile(key: string) { public async deleteObjectStorageFile(key: string) {
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
try {
const param = {
Bucket: meta.objectStorageBucket,
Key: key,
} as DeleteObjectCommandInput;
const s3 = this.s3Service.getS3(meta); await this.s3Service.delete(meta, param);
} catch (err: any) {
await s3.deleteObject({ if (err.name === 'NoSuchKey') {
Bucket: meta.objectStorageBucket!, this.deleteLogger.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err as Error);
Key: key, return;
}).promise(); } else {
throw new Error(`Failed to delete the file from the object storage with the given key: ${key}`, {
cause: err,
});
}
}
} }
@bindThis @bindThis

View File

@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { InstancesRepository } from '@/models/index.js'; import type { InstancesRepository } from '@/models/index.js';
import type { Instance } from '@/models/entities/Instance.js'; import type { Instance } from '@/models/entities/Instance.js';
import { Cache } from '@/misc/cache.js'; import { MemoryKVCache } from '@/misc/cache.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
@ -9,7 +9,7 @@ import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class FederatedInstanceService { export class FederatedInstanceService {
private cache: Cache<Instance>; private cache: MemoryKVCache<Instance>;
constructor( constructor(
@Inject(DI.instancesRepository) @Inject(DI.instancesRepository)
@ -18,7 +18,7 @@ export class FederatedInstanceService {
private utilityService: UtilityService, private utilityService: UtilityService,
private idService: IdService, private idService: IdService,
) { ) {
this.cache = new Cache<Instance>(1000 * 60 * 60); this.cache = new MemoryKVCache<Instance>(1000 * 60 * 60);
} }
@bindThis @bindThis

View File

@ -16,7 +16,7 @@ import type {
UserListStreamTypes, UserListStreamTypes,
UserStreamTypes, UserStreamTypes,
} from '@/server/api/stream/types.js'; } from '@/server/api/stream/types.js';
import type { Packed } from '@/misc/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';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';

View File

@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { ulid } from 'ulid'; import { ulid } from 'ulid';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { genAid } from '@/misc/id/aid.js'; import { genAid, parseAid } from '@/misc/id/aid.js';
import { genMeid } from '@/misc/id/meid.js'; import { genMeid } from '@/misc/id/meid.js';
import { genMeidg } from '@/misc/id/meidg.js'; import { genMeidg } from '@/misc/id/meidg.js';
import { genObjectId } from '@/misc/id/object-id.js'; import { genObjectId } from '@/misc/id/object-id.js';
@ -32,4 +32,17 @@ export class IdService {
default: throw new Error('unrecognized id generation method'); default: throw new Error('unrecognized id generation method');
} }
} }
@bindThis
public parse(id: string): { date: Date; } {
switch (this.method) {
case 'aid': return parseAid(id);
// TODO
//case 'meid':
//case 'meidg':
//case 'ulid':
//case 'objectid':
default: throw new Error('unrecognized id generation method');
}
}
} }

View File

@ -15,15 +15,28 @@ export type IImageStream = {
type: string; type: string;
}; };
export type IImageStreamable = IImage | IImageStream; export type IImageSharp = {
data: sharp.Sharp;
ext: string | null;
type: string;
};
export type IImageStreamable = IImage | IImageStream | IImageSharp;
export const webpDefault: sharp.WebpOptions = { export const webpDefault: sharp.WebpOptions = {
quality: 85, quality: 77,
alphaQuality: 95, alphaQuality: 95,
lossless: false, lossless: false,
nearLossless: false, nearLossless: false,
smartSubsample: true, smartSubsample: true,
mixed: true, mixed: true,
effort: 2,
};
export const avifDefault: sharp.AvifOptions = {
quality: 60,
lossless: false,
effort: 2,
}; };
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@ -37,36 +50,6 @@ export class ImageProcessingService {
) { ) {
} }
/**
* Convert to JPEG
* with resize, remove metadata, resolve orientation, stop animation
*/
@bindThis
public async convertToJpeg(path: string, width: number, height: number): Promise<IImage> {
return this.convertSharpToJpeg(await sharp(path), width, height);
}
@bindThis
public async convertSharpToJpeg(sharp: sharp.Sharp, width: number, height: number): Promise<IImage> {
const data = await sharp
.resize(width, height, {
fit: 'inside',
withoutEnlargement: true,
})
.rotate()
.jpeg({
quality: 85,
progressive: true,
})
.toBuffer();
return {
data,
ext: 'jpg',
type: 'image/jpeg',
};
}
/** /**
* Convert to WebP * Convert to WebP
* with resize, remove metadata, resolve orientation, stop animation * with resize, remove metadata, resolve orientation, stop animation
@ -78,29 +61,22 @@ export class ImageProcessingService {
@bindThis @bindThis
public async convertSharpToWebp(sharp: sharp.Sharp, width: number, height: number, options: sharp.WebpOptions = webpDefault): Promise<IImage> { public async convertSharpToWebp(sharp: sharp.Sharp, width: number, height: number, options: sharp.WebpOptions = webpDefault): Promise<IImage> {
const data = await sharp const result = this.convertSharpToWebpStream(sharp, width, height, options);
.resize(width, height, {
fit: 'inside',
withoutEnlargement: true,
})
.rotate()
.webp(options)
.toBuffer();
return { return {
data, data: await result.data.toBuffer(),
ext: 'webp', ext: result.ext,
type: 'image/webp', type: result.type,
}; };
} }
@bindThis @bindThis
public convertToWebpStream(path: string, width: number, height: number, options: sharp.WebpOptions = webpDefault): IImageStream { public convertToWebpStream(path: string, width: number, height: number, options: sharp.WebpOptions = webpDefault): IImageSharp {
return this.convertSharpToWebpStream(sharp(path), width, height, options); return this.convertSharpToWebpStream(sharp(path), width, height, options);
} }
@bindThis @bindThis
public convertSharpToWebpStream(sharp: sharp.Sharp, width: number, height: number, options: sharp.WebpOptions = webpDefault): IImageStream { public convertSharpToWebpStream(sharp: sharp.Sharp, width: number, height: number, options: sharp.WebpOptions = webpDefault): IImageSharp {
const data = sharp const data = sharp
.resize(width, height, { .resize(width, height, {
fit: 'inside', fit: 'inside',
@ -115,13 +91,56 @@ export class ImageProcessingService {
type: 'image/webp', type: 'image/webp',
}; };
} }
/**
* Convert to Avif
* with resize, remove metadata, resolve orientation, stop animation
*/
@bindThis
public async convertToAvif(path: string, width: number, height: number, options: sharp.AvifOptions = avifDefault): Promise<IImage> {
return this.convertSharpToAvif(sharp(path), width, height, options);
}
@bindThis
public async convertSharpToAvif(sharp: sharp.Sharp, width: number, height: number, options: sharp.AvifOptions = avifDefault): Promise<IImage> {
const result = this.convertSharpToAvifStream(sharp, width, height, options);
return {
data: await result.data.toBuffer(),
ext: result.ext,
type: result.type,
};
}
@bindThis
public convertToAvifStream(path: string, width: number, height: number, options: sharp.AvifOptions = avifDefault): IImageSharp {
return this.convertSharpToAvifStream(sharp(path), width, height, options);
}
@bindThis
public convertSharpToAvifStream(sharp: sharp.Sharp, width: number, height: number, options: sharp.AvifOptions = avifDefault): IImageSharp {
const data = sharp
.resize(width, height, {
fit: 'inside',
withoutEnlargement: true,
})
.rotate()
.avif(options);
return {
data,
ext: 'avif',
type: 'image/avif',
};
}
/** /**
* Convert to PNG * Convert to PNG
* with resize, remove metadata, resolve orientation, stop animation * with resize, remove metadata, resolve orientation, stop animation
*/ */
@bindThis @bindThis
public async convertToPng(path: string, width: number, height: number): Promise<IImage> { public async convertToPng(path: string, width: number, height: number): Promise<IImage> {
return this.convertSharpToPng(await sharp(path), width, height); return this.convertSharpToPng(sharp(path), width, height);
} }
@bindThis @bindThis

View File

@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import type { LocalUser } from '@/models/entities/User.js'; import type { LocalUser } from '@/models/entities/User.js';
import type { UsersRepository } from '@/models/index.js'; import type { UsersRepository } from '@/models/index.js';
import { Cache } from '@/misc/cache.js'; import { MemoryCache } from '@/misc/cache.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js'; import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@ -11,7 +11,7 @@ const ACTOR_USERNAME = 'instance.actor' as const;
@Injectable() @Injectable()
export class InstanceActorService { export class InstanceActorService {
private cache: Cache<LocalUser>; private cache: MemoryCache<LocalUser>;
constructor( constructor(
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
@ -19,12 +19,12 @@ export class InstanceActorService {
private createSystemUserService: CreateSystemUserService, private createSystemUserService: CreateSystemUserService,
) { ) {
this.cache = new Cache<LocalUser>(Infinity); this.cache = new MemoryCache<LocalUser>(Infinity);
} }
@bindThis @bindThis
public async getInstanceActor(): Promise<LocalUser> { public async getInstanceActor(): Promise<LocalUser> {
const cached = this.cache.get(null); const cached = this.cache.get();
if (cached) return cached; if (cached) return cached;
const user = await this.usersRepository.findOneBy({ const user = await this.usersRepository.findOneBy({
@ -33,11 +33,11 @@ export class InstanceActorService {
}) as LocalUser | undefined; }) as LocalUser | undefined;
if (user) { if (user) {
this.cache.set(null, user); this.cache.set(user);
return user; return user;
} else { } else {
const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME) as LocalUser; const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME) as LocalUser;
this.cache.set(null, created); this.cache.set(created);
return created; return created;
} }
} }

View File

@ -1,6 +1,7 @@
import { setImmediate } from 'node:timers/promises'; import { setImmediate } from 'node:timers/promises';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import { In, DataSource } from 'typeorm'; import { In, DataSource } from 'typeorm';
import Redis from 'ioredis';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { extractMentions } from '@/misc/extract-mentions.js'; import { extractMentions } from '@/misc/extract-mentions.js';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
@ -19,7 +20,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js
import { checkWordMute } from '@/misc/check-word-mute.js'; import { checkWordMute } from '@/misc/check-word-mute.js';
import type { Channel } from '@/models/entities/Channel.js'; import type { Channel } from '@/models/entities/Channel.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { Cache } from '@/misc/cache.js'; import { MemoryCache } from '@/misc/cache.js';
import type { UserProfile } from '@/models/entities/UserProfile.js'; import type { UserProfile } from '@/models/entities/UserProfile.js';
import { RelayService } from '@/core/RelayService.js'; import { RelayService } from '@/core/RelayService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
@ -30,7 +31,7 @@ import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js';
import InstanceChart from '@/core/chart/charts/instance.js'; import InstanceChart from '@/core/chart/charts/instance.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { CreateNotificationService } from '@/core/CreateNotificationService.js'; import { NotificationService } from '@/core/NotificationService.js';
import { WebhookService } from '@/core/WebhookService.js'; import { WebhookService } from '@/core/WebhookService.js';
import { HashtagService } from '@/core/HashtagService.js'; import { HashtagService } from '@/core/HashtagService.js';
import { AntennaService } from '@/core/AntennaService.js'; import { AntennaService } from '@/core/AntennaService.js';
@ -44,8 +45,9 @@ import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.js';
const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); const mutedWordsCache = new MemoryCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@ -59,7 +61,7 @@ class NotificationManager {
constructor( constructor(
private mutingsRepository: MutingsRepository, private mutingsRepository: MutingsRepository,
private createNotificationService: CreateNotificationService, private notificationService: NotificationService,
notifier: { id: User['id']; }, notifier: { id: User['id']; },
note: Note, note: Note,
) { ) {
@ -100,7 +102,7 @@ class NotificationManager {
// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する // 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
if (!mentioneesMutedUserIds.includes(this.notifier.id)) { if (!mentioneesMutedUserIds.includes(this.notifier.id)) {
this.createNotificationService.createNotification(x.target, x.reason, { this.notificationService.createNotification(x.target, x.reason, {
notifierId: this.notifier.id, notifierId: this.notifier.id,
noteId: this.note.id, noteId: this.note.id,
}); });
@ -149,6 +151,9 @@ export class NoteCreateService implements OnApplicationShutdown {
@Inject(DI.db) @Inject(DI.db)
private db: DataSource, private db: DataSource,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -182,7 +187,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private queueService: QueueService, private queueService: QueueService,
private noteReadService: NoteReadService, private noteReadService: NoteReadService,
private createNotificationService: CreateNotificationService, private notificationService: NotificationService,
private relayService: RelayService, private relayService: RelayService,
private federatedInstanceService: FederatedInstanceService, private federatedInstanceService: FederatedInstanceService,
private hashtagService: HashtagService, private hashtagService: HashtagService,
@ -192,11 +197,12 @@ export class NoteCreateService implements OnApplicationShutdown {
private apDeliverManagerService: ApDeliverManagerService, private apDeliverManagerService: ApDeliverManagerService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private roleService: RoleService, private roleService: RoleService,
private metaService: MetaService,
private notesChart: NotesChart, private notesChart: NotesChart,
private perUserNotesChart: PerUserNotesChart, private perUserNotesChart: PerUserNotesChart,
private activeUsersChart: ActiveUsersChart, private activeUsersChart: ActiveUsersChart,
private instanceChart: InstanceChart, private instanceChart: InstanceChart,
) {} ) { }
@bindThis @bindThis
public async create(user: { public async create(user: {
@ -230,7 +236,9 @@ export class NoteCreateService implements OnApplicationShutdown {
if (data.channel != null) data.localOnly = true; if (data.channel != null) data.localOnly = true;
if (data.visibility === 'public' && data.channel == null) { if (data.visibility === 'public' && data.channel == null) {
if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) { if ((data.text != null) && (await this.metaService.fetch()).sensitiveWords.some(w => data.text!.includes(w))) {
data.visibility = 'home';
} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
data.visibility = 'home'; data.visibility = 'home';
} }
} }
@ -317,6 +325,14 @@ export class NoteCreateService implements OnApplicationShutdown {
const note = await this.insertNote(user, data, tags, emojis, mentionedUsers); const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
if (data.channel) {
this.redisClient.xadd(
`channelTimeline:${data.channel.id}`,
'MAXLEN', '~', '1000',
`${this.idService.parse(note.id).date.getTime()}-*`,
'note', note.id);
}
setImmediate('post created', { signal: this.#shutdownController.signal }).then( setImmediate('post created', { signal: this.#shutdownController.signal }).then(
() => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!), () => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!),
() => { /* aborted, ignore this */ }, () => { /* aborted, ignore this */ },
@ -387,7 +403,7 @@ export class NoteCreateService implements OnApplicationShutdown {
// 投稿を作成 // 投稿を作成
try { try {
if (insert.hasPoll) { if (insert.hasPoll) {
// Start transaction // Start transaction
await this.db.transaction(async transactionalEntityManager => { await this.db.transaction(async transactionalEntityManager => {
await transactionalEntityManager.insert(Note, insert); await transactionalEntityManager.insert(Note, insert);
@ -410,7 +426,7 @@ export class NoteCreateService implements OnApplicationShutdown {
return insert; return insert;
} catch (e) { } catch (e) {
// duplicate key error // duplicate key error
if (isDuplicateKeyValueError(e)) { if (isDuplicateKeyValueError(e)) {
const err = new Error('Duplicated note'); const err = new Error('Duplicated note');
err.name = 'duplicated'; err.name = 'duplicated';
@ -431,15 +447,20 @@ export class NoteCreateService implements OnApplicationShutdown {
createdAt: User['createdAt']; createdAt: User['createdAt'];
isBot: User['isBot']; isBot: User['isBot'];
}, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) { }, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) {
// 統計を更新 const meta = await this.metaService.fetch();
this.notesChart.update(note, true); this.notesChart.update(note, true);
this.perUserNotesChart.update(user, note, true); if (meta.enableChartsForRemoteUser || (user.host == null)) {
this.perUserNotesChart.update(user, note, true);
}
// Register host // Register host
if (this.userEntityService.isRemoteUser(user)) { if (this.userEntityService.isRemoteUser(user)) {
this.federatedInstanceService.fetch(user.host).then(i => { this.federatedInstanceService.fetch(user.host).then(async i => {
this.instancesRepository.increment({ id: i.id }, 'notesCount', 1); this.instancesRepository.increment({ id: i.id }, 'notesCount', 1);
this.instanceChart.updateNote(i.host, note, true); if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateNote(i.host, note, true);
}
}); });
} }
@ -452,7 +473,7 @@ export class NoteCreateService implements OnApplicationShutdown {
this.incNotesCountOfUser(user); this.incNotesCountOfUser(user);
// Word mute // Word mute
mutedWordsCache.fetch(null, () => this.userProfilesRepository.find({ mutedWordsCache.fetch(() => this.userProfilesRepository.find({
where: { where: {
enableWordMute: true, enableWordMute: true,
}, },
@ -554,7 +575,7 @@ export class NoteCreateService implements OnApplicationShutdown {
} }
}); });
const nm = new NotificationManager(this.mutingsRepository, this.createNotificationService, user, note); const nm = new NotificationManager(this.mutingsRepository, this.notificationService, user, note);
await this.createMentionedEvents(mentionedUsers, note, nm); await this.createMentionedEvents(mentionedUsers, note, nm);

View File

@ -16,6 +16,7 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
@Injectable() @Injectable()
export class NoteDeleteService { export class NoteDeleteService {
@ -39,6 +40,7 @@ export class NoteDeleteService {
private federatedInstanceService: FederatedInstanceService, private federatedInstanceService: FederatedInstanceService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private apDeliverManagerService: ApDeliverManagerService, private apDeliverManagerService: ApDeliverManagerService,
private metaService: MetaService,
private notesChart: NotesChart, private notesChart: NotesChart,
private perUserNotesChart: PerUserNotesChart, private perUserNotesChart: PerUserNotesChart,
private instanceChart: InstanceChart, private instanceChart: InstanceChart,
@ -95,14 +97,19 @@ export class NoteDeleteService {
} }
//#endregion //#endregion
// 統計を更新 const meta = await this.metaService.fetch();
this.notesChart.update(note, false); this.notesChart.update(note, false);
this.perUserNotesChart.update(user, note, false); if (meta.enableChartsForRemoteUser || (user.host == null)) {
this.perUserNotesChart.update(user, note, false);
}
if (this.userEntityService.isRemoteUser(user)) { if (this.userEntityService.isRemoteUser(user)) {
this.federatedInstanceService.fetch(user.host).then(i => { this.federatedInstanceService.fetch(user.host).then(async i => {
this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1); this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1);
this.instanceChart.updateNote(i.host, note, false); if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateNote(i.host, note, false);
}
}); });
} }
} }

View File

@ -4,11 +4,11 @@ import { In, IsNull, Not } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import type { Channel } from '@/models/entities/Channel.js'; import type { Channel } from '@/models/entities/Channel.js';
import type { Packed } from '@/misc/schema.js'; import type { Packed } from '@/misc/json-schema.js';
import type { Note } from '@/models/entities/Note.js'; import type { Note } from '@/models/entities/Note.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { UsersRepository, NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository, FollowingsRepository, ChannelFollowingsRepository, AntennaNotesRepository } from '@/models/index.js'; import type { UsersRepository, NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository, FollowingsRepository, ChannelFollowingsRepository } from '@/models/index.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 { NotificationService } from './NotificationService.js'; import { NotificationService } from './NotificationService.js';
@ -38,9 +38,6 @@ export class NoteReadService implements OnApplicationShutdown {
@Inject(DI.channelFollowingsRepository) @Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository, private channelFollowingsRepository: ChannelFollowingsRepository,
@Inject(DI.antennaNotesRepository)
private antennaNotesRepository: AntennaNotesRepository,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private idService: IdService, private idService: IdService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
@ -121,7 +118,6 @@ export class NoteReadService implements OnApplicationShutdown {
const readMentions: (Note | Packed<'Note'>)[] = []; const readMentions: (Note | Packed<'Note'>)[] = [];
const readSpecifiedNotes: (Note | Packed<'Note'>)[] = []; const readSpecifiedNotes: (Note | Packed<'Note'>)[] = [];
const readChannelNotes: (Note | Packed<'Note'>)[] = []; const readChannelNotes: (Note | Packed<'Note'>)[] = [];
const readAntennaNotes: (Note | Packed<'Note'>)[] = [];
for (const note of notes) { for (const note of notes) {
if (note.mentions && note.mentions.includes(userId)) { if (note.mentions && note.mentions.includes(userId)) {
@ -133,14 +129,6 @@ export class NoteReadService implements OnApplicationShutdown {
if (note.channelId && followingChannels.has(note.channelId)) { if (note.channelId && followingChannels.has(note.channelId)) {
readChannelNotes.push(note); readChannelNotes.push(note);
} }
if (note.user != null) { // たぶんnullになることは無いはずだけど一応
for (const antenna of myAntennas) {
if (await this.antennaService.checkHitAntenna(antenna, note, note.user)) {
readAntennaNotes.push(note);
}
}
}
} }
if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0) || (readChannelNotes.length > 0)) { if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0) || (readChannelNotes.length > 0)) {
@ -181,39 +169,6 @@ export class NoteReadService implements OnApplicationShutdown {
this.globalEventService.publishMainStream(userId, 'readAllChannels'); this.globalEventService.publishMainStream(userId, 'readAllChannels');
} }
}); });
this.notificationService.readNotificationByQuery(userId, {
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]),
});
}
if (readAntennaNotes.length > 0) {
await this.antennaNotesRepository.update({
antennaId: In(myAntennas.map(a => a.id)),
noteId: In(readAntennaNotes.map(n => n.id)),
}, {
read: true,
});
// TODO: まとめてクエリしたい
for (const antenna of myAntennas) {
const count = await this.antennaNotesRepository.countBy({
antennaId: antenna.id,
read: false,
});
if (count === 0) {
this.globalEventService.publishMainStream(userId, 'readAntenna', antenna);
this.pushNotificationService.pushNotification(userId, 'readAntenna', { antennaId: antenna.id });
}
}
this.userEntityService.getHasUnreadAntenna(userId).then(unread => {
if (!unread) {
this.globalEventService.publishMainStream(userId, 'readAllAntennas');
this.pushNotificationService.pushNotification(userId, 'readAllAntennas', undefined);
}
});
} }
} }

View File

@ -1,70 +1,156 @@
import { Inject, Injectable } from '@nestjs/common'; import { setTimeout } from 'node:timers/promises';
import Redis from 'ioredis';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { NotificationsRepository } from '@/models/index.js'; import type { MutingsRepository, UserProfile, UserProfilesRepository, UsersRepository } from '@/models/index.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import type { Notification } from '@/models/entities/Notification.js'; import type { Notification } from '@/models/entities/Notification.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 { GlobalEventService } from './GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { PushNotificationService } from './PushNotificationService.js'; import { PushNotificationService } from '@/core/PushNotificationService.js';
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
import { IdService } from '@/core/IdService.js';
import { CacheService } from '@/core/CacheService.js';
@Injectable() @Injectable()
export class NotificationService { export class NotificationService implements OnApplicationShutdown {
constructor( #shutdownController = new AbortController();
@Inject(DI.notificationsRepository)
private notificationsRepository: NotificationsRepository,
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
private notificationEntityService: NotificationEntityService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private idService: IdService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private pushNotificationService: PushNotificationService, private pushNotificationService: PushNotificationService,
private cacheService: CacheService,
) { ) {
} }
@bindThis @bindThis
public async readNotification( public async readAllNotification(
userId: User['id'], userId: User['id'],
notificationIds: Notification['id'][],
) { ) {
if (notificationIds.length === 0) return; const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
// Update documents const latestNotificationIdsRes = await this.redisClient.xrevrange(
const result = await this.notificationsRepository.update({ `notificationTimeline:${userId}`,
notifieeId: userId, '+',
id: In(notificationIds), '-',
isRead: false, 'COUNT', 1);
}, { const latestNotificationId = latestNotificationIdsRes[0]?.[0];
isRead: true,
});
if (result.affected === 0) return; if (latestNotificationId == null) return;
if (!await this.userEntityService.getHasUnreadNotification(userId)) return this.postReadAllNotifications(userId); this.redisClient.set(`latestReadNotification:${userId}`, latestNotificationId);
else return this.postReadNotifications(userId, notificationIds);
}
@bindThis if (latestReadNotificationId == null || (latestReadNotificationId < latestNotificationId)) {
public async readNotificationByQuery( return this.postReadAllNotifications(userId);
userId: User['id'], }
query: Record<string, any>,
) {
const notificationIds = await this.notificationsRepository.findBy({
...query,
notifieeId: userId,
isRead: false,
}).then(notifications => notifications.map(notification => notification.id));
return this.readNotification(userId, notificationIds);
} }
@bindThis @bindThis
private postReadAllNotifications(userId: User['id']) { private postReadAllNotifications(userId: User['id']) {
this.globalEventService.publishMainStream(userId, 'readAllNotifications'); this.globalEventService.publishMainStream(userId, 'readAllNotifications');
return this.pushNotificationService.pushNotification(userId, 'readAllNotifications', undefined);
} }
@bindThis @bindThis
private postReadNotifications(userId: User['id'], notificationIds: Notification['id'][]) { public async createNotification(
return this.pushNotificationService.pushNotification(userId, 'readNotifications', { notificationIds }); notifieeId: User['id'],
type: Notification['type'],
data: Partial<Notification>,
): Promise<Notification | null> {
const profile = await this.cacheService.userProfileCache.fetch(notifieeId, () => this.userProfilesRepository.findOneByOrFail({ userId: notifieeId }));
const isMuted = profile.mutingNotificationTypes.includes(type);
if (isMuted) return null;
if (data.notifierId) {
if (notifieeId === data.notifierId) {
return null;
}
const mutings = await this.cacheService.userMutingsCache.fetch(notifieeId, () => this.mutingsRepository.findBy({ muterId: notifieeId }).then(xs => xs.map(x => x.muteeId)));
if (mutings.includes(data.notifierId)) {
return null;
}
}
const notification = {
id: this.idService.genId(),
createdAt: new Date(),
type: type,
...data,
} as Notification;
this.redisClient.xadd(
`notificationTimeline:${notifieeId}`,
'MAXLEN', '~', '300',
`${this.idService.parse(notification.id).date.getTime()}-*`,
'data', JSON.stringify(notification));
const packed = await this.notificationEntityService.pack(notification, notifieeId, {});
// Publish notification event
this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
setTimeout(2000, 'unread notification', { signal: this.#shutdownController.signal }).then(async () => {
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${notifieeId}`);
if (latestReadNotificationId && (latestReadNotificationId >= notification.id)) return;
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! }));
if (type === 'receiveFollowRequest') this.emailNotificationReceiveFollowRequest(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! }));
}, () => { /* aborted, ignore it */ });
return notification;
}
// TODO
//const locales = await import('../../../../locales/index.js');
// TODO: locale ファイルをクライアント用とサーバー用で分けたい
@bindThis
private async emailNotificationFollow(userId: User['id'], follower: User) {
/*
const userProfile = await UserProfiles.findOneByOrFail({ userId: userId });
if (!userProfile.email || !userProfile.emailNotificationTypes.includes('follow')) return;
const locale = locales[userProfile.lang ?? 'ja-JP'];
const i18n = new I18n(locale);
// TODO: render user information html
sendEmail(userProfile.email, i18n.t('_email._follow.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`);
*/
}
@bindThis
private async emailNotificationReceiveFollowRequest(userId: User['id'], follower: User) {
/*
const userProfile = await UserProfiles.findOneByOrFail({ userId: userId });
if (!userProfile.email || !userProfile.emailNotificationTypes.includes('receiveFollowRequest')) return;
const locale = locales[userProfile.lang ?? 'ja-JP'];
const i18n = new I18n(locale);
// TODO: render user information html
sendEmail(userProfile.email, i18n.t('_email._receiveFollowRequest.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`);
*/
}
onApplicationShutdown(signal?: string | undefined): void {
this.#shutdownController.abort();
} }
} }

View File

@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import push from 'web-push'; import push from 'web-push';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { Packed } from '@/misc/schema'; import type { Packed } from '@/misc/json-schema';
import { getNoteSummary } from '@/misc/get-note-summary.js'; import { getNoteSummary } from '@/misc/get-note-summary.js';
import type { SwSubscriptionsRepository } from '@/models/index.js'; import type { SwSubscriptionsRepository } from '@/models/index.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
@ -15,10 +15,6 @@ type PushNotificationsTypes = {
antenna: { id: string, name: string }; antenna: { id: string, name: string };
note: Packed<'Note'>; note: Packed<'Note'>;
}; };
'readNotifications': { notificationIds: string[] };
'readAllNotifications': undefined;
'readAntenna': { antennaId: string };
'readAllAntennas': undefined;
}; };
// Reduce length because push message servers have character limits // Reduce length because push message servers have character limits
@ -72,14 +68,6 @@ export class PushNotificationService {
}); });
for (const subscription of subscriptions) { for (const subscription of subscriptions) {
// Continue if sendReadMessage is false
if ([
'readNotifications',
'readAllNotifications',
'readAntenna',
'readAllAntennas',
].includes(type) && !subscription.sendReadMessage) continue;
const pushSubscription = { const pushSubscription = {
endpoint: subscription.endpoint, endpoint: subscription.endpoint,
keys: { keys: {

View File

@ -26,7 +26,7 @@ export class QueueService {
) {} ) {}
@bindThis @bindThis
public deliver(user: ThinUser, content: IActivity | null, to: string | null) { public deliver(user: ThinUser, content: IActivity | null, to: string | null, isSharedInbox: boolean) {
if (content == null) return null; if (content == null) return null;
if (to == null) return null; if (to == null) return null;
@ -36,6 +36,7 @@ export class QueueService {
}, },
content, content,
to, to,
isSharedInbox,
}; };
return this.deliverQueue.add(data, { return this.deliverQueue.add(data, {

View File

@ -9,7 +9,7 @@ import { IdService } from '@/core/IdService.js';
import type { NoteReaction } from '@/models/entities/NoteReaction.js'; import type { NoteReaction } from '@/models/entities/NoteReaction.js';
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { CreateNotificationService } from '@/core/CreateNotificationService.js'; import { NotificationService } from '@/core/NotificationService.js';
import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js'; import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js';
import { emojiRegex } from '@/misc/emoji-regex.js'; import { emojiRegex } from '@/misc/emoji-regex.js';
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
@ -21,6 +21,8 @@ import { bindThis } from '@/decorators.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js';
const FALLBACK = '❤';
const legacies: Record<string, string> = { const legacies: Record<string, string> = {
'like': '👍', 'like': '👍',
'love': '❤', // ここに記述する場合は異体字セレクタを入れない 'love': '❤', // ここに記述する場合は異体字セレクタを入れない
@ -79,7 +81,7 @@ export class ReactionService {
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private apDeliverManagerService: ApDeliverManagerService, private apDeliverManagerService: ApDeliverManagerService,
private createNotificationService: CreateNotificationService, private notificationService: NotificationService,
private perUserReactionsChart: PerUserReactionsChart, private perUserReactionsChart: PerUserReactionsChart,
) { ) {
} }
@ -147,7 +149,11 @@ export class ReactionService {
.where('id = :id', { id: note.id }) .where('id = :id', { id: note.id })
.execute(); .execute();
this.perUserReactionsChart.update(user, note); const meta = await this.metaService.fetch();
if (meta.enableChartsForRemoteUser || (user.host == null)) {
this.perUserReactionsChart.update(user, note);
}
// カスタム絵文字リアクションだったら絵文字情報も送る // カスタム絵文字リアクションだったら絵文字情報も送る
const decodedReaction = this.decodeReaction(reaction); const decodedReaction = this.decodeReaction(reaction);
@ -172,7 +178,7 @@ export class ReactionService {
// リアクションされたユーザーがローカルユーザーなら通知を作成 // リアクションされたユーザーがローカルユーザーなら通知を作成
if (note.userHost === null) { if (note.userHost === null) {
this.createNotificationService.createNotification(note.userId, 'reaction', { this.notificationService.createNotification(note.userId, 'reaction', {
notifierId: user.id, notifierId: user.id,
noteId: note.id, noteId: note.id,
reaction: reaction, reaction: reaction,
@ -251,12 +257,6 @@ export class ReactionService {
//#endregion //#endregion
} }
@bindThis
public async getFallbackReaction(): Promise<string> {
const meta = await this.metaService.fetch();
return meta.useStarForReactionFallback ? '⭐' : '👍';
}
@bindThis @bindThis
public convertLegacyReactions(reactions: Record<string, number>) { public convertLegacyReactions(reactions: Record<string, number>) {
const _reactions = {} as Record<string, number>; const _reactions = {} as Record<string, number>;
@ -290,7 +290,7 @@ export class ReactionService {
@bindThis @bindThis
public async toDbReaction(reaction?: string | null, reacterHost?: string | null): Promise<string> { public async toDbReaction(reaction?: string | null, reacterHost?: string | null): Promise<string> {
if (reaction == null) return await this.getFallbackReaction(); if (reaction == null) return FALLBACK;
reacterHost = this.utilityService.toPunyNullable(reacterHost); reacterHost = this.utilityService.toPunyNullable(reacterHost);
@ -300,7 +300,7 @@ export class ReactionService {
// Unicode絵文字 // Unicode絵文字
const match = emojiRegex.exec(reaction); const match = emojiRegex.exec(reaction);
if (match) { if (match) {
// 合字を含む1つの絵文字 // 合字を含む1つの絵文字
const unicode = match[0]; const unicode = match[0];
// 異体字セレクタ除去 // 異体字セレクタ除去
@ -318,7 +318,7 @@ export class ReactionService {
if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`; if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
} }
return await this.getFallbackReaction(); return FALLBACK;
} }
@bindThis @bindThis

View File

@ -3,7 +3,7 @@ import { IsNull } from 'typeorm';
import type { LocalUser, User } from '@/models/entities/User.js'; import type { LocalUser, User } from '@/models/entities/User.js';
import type { RelaysRepository, UsersRepository } from '@/models/index.js'; import type { RelaysRepository, UsersRepository } from '@/models/index.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { Cache } from '@/misc/cache.js'; import { MemoryCache } from '@/misc/cache.js';
import type { Relay } from '@/models/entities/Relay.js'; import type { Relay } from '@/models/entities/Relay.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js'; import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
@ -16,7 +16,7 @@ const ACTOR_USERNAME = 'relay.actor' as const;
@Injectable() @Injectable()
export class RelayService { export class RelayService {
private relaysCache: Cache<Relay[]>; private relaysCache: MemoryCache<Relay[]>;
constructor( constructor(
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
@ -30,7 +30,7 @@ export class RelayService {
private createSystemUserService: CreateSystemUserService, private createSystemUserService: CreateSystemUserService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
) { ) {
this.relaysCache = new Cache<Relay[]>(1000 * 60 * 10); this.relaysCache = new MemoryCache<Relay[]>(1000 * 60 * 10);
} }
@bindThis @bindThis
@ -57,7 +57,7 @@ export class RelayService {
const relayActor = await this.getRelayActor(); const relayActor = await this.getRelayActor();
const follow = await this.apRendererService.renderFollowRelay(relay, relayActor); const follow = await this.apRendererService.renderFollowRelay(relay, relayActor);
const activity = this.apRendererService.addContext(follow); const activity = this.apRendererService.addContext(follow);
this.queueService.deliver(relayActor, activity, relay.inbox); this.queueService.deliver(relayActor, activity, relay.inbox, false);
return relay; return relay;
} }
@ -76,7 +76,7 @@ export class RelayService {
const follow = this.apRendererService.renderFollowRelay(relay, relayActor); const follow = this.apRendererService.renderFollowRelay(relay, relayActor);
const undo = this.apRendererService.renderUndo(follow, relayActor); const undo = this.apRendererService.renderUndo(follow, relayActor);
const activity = this.apRendererService.addContext(undo); const activity = this.apRendererService.addContext(undo);
this.queueService.deliver(relayActor, activity, relay.inbox); this.queueService.deliver(relayActor, activity, relay.inbox, false);
await this.relaysRepository.delete(relay.id); await this.relaysRepository.delete(relay.id);
} }
@ -109,7 +109,7 @@ export class RelayService {
public async deliverToRelays(user: { id: User['id']; host: null; }, activity: any): Promise<void> { public async deliverToRelays(user: { id: User['id']; host: null; }, activity: any): Promise<void> {
if (activity == null) return; if (activity == null) return;
const relays = await this.relaysCache.fetch(null, () => this.relaysRepository.findBy({ const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({
status: 'accepted', status: 'accepted',
})); }));
if (relays.length === 0) return; if (relays.length === 0) return;
@ -120,7 +120,7 @@ export class RelayService {
const signed = await this.apRendererService.attachLdSignature(copy, user); const signed = await this.apRendererService.attachLdSignature(copy, user);
for (const relay of relays) { for (const relay of relays) {
this.queueService.deliver(user, signed, relay.inbox); this.queueService.deliver(user, signed, relay.inbox, false);
} }
} }
} }

View File

@ -2,12 +2,12 @@ import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis'; import Redis from 'ioredis';
import { In } from 'typeorm'; import { In } from 'typeorm';
import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js'; import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js';
import { Cache } from '@/misc/cache.js'; import { MemoryKVCache, MemoryCache } from '@/misc/cache.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { UserCacheService } from '@/core/UserCacheService.js'; import { CacheService } from '@/core/CacheService.js';
import type { RoleCondFormulaValue } from '@/models/entities/Role.js'; import type { RoleCondFormulaValue } from '@/models/entities/Role.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { StreamMessages } from '@/server/api/stream/types.js'; import { StreamMessages } from '@/server/api/stream/types.js';
@ -21,6 +21,7 @@ export type RolePolicies = {
canPublicNote: boolean; canPublicNote: boolean;
canInvite: boolean; canInvite: boolean;
canManageCustomEmojis: boolean; canManageCustomEmojis: boolean;
canSearchNotes: boolean;
canHideAds: boolean; canHideAds: boolean;
driveCapacityMb: number; driveCapacityMb: number;
pinLimit: number; pinLimit: number;
@ -40,6 +41,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
canPublicNote: true, canPublicNote: true,
canInvite: false, canInvite: false,
canManageCustomEmojis: false, canManageCustomEmojis: false,
canSearchNotes: false,
canHideAds: false, canHideAds: false,
driveCapacityMb: 100, driveCapacityMb: 100,
pinLimit: 5, pinLimit: 5,
@ -55,8 +57,8 @@ export const DEFAULT_POLICIES: RolePolicies = {
@Injectable() @Injectable()
export class RoleService implements OnApplicationShutdown { export class RoleService implements OnApplicationShutdown {
private rolesCache: Cache<Role[]>; private rolesCache: MemoryCache<Role[]>;
private roleAssignmentByUserIdCache: Cache<RoleAssignment[]>; private roleAssignmentByUserIdCache: MemoryKVCache<RoleAssignment[]>;
public static AlreadyAssignedError = class extends Error {}; public static AlreadyAssignedError = class extends Error {};
public static NotAssignedError = class extends Error {}; public static NotAssignedError = class extends Error {};
@ -75,15 +77,15 @@ export class RoleService implements OnApplicationShutdown {
private roleAssignmentsRepository: RoleAssignmentsRepository, private roleAssignmentsRepository: RoleAssignmentsRepository,
private metaService: MetaService, private metaService: MetaService,
private userCacheService: UserCacheService, private cacheService: CacheService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private idService: IdService, private idService: IdService,
) { ) {
//this.onMessage = this.onMessage.bind(this); //this.onMessage = this.onMessage.bind(this);
this.rolesCache = new Cache<Role[]>(Infinity); this.rolesCache = new MemoryCache<Role[]>(Infinity);
this.roleAssignmentByUserIdCache = new Cache<RoleAssignment[]>(Infinity); this.roleAssignmentByUserIdCache = new MemoryKVCache<RoleAssignment[]>(Infinity);
this.redisSubscriber.on('message', this.onMessage); this.redisSubscriber.on('message', this.onMessage);
} }
@ -96,7 +98,7 @@ export class RoleService implements OnApplicationShutdown {
const { type, body } = obj.message as StreamMessages['internal']['payload']; const { type, body } = obj.message as StreamMessages['internal']['payload'];
switch (type) { switch (type) {
case 'roleCreated': { case 'roleCreated': {
const cached = this.rolesCache.get(null); const cached = this.rolesCache.get();
if (cached) { if (cached) {
cached.push({ cached.push({
...body, ...body,
@ -108,7 +110,7 @@ export class RoleService implements OnApplicationShutdown {
break; break;
} }
case 'roleUpdated': { case 'roleUpdated': {
const cached = this.rolesCache.get(null); const cached = this.rolesCache.get();
if (cached) { if (cached) {
const i = cached.findIndex(x => x.id === body.id); const i = cached.findIndex(x => x.id === body.id);
if (i > -1) { if (i > -1) {
@ -123,9 +125,9 @@ export class RoleService implements OnApplicationShutdown {
break; break;
} }
case 'roleDeleted': { case 'roleDeleted': {
const cached = this.rolesCache.get(null); const cached = this.rolesCache.get();
if (cached) { if (cached) {
this.rolesCache.set(null, cached.filter(x => x.id !== body.id)); this.rolesCache.set(cached.filter(x => x.id !== body.id));
} }
break; break;
} }
@ -190,6 +192,12 @@ export class RoleService implements OnApplicationShutdown {
case 'followingMoreThanOrEq': { case 'followingMoreThanOrEq': {
return user.followingCount >= value.value; return user.followingCount >= value.value;
} }
case 'notesLessThanOrEq': {
return user.notesCount <= value.value;
}
case 'notesMoreThanOrEq': {
return user.notesCount >= value.value;
}
default: default:
return false; return false;
} }
@ -206,9 +214,9 @@ export class RoleService implements OnApplicationShutdown {
// 期限切れのロールを除外 // 期限切れのロールを除外
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now)); assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
const assignedRoleIds = assigns.map(x => x.roleId); const assignedRoleIds = assigns.map(x => x.roleId);
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({})); const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const assignedRoles = roles.filter(r => assignedRoleIds.includes(r.id)); const assignedRoles = roles.filter(r => assignedRoleIds.includes(r.id));
const user = roles.some(r => r.target === 'conditional') ? await this.userCacheService.findById(userId) : null; const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, r.condFormula)); const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, r.condFormula));
return [...assignedRoles, ...matchedCondRoles]; return [...assignedRoles, ...matchedCondRoles];
} }
@ -223,11 +231,11 @@ export class RoleService implements OnApplicationShutdown {
// 期限切れのロールを除外 // 期限切れのロールを除外
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now)); assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
const assignedRoleIds = assigns.map(x => x.roleId); const assignedRoleIds = assigns.map(x => x.roleId);
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({})); const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id)); const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id));
const badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional')); const badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional'));
if (badgeCondRoles.length > 0) { if (badgeCondRoles.length > 0) {
const user = roles.some(r => r.target === 'conditional') ? await this.userCacheService.findById(userId) : null; const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, r.condFormula)); const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, r.condFormula));
return [...assignedBadgeRoles, ...matchedBadgeCondRoles]; return [...assignedBadgeRoles, ...matchedBadgeCondRoles];
} else { } else {
@ -264,6 +272,7 @@ export class RoleService implements OnApplicationShutdown {
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)), canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
canInvite: calc('canInvite', vs => vs.some(v => v === true)), canInvite: calc('canInvite', vs => vs.some(v => v === true)),
canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)), canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)),
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)), canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)), driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
pinLimit: calc('pinLimit', vs => Math.max(...vs)), pinLimit: calc('pinLimit', vs => Math.max(...vs)),
@ -292,7 +301,7 @@ export class RoleService implements OnApplicationShutdown {
@bindThis @bindThis
public async getModeratorIds(includeAdmins = true): Promise<User['id'][]> { public async getModeratorIds(includeAdmins = true): Promise<User['id'][]> {
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({})); const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator); const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator);
const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({ const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
roleId: In(moderatorRoles.map(r => r.id)), roleId: In(moderatorRoles.map(r => r.id)),
@ -312,7 +321,7 @@ export class RoleService implements OnApplicationShutdown {
@bindThis @bindThis
public async getAdministratorIds(): Promise<User['id'][]> { public async getAdministratorIds(): Promise<User['id'][]> {
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({})); const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const administratorRoles = roles.filter(r => r.isAdministrator); const administratorRoles = roles.filter(r => r.isAdministrator);
const assigns = administratorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({ const assigns = administratorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
roleId: In(administratorRoles.map(r => r.id)), roleId: In(administratorRoles.map(r => r.id)),

View File

@ -1,11 +1,16 @@
import { URL } from 'node:url'; import { URL } from 'node:url';
import * as http from 'node:http';
import * as https from 'node:https';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import S3 from 'aws-sdk/clients/s3.js'; import { DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { Upload } from '@aws-sdk/lib-storage';
import { NodeHttpHandler, NodeHttpHandlerOptions } from '@aws-sdk/node-http-handler';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { Meta } from '@/models/entities/Meta.js'; import type { Meta } from '@/models/entities/Meta.js';
import { HttpRequestService } from '@/core/HttpRequestService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { DeleteObjectCommandInput, PutObjectCommandInput } from '@aws-sdk/client-s3';
@Injectable() @Injectable()
export class S3Service { export class S3Service {
@ -18,23 +23,47 @@ export class S3Service {
} }
@bindThis @bindThis
public getS3(meta: Meta) { public getS3Client(meta: Meta): S3Client {
const u = meta.objectStorageEndpoint != null const u = meta.objectStorageEndpoint
? `${meta.objectStorageUseSSL ? 'https://' : 'http://'}${meta.objectStorageEndpoint}` ? `${meta.objectStorageUseSSL ? 'https' : 'http'}://${meta.objectStorageEndpoint}`
: `${meta.objectStorageUseSSL ? 'https://' : 'http://'}example.net`; : `${meta.objectStorageUseSSL ? 'https' : 'http'}://example.net`; // dummy url to select http(s) agent
return new S3({ const agent = this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy);
endpoint: meta.objectStorageEndpoint ?? undefined, const handlerOption: NodeHttpHandlerOptions = {};
accessKeyId: meta.objectStorageAccessKey!, if (meta.objectStorageUseSSL) {
secretAccessKey: meta.objectStorageSecretKey!, handlerOption.httpsAgent = agent as https.Agent;
region: meta.objectStorageRegion ?? undefined, } else {
sslEnabled: meta.objectStorageUseSSL, handlerOption.httpAgent = agent as http.Agent;
s3ForcePathStyle: !meta.objectStorageEndpoint // AWS with endPoint omitted }
? false
: meta.objectStorageS3ForcePathStyle, return new S3Client({
httpOptions: { endpoint: meta.objectStorageEndpoint ? u : undefined,
agent: this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy), credentials: (meta.objectStorageAccessKey !== null && meta.objectStorageSecretKey !== null) ? {
}, accessKeyId: meta.objectStorageAccessKey,
secretAccessKey: meta.objectStorageSecretKey,
} : undefined,
region: meta.objectStorageRegion ? meta.objectStorageRegion : undefined, // 空文字列もundefinedにするため ?? は使わない
tls: meta.objectStorageUseSSL,
forcePathStyle: meta.objectStorageEndpoint ? meta.objectStorageS3ForcePathStyle : false, // AWS with endPoint omitted
requestHandler: new NodeHttpHandler(handlerOption),
}); });
} }
@bindThis
public async upload(meta: Meta, input: PutObjectCommandInput) {
const client = this.getS3Client(meta);
return new Upload({
client,
params: input,
partSize: (client.config.endpoint && (await client.config.endpoint()).hostname === 'storage.googleapis.com')
? 500 * 1024 * 1024
: 8 * 1024 * 1024,
}).done();
}
@bindThis
public delete(meta: Meta, input: DeleteObjectCommandInput) {
const client = this.getS3Client(meta);
return client.send(new DeleteObjectCommand(input));
}
} }

View File

@ -90,7 +90,7 @@ export class SignupService {
cipher: undefined, cipher: undefined,
passphrase: undefined, passphrase: undefined,
}, },
} as any, (err, publicKey, privateKey) => }, (err, publicKey, privateKey) =>
err ? rej(err) : res([publicKey, privateKey]), err ? rej(err) : res([publicKey, privateKey]),
)); ));

View File

@ -15,7 +15,7 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import { WebhookService } from '@/core/WebhookService.js'; import { WebhookService } from '@/core/WebhookService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { Cache } from '@/misc/cache.js'; import { MemoryKVCache } from '@/misc/cache.js';
import { StreamMessages } from '@/server/api/stream/types.js'; import { StreamMessages } from '@/server/api/stream/types.js';
@Injectable() @Injectable()
@ -23,7 +23,7 @@ export class UserBlockingService implements OnApplicationShutdown {
private logger: Logger; private logger: Logger;
// キーがユーザーIDで、値がそのユーザーがブロックしているユーザーのIDのリストなキャッシュ // キーがユーザーIDで、値がそのユーザーがブロックしているユーザーのIDのリストなキャッシュ
private blockingsByUserIdCache: Cache<User['id'][]>; private blockingsByUserIdCache: MemoryKVCache<User['id'][]>;
constructor( constructor(
@Inject(DI.redisSubscriber) @Inject(DI.redisSubscriber)
@ -58,7 +58,7 @@ export class UserBlockingService implements OnApplicationShutdown {
) { ) {
this.logger = this.loggerService.getLogger('user-block'); this.logger = this.loggerService.getLogger('user-block');
this.blockingsByUserIdCache = new Cache<User['id'][]>(Infinity); this.blockingsByUserIdCache = new MemoryKVCache<User['id'][]>(Infinity);
this.redisSubscriber.on('message', this.onMessage); this.redisSubscriber.on('message', this.onMessage);
} }
@ -118,7 +118,7 @@ export class UserBlockingService implements OnApplicationShutdown {
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) { if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderBlock(blocking)); const content = this.apRendererService.addContext(this.apRendererService.renderBlock(blocking));
this.queueService.deliver(blocker, content, blockee.inbox); this.queueService.deliver(blocker, content, blockee.inbox, false);
} }
} }
@ -163,13 +163,13 @@ export class UserBlockingService implements OnApplicationShutdown {
// リモートにフォローリクエストをしていたらUndoFollow送信 // リモートにフォローリクエストをしていたらUndoFollow送信
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
this.queueService.deliver(follower, content, followee.inbox); this.queueService.deliver(follower, content, followee.inbox, false);
} }
// リモートからフォローリクエストを受けていたらReject送信 // リモートからフォローリクエストを受けていたらReject送信
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee)); const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee));
this.queueService.deliver(followee, content, follower.inbox); this.queueService.deliver(followee, content, follower.inbox, false);
} }
} }
@ -211,13 +211,13 @@ export class UserBlockingService implements OnApplicationShutdown {
// リモートにフォローをしていたらUndoFollow送信 // リモートにフォローをしていたらUndoFollow送信
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
this.queueService.deliver(follower, content, followee.inbox); this.queueService.deliver(follower, content, followee.inbox, false);
} }
// リモートからフォローをされていたらRejectFollow送信 // リモートからフォローをされていたらRejectFollow送信
if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) { if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) {
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee)); const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee));
this.queueService.deliver(followee, content, follower.inbox); this.queueService.deliver(followee, content, follower.inbox, false);
} }
} }
@ -262,7 +262,7 @@ export class UserBlockingService implements OnApplicationShutdown {
// deliver if remote bloking // deliver if remote bloking
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) { if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking), blocker)); const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking), blocker));
this.queueService.deliver(blocker, content, blockee.inbox); this.queueService.deliver(blocker, content, blockee.inbox, false);
} }
} }

View File

@ -6,17 +6,18 @@ import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
import type { Packed } from '@/misc/schema.js'; import type { Packed } from '@/misc/json-schema.js';
import InstanceChart from '@/core/chart/charts/instance.js'; import InstanceChart from '@/core/chart/charts/instance.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { WebhookService } from '@/core/WebhookService.js'; import { WebhookService } from '@/core/WebhookService.js';
import { CreateNotificationService } from '@/core/CreateNotificationService.js'; import { NotificationService } from '@/core/NotificationService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UserBlockingService } from '@/core/UserBlockingService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js';
import { MetaService } from '@/core/MetaService.js';
import Logger from '../logger.js'; import Logger from '../logger.js';
const logger = new Logger('following/create'); const logger = new Logger('following/create');
@ -57,7 +58,8 @@ export class UserFollowingService {
private idService: IdService, private idService: IdService,
private queueService: QueueService, private queueService: QueueService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private createNotificationService: CreateNotificationService, private metaService: MetaService,
private notificationService: NotificationService,
private federatedInstanceService: FederatedInstanceService, private federatedInstanceService: FederatedInstanceService,
private webhookService: WebhookService, private webhookService: WebhookService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
@ -82,7 +84,7 @@ export class UserFollowingService {
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocked) { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocked) {
// リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。 // リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, requestId), followee)); const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, requestId), followee));
this.queueService.deliver(followee, content, follower.inbox); this.queueService.deliver(followee, content, follower.inbox, false);
return; return;
} else if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocking) { } else if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocking) {
// リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。 // リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。
@ -131,7 +133,7 @@ export class UserFollowingService {
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee)); const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee));
this.queueService.deliver(followee, content, follower.inbox); this.queueService.deliver(followee, content, follower.inbox, false);
} }
} }
@ -182,7 +184,7 @@ export class UserFollowingService {
}); });
// 通知を作成 // 通知を作成
this.createNotificationService.createNotification(follower.id, 'followRequestAccepted', { this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
notifierId: followee.id, notifierId: followee.id,
}); });
} }
@ -200,14 +202,18 @@ export class UserFollowingService {
//#region Update instance stats //#region Update instance stats
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
this.federatedInstanceService.fetch(follower.host).then(i => { this.federatedInstanceService.fetch(follower.host).then(async i => {
this.instancesRepository.increment({ id: i.id }, 'followingCount', 1); this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
this.instanceChart.updateFollowing(i.host, true); if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateFollowing(i.host, true);
}
}); });
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
this.federatedInstanceService.fetch(followee.host).then(i => { this.federatedInstanceService.fetch(followee.host).then(async i => {
this.instancesRepository.increment({ id: i.id }, 'followersCount', 1); this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
this.instanceChart.updateFollowers(i.host, true); if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateFollowers(i.host, true);
}
}); });
} }
//#endregion //#endregion
@ -245,7 +251,7 @@ export class UserFollowingService {
}); });
// 通知を作成 // 通知を作成
this.createNotificationService.createNotification(followee.id, 'follow', { this.notificationService.createNotification(followee.id, 'follow', {
notifierId: follower.id, notifierId: follower.id,
}); });
} }
@ -294,19 +300,19 @@ export class UserFollowingService {
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
this.queueService.deliver(follower, content, followee.inbox); this.queueService.deliver(follower, content, followee.inbox, false);
} }
if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) { if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) {
// local user has null host // local user has null host
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee)); const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee));
this.queueService.deliver(followee, content, follower.inbox); this.queueService.deliver(followee, content, follower.inbox, false);
} }
} }
@bindThis @bindThis
private async decrementFollowing( private async decrementFollowing(
follower: {id: User['id']; host: User['host']; }, follower: { id: User['id']; host: User['host']; },
followee: { id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; },
): Promise<void> { ): Promise<void> {
this.globalEventService.publishInternalEvent('unfollow', { followerId: follower.id, followeeId: followee.id }); this.globalEventService.publishInternalEvent('unfollow', { followerId: follower.id, followeeId: followee.id });
@ -320,14 +326,18 @@ export class UserFollowingService {
//#region Update instance stats //#region Update instance stats
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
this.federatedInstanceService.fetch(follower.host).then(i => { this.federatedInstanceService.fetch(follower.host).then(async i => {
this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1); this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
this.instanceChart.updateFollowing(i.host, false); if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateFollowing(i.host, false);
}
}); });
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
this.federatedInstanceService.fetch(followee.host).then(i => { this.federatedInstanceService.fetch(followee.host).then(async i => {
this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1); this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
this.instanceChart.updateFollowers(i.host, false); if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateFollowers(i.host, false);
}
}); });
} }
//#endregion //#endregion
@ -381,7 +391,7 @@ export class UserFollowingService {
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
// 通知を作成 // 通知を作成
this.createNotificationService.createNotification(followee.id, 'receiveFollowRequest', { this.notificationService.createNotification(followee.id, 'receiveFollowRequest', {
notifierId: follower.id, notifierId: follower.id,
followRequestId: followRequest.id, followRequestId: followRequest.id,
}); });
@ -389,7 +399,7 @@ export class UserFollowingService {
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee)); const content = this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee));
this.queueService.deliver(follower, content, followee.inbox); this.queueService.deliver(follower, content, followee.inbox, false);
} }
} }
@ -406,7 +416,7 @@ export class UserFollowingService {
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
if (this.userEntityService.isLocalUser(follower)) { // 本来このチェックは不要だけどTSに怒られるので if (this.userEntityService.isLocalUser(follower)) { // 本来このチェックは不要だけどTSに怒られるので
this.queueService.deliver(follower, content, followee.inbox); this.queueService.deliver(follower, content, followee.inbox, false);
} }
} }
@ -449,7 +459,7 @@ export class UserFollowingService {
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee)); const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee));
this.queueService.deliver(followee, content, follower.inbox); this.queueService.deliver(followee, content, follower.inbox, false);
} }
this.userEntityService.pack(followee.id, followee, { this.userEntityService.pack(followee.id, followee, {
@ -557,7 +567,7 @@ export class UserFollowingService {
}); });
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, request?.requestId ?? undefined), followee)); const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, request?.requestId ?? undefined), followee));
this.queueService.deliver(followee, content, follower.inbox); this.queueService.deliver(followee, content, follower.inbox, false);
} }
/** /**

View File

@ -1,20 +1,20 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import type { UserKeypairsRepository } from '@/models/index.js'; import type { UserKeypairsRepository } from '@/models/index.js';
import { Cache } from '@/misc/cache.js'; import { MemoryKVCache } from '@/misc/cache.js';
import type { UserKeypair } from '@/models/entities/UserKeypair.js'; import type { UserKeypair } from '@/models/entities/UserKeypair.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class UserKeypairStoreService { export class UserKeypairStoreService {
private cache: Cache<UserKeypair>; private cache: MemoryKVCache<UserKeypair>;
constructor( constructor(
@Inject(DI.userKeypairsRepository) @Inject(DI.userKeypairsRepository)
private userKeypairsRepository: UserKeypairsRepository, private userKeypairsRepository: UserKeypairsRepository,
) { ) {
this.cache = new Cache<UserKeypair>(Infinity); this.cache = new MemoryKVCache<UserKeypair>(Infinity);
} }
@bindThis @bindThis

View File

@ -54,7 +54,7 @@ export class UserSuspendService {
} }
for (const inbox of queue) { for (const inbox of queue) {
this.queueService.deliver(user, content, inbox); this.queueService.deliver(user, content, inbox, true);
} }
} }
} }
@ -84,7 +84,7 @@ export class UserSuspendService {
} }
for (const inbox of queue) { for (const inbox of queue) {
this.queueService.deliver(user as any, content, inbox); this.queueService.deliver(user as any, content, inbox, true);
} }
} }
} }

View File

@ -37,7 +37,7 @@ export class VideoProcessingService {
}); });
}); });
return await this.imageProcessingService.convertToWebp(`${dir}/out.png`, 498, 280); return await this.imageProcessingService.convertToWebp(`${dir}/out.png`, 498, 422);
} finally { } finally {
cleanup(); cleanup();
} }

View File

@ -3,9 +3,9 @@ import escapeRegexp from 'escape-regexp';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { Cache } from '@/misc/cache.js'; import { MemoryKVCache } from '@/misc/cache.js';
import type { UserPublickey } from '@/models/entities/UserPublickey.js'; import type { UserPublickey } from '@/models/entities/UserPublickey.js';
import { UserCacheService } from '@/core/UserCacheService.js'; import { CacheService } from '@/core/CacheService.js';
import type { Note } from '@/models/entities/Note.js'; import type { Note } from '@/models/entities/Note.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RemoteUser, User } from '@/models/entities/User.js'; import { RemoteUser, User } from '@/models/entities/User.js';
@ -31,8 +31,8 @@ export type UriParseResult = {
@Injectable() @Injectable()
export class ApDbResolverService { export class ApDbResolverService {
private publicKeyCache: Cache<UserPublickey | null>; private publicKeyCache: MemoryKVCache<UserPublickey | null>;
private publicKeyByUserIdCache: Cache<UserPublickey | null>; private publicKeyByUserIdCache: MemoryKVCache<UserPublickey | null>;
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
@ -47,11 +47,11 @@ export class ApDbResolverService {
@Inject(DI.userPublickeysRepository) @Inject(DI.userPublickeysRepository)
private userPublickeysRepository: UserPublickeysRepository, private userPublickeysRepository: UserPublickeysRepository,
private userCacheService: UserCacheService, private cacheService: CacheService,
private apPersonService: ApPersonService, private apPersonService: ApPersonService,
) { ) {
this.publicKeyCache = new Cache<UserPublickey | null>(Infinity); this.publicKeyCache = new MemoryKVCache<UserPublickey | null>(Infinity);
this.publicKeyByUserIdCache = new Cache<UserPublickey | null>(Infinity); this.publicKeyByUserIdCache = new MemoryKVCache<UserPublickey | null>(Infinity);
} }
@bindThis @bindThis
@ -107,11 +107,11 @@ export class ApDbResolverService {
if (parsed.local) { if (parsed.local) {
if (parsed.type !== 'users') return null; if (parsed.type !== 'users') return null;
return await this.userCacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({ return await this.cacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({
id: parsed.id, id: parsed.id,
}).then(x => x ?? undefined)) ?? null; }).then(x => x ?? undefined)) ?? null;
} else { } else {
return await this.userCacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({ return await this.cacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({
uri: parsed.uri, uri: parsed.uri,
})); }));
} }
@ -138,7 +138,7 @@ export class ApDbResolverService {
if (key == null) return null; if (key == null) return null;
return { return {
user: await this.userCacheService.findById(key.userId) as RemoteUser, user: await this.cacheService.findUserById(key.userId) as RemoteUser,
key, key,
}; };
} }

View File

@ -157,7 +157,8 @@ class DeliverManager {
public async execute() { public async execute() {
if (!this.userEntityService.isLocalUser(this.actor)) return; if (!this.userEntityService.isLocalUser(this.actor)) return;
const inboxes = new Set<string>(); // The value flags whether it is shared or not.
const inboxes = new Map<string, boolean>();
/* /*
build inbox list build inbox list
@ -185,7 +186,7 @@ class DeliverManager {
for (const following of followers) { for (const following of followers) {
const inbox = following.followerSharedInbox ?? following.followerInbox; const inbox = following.followerSharedInbox ?? following.followerInbox;
inboxes.add(inbox); inboxes.set(inbox, following.followerSharedInbox === null);
} }
} }
@ -197,11 +198,12 @@ class DeliverManager {
// check that they actually have an inbox // check that they actually have an inbox
&& recipe.to.inbox != null, && recipe.to.inbox != null,
) )
.forEach(recipe => inboxes.add(recipe.to.inbox!)); .forEach(recipe => inboxes.set(recipe.to.inbox!, false));
// deliver // deliver
for (const inbox of inboxes) { for (const inbox of inboxes) {
this.queueService.deliver(this.actor, this.activity, inbox); // inbox[0]: inbox, inbox[1]: whether it is sharedInbox
this.queueService.deliver(this.actor, this.activity, inbox[0], inbox[1]);
} }
} }
} }

View File

@ -140,7 +140,7 @@ export class ApInboxService {
} else if (isFlag(activity)) { } else if (isFlag(activity)) {
await this.flag(actor, activity); await this.flag(actor, activity);
} else { } else {
this.logger.warn(`unrecognized activity type: ${(activity as any).type}`); this.logger.warn(`unrecognized activity type: ${activity.type}`);
} }
} }

View File

@ -91,6 +91,9 @@ export class ApRendererService {
} else if (note.visibility === 'home') { } else if (note.visibility === 'home') {
to = [`${attributedTo}/followers`]; to = [`${attributedTo}/followers`];
cc = ['https://www.w3.org/ns/activitystreams#Public']; cc = ['https://www.w3.org/ns/activitystreams#Public'];
} else if (note.visibility === 'followers') {
to = [`${attributedTo}/followers`];
cc = [];
} else { } else {
throw new Error('renderAnnounce: cannot render non-public note'); throw new Error('renderAnnounce: cannot render non-public note');
} }
@ -155,7 +158,7 @@ export class ApRendererService {
public renderDocument(file: DriveFile): IApDocument { public renderDocument(file: DriveFile): IApDocument {
return { return {
type: 'Document', type: 'Document',
mediaType: file.type, mediaType: file.webpublicType ?? file.type,
url: this.driveFileEntityService.getPublicUrl(file), url: this.driveFileEntityService.getPublicUrl(file),
name: file.comment, name: file.comment,
}; };

View File

@ -124,7 +124,7 @@ export class ApNoteService {
throw new Error('invalid note'); throw new Error('invalid note');
} }
const note: IPost = object as any; const note = object as IPost;
this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
@ -180,7 +180,7 @@ export class ApNoteService {
const reply: Note | null = note.inReplyTo const reply: Note | null = note.inReplyTo
? await this.resolveNote(note.inReplyTo, resolver).then(x => { ? await this.resolveNote(note.inReplyTo, resolver).then(x => {
if (x == null) { if (x == null) {
this.logger.warn('Specified inReplyTo, but nout found'); this.logger.warn('Specified inReplyTo, but not found');
throw new Error('inReplyTo not found'); throw new Error('inReplyTo not found');
} else { } else {
return x; return x;

View File

@ -8,7 +8,7 @@ import type { Config } from '@/config.js';
import type { RemoteUser } from '@/models/entities/User.js'; import type { RemoteUser } from '@/models/entities/User.js';
import { User } from '@/models/entities/User.js'; import { User } from '@/models/entities/User.js';
import { truncate } from '@/misc/truncate.js'; import { truncate } from '@/misc/truncate.js';
import type { UserCacheService } from '@/core/UserCacheService.js'; import type { CacheService } from '@/core/CacheService.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
@ -30,6 +30,7 @@ import { StatusError } from '@/misc/status-error.js';
import type { UtilityService } from '@/core/UtilityService.js'; import type { UtilityService } from '@/core/UtilityService.js';
import type { UserEntityService } from '@/core/entities/UserEntityService.js'; import type { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js'; import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
import { extractApHashtags } from './tag.js'; import { extractApHashtags } from './tag.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
@ -50,9 +51,10 @@ export class ApPersonService implements OnModuleInit {
private userEntityService: UserEntityService; private userEntityService: UserEntityService;
private idService: IdService; private idService: IdService;
private globalEventService: GlobalEventService; private globalEventService: GlobalEventService;
private metaService: MetaService;
private federatedInstanceService: FederatedInstanceService; private federatedInstanceService: FederatedInstanceService;
private fetchInstanceMetadataService: FetchInstanceMetadataService; private fetchInstanceMetadataService: FetchInstanceMetadataService;
private userCacheService: UserCacheService; private cacheService: CacheService;
private apResolverService: ApResolverService; private apResolverService: ApResolverService;
private apNoteService: ApNoteService; private apNoteService: ApNoteService;
private apImageService: ApImageService; private apImageService: ApImageService;
@ -92,9 +94,10 @@ export class ApPersonService implements OnModuleInit {
//private userEntityService: UserEntityService, //private userEntityService: UserEntityService,
//private idService: IdService, //private idService: IdService,
//private globalEventService: GlobalEventService, //private globalEventService: GlobalEventService,
//private metaService: MetaService,
//private federatedInstanceService: FederatedInstanceService, //private federatedInstanceService: FederatedInstanceService,
//private fetchInstanceMetadataService: FetchInstanceMetadataService, //private fetchInstanceMetadataService: FetchInstanceMetadataService,
//private userCacheService: UserCacheService, //private cacheService: CacheService,
//private apResolverService: ApResolverService, //private apResolverService: ApResolverService,
//private apNoteService: ApNoteService, //private apNoteService: ApNoteService,
//private apImageService: ApImageService, //private apImageService: ApImageService,
@ -112,9 +115,10 @@ export class ApPersonService implements OnModuleInit {
this.userEntityService = this.moduleRef.get('UserEntityService'); this.userEntityService = this.moduleRef.get('UserEntityService');
this.idService = this.moduleRef.get('IdService'); this.idService = this.moduleRef.get('IdService');
this.globalEventService = this.moduleRef.get('GlobalEventService'); this.globalEventService = this.moduleRef.get('GlobalEventService');
this.metaService = this.moduleRef.get('MetaService');
this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService'); this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService'); this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService');
this.userCacheService = this.moduleRef.get('UserCacheService'); this.cacheService = this.moduleRef.get('CacheService');
this.apResolverService = this.moduleRef.get('ApResolverService'); this.apResolverService = this.moduleRef.get('ApResolverService');
this.apNoteService = this.moduleRef.get('ApNoteService'); this.apNoteService = this.moduleRef.get('ApNoteService');
this.apImageService = this.moduleRef.get('ApImageService'); this.apImageService = this.moduleRef.get('ApImageService');
@ -164,6 +168,9 @@ export class ApPersonService implements OnModuleInit {
throw new Error('invalid Actor: wrong name'); throw new Error('invalid Actor: wrong name');
} }
x.name = truncate(x.name, nameLength); x.name = truncate(x.name, nameLength);
} else if (x.name === '') {
// Mastodon emits empty string when the name is not set.
x.name = undefined;
} }
if (x.summary) { if (x.summary) {
if (!(typeof x.summary === 'string' && x.summary.length > 0)) { if (!(typeof x.summary === 'string' && x.summary.length > 0)) {
@ -200,14 +207,14 @@ export class ApPersonService implements OnModuleInit {
public async fetchPerson(uri: string, resolver?: Resolver): Promise<User | null> { public async fetchPerson(uri: string, resolver?: Resolver): Promise<User | null> {
if (typeof uri !== 'string') throw new Error('uri is not string'); if (typeof uri !== 'string') throw new Error('uri is not string');
const cached = this.userCacheService.uriPersonCache.get(uri); const cached = this.cacheService.uriPersonCache.get(uri);
if (cached) return cached; if (cached) return cached;
// URIがこのサーバーを指しているならデータベースからフェッチ // URIがこのサーバーを指しているならデータベースからフェッチ
if (uri.startsWith(this.config.url + '/')) { if (uri.startsWith(this.config.url + '/')) {
const id = uri.split('/').pop(); const id = uri.split('/').pop();
const u = await this.usersRepository.findOneBy({ id }); const u = await this.usersRepository.findOneBy({ id });
if (u) this.userCacheService.uriPersonCache.set(uri, u); if (u) this.cacheService.uriPersonCache.set(uri, u);
return u; return u;
} }
@ -215,7 +222,7 @@ export class ApPersonService implements OnModuleInit {
const exist = await this.usersRepository.findOneBy({ uri }); const exist = await this.usersRepository.findOneBy({ uri });
if (exist) { if (exist) {
this.userCacheService.uriPersonCache.set(uri, exist); this.cacheService.uriPersonCache.set(uri, exist);
return exist; return exist;
} }
//#endregion //#endregion
@ -324,10 +331,12 @@ export class ApPersonService implements OnModuleInit {
} }
// Register host // Register host
this.federatedInstanceService.fetch(host).then(i => { this.federatedInstanceService.fetch(host).then(async i => {
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1); this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
this.instanceChart.newUser(i.host);
this.fetchInstanceMetadataService.fetchInstanceMetadata(i); this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.newUser(i.host);
}
}); });
this.usersChart.update(user!, true); this.usersChart.update(user!, true);

View File

@ -195,7 +195,8 @@ export const isPropertyValue = (object: IObject): object is IApPropertyValue =>
object && object &&
getApType(object) === 'PropertyValue' && getApType(object) === 'PropertyValue' &&
typeof object.name === 'string' && typeof object.name === 'string' &&
typeof (object as any).value === 'string'; 'value' in object &&
typeof object.value === 'string';
export interface IApMention extends IObject { export interface IApMention extends IObject {
type: 'Mention'; type: 'Mention';

View File

@ -3,15 +3,15 @@ import Chart from '../../core.js';
export const name = 'activeUsers'; export const name = 'activeUsers';
export const schema = { export const schema = {
'readWrite': { intersection: ['read', 'write'], range: 'small' }, 'readWrite': { intersection: ['read', 'write'] },
'read': { uniqueIncrement: true, range: 'small' }, 'read': { uniqueIncrement: true },
'write': { uniqueIncrement: true, range: 'small' }, 'write': { uniqueIncrement: true },
'registeredWithinWeek': { uniqueIncrement: true, range: 'small' }, 'registeredWithinWeek': { uniqueIncrement: true },
'registeredWithinMonth': { uniqueIncrement: true, range: 'small' }, 'registeredWithinMonth': { uniqueIncrement: true },
'registeredWithinYear': { uniqueIncrement: true, range: 'small' }, 'registeredWithinYear': { uniqueIncrement: true },
'registeredOutsideWeek': { uniqueIncrement: true, range: 'small' }, 'registeredOutsideWeek': { uniqueIncrement: true },
'registeredOutsideMonth': { uniqueIncrement: true, range: 'small' }, 'registeredOutsideMonth': { uniqueIncrement: true },
'registeredOutsideYear': { uniqueIncrement: true, range: 'small' }, 'registeredOutsideYear': { uniqueIncrement: true },
} as const; } as const;
export const entity = Chart.schemaToEntity(name, schema); export const entity = Chart.schemaToEntity(name, schema);

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