Merge branch 'misskey-dev:develop' into develop
This commit is contained in:
commit
c094f76d2e
36
.github/workflows/api-misskey-js.yml
vendored
Normal file
36
.github/workflows/api-misskey-js.yml
vendored
Normal 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
|
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@ -36,6 +36,7 @@ jobs:
|
||||
- backend
|
||||
- frontend
|
||||
- sw
|
||||
- misskey-js
|
||||
steps:
|
||||
- uses: actions/checkout@v3.3.0
|
||||
with:
|
||||
@ -61,6 +62,7 @@ jobs:
|
||||
matrix:
|
||||
workspace:
|
||||
- backend
|
||||
- misskey-js
|
||||
steps:
|
||||
- uses: actions/checkout@v3.3.0
|
||||
with:
|
||||
|
56
.github/workflows/storybook.yml
vendored
Normal file
56
.github/workflows/storybook.yml
vendored
Normal 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
52
.github/workflows/test-misskey-js.yml
vendored
Normal 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
2
.gitignore
vendored
@ -55,6 +55,8 @@ api-docs.json
|
||||
.DS_Store
|
||||
/files
|
||||
ormconfig.json
|
||||
temp
|
||||
/packages/frontend/src/**/*.stories.ts
|
||||
|
||||
# blender backups
|
||||
*.blend1
|
||||
|
130
CHANGELOG.md
130
CHANGELOG.md
@ -1,25 +1,129 @@
|
||||
<!--
|
||||
## 13.x.x (unreleased)
|
||||
|
||||
### Improvements
|
||||
### General
|
||||
-
|
||||
|
||||
### Client
|
||||
-
|
||||
|
||||
### Bugfixes
|
||||
-
|
||||
### Server
|
||||
-
|
||||
|
||||
You should also include the user name that made the change.
|
||||
-->
|
||||
|
||||
## 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をミュートできるように
|
||||
- ノートごとに絵文字リアクションを受け取るか設定できるように
|
||||
- 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でプレビューがでてこない問題を修正
|
||||
- /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)
|
||||
|
||||
@ -257,8 +361,8 @@ You should also include the user name that made the change.
|
||||
## 13.3.2 (2023/02/04)
|
||||
|
||||
### Improvements
|
||||
- 外部メディアプロキシへの対応を強化しました
|
||||
外部メディアプロキシのFastify実装を作りました
|
||||
- 外部メディアプロキシへの対応を強化しました
|
||||
外部メディアプロキシのFastify実装を作りました
|
||||
https://github.com/misskey-dev/media-proxy
|
||||
- Server: improve performance
|
||||
|
||||
@ -421,7 +525,7 @@ You should also include the user name that made the change.
|
||||
- ユーザーごとのドライブ容量設定はロールに統合されました。
|
||||
- インスタンスデフォルトのドライブ容量設定はロールに統合されました。アップデート後、ベースロールもしくはコンディショナルロールでドライブ容量を編集してください。
|
||||
- LTL/GTLの解放状態はロールに統合されました。
|
||||
- Dockerの実行をrootで行わないようにしました。Dockerかつオブジェクトストレージを使用していない場合は`chown -hR 991.991 ./files`を実行してください。
|
||||
- Dockerの実行をrootで行わないようにしました。Dockerかつオブジェクトストレージを使用していない場合は`chown -hR 991.991 ./files`を実行してください。
|
||||
https://github.com/misskey-dev/misskey/pull/9560
|
||||
|
||||
#### For users
|
||||
@ -649,7 +753,7 @@ You should also include the user name that made the change.
|
||||
## 12.112.2 (2022/07/08)
|
||||
|
||||
### Bugfixes
|
||||
- Fix Docker doesn't work @mei23
|
||||
- Fix Docker doesn't work @mei23
|
||||
Still not working on arm64 environment. (See 12.112.0)
|
||||
|
||||
## 12.112.1 (2022/07/07)
|
||||
@ -691,7 +795,7 @@ same as 12.112.0
|
||||
- Improve player detection in URL preview @mei23
|
||||
- Add Badge Image to Push Notification #8012 @tamaina
|
||||
- Server: Improve performance
|
||||
- Server: Supports IPv6 on Redis transport. @mei23
|
||||
- Server: Supports IPv6 on Redis transport. @mei23
|
||||
IPv4/IPv6 is used by default. You can tune this behavior via `redis.family`.
|
||||
- Server: Add possibility to log IP addresses of users @syuilo
|
||||
- Add additional drive capacity change support @CyberRex0
|
||||
|
110
CONTRIBUTING.md
110
CONTRIBUTING.md
@ -203,6 +203,116 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド
|
||||
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
|
||||
### How to resolve conflictions occurred at pnpm-lock.yaml?
|
||||
|
||||
|
@ -23,6 +23,7 @@ COPY --link ["scripts", "./scripts"]
|
||||
COPY --link ["packages/backend/package.json", "./packages/backend/"]
|
||||
COPY --link ["packages/frontend/package.json", "./packages/frontend/"]
|
||||
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 \
|
||||
pnpm i --frozen-lockfile --aggregate-output
|
||||
|
@ -545,7 +545,6 @@ tokenRequested: "منح حق الوصول إلى الحساب"
|
||||
pluginTokenRequestedDescription: "ستتمكن الإضافة من استخدام هذه الأذونات."
|
||||
notificationType: "أنواع الإشعارات"
|
||||
edit: "التعديل"
|
||||
useStarForReactionFallback: "استخدم ★ كبديل إذا كان التفاعل مجهولًا"
|
||||
emailServer: "خادم البريد الإلكتروني"
|
||||
emailConfigInfo: "يستخدم لتأكيد عنوان بريدك الإلكتروني ولإعادة تعيين كلمة المرور إن نسيتها."
|
||||
email: "البريد الإلكتروني "
|
||||
@ -1275,3 +1274,7 @@ _deck:
|
||||
channel: "القنوات"
|
||||
mentions: "الإشارات"
|
||||
direct: "مباشرة"
|
||||
_webhookSettings:
|
||||
name: "الإسم"
|
||||
active: "مفعّل"
|
||||
|
||||
|
@ -562,7 +562,6 @@ tokenRequested: "অ্যাকাউন্টে অ্যাক্সেস
|
||||
pluginTokenRequestedDescription: "এই প্লাগইনটি এখানে দেওয়া অনুমুতিসমূহ ব্যাবহার করবে"
|
||||
notificationType: "বিজ্ঞপ্তির ধরন"
|
||||
edit: "সম্পাদনা"
|
||||
useStarForReactionFallback: "রিঅ্যাকশনের ইমোজি না জানলে ★ ব্যবহার করুন"
|
||||
emailServer: "ইমেইল সার্ভার"
|
||||
enableEmail: "ইমেইল বিতরণ চালু করুন"
|
||||
emailConfigInfo: "আপনার ইমেল ঠিকানা নিশ্চিত করতে এবং আপনার পাসওয়ার্ড পুনরায় সেট করতে ব্যবহৃত হয়"
|
||||
@ -1354,3 +1353,7 @@ _deck:
|
||||
channel: "চ্যানেলগুলি"
|
||||
mentions: "উল্লেখসমূহ"
|
||||
direct: "ডাইরেক্ট নোটগুলি"
|
||||
_webhookSettings:
|
||||
name: "নাম"
|
||||
active: "চালু"
|
||||
|
||||
|
@ -460,3 +460,4 @@ _deck:
|
||||
list: "Llistes"
|
||||
mentions: "Mencions"
|
||||
direct: "Publicacions directes"
|
||||
|
||||
|
@ -776,3 +776,7 @@ _deck:
|
||||
list: "Seznamy"
|
||||
channel: "Kanály"
|
||||
mentions: "Zmínění"
|
||||
_webhookSettings:
|
||||
name: "Jméno"
|
||||
active: "Zapnuto"
|
||||
|
||||
|
@ -1,2 +1,3 @@
|
||||
---
|
||||
_lang_: "Dansk"
|
||||
|
||||
|
@ -122,6 +122,8 @@ unmarkAsSensitive: "Als nicht NSFW markieren"
|
||||
enterFileName: "Dateinamen eingeben"
|
||||
mute: "Stummschalten"
|
||||
unmute: "Stummschaltung aufheben"
|
||||
renoteMute: "Renotes stummschalten"
|
||||
renoteUnmute: "Renote-Stummschaltung aufheben"
|
||||
block: "Blockieren"
|
||||
unblock: "Blockierung aufheben"
|
||||
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."
|
||||
autoAcceptFollowed: "Follow-Anfragen von Benutzern, denen du folgst, automatisch akzeptieren"
|
||||
addAccount: "Benutzerkonto hinzufügen"
|
||||
reloadAccountsList: "Benutzerkontoliste aktualisieren"
|
||||
loginFailed: "Anmeldung fehlgeschlagen"
|
||||
showOnRemote: "Auf Ursprungsinstanz ansehen"
|
||||
general: "Allgemein"
|
||||
@ -457,7 +460,7 @@ aboutX: "Über {x}"
|
||||
emojiStyle: "Emoji-Stil"
|
||||
native: "Nativ"
|
||||
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"
|
||||
signinHistory: "Anmeldungsverlauf"
|
||||
enableAdvancedMfm: "Erweitertes MFM aktivieren"
|
||||
@ -544,6 +547,10 @@ userSuspended: "Dieser Benutzer wurde gesperrt."
|
||||
userSilenced: "Dieser Benutzer wurde instanzweit stummgeschaltet."
|
||||
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."
|
||||
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ü"
|
||||
divider: "Trenner"
|
||||
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."
|
||||
notificationType: "Art der Benachrichtigung"
|
||||
edit: "Bearbeiten"
|
||||
useStarForReactionFallback: "Verwende ★ falls das Reaktions-Emoji unbekannt ist"
|
||||
emailServer: "Email-Server"
|
||||
enableEmail: "Email-Versand aktivieren"
|
||||
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"
|
||||
postToTheChannel: "In Kanal senden"
|
||||
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:
|
||||
earnedAt: "Freigeschaltet am"
|
||||
_types:
|
||||
@ -1106,7 +1130,7 @@ _achievements:
|
||||
title: "Beliebt"
|
||||
description: "Die Anzahl deiner Follower hat 100 überschritten"
|
||||
_followers300:
|
||||
title: "Stellt euch bitte in einer Reihe auf"
|
||||
title: "Eine geordnete Reihe, bitte!"
|
||||
description: "Die Anzahl deiner Follower hat 300 überschritten"
|
||||
_followers500:
|
||||
title: "Funkmast"
|
||||
@ -1218,6 +1242,8 @@ _role:
|
||||
iconUrl: "Icon-URL"
|
||||
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."
|
||||
displayOrder: "Position"
|
||||
descriptionOfDisplayOrder: "Je höher die Nummer, desto höher die UI-Position."
|
||||
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."
|
||||
priority: "Priorität"
|
||||
@ -1243,6 +1269,7 @@ _role:
|
||||
rateLimitFactor: "Versuchsanzahl"
|
||||
descriptionOfRateLimitFactor: "Je niedriger desto weniger restriktiv, je höher destro restriktiver."
|
||||
canHideAds: "Kann Werbung ausblenden"
|
||||
canSearchNotes: "Nutzung der Notizsuchfunktion"
|
||||
_condition:
|
||||
isLocal: "Lokaler Benutzer"
|
||||
isRemote: "Benutzer fremder Instanz"
|
||||
@ -1252,6 +1279,8 @@ _role:
|
||||
followersMoreThanOrEq: "Hat X oder mehr Follower"
|
||||
followingLessThanOrEq: "Folgt X oder weniger Benutzern"
|
||||
followingMoreThanOrEq: "Folgt X oder mehr Benutzern"
|
||||
notesLessThanOrEq: "Beitragszahl ist kleiner-gleich"
|
||||
notesMoreThanOrEq: "Beitragszahl ist größer-gleich"
|
||||
and: "UND-Bedingung"
|
||||
or: "ODER-Bedingung"
|
||||
not: "NICHT-Bedingung"
|
||||
@ -1844,3 +1873,24 @@ _deck:
|
||||
_dialog:
|
||||
charactersExceeded: "Maximallänge überschritten! Momentan {current} von {max}"
|
||||
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"
|
||||
|
||||
|
@ -392,3 +392,6 @@ _deck:
|
||||
antenna: "Αντένες"
|
||||
list: "Λίστα"
|
||||
mentions: "Επισημάνσεις"
|
||||
_webhookSettings:
|
||||
name: "Όνομα"
|
||||
|
||||
|
@ -67,7 +67,7 @@ import: "Import"
|
||||
export: "Export"
|
||||
files: "Files"
|
||||
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}?"
|
||||
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."
|
||||
@ -122,6 +122,8 @@ unmarkAsSensitive: "Unmark as NSFW"
|
||||
enterFileName: "Enter filename"
|
||||
mute: "Mute"
|
||||
unmute: "Unmute"
|
||||
renoteMute: "Mute Renotes"
|
||||
renoteUnmute: "Unmute Renotes"
|
||||
block: "Block"
|
||||
unblock: "Unblock"
|
||||
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."
|
||||
autoAcceptFollowed: "Automatically approve follow requests from users you're following"
|
||||
addAccount: "Add account"
|
||||
reloadAccountsList: "Reload account list"
|
||||
loginFailed: "Failed to sign in"
|
||||
showOnRemote: "View on remote instance"
|
||||
general: "General"
|
||||
@ -527,7 +530,7 @@ nothing: "There's nothing to see here"
|
||||
installedDate: "Authorized at"
|
||||
lastUsedDate: "Last used at"
|
||||
state: "State"
|
||||
sort: "Sort"
|
||||
sort: "Sorting order"
|
||||
ascendingOrder: "Ascending"
|
||||
descendingOrder: "Descending"
|
||||
scratchpad: "Scratchpad"
|
||||
@ -544,6 +547,10 @@ userSuspended: "This user has been suspended."
|
||||
userSilenced: "This user is being silenced."
|
||||
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."
|
||||
tokenRevoked: "Invalid token"
|
||||
tokenRevokedDescription: "This token has expired. Please log in again."
|
||||
accountDeleted: "Account deleted"
|
||||
accountDeletedDescription: "This account has been deleted."
|
||||
menu: "Menu"
|
||||
divider: "Divider"
|
||||
addItem: "Add Item"
|
||||
@ -587,7 +594,6 @@ tokenRequested: "Grant access to account"
|
||||
pluginTokenRequestedDescription: "This plugin will be able to use the permissions set here."
|
||||
notificationType: "Notification type"
|
||||
edit: "Edit"
|
||||
useStarForReactionFallback: "Use ★ as fallback if the reaction emoji is unknown"
|
||||
emailServer: "Email server"
|
||||
enableEmail: "Enable email distribution"
|
||||
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"
|
||||
postToTheChannel: "Post to channel"
|
||||
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:
|
||||
earnedAt: "Unlocked at"
|
||||
_types:
|
||||
@ -1218,6 +1242,8 @@ _role:
|
||||
iconUrl: "Icon URL"
|
||||
asBadge: "Show as badge"
|
||||
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"
|
||||
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"
|
||||
@ -1243,6 +1269,7 @@ _role:
|
||||
rateLimitFactor: "Rate limit"
|
||||
descriptionOfRateLimitFactor: "Lower rate limits are less restrictive, higher ones more restrictive. "
|
||||
canHideAds: "Can hide ads"
|
||||
canSearchNotes: "Usage of note search"
|
||||
_condition:
|
||||
isLocal: "Local user"
|
||||
isRemote: "Remote user"
|
||||
@ -1252,6 +1279,8 @@ _role:
|
||||
followersMoreThanOrEq: "Has X or more followers"
|
||||
followingLessThanOrEq: "Follows X or fewer 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"
|
||||
or: "OR-Condition"
|
||||
not: "NOT-Condition"
|
||||
@ -1844,3 +1873,24 @@ _deck:
|
||||
_dialog:
|
||||
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}."
|
||||
_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"
|
||||
|
||||
|
@ -122,6 +122,8 @@ unmarkAsSensitive: "Desmarcar como sensible"
|
||||
enterFileName: "Ingrese el nombre del archivo"
|
||||
mute: "Silenciar"
|
||||
unmute: "Dejar de silenciar"
|
||||
renoteMute: "Silenciar renota"
|
||||
renoteUnmute: "Desilenciar renota"
|
||||
block: "Bloquear"
|
||||
unblock: "Dejar de bloquear"
|
||||
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"
|
||||
autoAcceptFollowed: "Aceptar automáticamente las solicitudes de seguimiento de los usuarios que sigues"
|
||||
addAccount: "Agregar Cuenta"
|
||||
reloadAccountsList: "Recargar lista de cuentas"
|
||||
loginFailed: "Error al iniciar sesión."
|
||||
showOnRemote: "Ver en una instancia remota"
|
||||
general: "General"
|
||||
@ -506,6 +509,7 @@ objectStorageSetPublicRead: "Seleccionar \"public-read\" al subir "
|
||||
serverLogs: "Registros del servidor"
|
||||
deleteAll: "Eliminar todos"
|
||||
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"
|
||||
sounds: "Sonidos"
|
||||
sound: "Sonidos"
|
||||
@ -543,6 +547,10 @@ userSuspended: "Este usuario ha sido suspendido."
|
||||
userSilenced: "Este usuario ha sido silenciado."
|
||||
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."
|
||||
tokenRevoked: "Token inválido"
|
||||
tokenRevokedDescription: "Este token expiró, vuelve a iniciar sesión."
|
||||
accountDeleted: "Cuenta borrada"
|
||||
accountDeletedDescription: "Esta cuenta ha sido borrada."
|
||||
menu: "Menú"
|
||||
divider: "Divisor"
|
||||
addItem: "Agregar elemento"
|
||||
@ -586,7 +594,6 @@ tokenRequested: "Permiso de acceso a la cuenta"
|
||||
pluginTokenRequestedDescription: "Este plugin podrá usar los permisos descritos aquí"
|
||||
notificationType: "Tipo de notificación"
|
||||
edit: "Editar"
|
||||
useStarForReactionFallback: "En caso de que los emojis de reacciones no sean claros, usar en su lugar una estrella"
|
||||
emailServer: "Servidor de correo"
|
||||
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"
|
||||
@ -955,6 +962,24 @@ exploreOtherServers: "Buscar otra instancia"
|
||||
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."
|
||||
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:
|
||||
earnedAt: "Desbloqueado el"
|
||||
_types:
|
||||
@ -1214,6 +1239,8 @@ _role:
|
||||
iconUrl: "URL del ícono"
|
||||
asBadge: "Mostrar como emblema"
|
||||
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"
|
||||
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"
|
||||
@ -1239,6 +1266,7 @@ _role:
|
||||
rateLimitFactor: "Limitador"
|
||||
descriptionOfRateLimitFactor: "Límites más bajos son menos restrictivos, más altos menos restrictivos"
|
||||
canHideAds: "Puede ocultar anuncios"
|
||||
canSearchNotes: "Uso de la búsqueda de notas"
|
||||
_condition:
|
||||
isLocal: "Usuario local"
|
||||
isRemote: "Usuario remoto"
|
||||
@ -1840,3 +1868,13 @@ _deck:
|
||||
_dialog:
|
||||
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}."
|
||||
_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"
|
||||
|
||||
|
@ -575,7 +575,6 @@ tokenRequested: "Autoriser l'accès au compte"
|
||||
pluginTokenRequestedDescription: "Ce plugin pourra utiliser les autorisations définies ici."
|
||||
notificationType: "Type de notifications"
|
||||
edit: "Editer"
|
||||
useStarForReactionFallback: "Utiliser ★ comme alternative si l’émoji de réaction est inconnu"
|
||||
emailServer: "Serveur mail"
|
||||
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 d’oubli."
|
||||
@ -1468,3 +1467,7 @@ _deck:
|
||||
channel: "Canaux"
|
||||
mentions: "Mentions"
|
||||
direct: "Direct"
|
||||
_webhookSettings:
|
||||
name: "Nom"
|
||||
active: "Activé"
|
||||
|
||||
|
@ -1 +1,2 @@
|
||||
---
|
||||
|
||||
|
@ -1 +1,2 @@
|
||||
---
|
||||
|
||||
|
@ -579,7 +579,6 @@ tokenRequested: "Berikan ijin akses ke akun"
|
||||
pluginTokenRequestedDescription: "Plugin ini dapat menggunakan setelan ijin disini."
|
||||
notificationType: "Jenis pemberitahuan"
|
||||
edit: "Sunting"
|
||||
useStarForReactionFallback: "Gunakan ★ sebagai fallback jika reaksi emoji tidak diketahui"
|
||||
emailServer: "Peladen surel"
|
||||
enableEmail: "Nyalakan distribusi surel"
|
||||
emailConfigInfo: "Digunakan untuk mengonfirmasi surel kamu disaat mendaftar dan lupa kata sandi"
|
||||
@ -1804,3 +1803,7 @@ _deck:
|
||||
channel: "Kanal"
|
||||
mentions: "Sebutan"
|
||||
direct: "Langsung"
|
||||
_webhookSettings:
|
||||
name: "Nama"
|
||||
active: "Aktif"
|
||||
|
||||
|
@ -122,6 +122,8 @@ unmarkAsSensitive: "Segna come non sensibile"
|
||||
enterFileName: "Nome del file"
|
||||
mute: "Silenzia"
|
||||
unmute: "Riattiva l'audio"
|
||||
renoteMute: "Silenzia i Rinota"
|
||||
renoteUnmute: "Non silenziare i Rinota"
|
||||
block: "Blocca"
|
||||
unblock: "Sblocca"
|
||||
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."
|
||||
autoAcceptFollowed: "Accetta automaticamente le richieste di follow da utenti che già segui"
|
||||
addAccount: "Aggiungi profilo"
|
||||
reloadAccountsList: "Ricarica l'elenco dei profili"
|
||||
loginFailed: "Accesso non riuscito"
|
||||
showOnRemote: "Leggi sull'istanza remota"
|
||||
general: "Generali"
|
||||
@ -544,6 +547,10 @@ userSuspended: "L'utente è in sospensione"
|
||||
userSilenced: "L'utente è silenziat@."
|
||||
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."
|
||||
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ù"
|
||||
divider: "Linea di separazione"
|
||||
addItem: "Aggiungi elemento"
|
||||
@ -558,8 +565,8 @@ enableInfiniteScroll: "Abilita scorrimento infinito"
|
||||
visibility: "Visibilità"
|
||||
poll: "Sondaggio"
|
||||
useCw: "Nascondere media"
|
||||
enablePlayer: "Apri in lettore video"
|
||||
disablePlayer: "Chiudi il lettore"
|
||||
enablePlayer: "Visualizza"
|
||||
disablePlayer: "Chiudi"
|
||||
expandTweet: "Espandi tweet"
|
||||
themeEditor: "Editor di temi"
|
||||
description: "Descrizione"
|
||||
@ -587,7 +594,6 @@ tokenRequested: "Autorizza accesso al profilo"
|
||||
pluginTokenRequestedDescription: "Il plugin potrà utilizzare le autorizzazioni impostate qui."
|
||||
notificationType: "Tipo di notifiche"
|
||||
edit: "Modifica"
|
||||
useStarForReactionFallback: "Se è sconosciuto l'emoji di reazione, usare la ★ come alternativa."
|
||||
emailServer: "Server email"
|
||||
enableEmail: "Abilita consegna email"
|
||||
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"
|
||||
postToTheChannel: "Pubblica sul canale"
|
||||
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:
|
||||
earnedAt: "Data di conseguimento"
|
||||
_types:
|
||||
@ -1218,6 +1239,8 @@ _role:
|
||||
iconUrl: "URL dell'icona"
|
||||
asBadge: "Mostra come badge"
|
||||
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"
|
||||
descriptionOfCanEditMembersByModerator: "Se disattivo, potranno farlo solamente gli Amministratori."
|
||||
priority: "Priorità"
|
||||
@ -1243,6 +1266,7 @@ _role:
|
||||
rateLimitFactor: "Limite del rapporto"
|
||||
descriptionOfRateLimitFactor: "I rapporti più bassi sono meno restrittivi, quelli più alti lo sono di più."
|
||||
canHideAds: "Può nascondere i banner"
|
||||
canSearchNotes: "Ricercare nelle Note"
|
||||
_condition:
|
||||
isLocal: "Profilo locale"
|
||||
isRemote: "Profilo remoto"
|
||||
@ -1844,3 +1868,13 @@ _deck:
|
||||
_dialog:
|
||||
charactersExceeded: "Hai superato il limite di {max} 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"
|
||||
|
||||
|
@ -67,7 +67,7 @@ import: "インポート"
|
||||
export: "エクスポート"
|
||||
files: "ファイル"
|
||||
download: "ダウンロード"
|
||||
driveFileDeleteConfirm: "ファイル「{name}」を削除しますか?このファイルを添付したノートも消えます。"
|
||||
driveFileDeleteConfirm: "ファイル「{name}」を削除しますか?このファイルを使用した全てのコンテンツからも削除されます。"
|
||||
unfollowConfirm: "{name}のフォローを解除しますか?"
|
||||
exportRequested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、「ドライブ」に追加されます。"
|
||||
importRequested: "インポートをリクエストしました。これには時間がかかる場合があります。"
|
||||
@ -460,7 +460,7 @@ aboutX: "{x}について"
|
||||
emojiStyle: "絵文字のスタイル"
|
||||
native: "ネイティブ"
|
||||
disableDrawer: "メニューをドロワーで表示しない"
|
||||
showNoteActionsOnlyHover: "ノートの操作部をホバー時のみ表示する"
|
||||
showNoteActionsOnlyHover: "ノートのアクションをホバー時のみ表示する"
|
||||
noHistory: "履歴はありません"
|
||||
signinHistory: "ログイン履歴"
|
||||
enableAdvancedMfm: "高度なMFMを有効にする"
|
||||
@ -500,12 +500,13 @@ objectStoragePrefixDesc: "このprefixのディレクトリ下に格納されま
|
||||
objectStorageEndpoint: "Endpoint"
|
||||
objectStorageEndpointDesc: "S3の場合は空、それ以外の場合は各サービスのendpointを指定してください。'<host>'または'<host>:<port>'のように指定します。"
|
||||
objectStorageRegion: "Region"
|
||||
objectStorageRegionDesc: "'xx-east-1'のようなregionを指定してください。使用サービスにregionの概念がない場合は、空または'us-east-1'にしてください。"
|
||||
objectStorageRegionDesc: "'xx-east-1'のようなregionを指定してください。使用サービスにregionの概念がない場合は'us-east-1'にしてください。AWS設定ファイルまたは環境変数を参照する場合は空にしてください。"
|
||||
objectStorageUseSSL: "SSLを使用する"
|
||||
objectStorageUseSSLDesc: "API接続にhttpsを使用しない場合はオフにしてください"
|
||||
objectStorageUseProxy: "Proxyを利用する"
|
||||
objectStorageUseProxyDesc: "API接続にproxyを利用しない場合はオフにしてください"
|
||||
objectStorageSetPublicRead: "アップロード時に'public-read'を設定する"
|
||||
s3ForcePathStyleDesc: "s3ForcePathStyleを有効にすると、バケット名をURLのホスト名ではなくパスの一部として指定することを強制します。セルフホストされたMinioなどの使用時に有効にする必要がある場合があります。"
|
||||
serverLogs: "サーバーログ"
|
||||
deleteAll: "全て削除"
|
||||
showFixedPostForm: "タイムライン上部に投稿フォームを表示する"
|
||||
@ -594,7 +595,6 @@ tokenRequested: "アカウントへのアクセス許可"
|
||||
pluginTokenRequestedDescription: "このプラグインはここで設定した権限を行使できるようになります。"
|
||||
notificationType: "通知の種類"
|
||||
edit: "編集"
|
||||
useStarForReactionFallback: "リアクション絵文字が不明な場合、代わりに★を使う"
|
||||
emailServer: "メールサーバー"
|
||||
enableEmail: "メール配信機能を有効化する"
|
||||
emailConfigInfo: "メールアドレスの確認やパスワードリセットの際に使います"
|
||||
@ -969,6 +969,23 @@ cannotBeChangedLater: "後から変更できません。"
|
||||
reactionAcceptance: "リアクションの受け入れ"
|
||||
likeOnly: "いいねのみ"
|
||||
likeOnlyForRemote: "リモートからはいいねのみ"
|
||||
rolesAssignedToMe: "自分に割り当てられたロール"
|
||||
resetPasswordConfirm: "パスワードリセットしますか?"
|
||||
sensitiveWords: "センシティブワード"
|
||||
sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。"
|
||||
notesSearchNotAvailable: "ノート検索は利用できません。"
|
||||
license: "ライセンス"
|
||||
unfavoriteConfirm: "お気に入り解除しますか?"
|
||||
myClips: "自分のクリップ"
|
||||
drivecleaner: "ドライブクリーナー"
|
||||
retryAllQueuesNow: "すべてのキューを今すぐ再試行"
|
||||
retryAllQueuesConfirmTitle: "今すぐ再試行しますか?"
|
||||
retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大することがあります。"
|
||||
enableChartsForRemoteUser: "リモートユーザーのチャートを生成"
|
||||
enableChartsForFederatedInstances: "リモートサーバーのチャートを生成"
|
||||
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
|
||||
largeNoteReactions: "ノートのリアクションを大きく表示"
|
||||
noteIdOrUrl: "ノートIDまたはURL"
|
||||
|
||||
_achievements:
|
||||
earnedAt: "獲得日時"
|
||||
@ -1230,6 +1247,8 @@ _role:
|
||||
iconUrl: "アイコン画像のURL"
|
||||
asBadge: "バッジとして表示"
|
||||
descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。"
|
||||
displayOrder: "表示順"
|
||||
descriptionOfDisplayOrder: "数値が大きいほどUI上で先頭に表示されます。"
|
||||
canEditMembersByModerator: "モデレーターのメンバー編集を許可"
|
||||
descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。"
|
||||
priority: "優先度"
|
||||
@ -1255,6 +1274,7 @@ _role:
|
||||
rateLimitFactor: "レートリミット"
|
||||
descriptionOfRateLimitFactor: "小さいほど制限が緩和され、大きいほど制限が強化されます。"
|
||||
canHideAds: "広告の非表示"
|
||||
canSearchNotes: "ノート検索の利用可否"
|
||||
_condition:
|
||||
isLocal: "ローカルユーザー"
|
||||
isRemote: "リモートユーザー"
|
||||
@ -1264,6 +1284,8 @@ _role:
|
||||
followersMoreThanOrEq: "フォロワー数が~以上"
|
||||
followingLessThanOrEq: "フォロー数が~以下"
|
||||
followingMoreThanOrEq: "フォロー数が~以上"
|
||||
notesLessThanOrEq: "投稿数が~以下"
|
||||
notesMoreThanOrEq: "投稿数が~以上"
|
||||
and: "~かつ~"
|
||||
or: "~または~"
|
||||
not: "~ではない"
|
||||
@ -1907,3 +1929,27 @@ _deck:
|
||||
_dialog:
|
||||
charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}"
|
||||
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: "メンションされたとき"
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
_lang_: "日本語 (関西弁)"
|
||||
headlineMisskey: "ノートでつながるネットワーク"
|
||||
introMisskey: "ようお越し!Misskeyは、オープンソースの分散型マイクロブログサービスやねん。\n「ノート」を作って、いま起こっとることを共有したり、あんたについて皆に発信しよう📡\n「リアクション」機能で、皆のノートに素早く反応を追加したりもできるで✌\nほな新しい世界を探検しよか🚀"
|
||||
poweredByMisskeyDescription: "{name}は、オープンソースのプラットフォーム<b>Misskey</b>を使ったサービス(Misskeyインスタンスと呼ばれるやつや)のひとつやで。"
|
||||
poweredByMisskeyDescription: "{name}は、オープンソースのプラットフォーム<b>Misskey</b>のサーバーのひとつなんやで。"
|
||||
monthAndDay: "{month}月 {day}日"
|
||||
search: "探す"
|
||||
notifications: "通知"
|
||||
@ -15,13 +15,13 @@ gotIt: "ほい"
|
||||
cancel: "やめとく"
|
||||
noThankYou: "やめとく"
|
||||
enterUsername: "ユーザー名を入れてや"
|
||||
renotedBy: "{user}がRenote"
|
||||
noNotes: "ノートはあらへん"
|
||||
noNotifications: "通知はあらへん"
|
||||
instance: "インスタンス"
|
||||
renotedBy: "{user}がRenoteしたで"
|
||||
noNotes: "ノートなんてあらへんで"
|
||||
noNotifications: "通知なんてあらへんで"
|
||||
instance: "サーバー"
|
||||
settings: "設定"
|
||||
basicSettings: "基本設定"
|
||||
otherSettings: "その他の設定"
|
||||
otherSettings: "ほかの設定"
|
||||
openInWindow: "ウィンドウで開くで"
|
||||
profile: "プロフィール"
|
||||
timeline: "タイムライン"
|
||||
@ -55,7 +55,7 @@ searchUser: "ユーザーを検索"
|
||||
reply: "返事"
|
||||
loadMore: "まだまだあるで!"
|
||||
showMore: "まだまだあるで!"
|
||||
showLess: "閉じる"
|
||||
showLess: "さいなら"
|
||||
youGotNewFollower: "フォローされたで"
|
||||
receiveFollowRequest: "フォローリクエストされたで"
|
||||
followRequestAccepted: "フォローが承認されたで"
|
||||
@ -84,12 +84,12 @@ error: "エラー"
|
||||
somethingHappened: "なんかアカンことが起こったで"
|
||||
retry: "もっぺんやる?"
|
||||
pageLoadError: "ページの読み込みに失敗してもうたわ…"
|
||||
pageLoadErrorDescription: "これは普通、ネットワークかブラウザキャッシュが原因やからね。キャッシュをクリアするか、もうちっとだけ待ってくれへんか?"
|
||||
pageLoadErrorDescription: "これは普通ならネットワークかブラウザキャッシュが悪さしてるんよ。キャッシュをほかすか、もうちょっとだけ待ってくれへん?"
|
||||
serverIsDead: "サーバーからの応答がないで。もうちょい待ってから試してみてな。"
|
||||
youShouldUpgradeClient: "このページを表示するには、リロードして新しいバージョンのクライアントを使ってなー。"
|
||||
enterListName: "リスト名を入れてや"
|
||||
privacy: "プライバシー"
|
||||
makeFollowManuallyApprove: "他人のフォローは許可してからや!"
|
||||
makeFollowManuallyApprove: "ええって言わなフォローできへんようにする"
|
||||
defaultNoteVisibility: "もとからの公開範囲"
|
||||
follow: "フォロー"
|
||||
followRequest: "フォローを頼む"
|
||||
@ -113,7 +113,7 @@ sensitive: "ちょっとアカンやつやで"
|
||||
add: "増やす"
|
||||
reaction: "リアクション"
|
||||
reactions: "リアクション"
|
||||
reactionSetting: "Reaction that will be displayed in Picker. "
|
||||
reactionSetting: "ピッカーに出しとくリアクション"
|
||||
reactionSettingDescription2: "ドラッグで並び替え、クリックで削除、+を押して追加やで。"
|
||||
rememberNoteVisibility: "公開範囲覚えといて"
|
||||
attachCancel: "のっけるのやめる"
|
||||
@ -122,6 +122,8 @@ unmarkAsSensitive: "そこまでアカンことないやろ"
|
||||
enterFileName: "ファイル名を入れてや"
|
||||
mute: "ミュート"
|
||||
unmute: "ミュートやめたる"
|
||||
renoteMute: "リノートは見いひん"
|
||||
renoteUnmute: "リノートもやっぱ見るわ"
|
||||
block: "ブロック"
|
||||
unblock: "ブロックやめたる"
|
||||
suspend: "凍結"
|
||||
@ -145,20 +147,21 @@ addEmoji: "絵文字を追加"
|
||||
settingGuide: "ええ感じの設定"
|
||||
cacheRemoteFiles: "リモートのファイルをキャッシュする"
|
||||
cacheRemoteFilesDescription: "この設定を切っとくと、リモートファイルをキャッシュせず直リンクするようになるで。サーバーの容量は節約できるけど、サムネイルが作られんくなるから通信量が増えるで。"
|
||||
flagAsBot: "Botやで"
|
||||
flagAsBotDescription: "もしこのアカウントがプログラムによって運用されるんやったら、このフラグをオンにしてたのむで。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Misskeyのシステム上での扱いがBotに合ったもんになるんやで。"
|
||||
flagAsBot: "Botにするで"
|
||||
flagAsBotDescription: "もしこのアカウントをプログラム使うて運用するんやったら、このフラグをオンにしてや。オンにすれば、反応がバーッて連鎖するのを避けるために開発者が使うたり、Misskeyのシステム上での扱いがBotに合ったもんになるからな。"
|
||||
flagAsCat: "Catやで"
|
||||
flagAsCatDescription: "ワレ、猫ちゃんならこのフラグをつけてみ?"
|
||||
flagShowTimelineReplies: "タイムラインにノートへの返信を表示するで"
|
||||
flagShowTimelineRepliesDescription: "オンにしたら、タイムラインにユーザーのノートの他にもそのユーザーの他のノートへの返信を表示するで。"
|
||||
autoAcceptFollowed: "フォローしとるユーザーからのフォローリクエストを勝手に許可しとく"
|
||||
addAccount: "アカウントを追加"
|
||||
reloadAccountsList: "アカウントリストの情報を更新"
|
||||
loginFailed: "ログインに失敗してもうた…"
|
||||
showOnRemote: "リモートで見る"
|
||||
general: "全般"
|
||||
wallpaper: "壁紙"
|
||||
setWallpaper: "壁紙を設定"
|
||||
removeWallpaper: "壁紙を削除"
|
||||
removeWallpaper: "壁紙ほかす"
|
||||
searchWith: "検索: {q}"
|
||||
youHaveNoLists: "リストがあらへんで?"
|
||||
followConfirm: "{name}をフォローしてええか?"
|
||||
@ -169,7 +172,7 @@ selectUser: "ユーザーを選ぶ"
|
||||
recipient: "宛先"
|
||||
annotation: "注釈"
|
||||
federation: "連合"
|
||||
instances: "インスタンス"
|
||||
instances: "サーバー"
|
||||
registeredAt: "初観測"
|
||||
latestRequestReceivedAt: "ちょっと前のリクエスト受信"
|
||||
latestStatus: "ちょっと前のステータス"
|
||||
@ -178,7 +181,7 @@ charts: "チャート"
|
||||
perHour: "1時間ごと"
|
||||
perDay: "1日ごと"
|
||||
stopActivityDelivery: "アクティビティの配送をやめる"
|
||||
blockThisInstance: "このインスタンスをブロック"
|
||||
blockThisInstance: "このサーバーをブロックすんで"
|
||||
operations: "操作"
|
||||
software: "ソフトウェア"
|
||||
version: "バージョン"
|
||||
@ -189,28 +192,28 @@ jobQueue: "ジョブキュー"
|
||||
cpuAndMemory: "CPUとメモリ"
|
||||
network: "ネットワーク"
|
||||
disk: "ディスク"
|
||||
instanceInfo: "インスタンス情報"
|
||||
instanceInfo: "サーバー情報"
|
||||
statistics: "統計"
|
||||
clearQueue: "キューにさいなら"
|
||||
clearQueueConfirmTitle: "キューをクリアしまっか?"
|
||||
clearQueueConfirmText: "未配達の投稿は配送されなくなるで。通常この操作を行う必要はあらへんや。"
|
||||
clearQueueConfirmText: "未配達の投稿は配送されなくなるで。ふつうこの操作を行う必要は無いんやけどな。"
|
||||
clearCachedFiles: "キャッシュにさいなら"
|
||||
clearCachedFilesConfirm: "キャッシュされとるリモートファイルをみんなほかしてええか?"
|
||||
blockedInstances: "インスタンスブロック"
|
||||
blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定してな。ブロックされてもうたインスタンスとはもう金輪際やり取りできひんくなるで。"
|
||||
blockedInstances: "ブロックしたサーバー"
|
||||
blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定してな。ブロックされてもうたサーバーとはもう金輪際やり取りできひんくなるで。ついでにそのサブドメインもブロックするで。"
|
||||
muteAndBlock: "ミュートとブロック"
|
||||
mutedUsers: "ミュートしたユーザー"
|
||||
blockedUsers: "ブロックしたユーザー"
|
||||
noUsers: "ユーザーはおらへん"
|
||||
noUsers: "ユーザーはおらん"
|
||||
editProfile: "プロフィールをいじる"
|
||||
noteDeleteConfirm: "このノートを削除しまっか?"
|
||||
pinLimitExceeded: "これ以上ピン留めできひん"
|
||||
intro: "Misskeyのインストールが完了してん!管理者アカウントを作ってや。"
|
||||
intro: "Misskeyのインストールが完了したで!管理者アカウントを作ってや。"
|
||||
done: "でけた"
|
||||
processing: "処理しとる"
|
||||
preview: "プレビュー"
|
||||
default: "デフォルト"
|
||||
defaultValueIs: "デフォルト"
|
||||
defaultValueIs: "デフォルト: {value}"
|
||||
noCustomEmojis: "絵文字はあらへん"
|
||||
noJobs: "ジョブはあらへん"
|
||||
federating: "連合しとる"
|
||||
@ -220,17 +223,17 @@ all: "みんな"
|
||||
subscribing: "購読しとる"
|
||||
publishing: "配信しとる"
|
||||
notResponding: "応答してへんで"
|
||||
instanceFollowing: "インスタンスのフォロー"
|
||||
instanceFollowers: "インスタンスのフォロワー\n"
|
||||
instanceUsers: "インスタンスのユーザー"
|
||||
instanceFollowing: "サーバーのフォロー"
|
||||
instanceFollowers: "サーバーのフォロワー\n"
|
||||
instanceUsers: "サーバーのユーザー"
|
||||
changePassword: "パスワード変える"
|
||||
security: "セキュリティ"
|
||||
retypedNotMatch: "そやないねん。"
|
||||
retypedNotMatch: "入れたやつ同じになってないで。"
|
||||
currentPassword: "今のパスワード"
|
||||
newPassword: "今度のパスワード"
|
||||
newPassword: "次のパスワード"
|
||||
newPasswordRetype: "今度のパスワード(もっぺん入れて)"
|
||||
attachFile: "ファイルのっける"
|
||||
more: "他のやつ!"
|
||||
more: "他のん"
|
||||
featured: "ハイライト"
|
||||
usernameOrUserId: "ユーザー名かユーザーID"
|
||||
noSuchUser: "ユーザーが見つからへんで"
|
||||
@ -238,15 +241,15 @@ lookup: "見てきて"
|
||||
announcements: "お知らせ"
|
||||
imageUrl: "画像URL"
|
||||
remove: "ほかす"
|
||||
removed: "削除したで!"
|
||||
removed: "ほかしたで!"
|
||||
removeAreYouSure: "「{x}」はほかしてええか?"
|
||||
deleteAreYouSure: "「{x}」はほかしてええか?"
|
||||
resetAreYouSure: "リセットしてええん?"
|
||||
saved: "保存したで!"
|
||||
messaging: "チャット"
|
||||
upload: "アップロード"
|
||||
keepOriginalUploading: "オリジナル画像を保持するわ"
|
||||
keepOriginalUploadingDescription: "画像を上げるときにオリジナル版を保持するで。オフにしたら上げたときにブラウザでWeb公開用の画像を生成するで。 "
|
||||
keepOriginalUploading: "オリジナル画像のまんま"
|
||||
keepOriginalUploadingDescription: "画像を上げるときにオリジナル版のまんまにするで。オフにしたら、上げたときにブラウザでWeb公開用の画像を生成するで。 "
|
||||
fromDrive: "ドライブから"
|
||||
fromUrl: "URLから"
|
||||
uploadFromUrl: "URLアップロード"
|
||||
@ -272,8 +275,8 @@ yearsOld: "{age}歳"
|
||||
registeredDate: "始めた日"
|
||||
location: "場所"
|
||||
theme: "テーマ"
|
||||
themeForLightMode: "ライトモードではこのテーマつこて"
|
||||
themeForDarkMode: "ダークモードではこのテーマつこて"
|
||||
themeForLightMode: "ライトモードではこのテーマ使うて"
|
||||
themeForDarkMode: "ダークモードではこのテーマ使うて"
|
||||
light: "ライト"
|
||||
dark: "ダーク"
|
||||
lightThemes: "デイゲーム"
|
||||
@ -289,13 +292,13 @@ renameFile: "ファイル名をいらう"
|
||||
folderName: "フォルダー名"
|
||||
createFolder: "フォルダー作る"
|
||||
renameFolder: "フォルダー名を変える"
|
||||
deleteFolder: "フォルダーを消してまう"
|
||||
deleteFolder: "フォルダーをほかす"
|
||||
addFile: "ファイルを追加"
|
||||
emptyDrive: "ドライブにはなんも残っとらん"
|
||||
emptyFolder: "ふぉろだーにはなんも残っとらん"
|
||||
emptyFolder: "このフォルダーは空や"
|
||||
unableToDelete: "消そうおもってんけどな、あかんかったわ"
|
||||
inputNewFileName: "今度のファイル名は何にするん?"
|
||||
inputNewDescription: "新しいキャプションを入力しましょ"
|
||||
inputNewDescription: "新しいキャプションを入れてや"
|
||||
inputNewFolderName: "今度のフォルダ名は何にするん?"
|
||||
circularReferenceFolder: "移動先のフォルダーは、移動するフォルダーのサブフォルダーや。"
|
||||
hasChildFilesOrFolders: "このフォルダ、まだなんか入っとるから消されへん"
|
||||
@ -303,8 +306,8 @@ copyUrl: "URLをコピー"
|
||||
rename: "名前を変えるで"
|
||||
avatar: "アイコン"
|
||||
banner: "バナー"
|
||||
nsfw: "閲覧注意"
|
||||
whenServerDisconnected: "サーバーとの接続が切れたとき"
|
||||
nsfw: "見るんは気いつけてな"
|
||||
whenServerDisconnected: "サーバーとの接続が失くなってしもうたとき"
|
||||
disconnectedFromServer: "サーバーが機嫌悪いねん"
|
||||
reload: "リロード"
|
||||
doNothing: "何もせんとく"
|
||||
@ -314,10 +317,10 @@ unwatch: "ウォッチやめる"
|
||||
accept: "ええで"
|
||||
reject: "あかん"
|
||||
normal: "ええ感じ"
|
||||
instanceName: "インスタンス名"
|
||||
instanceDescription: "インスタンスの紹介"
|
||||
maintainerName: "管理者の名前"
|
||||
maintainerEmail: "管理者のメールアドレス"
|
||||
instanceName: "サーバー名"
|
||||
instanceDescription: "サーバーの紹介"
|
||||
maintainerName: "管理者はんの名前"
|
||||
maintainerEmail: "管理者はんのメールアドレス"
|
||||
tosUrl: "利用規約のURL"
|
||||
thisYear: "今年"
|
||||
thisMonth: "今月"
|
||||
@ -329,23 +332,23 @@ pages: "ページ"
|
||||
integration: "連携"
|
||||
connectService: "つなげるで"
|
||||
disconnectService: "切るで"
|
||||
enableLocalTimeline: "ローカルタイムラインを使えるようにする"
|
||||
enableGlobalTimeline: "グローバルタイムラインを使えるようにする"
|
||||
enableLocalTimeline: "ローカルタイムラインを使えるようにするわ"
|
||||
enableGlobalTimeline: "グローバルタイムラインを使えるようにするわ"
|
||||
disablingTimelinesInfo: "ここらへんのタイムラインを使えんようにしてしもても、管理者とモデレーターは使えるままになってるで、そうやなかったら不便やからな。"
|
||||
registration: "登録"
|
||||
enableRegistration: "一見さんでも誰でもいらっしゃ~い"
|
||||
invite: "来てや"
|
||||
driveCapacityPerLocalAccount: "ローカルユーザーひとりあたりのドライブ容量"
|
||||
driveCapacityPerRemoteAccount: "リモートユーザーひとりあたりのドライブ容量"
|
||||
driveCapacityPerLocalAccount: "ローカルユーザーはんひとりあたりのドライブ容量"
|
||||
driveCapacityPerRemoteAccount: "リモートユーザーはんひとりあたりのドライブ容量"
|
||||
inMb: "メガバイト単位"
|
||||
iconUrl: "アイコン画像のURL"
|
||||
bannerUrl: "バナー画像のURL"
|
||||
backgroundImageUrl: "背景画像のURL"
|
||||
basicInfo: "基本情報"
|
||||
pinnedUsers: "ピン留めしたユーザー"
|
||||
pinnedUsersDescription: "「みつける」ページとかにピン留めしたいユーザーをここに書けばええんやで。他ん人との名前は改行で区切ればええんやで。"
|
||||
pinnedUsersDescription: "「みつける」ページとかにピン留めしたいユーザーをここに書けばええんやで。ユーザー毎に改行してや。"
|
||||
pinnedPages: "ピン留めページ"
|
||||
pinnedPagesDescription: "インスタンスのいっちゃん上にピン留めしたいページのパスを改行で区切って記述してな"
|
||||
pinnedPagesDescription: "サーバーのいっちゃん上にピン留めしたいページのパスを改行で区切って記述してな"
|
||||
pinnedClipId: "ピン留めするクリップのID"
|
||||
pinnedNotes: "ピン留めされとるノート"
|
||||
hcaptcha: "hCaptcha(キャプチャ)"
|
||||
@ -370,7 +373,7 @@ antennaExcludeKeywords: "除外キーワード"
|
||||
antennaKeywordsDescription: "スペースで区切ったるとAND指定で、改行で区切ったるとOR指定や"
|
||||
notifyAntenna: "新しいノートを通知すんで"
|
||||
withFileAntenna: "なんか添付されたノートだけ"
|
||||
enableServiceworker: "ServiceWorkerをつこて"
|
||||
enableServiceworker: "ブラウザにプッシュ通知が行くようにする"
|
||||
antennaUsersDescription: "ユーザー名を改行で区切ったってな"
|
||||
caseSensitive: "大文字と小文字は別もんや"
|
||||
withReplies: "返信も入れたって"
|
||||
@ -395,23 +398,23 @@ administrator: "管理者"
|
||||
token: "トークン"
|
||||
2fa: "二要素認証"
|
||||
totp: "認証アプリ"
|
||||
totpDescription: "認証アプリ使てワンタイムパスワードを入れる"
|
||||
totpDescription: "認証アプリ使うてワンタイムパスワードを入れる"
|
||||
moderator: "モデレーター"
|
||||
moderation: "モデレーション"
|
||||
nUsersMentioned: "{n}人が投稿"
|
||||
securityKeyAndPasskey: "セキュリティキー・パスキー"
|
||||
securityKey: "セキュリティキー"
|
||||
lastUsed: "最後につこうた日"
|
||||
lastUsedAt: "最後に使たん: {t}"
|
||||
lastUsedAt: "最後に使うたんは: {t}"
|
||||
unregister: "登録やめる"
|
||||
passwordLessLogin: "パスワード無くてもログインできるようにする"
|
||||
passwordLessLoginDescription: "パスワードやなくて、セキュリティキーとかパスキーだけでログインするわ"
|
||||
passwordLessLoginDescription: "パスワードなんかいらん、セキュリティキーとかパスキーだけでログインするわ"
|
||||
resetPassword: "パスワードをリセット"
|
||||
newPasswordIs: "今度のパスワードは「{password}」や"
|
||||
reduceUiAnimation: "UIの動きやアニメーションを減らす"
|
||||
reduceUiAnimation: "UIの動きやアニメーションを少なする"
|
||||
share: "わけわけ"
|
||||
notFound: "見つからへんね"
|
||||
notFoundDescription: "指定されたURLに該当するページはあらへんやった。"
|
||||
notFoundDescription: "言われたURLにはまるページはなかったで。"
|
||||
uploadFolder: "とりあえずアップロードしたやつ置いとく所"
|
||||
cacheClear: "キャッシュをほかす"
|
||||
markAsReadAllNotifications: "通知はもう全て読んだわっ"
|
||||
@ -419,37 +422,37 @@ markAsReadAllUnreadNotes: "投稿は全て読んだわっ"
|
||||
markAsReadAllTalkMessages: "チャットはもうぜんぶ読んだわっ"
|
||||
help: "ヘルプ"
|
||||
inputMessageHere: "ここにメッセージ書いてや"
|
||||
close: "閉じる"
|
||||
close: "さいなら"
|
||||
invites: "来てや"
|
||||
members: "メンバー"
|
||||
members: "メンバーはん"
|
||||
transfer: "譲渡"
|
||||
title: "タイトル"
|
||||
text: "テキスト"
|
||||
enable: "有効にするで"
|
||||
next: "次"
|
||||
retype: "もっかい入力"
|
||||
noteOf: "{user}のノート"
|
||||
noteOf: "{user}はんのノート"
|
||||
quoteAttached: "引用付いとるで"
|
||||
quoteQuestion: "引用として添付してもええか?"
|
||||
noMessagesYet: "まだチャットはあらへんで"
|
||||
newMessageExists: "新しいメッセージがきたで"
|
||||
onlyOneFileCanBeAttached: "すまん、メッセージに添付できるファイルはひとつだけなんや。"
|
||||
onlyOneFileCanBeAttached: "ごめんな、メッセージに添付できるファイルはひとつだけなんよ。"
|
||||
signinRequired: "ログインしてくれへん?"
|
||||
invitations: "来てや"
|
||||
invitationCode: "招待コード"
|
||||
checking: "確認しとるで"
|
||||
available: "利用できる\n"
|
||||
available: "使えるで"
|
||||
unavailable: "利用できん"
|
||||
usernameInvalidFormat: "a~z、A~Z、0~9、_が使えるで"
|
||||
tooShort: "短すぎやろ!"
|
||||
tooLong: "長すぎやろ!"
|
||||
weakPassword: "へぼいパスワード"
|
||||
normalPassword: "普通のパスワード"
|
||||
normalPassword: "ぼちぼちのパスワード"
|
||||
strongPassword: "ええ感じのパスワード"
|
||||
passwordMatched: "よし!一致や!"
|
||||
passwordNotMatched: "一致しとらんで?"
|
||||
signinWith: "{x}でログイン"
|
||||
signinFailed: "ログインできんかったで。もっかいユーザー名とパスワードを確認してみてな。"
|
||||
signinFailed: "ログインできんかったで。もっかいユーザー名とパスワードを確認してみてや。"
|
||||
or: "それか"
|
||||
language: "言語"
|
||||
uiLanguage: "UIの表示言語"
|
||||
@ -458,7 +461,7 @@ emojiStyle: "絵文字のスタイル"
|
||||
native: "ネイティブ"
|
||||
disableDrawer: "メニューをドロワーで表示せぇへん"
|
||||
showNoteActionsOnlyHover: "ノートの操作部をホバー時のみ表示するで"
|
||||
noHistory: "履歴はあらへんねぇ。"
|
||||
noHistory: "履歴はないわ。"
|
||||
signinHistory: "ログイン履歴"
|
||||
enableAdvancedMfm: "ややこしいMFMもありにする"
|
||||
enableAnimatedMfm: "動きがやかましいMFMも許したる"
|
||||
@ -466,12 +469,12 @@ doing: "やっとるがな"
|
||||
category: "カテゴリ"
|
||||
tags: "タグ"
|
||||
docSource: "このドキュメントのソース"
|
||||
createAccount: "アカウントを作成"
|
||||
existingAccount: "既存のアカウント"
|
||||
regenerate: "再生成"
|
||||
fontSize: "フォントサイズ"
|
||||
createAccount: "アカウントを作るで"
|
||||
existingAccount: "前に作ったアカウント"
|
||||
regenerate: "もっぺん生成するで"
|
||||
fontSize: "字の大きさ"
|
||||
noFollowRequests: "フォロー申請はあらへんで"
|
||||
openImageInNewTab: "画像を新しいタブで開く"
|
||||
openImageInNewTab: "画像を新しいタブで開くで"
|
||||
dashboard: "ダッシュボード"
|
||||
local: "ローカル"
|
||||
remote: "リモート"
|
||||
@ -504,7 +507,7 @@ objectStorageUseProxy: "Proxyを使う"
|
||||
objectStorageUseProxyDesc: "API接続にproxy使わんのやったら切ってくれへん?"
|
||||
objectStorageSetPublicRead: "アップロードした時に'public-read'を設定してや"
|
||||
serverLogs: "サーバーログ"
|
||||
deleteAll: "全て削除してや"
|
||||
deleteAll: "全部ほかす"
|
||||
showFixedPostForm: "タイムラインの上の方で投稿できるようにやってくれへん?"
|
||||
showFixedPostFormInChannel: "タイムラインの上の方で投稿できるようにするわ(チャンネル)"
|
||||
newNoteRecived: "新しいノートがあるで"
|
||||
@ -514,11 +517,11 @@ listen: "聴く"
|
||||
none: "なし"
|
||||
showInPage: "ページで表示"
|
||||
popout: "ポップアウト"
|
||||
volume: "音量"
|
||||
masterVolume: "全体の音量"
|
||||
volume: "やかましさ"
|
||||
masterVolume: "全体のやかましさ"
|
||||
details: "もっと"
|
||||
chooseEmoji: "絵文字を選ぶ"
|
||||
unableToProcess: "なんか作業が止まってしまったようやね"
|
||||
unableToProcess: "なんか奥の方で詰まってもうた"
|
||||
recentUsed: "最近使ったやつ"
|
||||
install: "インストール"
|
||||
uninstall: "アンインストール"
|
||||
@ -536,14 +539,18 @@ output: "出力"
|
||||
script: "スクリプト"
|
||||
disablePagesScript: "Pagesのスクリプトを無効にしてや"
|
||||
updateRemoteUser: "リモートユーザー情報の更新してくれん?"
|
||||
deleteAllFiles: "すべてのファイルを削除"
|
||||
deleteAllFilesConfirm: "ホンマにすべてのファイルを削除するん?消したもんはもう戻ってこんのやで?"
|
||||
deleteAllFiles: "ファイルを全部ほかす"
|
||||
deleteAllFilesConfirm: "ホンマにファイル全部ほかすんか?消したもんはもう戻ってこんのやで?"
|
||||
removeAllFollowing: "フォローを全解除"
|
||||
removeAllFollowingDescription: "{host}からのフォローをすべて解除するで。そのインスタンスが消えて無くなった時とかには便利な機能やで。"
|
||||
userSuspended: "このユーザーは...凍結されとる。"
|
||||
userSilenced: "このユーザーは...サイレンスされとる。"
|
||||
yourAccountSuspendedTitle: "あんたのアカウント凍結されとるで"
|
||||
yourAccountSuspendedDescription: "あんたのアカウントは、サーバーの利用規約に違反したとかの理由で、凍結されとるで。細かいことは管理者までお問い合わせたってなー。絶対に新しいアカウント作ったらあかんで。絶対やで。"
|
||||
tokenRevoked: "トークンが無効やで"
|
||||
tokenRevokedDescription: "ログイントークンが失効しとるで。もっかいログインしてもろてもええか?"
|
||||
accountDeleted: "アカウントは削除されとるで"
|
||||
accountDeletedDescription: "このアカウントは削除されとるで。"
|
||||
menu: "メニュー"
|
||||
divider: "分割線"
|
||||
addItem: "項目を追加"
|
||||
@ -566,7 +573,7 @@ description: "説明"
|
||||
describeFile: "キャプションを付ける"
|
||||
enterFileDescription: "キャプションを入力"
|
||||
author: "作者"
|
||||
leaveConfirm: "未保存の変更があるで!ほかしてええか?"
|
||||
leaveConfirm: "あんた、いじったのにまだ保存してないで!ほかしてええか?"
|
||||
manage: "管理"
|
||||
plugins: "プラグイン"
|
||||
preferencesBackups: "設定のバックアップ"
|
||||
@ -587,7 +594,6 @@ tokenRequested: "アカウントへのアクセス許してやったらどうや
|
||||
pluginTokenRequestedDescription: "このプラグインはここで設定した権限を使えるようになるで。"
|
||||
notificationType: "通知の種類"
|
||||
edit: "編集"
|
||||
useStarForReactionFallback: "リアクションがようわからん場合、★を使う"
|
||||
emailServer: "メールサーバー"
|
||||
enableEmail: "メール配信を受け取る"
|
||||
emailConfigInfo: "メールアドレスの確認とかパスワードリセットの時に使うで"
|
||||
@ -600,12 +606,12 @@ smtpUser: "ユーザー名"
|
||||
smtpPass: "パスワード"
|
||||
emptyToDisableSmtpAuth: "ユーザー名とパスワードになんも入れんかったら、SMTP認証を無効化するで"
|
||||
smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する"
|
||||
smtpSecureInfo: "STARTTLS使っとる時はオフにするで。"
|
||||
smtpSecureInfo: "STARTTLS使っとる時はオフにしてや。"
|
||||
testEmail: "配信テスト"
|
||||
wordMute: "ワードミュート"
|
||||
regexpError: "正規表現エラー"
|
||||
regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが出てきたで:"
|
||||
instanceMute: "インスタンスミュート"
|
||||
instanceMute: "サーバーミュート"
|
||||
userSaysSomething: "{name}が何か言うとるわ"
|
||||
makeActive: "使うで"
|
||||
display: "表示"
|
||||
@ -624,7 +630,7 @@ useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使
|
||||
other: "その他"
|
||||
regenerateLoginToken: "ログイントークンを再生成"
|
||||
regenerateLoginTokenDescription: "ログインに使われる内部トークンをもっかい作るで。いつもならこれをやる必要はないで。もっかい作ると、全部のデバイスでログアウトされるで気ぃつけてなー。"
|
||||
setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できるで。"
|
||||
setMultipleBySeparatingWithSpace: "スペースで区切って何個でも設定できるで。"
|
||||
fileIdOrUrl: "ファイルIDかURL"
|
||||
behavior: "動作"
|
||||
sample: "サンプル"
|
||||
@ -636,7 +642,7 @@ abuseReported: "無事内容が送信されたみたいやで。おおきに〜
|
||||
reporter: "通報者"
|
||||
reporteeOrigin: "通報先"
|
||||
reporterOrigin: "通報元"
|
||||
forwardReport: "リモートインスタンスに通報を転送するで"
|
||||
forwardReport: "リモートサーバーに通報を転送するで"
|
||||
forwardReportIsAnonymous: "リモートインスタンスからはあんたの情報は見れへんくって、匿名のシステムアカウントとして表示されるで。"
|
||||
send: "送信"
|
||||
abuseMarkAsResolved: "対応したで"
|
||||
@ -644,7 +650,7 @@ openInNewTab: "新しいタブで開く"
|
||||
openInSideView: "サイドビューで開く"
|
||||
defaultNavigationBehaviour: "デフォルトのナビゲーション"
|
||||
editTheseSettingsMayBreakAccount: "このへんの設定をようわからんままイジるとアカウントが壊れて使えんくなるかも知れへんで?"
|
||||
instanceTicker: "ノートのインスタンス情報"
|
||||
instanceTicker: "ノートのサーバー情報"
|
||||
waitingFor: "{x}を待っとるで"
|
||||
random: "ランダム"
|
||||
system: "システム"
|
||||
@ -655,7 +661,7 @@ createNew: "新しく作るで"
|
||||
optional: "任意"
|
||||
createNewClip: "新しいクリップを作るで"
|
||||
unclip: "クリップ解除するで"
|
||||
confirmToUnclipAlreadyClippedNote: "このノートはすでにクリップ「{name}」に含まれとるで。ノートをこのクリップから除外したる?"
|
||||
confirmToUnclipAlreadyClippedNote: "このノートはすでにクリップ「{name}」に含まれとるで。ノートをこのクリップから除外しよか?"
|
||||
public: "パブリック"
|
||||
i18nInfo: "Misskeyは有志によっていろんな言語に翻訳されとるで。{link}で翻訳に協力したってやー。"
|
||||
manageAccessTokens: "アクセストークンの管理"
|
||||
@ -672,15 +678,15 @@ receivedReactionsCount: "リアクションされた数"
|
||||
pollVotesCount: "アンケートに投票した数"
|
||||
pollVotedCount: "アンケートに投票された数"
|
||||
yes: "ええで"
|
||||
no: "あかんで"
|
||||
no: "あかん"
|
||||
driveFilesCount: "ドライブのファイル数"
|
||||
driveUsage: "ドライブ使用量やで"
|
||||
noCrawle: "クローラーによるインデックスを拒否するで"
|
||||
noCrawleDescription: "検索エンジンにあんたのユーザーページ、ノート、Pagesとかのコンテンツを登録(インデックス)せぇへんように頼むで。"
|
||||
noCrawleDescription: "検索エンジンにあんたのユーザーページ、ノート、Pagesとかのコンテンツを登録(インデックス)せんように頼むで。邪魔すんねんやったら帰って〜。"
|
||||
lockedAccountInfo: "フォローを承認制にしとっても、ノートの公開範囲を「フォロワー」にせぇへん限り、誰でもあんたのノートを見れるで。"
|
||||
alwaysMarkSensitive: "デフォルトでメディアを閲覧注意にするで"
|
||||
loadRawImages: "添付画像のサムネイルをオリジナル画質にするで"
|
||||
disableShowingAnimatedImages: "アニメーション画像を再生しやへんで"
|
||||
disableShowingAnimatedImages: "アニメーション画像を再生せんとくで"
|
||||
verificationEmailSent: "無事確認のメールを送れたで。メールに書いてあるリンクにアクセスして、設定を完了してなー。"
|
||||
notSet: "未設定"
|
||||
emailVerified: "メールアドレスは確認されたで"
|
||||
@ -690,14 +696,14 @@ pageLikedCount: "Pageにええやんと思ってくれた数"
|
||||
contact: "連絡先"
|
||||
useSystemFont: "システムのデフォルトのフォントを使うで"
|
||||
clips: "クリップ"
|
||||
experimentalFeatures: "実験的機能やで"
|
||||
experimentalFeatures: "おためし機能やで"
|
||||
developer: "開発者やで"
|
||||
makeExplorable: "アカウントを見つけやすくするで"
|
||||
makeExplorableDescription: "オフにすると、「みつける」にアカウントが載らんくなるで。"
|
||||
showGapBetweenNotesInTimeline: "タイムラインのノートを離して表示するで"
|
||||
duplicate: "複製"
|
||||
left: "左"
|
||||
center: "中央"
|
||||
center: "真ん中"
|
||||
wide: "広い"
|
||||
narrow: "狭い"
|
||||
reloadToApplySetting: "設定はページリロード後に反映されるで。今リロードしとくか?"
|
||||
@ -708,7 +714,7 @@ onlineUsersCount: "{n}人が起きとるで"
|
||||
nUsers: "{n}ユーザー"
|
||||
nNotes: "{n}ノート"
|
||||
sendErrorReports: "エラーリポートを送る"
|
||||
sendErrorReportsDescription: "オンにしたら、なんか変なことが起きたときにエラーの詳細がMisskeyに共有されて、ソフトウェアの品質向上に役立てられるんや。エラー情報には、OSのバージョン、ブラウザの種類、行動履歴などが含まれるで。"
|
||||
sendErrorReportsDescription: "オンにしたら、変なことが起きたときにエラーの詳細がMisskeyに送られて、ソフトウェアの品質向上に使えるようになるで。エラー情報には、OSのバージョン、ブラウザの種類、行動履歴なんかが含まれるで。"
|
||||
myTheme: "マイテーマ"
|
||||
backgroundColor: "背景"
|
||||
accentColor: "アクセント"
|
||||
@ -877,7 +883,7 @@ isSystemAccount: "システムが自動で作成・管理しとるアカウン
|
||||
typeToConfirm: "この操作をやるんなら {x} と入力してなー"
|
||||
deleteAccount: "アカウント削除するで"
|
||||
document: "ドキュメント"
|
||||
numberOfPageCache: "ページキャッシュ数やで"
|
||||
numberOfPageCache: "ページ、どんだけキャッシュすんの?"
|
||||
numberOfPageCacheDescription: "増やすと使いやすくなる、負荷とメモリ使用量が増えてくで。一長一短やな。"
|
||||
logoutConfirm: "ログアウトしまっか?"
|
||||
lastActiveDate: "最後に使った日時"
|
||||
@ -947,7 +953,7 @@ thisPostMayBeAnnoying: "この投稿は迷惑かもしらんで。"
|
||||
thisPostMayBeAnnoyingHome: "ホームに投稿"
|
||||
thisPostMayBeAnnoyingCancel: "やめとく"
|
||||
thisPostMayBeAnnoyingIgnore: "このまま投稿"
|
||||
collapseRenotes: "見たことあるRenoteは省略やで"
|
||||
collapseRenotes: "見たことあるRenoteは飛ばして表示するで"
|
||||
internalServerError: "サーバー内部エラー"
|
||||
internalServerErrorDescription: "サーバー内部でよう分からんエラーやわ"
|
||||
copyErrorInfo: "エラー情報をコピー"
|
||||
@ -959,6 +965,24 @@ invitationRequiredToRegister: "今このサーバー招待制になってもう
|
||||
emailNotSupported: "このサーバーはメール配信がサポートされてへんみたいやわ"
|
||||
postToTheChannel: "チャンネルに投稿"
|
||||
cannotBeChangedLater: "後からは変えられへんで。"
|
||||
reactionAcceptance: "リアクションの受け入れ"
|
||||
likeOnly: "いいねだけ"
|
||||
likeOnlyForRemote: "リモートからはいいねだけな"
|
||||
rolesAssignedToMe: "自分に割り当てられたロール"
|
||||
resetPasswordConfirm: "パスワード作り直すんでええな?"
|
||||
sensitiveWords: "けったいな単語"
|
||||
sensitiveWordsDescription: "設定した単語が入っとるノートの公開範囲をホームにしたるわ。改行で区切ったら複数設定できるで。"
|
||||
notesSearchNotAvailable: "ノート検索は使われへんで。"
|
||||
license: "ライセンス"
|
||||
unfavoriteConfirm: "ほんまに気に入らんの?"
|
||||
myClips: "自分のクリップ"
|
||||
drivecleaner: "ドライブキレイキレイ"
|
||||
retryAllQueuesNow: "キューを全部もっかいやり直す"
|
||||
retryAllQueuesConfirmTitle: "もっかいやってみるか?"
|
||||
retryAllQueuesConfirmText: "一時的にサーバー重なるかもしれへんで。"
|
||||
enableChartsForRemoteUser: "リモートユーザーのチャートを作る"
|
||||
enableChartsForFederatedInstances: "リモートサーバーのチャートを作る"
|
||||
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
|
||||
_achievements:
|
||||
earnedAt: "貰った日ぃ"
|
||||
_types:
|
||||
@ -976,7 +1000,7 @@ _achievements:
|
||||
title: "ノートの生駒山"
|
||||
description: "ノートを500回投稿した"
|
||||
_notes1000:
|
||||
title: "ノートの山"
|
||||
title: "ノートの六甲山"
|
||||
description: "ノートを1,000回投稿した"
|
||||
_notes5000:
|
||||
title: "箕面の滝からノート"
|
||||
@ -1218,6 +1242,8 @@ _role:
|
||||
iconUrl: "アイコン画像のURL"
|
||||
asBadge: "バッジとして見せる"
|
||||
descriptionOfAsBadge: "オンにすると、ユーザー名の横んとこにロールのアイコンが表示されるで。"
|
||||
displayOrder: "表示順"
|
||||
descriptionOfDisplayOrder: "数がでかいほど、UI上で先に表示されるで。"
|
||||
canEditMembersByModerator: "モデレーターのメンバー編集を許可"
|
||||
descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになるで。オフにすると管理者のみが行えるで。"
|
||||
priority: "優先度"
|
||||
@ -1243,6 +1269,7 @@ _role:
|
||||
rateLimitFactor: "レートリミット"
|
||||
descriptionOfRateLimitFactor: "ちっちゃいほど制限が緩くなって、大きいほど制限されるで。"
|
||||
canHideAds: "広告を表示させへん"
|
||||
canSearchNotes: "ノート検索を使わすかどうか"
|
||||
_condition:
|
||||
isLocal: "ローカルユーザー"
|
||||
isRemote: "リモートユーザー"
|
||||
@ -1252,6 +1279,8 @@ _role:
|
||||
followersMoreThanOrEq: "フォロワー数が~以上"
|
||||
followingLessThanOrEq: "フォロー数が~以下"
|
||||
followingMoreThanOrEq: "フォロー数が~以上"
|
||||
notesLessThanOrEq: "投稿数が~以下しかない"
|
||||
notesMoreThanOrEq: "投稿を~以上しとる"
|
||||
and: "~かつ~"
|
||||
or: "~または~"
|
||||
not: "~ではない"
|
||||
@ -1377,7 +1406,7 @@ _wordMute:
|
||||
mutedNotes: "ミュートされたノート"
|
||||
_instanceMute:
|
||||
instanceMuteDescription: "ミュートしたインスタンスのユーザーへの返信を含めて、設定したインスタンスの全てのノートとRenoteをミュートにするで。"
|
||||
instanceMuteDescription2: "改行で区切って設定するで"
|
||||
instanceMuteDescription2: "改行で区切って設定するんやで"
|
||||
title: "設定したインスタンスのノートを隠すで。"
|
||||
heading: "ミュートするインスタンス"
|
||||
_theme:
|
||||
@ -1464,7 +1493,7 @@ _sfx:
|
||||
channel: "チャンネル通知"
|
||||
_ago:
|
||||
future: "未来"
|
||||
justNow: "たった今"
|
||||
justNow: "ついさっき"
|
||||
secondsAgo: "{n}秒前"
|
||||
minutesAgo: "{n}分前"
|
||||
hoursAgo: "{n}時間前"
|
||||
@ -1587,7 +1616,7 @@ _weekday:
|
||||
saturday: "土曜日"
|
||||
_widgets:
|
||||
profile: "プロフィール"
|
||||
instanceInfo: "インスタンス情報"
|
||||
instanceInfo: "サーバー情報"
|
||||
memo: "付箋"
|
||||
notifications: "通知"
|
||||
timeline: "タイムライン"
|
||||
@ -1690,7 +1719,7 @@ _charts:
|
||||
apRequest: "リクエスト"
|
||||
usersIncDec: "ユーザーの増減"
|
||||
usersTotal: "ユーザーの合計"
|
||||
activeUsers: "アクティブユーザー数"
|
||||
activeUsers: "いまおるユーザー数"
|
||||
notesIncDec: "ノートの増減"
|
||||
localNotesIncDec: "ローカルのノートの増減"
|
||||
remoteNotesIncDec: "リモートのノートの増減"
|
||||
@ -1844,3 +1873,24 @@ _deck:
|
||||
_dialog:
|
||||
charactersExceeded: "最大の文字数を上回っとるで!今は {current} / 最大でも {max}"
|
||||
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: "メンションがあるとき~!"
|
||||
|
||||
|
@ -1 +1,2 @@
|
||||
---
|
||||
|
||||
|
@ -103,3 +103,4 @@ _deck:
|
||||
_columns:
|
||||
notifications: "Ilɣuyen"
|
||||
list: "Tibdarin"
|
||||
|
||||
|
@ -83,3 +83,4 @@ _deck:
|
||||
notifications: "ಅಧಿಸೂಚನೆಗಳು"
|
||||
tl: "ಸಮಯಸಾಲು"
|
||||
mentions: "ಹೆಸರಿಸಿದ"
|
||||
|
||||
|
@ -122,6 +122,8 @@ unmarkAsSensitive: "열람주의 해제"
|
||||
enterFileName: "파일명을 입력"
|
||||
mute: "뮤트"
|
||||
unmute: "뮤트 해제"
|
||||
renoteMute: "리노트를 뮤트"
|
||||
renoteUnmute: "리노트 뮤트 해제"
|
||||
block: "차단"
|
||||
unblock: "차단 해제"
|
||||
suspend: "정지"
|
||||
@ -153,6 +155,7 @@ flagShowTimelineReplies: "타임라인에 노트의 답글을 표시하기"
|
||||
flagShowTimelineRepliesDescription: "이 설정을 활성화하면 타임라인에 다른 유저 간의 답글을 표시합니다."
|
||||
autoAcceptFollowed: "팔로우 중인 유저로부터의 팔로우 요청을 자동 수락"
|
||||
addAccount: "계정 추가"
|
||||
reloadAccountsList: "계정 리스트 정보 갱신"
|
||||
loginFailed: "로그인에 실패했습니다"
|
||||
showOnRemote: "리모트에서 보기"
|
||||
general: "일반"
|
||||
@ -506,6 +509,7 @@ objectStorageSetPublicRead: "업로드할 때 'public-read'를 설정하기"
|
||||
serverLogs: "서버 로그"
|
||||
deleteAll: "모두 삭제"
|
||||
showFixedPostForm: "타임라인 상단에 글 작성란을 표시"
|
||||
showFixedPostFormInChannel: "채널 타임라인 상단에 글 작성란을 표시"
|
||||
newNoteRecived: "새 노트가 있습니다"
|
||||
sounds: "소리"
|
||||
sound: "소리"
|
||||
@ -543,6 +547,8 @@ userSuspended: "이 계정은 정지된 상태입니다."
|
||||
userSilenced: "이 계정은 사일런스된 상태입니다."
|
||||
yourAccountSuspendedTitle: "계정이 정지되었습니다"
|
||||
yourAccountSuspendedDescription: "이 계정은 서버의 이용 약관을 위반하거나, 기타 다른 이유로 인해 정지되었습니다. 자세한 사항은 관리자에게 문의해 주십시오. 계정을 새로 생성하지 마십시오."
|
||||
accountDeleted: "계정이 정지되었습니다"
|
||||
accountDeletedDescription: "이 계정이 삭제되었습니다."
|
||||
menu: "메뉴"
|
||||
divider: "구분선"
|
||||
addItem: "항목 추가"
|
||||
@ -586,7 +592,6 @@ tokenRequested: "계정 접근 허용"
|
||||
pluginTokenRequestedDescription: "이 플러그인은 여기서 설정한 권한을 사용할 수 있게 됩니다."
|
||||
notificationType: "알림 유형"
|
||||
edit: "편집"
|
||||
useStarForReactionFallback: "알 수 없는 리액션 이모지 대신 ★ 사용"
|
||||
emailServer: "메일 서버"
|
||||
enableEmail: "이메일 송신 기능 활성화"
|
||||
emailConfigInfo: "가입 시 메일 주소 확인이나 비밀번호 초기화 시에 사용합니다."
|
||||
@ -955,6 +960,9 @@ exploreOtherServers: "다른 서버 둘러보기"
|
||||
letsLookAtTimeline: "타임라인 구경하기"
|
||||
disableFederationWarn: "연합이 비활성화됩니다. 비활성화해도 게시물이 비공개가 되지는 않습니다. 대부분의 경우 이 옵션을 활성화할 필요가 없습니다."
|
||||
invitationRequiredToRegister: "현재 이 서버는 비공개입니다. 회원가입을 하시려면 초대 코드가 필요합니다."
|
||||
emailNotSupported: "이 서버에서는 메일 전송을 지원하지 않습니다"
|
||||
postToTheChannel: "채널에 게시하기"
|
||||
cannotBeChangedLater: "나중에 변경할 수 없습니다."
|
||||
_achievements:
|
||||
earnedAt: "달성 일시"
|
||||
_types:
|
||||
@ -1840,3 +1848,7 @@ _deck:
|
||||
_dialog:
|
||||
charactersExceeded: "최대 글자수를 초과하였습니다! 현재 {current} / 최대 {min}"
|
||||
charactersBelow: "최소 글자수 미만입니다! 현재 {current} / 최소 {min}"
|
||||
_webhookSettings:
|
||||
name: "이름"
|
||||
active: "활성화"
|
||||
|
||||
|
@ -368,3 +368,4 @@ _deck:
|
||||
list: "ລາຍການ"
|
||||
channel: "ຊ່ອງ"
|
||||
mentions: "ກ່າວເຖິງ"
|
||||
|
||||
|
@ -483,3 +483,6 @@ _deck:
|
||||
antenna: "Antennes"
|
||||
list: "Lijsten"
|
||||
mentions: "Vermeldingen"
|
||||
_webhookSettings:
|
||||
name: "Naam"
|
||||
|
||||
|
@ -1,2 +1,3 @@
|
||||
---
|
||||
_lang_: "Norsk Bokmål"
|
||||
|
||||
|
@ -129,6 +129,7 @@ unblockConfirm: "Czy na pewno chcesz odblokować to konto?"
|
||||
suspendConfirm: "Czy na pewno chcesz zawiesić to konto?"
|
||||
unsuspendConfirm: "Czy na pewno chcesz cofnąć zawieszenie tego konta?"
|
||||
selectList: "Wybierz listę"
|
||||
selectChannel: "Wybierz kanał"
|
||||
selectAntenna: "Wybierz Antennę"
|
||||
selectWidget: "Wybierz widżet"
|
||||
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"
|
||||
autoAcceptFollowed: "Automatycznie przyjmuj prośby o możliwość obserwacji od użytkowników, których obserwujesz"
|
||||
addAccount: "Dodaj konto"
|
||||
reloadAccountsList: "Odśwież listę kont"
|
||||
loginFailed: "Nie udało się zalogować"
|
||||
showOnRemote: "Zobacz na zdalnej instancji"
|
||||
general: "Ogólne"
|
||||
@ -159,6 +161,7 @@ searchWith: "Szukaj: {q}"
|
||||
youHaveNoLists: "Nie masz żadnej listy"
|
||||
followConfirm: "Czy na pewno chcesz zaobserwować {name}?"
|
||||
proxyAccount: "Konto proxy"
|
||||
proxyAccountDescription: "Opis konta pełnomocniczego"
|
||||
host: "Host"
|
||||
selectUser: "Wybierz użytkownika"
|
||||
recipient: "Odbiorca"
|
||||
@ -253,6 +256,7 @@ noMoreHistory: "Nie ma dalszej historii"
|
||||
startMessaging: "Rozpocznij czat"
|
||||
nUsersRead: "przeczytano przez {n}"
|
||||
agreeTo: "Wyrażam zgodę na {0}"
|
||||
agreeBelow: "Zaakceptuj poniżej"
|
||||
tos: "Regulamin"
|
||||
start: "Rozpocznij"
|
||||
home: "Strona główna"
|
||||
@ -385,13 +389,19 @@ about: "Informacje"
|
||||
aboutMisskey: "O Misskey"
|
||||
administrator: "Admin"
|
||||
token: "Token"
|
||||
2fa: "Klucz 2FA "
|
||||
totp: "Klucz aplikacji uwierzytelniającej (totp)"
|
||||
totpDescription: "Opis klucza czasowego"
|
||||
moderator: "Moderator"
|
||||
moderation: "Moderacja"
|
||||
nUsersMentioned: "{n} wspomnianych użytkowników"
|
||||
securityKeyAndPasskey: "Klucz bezpieczeństwa i klucze Passkey"
|
||||
securityKey: "Klucz bezpieczeństwa"
|
||||
lastUsed: "Ostatnio używane"
|
||||
lastUsedAt: "Ostatnio używane w"
|
||||
unregister: "Cofnij rejestrację"
|
||||
passwordLessLogin: "Skonfiguruj logowanie bez użycia hasła"
|
||||
passwordLessLoginDescription: "Opis logowania bez użycia hasła"
|
||||
resetPassword: "Zresetuj hasło"
|
||||
newPasswordIs: "Nowe hasło to „{password}”"
|
||||
reduceUiAnimation: "Ogranicz animacje w UI"
|
||||
@ -518,11 +528,16 @@ disablePagesScript: "Wyłącz AiScript na Stronach"
|
||||
updateRemoteUser: "Aktualizuj zdalne dane o użytkowniku"
|
||||
deleteAllFiles: "Usuń 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."
|
||||
userSuspended: "To konto zostało zawieszone."
|
||||
userSilenced: "Ten użytkownik został wyciszony."
|
||||
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."
|
||||
tokenRevoked: "Token odrzucony"
|
||||
tokenRevokedDescription: "Opis odrzuconego tokena"
|
||||
accountDeleted: "Konto usunięte"
|
||||
accountDeletedDescription: "Opis konta usuniętego"
|
||||
menu: "Menu"
|
||||
divider: "Rozdzielacz"
|
||||
addItem: "Dodaj element"
|
||||
@ -548,7 +563,9 @@ author: "Autor"
|
||||
leaveConfirm: "Są niezapisane zmiany. Czy chcesz je odrzucić?"
|
||||
manage: "Zarządzanie"
|
||||
plugins: "Wtyczki"
|
||||
preferencesBackups: "Kopia zapasowa ustawień"
|
||||
deck: "Tablica"
|
||||
undeck: "oddkouj"
|
||||
useBlurEffectForModal: "Używaj efektu rozmycia w modalach"
|
||||
useFullReactionPicker: "Używaj pełnowymiarowego wybornika reakcji"
|
||||
width: "Szerokość"
|
||||
@ -564,7 +581,6 @@ tokenRequested: "Przydziel dostęp do konta"
|
||||
pluginTokenRequestedDescription: "Ta wtyczka będzie mogła korzystać z ustawionych tu uprawnień."
|
||||
notificationType: "Rodzaj powiadomień"
|
||||
edit: "Edytuj"
|
||||
useStarForReactionFallback: "Użyj ★ jako zapasowego emoji, gdy emoji reakcji jest nieznane"
|
||||
emailServer: "Serwer poczty 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"
|
||||
@ -816,6 +832,8 @@ tenMinutes: "10 minut"
|
||||
oneHour: "1 godzina"
|
||||
oneDay: "1 dzień"
|
||||
oneWeek: "1 tydzień"
|
||||
oneMonth: "jeden miesiąc"
|
||||
failedToFetchAccountInformation: "Nie udało się uzyskać informacji o koncie"
|
||||
file: "Pliki"
|
||||
recommended: "Zalecane"
|
||||
check: "Zweryfikuj"
|
||||
@ -1358,3 +1376,7 @@ _deck:
|
||||
channel: "Kanały"
|
||||
mentions: "Wspomnienia"
|
||||
direct: "Bezpośredni"
|
||||
_webhookSettings:
|
||||
name: "Nazwa"
|
||||
active: "Właczono"
|
||||
|
||||
|
@ -555,3 +555,6 @@ _deck:
|
||||
list: "Listas"
|
||||
mentions: "Menções"
|
||||
direct: "Notas diretas"
|
||||
_webhookSettings:
|
||||
name: "Nome"
|
||||
|
||||
|
@ -561,7 +561,6 @@ tokenRequested: "Acordă acces la cont"
|
||||
pluginTokenRequestedDescription: "Acest plugin va putea să folosească permisiunile setate aici."
|
||||
notificationType: "Tipul notificării"
|
||||
edit: "Editează"
|
||||
useStarForReactionFallback: "Folosește ★ ca fallback dacă emoji-ul este necunoscut"
|
||||
emailServer: "Server email"
|
||||
enableEmail: "Activează distribuția de emailuri"
|
||||
emailConfigInfo: "Folosit pentru a confirma emailul tău în timpul logări dacă îți uiți parola"
|
||||
@ -702,3 +701,6 @@ _deck:
|
||||
list: "Liste"
|
||||
channel: "Canale"
|
||||
mentions: "Mențiuni"
|
||||
_webhookSettings:
|
||||
name: "Nume"
|
||||
|
||||
|
@ -585,7 +585,6 @@ tokenRequested: "Открыть доступ к учётной записи"
|
||||
pluginTokenRequestedDescription: "Это расширение сможет пользоваться разрешениями, установленными здесь."
|
||||
notificationType: "Тип уведомления"
|
||||
edit: "Изменить"
|
||||
useStarForReactionFallback: "Ставить ★ в качестве реакции вместо неизвестного эмодзи"
|
||||
emailServer: "Сервер электронной почты"
|
||||
enableEmail: "Включить обмен электронной почтой"
|
||||
emailConfigInfo: "Используется для подтверждения адреса электронной почты и сброса пароля."
|
||||
@ -1837,3 +1836,7 @@ _deck:
|
||||
_dialog:
|
||||
charactersExceeded: "Превышено максимальное количество символов! У вас {current} / из {max}"
|
||||
charactersBelow: "Это ниже минимального количества символов! У вас {current} / из {min}"
|
||||
_webhookSettings:
|
||||
name: "Название"
|
||||
active: "Вкл."
|
||||
|
||||
|
@ -1 +1,2 @@
|
||||
---
|
||||
|
||||
|
@ -586,7 +586,6 @@ tokenRequested: "Povoliť prístup k účtu"
|
||||
pluginTokenRequestedDescription: "Tento plugin bude môcť používať oprávnenia nastavené tu."
|
||||
notificationType: "Typ oznámenia"
|
||||
edit: "Upraviť"
|
||||
useStarForReactionFallback: "Použiť ★ keď emoji reakcie nie je známe"
|
||||
emailServer: "Email server"
|
||||
enableEmail: "Zapnúť email"
|
||||
emailConfigInfo: "Používa sa na overenie emaily pri registrácii alebo pri zabudnutí hesla"
|
||||
@ -1475,3 +1474,7 @@ _deck:
|
||||
channel: "Kanály"
|
||||
mentions: "Zmienky"
|
||||
direct: "Priame poznámky"
|
||||
_webhookSettings:
|
||||
name: "Názov"
|
||||
active: "Zapnuté"
|
||||
|
||||
|
@ -442,3 +442,6 @@ _deck:
|
||||
antenna: "Antenner"
|
||||
list: "Listor"
|
||||
mentions: "Omnämningar"
|
||||
_webhookSettings:
|
||||
active: "Aktiverad"
|
||||
|
||||
|
@ -544,6 +544,8 @@ userSuspended: "ผู้ใช้รายนี้ถูกระงับก
|
||||
userSilenced: "ผู้ใช้รายนี้กำลังถูกปิดกั้น"
|
||||
yourAccountSuspendedTitle: "บัญชีนี้นั้นถูกระงับ"
|
||||
yourAccountSuspendedDescription: "บัญชีนี้ถูกระงับ เนื่องจากละเมิดข้อกำหนดในการให้บริการของเซิร์ฟเวอร์หรืออาจจะละเมิดหลักเกณฑ์ชุมชน หรือ อาจจะโดนร้องเรียนเรื่องการละเมิดลิขสิทธิ์และอื่นๆอย่างต่อเนื่องซ้ำๆ หากคุณคิดว่าไม่ได้ทำผิดจริงๆหรือตัดสินผิดพลาด ได้โปรดกรุณาติดต่อผู้ดูแลระบบหากคุณต้องการทราบเหตุผลโดยละเอียดเพิ่มเติม และขอความกรุณาอย่าสร้างบัญชีใหม่"
|
||||
tokenRevoked: "โทเค็นไม่ถูกต้อง"
|
||||
accountDeleted: "ลบบัญชีแล้ว"
|
||||
menu: "เมนู"
|
||||
divider: "ตัวแบ่ง"
|
||||
addItem: "เพิ่มรายการ"
|
||||
@ -587,7 +589,6 @@ tokenRequested: "ให้สิทธิ์การเข้าถึงบั
|
||||
pluginTokenRequestedDescription: "ปลั๊กอินนี้จะสามารถใช้การอนุญาตที่ตั้งค่าไว้ที่นี่นะ"
|
||||
notificationType: "ประเภทการแจ้งเตือน"
|
||||
edit: "แก้ไข"
|
||||
useStarForReactionFallback: "ใช้ ★ เป็นทางเลือกแทนถ้าหากไม่ทราบอิโมจิ"
|
||||
emailServer: "อีเมล์เซิร์ฟเวอร์"
|
||||
enableEmail: "เปิดใช้งานการกระจายอีเมล"
|
||||
emailConfigInfo: "ใช้เพื่อยืนยันอีเมลของคุณระหว่างการสมัครหรือถ้าหากคุณลืมรหัสผ่าน"
|
||||
@ -959,6 +960,18 @@ invitationRequiredToRegister: "อินสแตนซ์นี้เป็น
|
||||
emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมลนะค่ะ"
|
||||
postToTheChannel: "โพสต์ลงช่อง"
|
||||
cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนแปลงได้ในภายหลังนะ"
|
||||
likeOnly: "ที่ชอบเท่านั้น"
|
||||
resetPasswordConfirm: "รีเซ็ตรหัสผ่านของคุณจริงๆหรอ?"
|
||||
sensitiveWords: "คำที่ละเอียดอ่อน"
|
||||
sensitiveWordsDescription: "การเปิดเผยโน้ตทั้งหมดที่มีคำที่กำหนดค่าไว้จะถูกตั้งค่าเป็น \"หน้าแรก\" โดยอัตโนมัติ คุณยังสามารถแสดงหลายรายการได้โดยแยกรายการโดยใช้ตัวแบ่งบรรทัดได้นะ"
|
||||
notesSearchNotAvailable: "การค้นหาโน้ตไม่พร้อมใช้งานนะค่ะ"
|
||||
license: "ใบอนุญาต"
|
||||
unfavoriteConfirm: "ลบออกจากรายการโปรดแน่ใจหรอ?"
|
||||
myClips: "คลิปของฉัน"
|
||||
drivecleaner: "ทำความสะอาดไดรฟ์"
|
||||
retryAllQueuesNow: "ลองเรียกใช้คิวทั้งหมดอีกครั้ง"
|
||||
retryAllQueuesConfirmTitle: "ลองใหม่ทั้งหมดจริงๆหรอแน่ใจนะ?"
|
||||
retryAllQueuesConfirmText: "สิ่งนี้จะเพิ่มการโหลดเซิร์ฟเวอร์ชั่วคราวนะ"
|
||||
_achievements:
|
||||
earnedAt: "ได้รับเมื่อ"
|
||||
_types:
|
||||
@ -1218,6 +1231,8 @@ _role:
|
||||
iconUrl: "ไอคอน URL"
|
||||
asBadge: "แสดงเป็นตรา"
|
||||
descriptionOfAsBadge: "ไอคอนของบทบาทนี้จะปรากฏถัดจากชื่อผู้ใช้ของผู้ใช้งานด้วยบทบาทนี้ถ้าหากเปิดใช้งาน"
|
||||
displayOrder: "ตำแหน่ง"
|
||||
descriptionOfDisplayOrder: "ยิ่งตัวเลขสูง ตำแหน่ง UI ก็ยิ่งสูงขึ้นนะ"
|
||||
canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก"
|
||||
descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ"
|
||||
priority: "ลำดับความสำคัญ"
|
||||
@ -1243,6 +1258,7 @@ _role:
|
||||
rateLimitFactor: "ขีดจำกัดอัตรา"
|
||||
descriptionOfRateLimitFactor: "ขีดจํากัดอัตราที่ต่ำกว่ามีข้อจํากัดน้อยกว่าข้อจํากัดที่สูงกว่า"
|
||||
canHideAds: "ซ่อนโฆษณา"
|
||||
canSearchNotes: "การใช้การค้นหาโน้ต"
|
||||
_condition:
|
||||
isLocal: "ผู้ใช้ภายใน"
|
||||
isRemote: "ผู้ใช้ระยะไกล"
|
||||
@ -1844,3 +1860,13 @@ _deck:
|
||||
_dialog:
|
||||
charactersExceeded: "คุณกำลังมีตัวอักขระเกินขีดจำกัดสูงสุดแล้วนะ! ปัจจุบันอยู่ที่ {current} จาก {max}"
|
||||
charactersBelow: "คุณกำลังใช้อักขระต่ำกว่าขีดจำกัดขั้นต่ำเลยนะ! ปัจจุบันอยู่ที่ {current} จาก {min}"
|
||||
_disabledTimeline:
|
||||
title: "ปิดใช้งานไทม์ไลน์"
|
||||
description: "คุณไม่สามารถใช้ไทม์ไลน์นี้ภายใต้บทบาทปัจจุบันของคุณได้"
|
||||
_drivecleaner:
|
||||
orderBySizeDesc: "ขนาดไฟล์จากมากไปหาน้อย"
|
||||
orderByCreatedAtAsc: "วันที่จากน้อยไปหามาก"
|
||||
_webhookSettings:
|
||||
name: "ชื่อ"
|
||||
active: "เปิดใช้งาน"
|
||||
|
||||
|
@ -60,3 +60,4 @@ _deck:
|
||||
_columns:
|
||||
notifications: "Bildirim"
|
||||
tl: "Zaman çizelgesi"
|
||||
|
||||
|
@ -2,3 +2,4 @@
|
||||
_lang_: "ياپونچە"
|
||||
search: "ئىزدەش"
|
||||
searchByGoogle: "ئىزدەش"
|
||||
|
||||
|
@ -576,7 +576,6 @@ tokenRequested: "Надати доступ до акаунту"
|
||||
pluginTokenRequestedDescription: "Цей плагін зможе використовувати дозволи які тут вказані."
|
||||
notificationType: "Тип сповіщення"
|
||||
edit: "Редагувати"
|
||||
useStarForReactionFallback: "Використовувати ★ як запасний варіант, якщо емодзі реакції невідомий"
|
||||
emailServer: "Email сервер"
|
||||
enableEmail: "Увімкнути функцію доставки пошти"
|
||||
emailConfigInfo: "Використовується для підтвердження електронної пошти підчас реєстрації, а також для відновлення паролю."
|
||||
@ -1639,3 +1638,7 @@ _deck:
|
||||
channel: "Канали"
|
||||
mentions: "Згадки"
|
||||
direct: "Особисте"
|
||||
_webhookSettings:
|
||||
name: "Ім'я"
|
||||
active: "Увімкнено"
|
||||
|
||||
|
@ -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."
|
||||
notificationType: "Loại thông báo"
|
||||
edit: "Sửa"
|
||||
useStarForReactionFallback: "Dùng ★ nếu emoji biểu cảm không có"
|
||||
emailServer: "Email máy chủ"
|
||||
enableEmail: "Bật phân phối email"
|
||||
emailConfigInfo: "Được dùng để xác minh email của bạn lúc đăng ký hoặc nếu bạn quên mật khẩu của mình"
|
||||
@ -1705,3 +1704,7 @@ _deck:
|
||||
_dialog:
|
||||
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}"
|
||||
_webhookSettings:
|
||||
name: "Tên"
|
||||
active: "Đã bật"
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
_lang_: "中文(简体)"
|
||||
headlineMisskey: "通过帖子连接在一起的网络"
|
||||
introMisskey: "欢迎!Misskey是一个开源的、去中心化的“微博客”服务。\n通过编写「帖文」来和大家分享你的以及你周围的事情吧!📡\n通过「回应」功能,可以让你快速地对大家的帖文表达反馈👍\n来探索新的世界吧!🚀"
|
||||
poweredByMisskeyDescription: "{name} 由开源平台 <b>Misskey</b> 驱动(也被称为 Misskey 服务器)"
|
||||
poweredByMisskeyDescription: "{name} 是开源平台 <b>Misskey</b> 的服务器之一。"
|
||||
monthAndDay: "{month}月 {day}日"
|
||||
search: "搜索"
|
||||
notifications: "通知"
|
||||
@ -122,6 +122,8 @@ unmarkAsSensitive: "取消标记为敏感内容"
|
||||
enterFileName: "请输入文件名"
|
||||
mute: "屏蔽"
|
||||
unmute: "解除屏蔽"
|
||||
renoteMute: "屏蔽转帖"
|
||||
renoteUnmute: "解除屏蔽转帖"
|
||||
block: "拉黑"
|
||||
unblock: "取消拉黑"
|
||||
suspend: "冻结"
|
||||
@ -153,6 +155,7 @@ flagShowTimelineReplies: "在时间线上显示帖子的回复"
|
||||
flagShowTimelineRepliesDescription: "启用时,时间线除了显示用户的帖子外,还会显示其他用户对帖子的回复。"
|
||||
autoAcceptFollowed: "自动允许关注者的关注"
|
||||
addAccount: "添加账户"
|
||||
reloadAccountsList: "更新账户列表"
|
||||
loginFailed: "登录失败"
|
||||
showOnRemote: "转到所在服务器显示"
|
||||
general: "常规设置"
|
||||
@ -355,7 +358,7 @@ hcaptchaSecretKey: "hCaptcha 密钥(SecretKey)"
|
||||
recaptcha: "reCAPTCHA"
|
||||
enableRecaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)"
|
||||
recaptchaSiteKey: "网站密钥"
|
||||
recaptchaSecretKey: "reCAPTCHA 密钥"
|
||||
recaptchaSecretKey: "reCAPTCHA 密钥(SecretKey)"
|
||||
turnstile: "Turnstile"
|
||||
enableTurnstile: "启用Turnstile"
|
||||
turnstileSiteKey: "网站密钥"
|
||||
@ -489,7 +492,7 @@ showFeaturedNotesInTimeline: "在时间线上显示热门推荐"
|
||||
objectStorage: "对象存储"
|
||||
useObjectStorage: "使用对象存储"
|
||||
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: "存储桶"
|
||||
objectStorageBucketDesc: "请指定使用的对象存储服务的存储桶名称。"
|
||||
objectStoragePrefix: "前缀"
|
||||
@ -544,6 +547,10 @@ userSuspended: "该用户已被冻结。"
|
||||
userSilenced: "该用户已被禁言。"
|
||||
yourAccountSuspendedTitle: "账户已被冻结"
|
||||
yourAccountSuspendedDescription: "由于违反了服务器的服务条款或其他原因,该账户已被冻结。 您可以与管理员联系以了解更多信息。 请不要创建一个新的账户。"
|
||||
tokenRevoked: "令牌无效"
|
||||
tokenRevokedDescription: "登录令牌已经失效。请重新登录。"
|
||||
accountDeleted: "帐户已删除"
|
||||
accountDeletedDescription: "此帐户已经被删除。"
|
||||
menu: "菜单"
|
||||
divider: "分割线"
|
||||
addItem: "添加项目"
|
||||
@ -587,7 +594,6 @@ tokenRequested: "允许访问账户"
|
||||
pluginTokenRequestedDescription: "此插件将能够拥有此处设置的权限"
|
||||
notificationType: "通知类型"
|
||||
edit: "编辑"
|
||||
useStarForReactionFallback: "如果回应的是未知表情符号,则使用★作为代替"
|
||||
emailServer: "邮件服务器"
|
||||
enableEmail: "启用发送邮件功能"
|
||||
emailConfigInfo: "用于确认电子邮件和密码重置"
|
||||
@ -959,6 +965,24 @@ invitationRequiredToRegister: "此服务器目前只允许拥有邀请码的人
|
||||
emailNotSupported: "此服务器不支持发送邮件"
|
||||
postToTheChannel: "发布到频道"
|
||||
cannotBeChangedLater: "之后不能再更改。"
|
||||
reactionAcceptance: "接受表情回应"
|
||||
likeOnly: "仅点赞"
|
||||
likeOnlyForRemote: "远程仅点赞"
|
||||
rolesAssignedToMe: "指派给自己的角色"
|
||||
resetPasswordConfirm: "确定重置密码?"
|
||||
sensitiveWords: "敏感词"
|
||||
sensitiveWordsDescription: "将包含设置词的帖子的可见范围设置为首页。可以通过用换行符分隔来设置多个。"
|
||||
notesSearchNotAvailable: "帖子检索不可用"
|
||||
license: "许可信息"
|
||||
unfavoriteConfirm: "确定要取消收藏吗?"
|
||||
myClips: "我的便签"
|
||||
drivecleaner: "网盘整理"
|
||||
retryAllQueuesNow: "立刻重试所有队列"
|
||||
retryAllQueuesConfirmTitle: "要再尝试一次吗?"
|
||||
retryAllQueuesConfirmText: "可能会使服务器负荷在一定时间内增加"
|
||||
enableChartsForRemoteUser: "生成远程用户的图表"
|
||||
enableChartsForFederatedInstances: "生成远程服务器的图表"
|
||||
showClipButtonInNoteFooter: "在贴文下方显示便签按钮"
|
||||
_achievements:
|
||||
earnedAt: "达成时间"
|
||||
_types:
|
||||
@ -1218,6 +1242,8 @@ _role:
|
||||
iconUrl: "图标URL"
|
||||
asBadge: "作为徽章显示"
|
||||
descriptionOfAsBadge: "开启后,用户名旁边将会出现角色图标。"
|
||||
displayOrder: "显示顺序"
|
||||
descriptionOfDisplayOrder: "数字越大,显示位置越靠前。"
|
||||
canEditMembersByModerator: "允许监察者编辑成员"
|
||||
descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。"
|
||||
priority: "优先级"
|
||||
@ -1243,6 +1269,7 @@ _role:
|
||||
rateLimitFactor: "速率限制"
|
||||
descriptionOfRateLimitFactor: "值越小限制越少,值越大限制越多。"
|
||||
canHideAds: "可以隐藏广告"
|
||||
canSearchNotes: "是否可以搜索帖子"
|
||||
_condition:
|
||||
isLocal: "是本地用户"
|
||||
isRemote: "是远程用户"
|
||||
@ -1252,6 +1279,8 @@ _role:
|
||||
followersMoreThanOrEq: "关注者不少于"
|
||||
followingLessThanOrEq: "关注中不多于"
|
||||
followingMoreThanOrEq: "关注中不少于"
|
||||
notesLessThanOrEq: "帖子数在~以下"
|
||||
notesMoreThanOrEq: "帖子数在~以上"
|
||||
and: "符合以下全部条件"
|
||||
or: "符合以下任一条件"
|
||||
not: "不符合以下任何条件"
|
||||
@ -1517,7 +1546,7 @@ _2fa:
|
||||
step4: "从现在开始,任何登录操作都将要求您提供动态口令。"
|
||||
securityKeyNotSupported: "您的浏览器不支持安全密钥。"
|
||||
registerTOTPBeforeKey: "要注册安全密钥或Passkey,请先设置验证器应用程序。"
|
||||
securityKeyInfo: "您可以设置使用支持FIDO2的硬件安全密钥、设备上的指纹或PIN来保护您的登录过程。"
|
||||
securityKeyInfo: "注册兼容 WebAuthn 的密钥,例如支持 FIDO2 的硬件安全密钥、设备上的生物识别功能、PIN 码以及 Passkey 等。"
|
||||
chromePasskeyNotSupported: "目前不支持 Chrome 的Passkey。"
|
||||
registerSecurityKey: "注册安全密钥或Passkey"
|
||||
securityKeyName: "输入密钥名称"
|
||||
@ -1844,3 +1873,24 @@ _deck:
|
||||
_dialog:
|
||||
charactersExceeded: "已经超过了最大字符数! 当前字符数 {current} / 限制字符数 {max}"
|
||||
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: "被提及时"
|
||||
|
||||
|
@ -15,7 +15,7 @@ gotIt: "知道了"
|
||||
cancel: "取消"
|
||||
noThankYou: "現在不要"
|
||||
enterUsername: "輸入使用者名稱"
|
||||
renotedBy: "{user} 轉傳了"
|
||||
renotedBy: "{user} 轉發了"
|
||||
noNotes: "無貼文。"
|
||||
noNotifications: "沒有通知"
|
||||
instance: "實例"
|
||||
@ -99,9 +99,9 @@ followRequestPending: "追隨許可批准中"
|
||||
enterEmoji: "輸入表情符號"
|
||||
renote: "轉發"
|
||||
unrenote: "取消轉發"
|
||||
renoted: "轉傳成功"
|
||||
renoted: "轉發成功"
|
||||
cantRenote: "無法轉發此貼文。"
|
||||
cantReRenote: "無法轉傳之前已經轉傳過的內容。"
|
||||
cantReRenote: "無法轉發之前已經轉發過的內容。"
|
||||
quote: "引用"
|
||||
inChannelRenote: "在頻道內轉發"
|
||||
inChannelQuote: "在頻道內引用"
|
||||
@ -122,14 +122,16 @@ unmarkAsSensitive: "取消標記為敏感內容"
|
||||
enterFileName: "請輸入檔案名稱"
|
||||
mute: "靜音"
|
||||
unmute: "解除靜音"
|
||||
renoteMute: "將轉發貼文靜音"
|
||||
renoteUnmute: "解除轉發貼文的靜音"
|
||||
block: "封鎖"
|
||||
unblock: "解除封鎖"
|
||||
suspend: "凍結"
|
||||
unsuspend: "解除凍結"
|
||||
blockConfirm: "確定要封鎖此用戶?"
|
||||
unblockConfirm: "確定解除封鎖此用戶?"
|
||||
suspendConfirm: "確定凍結此帳號?"
|
||||
unsuspendConfirm: "確定解凍此帳號?"
|
||||
suspendConfirm: "確定凍結此帳戶?"
|
||||
unsuspendConfirm: "確定解凍此帳戶?"
|
||||
selectList: "選擇清單"
|
||||
selectChannel: "選擇頻道"
|
||||
selectAntenna: "選擇天線"
|
||||
@ -153,6 +155,7 @@ flagShowTimelineReplies: "在時間軸上顯示貼文的回覆"
|
||||
flagShowTimelineRepliesDescription: "啟用時,時間線除了顯示用戶的貼文以外,還會顯示用戶對其他貼文的回覆。"
|
||||
autoAcceptFollowed: "自動追隨中使用者的追隨請求"
|
||||
addAccount: "添加帳戶"
|
||||
reloadAccountsList: "更新帳戶清單的資訊"
|
||||
loginFailed: "登入失敗"
|
||||
showOnRemote: "轉到所在實例顯示"
|
||||
general: "一般"
|
||||
@ -169,7 +172,7 @@ selectUser: "選取使用者"
|
||||
recipient: "收件人"
|
||||
annotation: "註解"
|
||||
federation: "站台聯邦"
|
||||
instances: "實例"
|
||||
instances: "伺服器"
|
||||
registeredAt: "初次觀測"
|
||||
latestRequestReceivedAt: "上次收到的請求"
|
||||
latestStatus: "最後狀態"
|
||||
@ -403,7 +406,7 @@ securityKeyAndPasskey: "安全金鑰・Passkey"
|
||||
securityKey: "安全金鑰"
|
||||
lastUsed: "上次使用"
|
||||
lastUsedAt: "最後使用:{t}"
|
||||
unregister: "註銷帳號"
|
||||
unregister: "註銷帳戶"
|
||||
passwordLessLogin: "設置無密碼登入"
|
||||
passwordLessLoginDescription: "不使用密碼,以安全金鑰或 Passkey 登入"
|
||||
resetPassword: "重置密碼"
|
||||
@ -528,8 +531,8 @@ installedDate: "安裝時間"
|
||||
lastUsedDate: "最後上線日期"
|
||||
state: "狀態"
|
||||
sort: "排序"
|
||||
ascendingOrder: "遞增"
|
||||
descendingOrder: "遞減"
|
||||
ascendingOrder: "昇冪"
|
||||
descendingOrder: "降冪"
|
||||
scratchpad: "暫存記憶體"
|
||||
scratchpadDescription: "AiScript控制台為AiScript提供了實驗環境。您可以在此編寫、執行和確認代碼與Misskey互動的结果。"
|
||||
output: "輸出"
|
||||
@ -544,6 +547,10 @@ userSuspended: "該使用者已被停用"
|
||||
userSilenced: "該用戶已被禁言。"
|
||||
yourAccountSuspendedTitle: "帳戶已被凍結"
|
||||
yourAccountSuspendedDescription: "由於違反了伺服器的服務條款或其他原因,該帳戶已被凍結。 您可以與管理員連繫以了解更多訊息。 請不要創建一個新的帳戶。"
|
||||
tokenRevoked: "權杖無效"
|
||||
tokenRevokedDescription: "登入權杖失效,請重新登入。"
|
||||
accountDeleted: "帳戶已被刪除"
|
||||
accountDeletedDescription: "這個帳戶已被刪除。"
|
||||
menu: "選單"
|
||||
divider: "分割線"
|
||||
addItem: "新增項目"
|
||||
@ -587,7 +594,6 @@ tokenRequested: "允許存取帳戶"
|
||||
pluginTokenRequestedDescription: "此外掛將擁有在此設定的權限。"
|
||||
notificationType: "通知形式"
|
||||
edit: "編輯"
|
||||
useStarForReactionFallback: "以★代替未知的表情符號"
|
||||
emailServer: "電郵伺服器"
|
||||
enableEmail: "啟用發送電郵功能"
|
||||
emailConfigInfo: "用於確認電郵地址及密碼重置"
|
||||
@ -671,8 +677,8 @@ sentReactionsCount: "反應發送次數"
|
||||
receivedReactionsCount: "收到反應次數"
|
||||
pollVotesCount: "已統計的投票數"
|
||||
pollVotedCount: "已投票數"
|
||||
yes: "確定"
|
||||
no: "取消"
|
||||
yes: "是"
|
||||
no: "否"
|
||||
driveFilesCount: "雲端硬碟檔案數量"
|
||||
driveUsage: "雲端硬碟使用量"
|
||||
noCrawle: "拒絕搜尋引擎索引"
|
||||
@ -872,10 +878,10 @@ recommended: "推薦"
|
||||
check: "檢查"
|
||||
driveCapOverrideLabel: "更改這個使用者的雲端硬碟容量上限"
|
||||
driveCapOverrideCaption: "如果指定0以下的值,就會被取消。"
|
||||
requireAdminForView: "必須以管理員帳號登入才可以檢視。"
|
||||
isSystemAccount: "由系統自動建立與管理的帳號。"
|
||||
requireAdminForView: "必須以管理員帳戶登入才可以檢視。"
|
||||
isSystemAccount: "由系統自動建立與管理的帳戶。"
|
||||
typeToConfirm: "要執行這項操作,請輸入 {x} "
|
||||
deleteAccount: "刪除帳號"
|
||||
deleteAccount: "刪除帳戶"
|
||||
document: "文件"
|
||||
numberOfPageCache: "快取頁面數"
|
||||
numberOfPageCacheDescription: "增加數量會提高便利性,但也會增加負荷與記憶體使用量。"
|
||||
@ -915,7 +921,7 @@ sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」通
|
||||
windowMaximize: "最大化"
|
||||
windowRestore: "復原"
|
||||
caption: "標題"
|
||||
loggedInAsBot: "以機器人帳號登入中"
|
||||
loggedInAsBot: "以機器人帳戶登入中"
|
||||
tools: "工具"
|
||||
cannotLoad: "無法載入"
|
||||
numberOfProfileView: "個人檔案檢視次數"
|
||||
@ -958,6 +964,25 @@ disableFederationWarn: "聯邦被停用了。即使停用也不會讓您的貼
|
||||
invitationRequiredToRegister: "目前這個伺服器為邀請制,必須擁有邀請碼才能註冊。"
|
||||
emailNotSupported: "這個伺服器不支援寄送郵件"
|
||||
postToTheChannel: "發布到頻道"
|
||||
cannotBeChangedLater: "之後不能變更。"
|
||||
reactionAcceptance: "接受表情反應"
|
||||
likeOnly: "僅限讚"
|
||||
likeOnlyForRemote: "遠端僅限讚"
|
||||
rolesAssignedToMe: "指派給自己的角色"
|
||||
resetPasswordConfirm: "重設密碼?"
|
||||
sensitiveWords: "敏感詞"
|
||||
sensitiveWordsDescription: "將含有設定詞彙的貼文可見性設為發送至首頁。可以用換行來進行複數的設定。"
|
||||
notesSearchNotAvailable: "無法使用搜尋貼文功能。"
|
||||
license: "授權"
|
||||
unfavoriteConfirm: "要取消收錄我的最愛嗎?"
|
||||
myClips: "我的摘錄"
|
||||
drivecleaner: "雲端硬碟清掃器"
|
||||
retryAllQueuesNow: "立刻重試所有佇列"
|
||||
retryAllQueuesConfirmTitle: "要現在重試嗎?"
|
||||
retryAllQueuesConfirmText: "伺服器的負荷可能會暫時增加。"
|
||||
enableChartsForRemoteUser: "生成遠端用戶的圖表"
|
||||
enableChartsForFederatedInstances: "生成遠端伺服器的圖表"
|
||||
showClipButtonInNoteFooter: "將摘錄添加至貼文"
|
||||
_achievements:
|
||||
earnedAt: "獲得日期"
|
||||
_types:
|
||||
@ -1074,7 +1099,7 @@ _achievements:
|
||||
title: "有備而來"
|
||||
description: "設定了個人檔案"
|
||||
_markedAsCat:
|
||||
title: "我是貓"
|
||||
title: "吾輩乃貓是也"
|
||||
description: "已將帳戶設定為貓"
|
||||
flavor: "還沒有名字。"
|
||||
_following1:
|
||||
@ -1217,6 +1242,8 @@ _role:
|
||||
iconUrl: "圖示的URL"
|
||||
asBadge: "顯示為徽章"
|
||||
descriptionOfAsBadge: "開啟的話,角色圖示會顯示在用戶名旁邊。"
|
||||
displayOrder: "顯示順序"
|
||||
descriptionOfDisplayOrder: "數字越大,顯示在UI上的越上面。"
|
||||
canEditMembersByModerator: "允許編輯審查員的成員"
|
||||
descriptionOfCanEditMembersByModerator: "如果開啟,管理員與審查員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。"
|
||||
priority: "優先級"
|
||||
@ -1242,6 +1269,7 @@ _role:
|
||||
rateLimitFactor: "速率限制"
|
||||
descriptionOfRateLimitFactor: "值越小限制越少,值越大限制越多。"
|
||||
canHideAds: "不顯示廣告"
|
||||
canSearchNotes: "可否搜尋貼文"
|
||||
_condition:
|
||||
isLocal: "本地使用者"
|
||||
isRemote: "遠端使用者"
|
||||
@ -1251,6 +1279,8 @@ _role:
|
||||
followersMoreThanOrEq: "追隨者人數在~以上"
|
||||
followingLessThanOrEq: "追隨人數在~以下"
|
||||
followingMoreThanOrEq: "追隨人數在~以上"
|
||||
notesLessThanOrEq: "發布數在~以下"
|
||||
notesMoreThanOrEq: "發布數在~以上"
|
||||
and: "~和~"
|
||||
or: "~或~"
|
||||
not: "~否"
|
||||
@ -1480,7 +1510,7 @@ _time:
|
||||
_tutorial:
|
||||
title: "Misskey使用方法"
|
||||
step1_1: "歡迎!"
|
||||
step1_2: "此為「時間軸」頁面,它會按照時間順序顯示你「追隨」的人發出的「貼文」"
|
||||
step1_2: "此為「時間軸」頁面,它會按照時間順序顯示你「追隨」的人發出的「貼文」。"
|
||||
step1_3: "由於你沒有發佈任何貼文,也沒有追隨任何人,所以你的時間軸目前是空的。"
|
||||
step2_1: "在發文或追隨其他人之前先讓我們設定一下個人資料吧。"
|
||||
step2_2: "提供一些關於自己的資訊來讓其他人更有追隨你的意願。"
|
||||
@ -1713,7 +1743,7 @@ _instanceCharts:
|
||||
_timelines:
|
||||
home: "首頁"
|
||||
local: "本地"
|
||||
social: "社群"
|
||||
social: "社交"
|
||||
global: "公開"
|
||||
_play:
|
||||
new: "新增Play"
|
||||
@ -1843,3 +1873,24 @@ _deck:
|
||||
_dialog:
|
||||
charactersExceeded: "已超過最大字數!現在 {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: "當被提到時"
|
||||
|
||||
|
16
package.json
16
package.json
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "13.9.2",
|
||||
"version": "13.10.3",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/misskey-dev/misskey.git"
|
||||
},
|
||||
"packageManager": "pnpm@7.27.0",
|
||||
"packageManager": "pnpm@8.1.0",
|
||||
"workspaces": [
|
||||
"packages/frontend",
|
||||
"packages/backend",
|
||||
@ -50,17 +50,17 @@
|
||||
"gulp-replace": "1.1.4",
|
||||
"gulp-terser": "2.1.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"typescript": "4.9.5"
|
||||
"typescript": "5.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/gulp": "4.0.10",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.53.0",
|
||||
"@typescript-eslint/parser": "5.53.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.57.0",
|
||||
"@typescript-eslint/parser": "5.57.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "12.7.0",
|
||||
"eslint": "8.35.0",
|
||||
"start-server-and-test": "1.15.4"
|
||||
"cypress": "12.9.0",
|
||||
"eslint": "8.37.0",
|
||||
"start-server-and-test": "2.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tensorflow/tfjs-core": "4.2.0"
|
||||
|
@ -19,6 +19,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<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>
|
||||
</html>
|
||||
|
@ -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"`);
|
||||
}
|
||||
}
|
@ -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"`);
|
||||
}
|
||||
}
|
13
packages/backend/migration/1678427401214-remove-unused.js
Normal file
13
packages/backend/migration/1678427401214-remove-unused.js
Normal 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}'`);
|
||||
}
|
||||
}
|
@ -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"`);
|
||||
}
|
||||
}
|
11
packages/backend/migration/1678694614599-sensitive-words.js
Normal file
11
packages/backend/migration/1678694614599-sensitive-words.js
Normal 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"`);
|
||||
}
|
||||
}
|
@ -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"`);
|
||||
}
|
||||
}
|
@ -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"`);
|
||||
}
|
||||
}
|
23
packages/backend/migration/1678953978856-clip-favorite.js
Normal file
23
packages/backend/migration/1678953978856-clip-favorite.js
Normal 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"`);
|
||||
}
|
||||
}
|
17
packages/backend/migration/1679309757174-antenna-active.js
Normal file
17
packages/backend/migration/1679309757174-antenna-active.js
Normal 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"`);
|
||||
}
|
||||
}
|
@ -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"`);
|
||||
}
|
||||
}
|
11
packages/backend/migration/1679651580149-cleanup.js
Normal file
11
packages/backend/migration/1679651580149-cleanup.js
Normal 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`);
|
||||
}
|
||||
}
|
@ -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"`);
|
||||
}
|
||||
}
|
21
packages/backend/migration/1680228513388-channelFavorite.js
Normal file
21
packages/backend/migration/1680228513388-channelFavorite.js
Normal 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"`);
|
||||
}
|
||||
}
|
@ -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"`);
|
||||
}
|
||||
}
|
10
packages/backend/migration/1680491187535-cleanup.js
Normal file
10
packages/backend/migration/1680491187535-cleanup.js
Normal file
@ -0,0 +1,10 @@
|
||||
export class cleanup1680491187535 {
|
||||
name = 'cleanup1680491187535'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`DROP TABLE "antenna_note" `);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
}
|
||||
}
|
11
packages/backend/migration/1680582195041-cleanup.js
Normal file
11
packages/backend/migration/1680582195041-cleanup.js
Normal file
@ -0,0 +1,11 @@
|
||||
export class cleanup1680582195041 {
|
||||
name = 'cleanup1680582195041'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`DROP TABLE "notification" `);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
|
||||
}
|
||||
}
|
@ -23,43 +23,45 @@
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-android-arm64": "^1.3.11",
|
||||
"@swc/core-darwin-arm64": "^1.3.36",
|
||||
"@swc/core-darwin-x64": "^1.3.36",
|
||||
"@swc/core-linux-arm-gnueabihf": "^1.3.36",
|
||||
"@swc/core-linux-arm64-gnu": "^1.3.36",
|
||||
"@swc/core-linux-arm64-musl": "^1.3.36",
|
||||
"@swc/core-linux-x64-gnu": "^1.3.36",
|
||||
"@swc/core-linux-x64-musl": "^1.3.36",
|
||||
"@swc/core-win32-arm64-msvc": "^1.3.36",
|
||||
"@swc/core-win32-ia32-msvc": "^1.3.36",
|
||||
"@swc/core-win32-x64-msvc": "^1.3.36",
|
||||
"@swc/core-darwin-arm64": "^1.3.42",
|
||||
"@swc/core-darwin-x64": "^1.3.42",
|
||||
"@swc/core-linux-arm-gnueabihf": "^1.3.42",
|
||||
"@swc/core-linux-arm64-gnu": "^1.3.42",
|
||||
"@swc/core-linux-arm64-musl": "^1.3.42",
|
||||
"@swc/core-linux-x64-gnu": "^1.3.42",
|
||||
"@swc/core-linux-x64-musl": "^1.3.42",
|
||||
"@swc/core-win32-arm64-msvc": "^1.3.42",
|
||||
"@swc/core-win32-ia32-msvc": "^1.3.42",
|
||||
"@swc/core-win32-x64-msvc": "^1.3.42",
|
||||
"@tensorflow/tfjs": "4.2.0",
|
||||
"@tensorflow/tfjs-node": "4.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bull-board/api": "4.12.1",
|
||||
"@bull-board/fastify": "4.12.1",
|
||||
"@bull-board/ui": "4.12.1",
|
||||
"@discordapp/twemoji": "14.0.2",
|
||||
"@aws-sdk/client-s3": "3.301.0",
|
||||
"@aws-sdk/lib-storage": "3.301.0",
|
||||
"@aws-sdk/node-http-handler": "3.296.0",
|
||||
"@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/cookie": "8.3.0",
|
||||
"@fastify/cors": "8.2.0",
|
||||
"@fastify/http-proxy": "8.4.0",
|
||||
"@fastify/multipart": "7.4.1",
|
||||
"@fastify/cors": "8.2.1",
|
||||
"@fastify/http-proxy": "9.0.0",
|
||||
"@fastify/multipart": "7.5.0",
|
||||
"@fastify/static": "6.9.0",
|
||||
"@fastify/view": "7.4.1",
|
||||
"@nestjs/common": "9.3.9",
|
||||
"@nestjs/core": "9.3.9",
|
||||
"@nestjs/testing": "9.3.9",
|
||||
"@nestjs/common": "9.3.12",
|
||||
"@nestjs/core": "9.3.12",
|
||||
"@nestjs/testing": "9.3.12",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@sinonjs/fake-timers": "10.0.2",
|
||||
"@swc/cli": "0.1.62",
|
||||
"@swc/core": "1.3.36",
|
||||
"@swc/core": "1.3.42",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.12.0",
|
||||
"archiver": "5.3.1",
|
||||
"autwh": "0.1.0",
|
||||
"aws-sdk": "2.1318.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "2.0.5",
|
||||
"bull": "4.10.4",
|
||||
@ -74,35 +76,35 @@
|
||||
"date-fns": "2.29.3",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"escape-regexp": "0.0.1",
|
||||
"fastify": "4.13.0",
|
||||
"fastify": "4.15.0",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "18.2.1",
|
||||
"fluent-ffmpeg": "2.1.2",
|
||||
"form-data": "4.0.0",
|
||||
"got": "12.5.3",
|
||||
"got": "12.6.0",
|
||||
"happy-dom": "8.9.0",
|
||||
"hpagent": "1.2.0",
|
||||
"ioredis": "4.28.5",
|
||||
"ip-cidr": "3.1.0",
|
||||
"is-svg": "4.3.2",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsdom": "21.1.0",
|
||||
"jsdom": "21.1.1",
|
||||
"json5": "2.2.3",
|
||||
"jsonld": "8.1.1",
|
||||
"jsrsasign": "10.6.1",
|
||||
"jsrsasign": "10.7.0",
|
||||
"mfm-js": "0.23.3",
|
||||
"mime-types": "2.1.35",
|
||||
"misskey-js": "0.0.15",
|
||||
"misskey-js": "workspace:*",
|
||||
"ms": "3.0.0-canary.1",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.3.0",
|
||||
"node-fetch": "3.3.1",
|
||||
"nodemailer": "6.9.1",
|
||||
"nsfwjs": "2.4.2",
|
||||
"oauth": "0.10.0",
|
||||
"os-utils": "0.0.14",
|
||||
"otpauth": "^9.0.2",
|
||||
"otpauth": "9.1.1",
|
||||
"parse5": "7.1.2",
|
||||
"pg": "8.9.0",
|
||||
"pg": "8.10.0",
|
||||
"private-ip": "3.0.0",
|
||||
"probe-image-size": "7.2.3",
|
||||
"promise-limit": "2.7.0",
|
||||
@ -123,33 +125,33 @@
|
||||
"sanitize-html": "2.10.0",
|
||||
"seedrandom": "3.0.5",
|
||||
"semver": "7.3.8",
|
||||
"sharp": "0.31.3",
|
||||
"sharp": "0.32.0",
|
||||
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"summaly": "github:misskey-dev/summaly",
|
||||
"systeminformation": "5.17.10",
|
||||
"systeminformation": "5.17.12",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.1",
|
||||
"tsc-alias": "1.8.2",
|
||||
"tsconfig-paths": "4.1.2",
|
||||
"tsc-alias": "1.8.5",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typeorm": "0.3.11",
|
||||
"typescript": "4.9.5",
|
||||
"typescript": "5.0.2",
|
||||
"ulid": "2.3.0",
|
||||
"unzipper": "0.10.11",
|
||||
"uuid": "9.0.0",
|
||||
"vary": "1.1.2",
|
||||
"web-push": "3.5.0",
|
||||
"websocket": "1.0.34",
|
||||
"ws": "8.12.1",
|
||||
"ws": "8.13.0",
|
||||
"xev": "3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.4.3",
|
||||
"@jest/globals": "29.5.0",
|
||||
"@swc/jest": "0.2.24",
|
||||
"@types/accepts": "1.3.5",
|
||||
"@types/archiver": "5.3.1",
|
||||
"@types/archiver": "5.3.2",
|
||||
"@types/bcryptjs": "2.4.2",
|
||||
"@types/bull": "4.10.0",
|
||||
"@types/cbor": "6.0.0",
|
||||
@ -158,13 +160,13 @@
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
"@types/fluent-ffmpeg": "2.1.21",
|
||||
"@types/ioredis": "4.28.10",
|
||||
"@types/jest": "29.4.0",
|
||||
"@types/jest": "29.5.0",
|
||||
"@types/js-yaml": "4.0.5",
|
||||
"@types/jsdom": "21.1.0",
|
||||
"@types/jsdom": "21.1.1",
|
||||
"@types/jsonld": "1.5.8",
|
||||
"@types/jsrsasign": "10.5.5",
|
||||
"@types/jsrsasign": "10.5.8",
|
||||
"@types/mime-types": "2.1.1",
|
||||
"@types/node": "18.14.1",
|
||||
"@types/node": "18.15.11",
|
||||
"@types/node-fetch": "3.0.3",
|
||||
"@types/nodemailer": "6.4.7",
|
||||
"@types/oauth": "0.9.1",
|
||||
@ -176,7 +178,7 @@
|
||||
"@types/ratelimiter": "3.4.4",
|
||||
"@types/redis": "4.0.11",
|
||||
"@types/rename": "1.0.4",
|
||||
"@types/sanitize-html": "2.8.0",
|
||||
"@types/sanitize-html": "2.9.0",
|
||||
"@types/semver": "7.3.13",
|
||||
"@types/sharp": "0.31.1",
|
||||
"@types/sinonjs__fake-timers": "8.1.2",
|
||||
@ -188,13 +190,14 @@
|
||||
"@types/web-push": "3.3.2",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.52.0",
|
||||
"@typescript-eslint/parser": "5.53.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.57.0",
|
||||
"@typescript-eslint/parser": "5.57.0",
|
||||
"aws-sdk-client-mock": "^2.1.1",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.35.0",
|
||||
"eslint": "8.37.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"execa": "6.1.0",
|
||||
"jest": "29.4.3",
|
||||
"jest-mock": "29.4.3"
|
||||
"jest": "29.5.0",
|
||||
"jest-mock": "29.5.0"
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import type { UserProfilesRepository, UsersRepository } from '@/models/index.js'
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
|
||||
export const ACHIEVEMENT_TYPES = [
|
||||
'notes1',
|
||||
@ -90,7 +90,7 @@ export class AchievementService {
|
||||
@Inject(DI.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,
|
||||
});
|
||||
}
|
||||
|
@ -10,9 +10,9 @@ import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { PushNotificationService } from '@/core/PushNotificationService.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 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 { bindThis } from '@/decorators.js';
|
||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||
@ -24,6 +24,9 @@ export class AntennaService implements OnApplicationShutdown {
|
||||
private antennas: Antenna[];
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.redisSubscriber)
|
||||
private redisSubscriber: Redis.Redis,
|
||||
|
||||
@ -33,9 +36,6 @@ export class AntennaService implements OnApplicationShutdown {
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
@Inject(DI.antennaNotesRepository)
|
||||
private antennaNotesRepository: AntennaNotesRepository,
|
||||
|
||||
@Inject(DI.antennasRepository)
|
||||
private antennasRepository: AntennasRepository,
|
||||
|
||||
@ -71,12 +71,14 @@ export class AntennaService implements OnApplicationShutdown {
|
||||
this.antennas.push({
|
||||
...body,
|
||||
createdAt: new Date(body.createdAt),
|
||||
lastUsedAt: new Date(body.lastUsedAt),
|
||||
});
|
||||
break;
|
||||
case 'antennaUpdated':
|
||||
this.antennas[this.antennas.findIndex(a => a.id === body.id)] = {
|
||||
...body,
|
||||
createdAt: new Date(body.createdAt),
|
||||
lastUsedAt: new Date(body.lastUsedAt),
|
||||
};
|
||||
break;
|
||||
case 'antennaDeleted':
|
||||
@ -90,54 +92,13 @@ export class AntennaService implements OnApplicationShutdown {
|
||||
|
||||
@bindThis
|
||||
public async addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }): Promise<void> {
|
||||
// 通知しない設定になっているか、自分自身の投稿なら既読にする
|
||||
const read = !antenna.notify || (antenna.userId === noteUser.id);
|
||||
|
||||
this.antennaNotesRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
antennaId: antenna.id,
|
||||
noteId: note.id,
|
||||
read: read,
|
||||
});
|
||||
|
||||
this.redisClient.xadd(
|
||||
`antennaTimeline:${antenna.id}`,
|
||||
'MAXLEN', '~', '200',
|
||||
`${this.idService.parse(note.id).date.getTime()}-*`,
|
||||
'note', note.id);
|
||||
|
||||
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: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
|
||||
@ -217,7 +178,9 @@ export class AntennaService implements OnApplicationShutdown {
|
||||
@bindThis
|
||||
public async getAntennas() {
|
||||
if (!this.antennasFetched) {
|
||||
this.antennas = await this.antennasRepository.find();
|
||||
this.antennas = await this.antennasRepository.findBy({
|
||||
isActive: true,
|
||||
});
|
||||
this.antennasFetched = true;
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import type { UsersRepository } from '@/models/index.js';
|
||||
import { Cache } from '@/misc/cache.js';
|
||||
import type { UserProfile, UsersRepository } from '@/models/index.js';
|
||||
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
|
||||
import type { LocalUser, User } from '@/models/entities/User.js';
|
||||
import { DI } from '@/di-symbols.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';
|
||||
|
||||
@Injectable()
|
||||
export class UserCacheService implements OnApplicationShutdown {
|
||||
public userByIdCache: Cache<User>;
|
||||
public localUserByNativeTokenCache: Cache<LocalUser | null>;
|
||||
public localUserByIdCache: Cache<LocalUser>;
|
||||
public uriPersonCache: Cache<User | null>;
|
||||
export class CacheService implements OnApplicationShutdown {
|
||||
public userByIdCache: MemoryKVCache<User>;
|
||||
public localUserByNativeTokenCache: MemoryKVCache<LocalUser | null>;
|
||||
public localUserByIdCache: MemoryKVCache<LocalUser>;
|
||||
public uriPersonCache: MemoryKVCache<User | null>;
|
||||
public userProfileCache: RedisKVCache<UserProfile>;
|
||||
public userMutingsCache: RedisKVCache<string[]>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.redisSubscriber)
|
||||
private redisSubscriber: Redis.Redis,
|
||||
|
||||
@ -27,10 +32,12 @@ export class UserCacheService implements OnApplicationShutdown {
|
||||
) {
|
||||
//this.onMessage = this.onMessage.bind(this);
|
||||
|
||||
this.userByIdCache = new Cache<User>(Infinity);
|
||||
this.localUserByNativeTokenCache = new Cache<LocalUser | null>(Infinity);
|
||||
this.localUserByIdCache = new Cache<LocalUser>(Infinity);
|
||||
this.uriPersonCache = new Cache<User | null>(Infinity);
|
||||
this.userByIdCache = new MemoryKVCache<User>(Infinity);
|
||||
this.localUserByNativeTokenCache = new MemoryKVCache<LocalUser | null>(Infinity);
|
||||
this.localUserByIdCache = new MemoryKVCache<LocalUser>(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);
|
||||
}
|
||||
@ -52,7 +59,7 @@ export class UserCacheService implements OnApplicationShutdown {
|
||||
}
|
||||
}
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
this.localUserByNativeTokenCache.set(user.token, user);
|
||||
this.localUserByNativeTokenCache.set(user.token!, user);
|
||||
this.localUserByIdCache.set(user.id, user);
|
||||
}
|
||||
break;
|
||||
@ -77,7 +84,7 @@ export class UserCacheService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public findById(userId: User['id']) {
|
||||
public findUserById(userId: User['id']) {
|
||||
return this.userByIdCache.fetch(userId, () => this.usersRepository.findOneByOrFail({ id: userId }));
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import { AntennaService } from './AntennaService.js';
|
||||
import { AppLockService } from './AppLockService.js';
|
||||
import { AchievementService } from './AchievementService.js';
|
||||
import { CaptchaService } from './CaptchaService.js';
|
||||
import { CreateNotificationService } from './CreateNotificationService.js';
|
||||
import { CreateSystemUserService } from './CreateSystemUserService.js';
|
||||
import { CustomEmojiService } from './CustomEmojiService.js';
|
||||
import { DeleteAccountService } from './DeleteAccountService.js';
|
||||
@ -39,7 +38,7 @@ import { S3Service } from './S3Service.js';
|
||||
import { SignupService } from './SignupService.js';
|
||||
import { TwoFactorAuthenticationService } from './TwoFactorAuthenticationService.js';
|
||||
import { UserBlockingService } from './UserBlockingService.js';
|
||||
import { UserCacheService } from './UserCacheService.js';
|
||||
import { CacheService } from './CacheService.js';
|
||||
import { UserFollowingService } from './UserFollowingService.js';
|
||||
import { UserKeypairStoreService } from './UserKeypairStoreService.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 $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService };
|
||||
const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService };
|
||||
const $CreateNotificationService: Provider = { provide: 'CreateNotificationService', useExisting: CreateNotificationService };
|
||||
const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService };
|
||||
const $CustomEmojiService: Provider = { provide: 'CustomEmojiService', useExisting: CustomEmojiService };
|
||||
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 $TwoFactorAuthenticationService: Provider = { provide: 'TwoFactorAuthenticationService', useExisting: TwoFactorAuthenticationService };
|
||||
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 $UserKeypairStoreService: Provider = { provide: 'UserKeypairStoreService', useExisting: UserKeypairStoreService };
|
||||
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
|
||||
@ -250,7 +248,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
AppLockService,
|
||||
AchievementService,
|
||||
CaptchaService,
|
||||
CreateNotificationService,
|
||||
CreateSystemUserService,
|
||||
CustomEmojiService,
|
||||
DeleteAccountService,
|
||||
@ -285,7 +282,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
SignupService,
|
||||
TwoFactorAuthenticationService,
|
||||
UserBlockingService,
|
||||
UserCacheService,
|
||||
CacheService,
|
||||
UserFollowingService,
|
||||
UserKeypairStoreService,
|
||||
UserListService,
|
||||
@ -368,7 +365,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$AppLockService,
|
||||
$AchievementService,
|
||||
$CaptchaService,
|
||||
$CreateNotificationService,
|
||||
$CreateSystemUserService,
|
||||
$CustomEmojiService,
|
||||
$DeleteAccountService,
|
||||
@ -403,7 +399,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$SignupService,
|
||||
$TwoFactorAuthenticationService,
|
||||
$UserBlockingService,
|
||||
$UserCacheService,
|
||||
$CacheService,
|
||||
$UserFollowingService,
|
||||
$UserKeypairStoreService,
|
||||
$UserListService,
|
||||
@ -487,7 +483,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
AppLockService,
|
||||
AchievementService,
|
||||
CaptchaService,
|
||||
CreateNotificationService,
|
||||
CreateSystemUserService,
|
||||
CustomEmojiService,
|
||||
DeleteAccountService,
|
||||
@ -522,7 +517,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
SignupService,
|
||||
TwoFactorAuthenticationService,
|
||||
UserBlockingService,
|
||||
UserCacheService,
|
||||
CacheService,
|
||||
UserFollowingService,
|
||||
UserKeypairStoreService,
|
||||
UserListService,
|
||||
@ -604,7 +599,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$AppLockService,
|
||||
$AchievementService,
|
||||
$CaptchaService,
|
||||
$CreateNotificationService,
|
||||
$CreateSystemUserService,
|
||||
$CustomEmojiService,
|
||||
$DeleteAccountService,
|
||||
@ -639,7 +633,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$SignupService,
|
||||
$TwoFactorAuthenticationService,
|
||||
$UserBlockingService,
|
||||
$UserCacheService,
|
||||
$CacheService,
|
||||
$UserFollowingService,
|
||||
$UserKeypairStoreService,
|
||||
$UserListService,
|
||||
@ -714,4 +708,4 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
//#endregion
|
||||
],
|
||||
})
|
||||
export class CoreModule {}
|
||||
export class CoreModule { }
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ import type { DriveFile } from '@/models/entities/DriveFile.js';
|
||||
import type { Emoji } from '@/models/entities/Emoji.js';
|
||||
import type { EmojisRepository, Note } from '@/models/index.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 type { Config } from '@/config.js';
|
||||
import { ReactionService } from '@/core/ReactionService.js';
|
||||
@ -16,7 +16,7 @@ import { query } from '@/misc/prelude/url.js';
|
||||
|
||||
@Injectable()
|
||||
export class CustomEmojiService {
|
||||
private cache: Cache<Emoji | null>;
|
||||
private cache: MemoryKVCache<Emoji | null>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
@ -34,7 +34,7 @@ export class CustomEmojiService {
|
||||
private globalEventService: GlobalEventService,
|
||||
private reactionService: ReactionService,
|
||||
) {
|
||||
this.cache = new Cache<Emoji | null>(1000 * 60 * 60 * 12);
|
||||
this.cache = new MemoryKVCache<Emoji | null>(1000 * 60 * 60 * 12);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@ -44,6 +44,7 @@ export class CustomEmojiService {
|
||||
category: string | null;
|
||||
aliases: string[];
|
||||
host: string | null;
|
||||
license: string | null;
|
||||
}): Promise<Emoji> {
|
||||
const emoji = await this.emojisRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
@ -55,10 +56,11 @@ export class CustomEmojiService {
|
||||
originalUrl: data.driveFile.url,
|
||||
publicUrl: data.driveFile.webpublicUrl ?? data.driveFile.url,
|
||||
type: data.driveFile.webpublicType ?? data.driveFile.type,
|
||||
license: data.license,
|
||||
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
if (data.host == null) {
|
||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||
await this.db.queryResultCache?.remove(['meta_emojis']);
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
||||
emoji: await this.emojiEntityService.packDetailed(emoji.id),
|
||||
|
@ -2,7 +2,9 @@ import * as fs from 'node:fs';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import sharp from 'sharp';
|
||||
import { sharpBmp } from 'sharp-read-bmp';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/index.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 { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import type S3 from 'aws-sdk/clients/s3.js';
|
||||
import { correctFilename } from '@/misc/correct-filename.js';
|
||||
import { isMimeImage } from '@/misc/is-mime-image.js';
|
||||
|
||||
type AddFileArgs = {
|
||||
/** User who wish to add file */
|
||||
@ -79,6 +81,7 @@ type UploadFromUrlArgs = {
|
||||
export class DriveService {
|
||||
private registerLogger: Logger;
|
||||
private downloaderLogger: Logger;
|
||||
private deleteLogger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
@ -116,6 +119,7 @@ export class DriveService {
|
||||
const logger = new Logger('drive', 'blue');
|
||||
this.registerLogger = logger.createSubLogger('register', 'yellow');
|
||||
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)) {
|
||||
this.registerLogger.debug('web image and thumbnail not created (not an required file)');
|
||||
if (!isMimeImage(type, 'sharp-convertible-image-with-bmp')) {
|
||||
this.registerLogger.debug('web image and thumbnail not created (cannot convert by sharp)');
|
||||
return {
|
||||
webpublic: null,
|
||||
thumbnail: null,
|
||||
@ -284,22 +288,16 @@ export class DriveService {
|
||||
|
||||
let img: sharp.Sharp | null = null;
|
||||
let satisfyWebpublic: boolean;
|
||||
let isAnimated: boolean;
|
||||
|
||||
try {
|
||||
img = sharp(path);
|
||||
img = await sharpBmp(path, type);
|
||||
const metadata = await img.metadata();
|
||||
const isAnimated = metadata.pages && metadata.pages > 1;
|
||||
|
||||
// skip animated
|
||||
if (isAnimated) {
|
||||
return {
|
||||
webpublic: null,
|
||||
thumbnail: null,
|
||||
};
|
||||
}
|
||||
isAnimated = !!(metadata.pages && metadata.pages > 1);
|
||||
|
||||
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.width && metadata.width <= 2048 &&
|
||||
metadata.height && metadata.height <= 2048
|
||||
@ -315,15 +313,13 @@ export class DriveService {
|
||||
// #region webpublic
|
||||
let webpublic: IImage | null = null;
|
||||
|
||||
if (generateWeb && !satisfyWebpublic) {
|
||||
if (generateWeb && !satisfyWebpublic && !isAnimated) {
|
||||
this.registerLogger.info('creating web image');
|
||||
|
||||
try {
|
||||
if (['image/jpeg', 'image/webp', 'image/avif'].includes(type)) {
|
||||
webpublic = await this.imageProcessingService.convertSharpToJpeg(img, 2048, 2048);
|
||||
} else if (['image/png'].includes(type)) {
|
||||
webpublic = await this.imageProcessingService.convertSharpToPng(img, 2048, 2048);
|
||||
} else if (['image/svg+xml'].includes(type)) {
|
||||
webpublic = await this.imageProcessingService.convertSharpToWebp(img, 2048, 2048);
|
||||
} else if (['image/png', 'image/bmp', 'image/svg+xml'].includes(type)) {
|
||||
webpublic = await this.imageProcessingService.convertSharpToPng(img, 2048, 2048);
|
||||
} else {
|
||||
this.registerLogger.debug('web image not created (not an required image)');
|
||||
@ -333,6 +329,7 @@ export class DriveService {
|
||||
}
|
||||
} else {
|
||||
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)');
|
||||
}
|
||||
// #endregion webpublic
|
||||
@ -341,10 +338,10 @@ export class DriveService {
|
||||
let thumbnail: IImage | null = null;
|
||||
|
||||
try {
|
||||
if (['image/jpeg', 'image/webp', 'image/avif', 'image/png', 'image/svg+xml'].includes(type)) {
|
||||
thumbnail = await this.imageProcessingService.convertSharpToWebp(img, 498, 280);
|
||||
if (isAnimated) {
|
||||
thumbnail = await this.imageProcessingService.convertSharpToWebp(sharp(path, { animated: true }), 374, 317, { alphaQuality: 70 });
|
||||
} else {
|
||||
this.registerLogger.debug('thumbnail not created (not an required file)');
|
||||
thumbnail = await this.imageProcessingService.convertSharpToWebp(img, 498, 422);
|
||||
}
|
||||
} catch (err) {
|
||||
this.registerLogger.warn('thumbnail not created (an error occured)', err as Error);
|
||||
@ -373,7 +370,7 @@ export class DriveService {
|
||||
Body: stream,
|
||||
ContentType: type,
|
||||
CacheControl: 'max-age=31536000, immutable',
|
||||
} as S3.PutObjectRequest;
|
||||
} as PutObjectCommandInput;
|
||||
|
||||
if (filename) params.ContentDisposition = contentDisposition(
|
||||
'inline',
|
||||
@ -383,21 +380,16 @@ export class DriveService {
|
||||
);
|
||||
if (meta.objectStorageSetPublicRead) params.ACL = 'public-read';
|
||||
|
||||
const s3 = this.s3Service.getS3(meta);
|
||||
|
||||
const upload = s3.upload(params, {
|
||||
partSize: s3.endpoint.hostname === 'storage.googleapis.com' ? 500 * 1024 * 1024 : 8 * 1024 * 1024,
|
||||
});
|
||||
|
||||
await upload.promise()
|
||||
await this.s3Service.upload(meta, params)
|
||||
.then(
|
||||
result => {
|
||||
if (result) {
|
||||
if ('Bucket' in result) { // CompleteMultipartUploadCommandOutput
|
||||
this.registerLogger.debug(`Uploaded: ${result.Bucket}/${result.Key} => ${result.Location}`);
|
||||
} else {
|
||||
this.registerLogger.error(`Upload Result Empty: key = ${key}, filename = ${filename}`);
|
||||
} else { // AbortMultipartUploadCommandOutput
|
||||
this.registerLogger.error(`Upload Result Aborted: key = ${key}, filename = ${filename}`);
|
||||
}
|
||||
},
|
||||
})
|
||||
.catch(
|
||||
err => {
|
||||
this.registerLogger.error(`Upload Failed: key = ${key}, filename = ${filename}`, err);
|
||||
},
|
||||
@ -476,7 +468,7 @@ export class DriveService {
|
||||
// DriveFile.nameは256文字, validateFileNameは200文字制限であるため、
|
||||
// extを付加してデータベースの文字数制限に当たることはまずない
|
||||
(name && this.driveFileEntityService.validateFileName(name)) ? name : 'untitled',
|
||||
info.type.ext
|
||||
info.type.ext,
|
||||
);
|
||||
|
||||
if (user && !force) {
|
||||
@ -533,10 +525,10 @@ export class DriveService {
|
||||
};
|
||||
|
||||
const properties: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
orientation?: number;
|
||||
} = {};
|
||||
width?: number;
|
||||
height?: number;
|
||||
orientation?: number;
|
||||
} = {};
|
||||
|
||||
if (info.width) {
|
||||
properties['width'] = info.width;
|
||||
@ -621,17 +613,20 @@ export class DriveService {
|
||||
|
||||
if (user) {
|
||||
this.driveFileEntityService.pack(file, { self: true }).then(packedFile => {
|
||||
// Publish driveFileCreated event
|
||||
// Publish driveFileCreated event
|
||||
this.globalEventService.publishMainStream(user.id, 'driveFileCreated', packedFile);
|
||||
this.globalEventService.publishDriveStream(user.id, 'fileCreated', packedFile);
|
||||
});
|
||||
}
|
||||
|
||||
// 統計を更新
|
||||
this.driveChart.update(file, true);
|
||||
this.perUserDriveChart.update(file, true);
|
||||
if (file.userHost !== null) {
|
||||
this.instanceChart.updateDrive(file, true);
|
||||
if (file.userHost == null) {
|
||||
// ローカルユーザーのみ
|
||||
this.perUserDriveChart.update(file, true);
|
||||
} else {
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateDrive(file, true);
|
||||
}
|
||||
}
|
||||
|
||||
return file;
|
||||
@ -697,7 +692,7 @@ export class DriveService {
|
||||
|
||||
@bindThis
|
||||
private async deletePostProcess(file: DriveFile, isExpired = false) {
|
||||
// リモートファイル期限切れ削除後は直リンクにする
|
||||
// リモートファイル期限切れ削除後は直リンクにする
|
||||
if (isExpired && file.userHost !== null && file.uri != null) {
|
||||
this.driveFilesRepository.update(file.id, {
|
||||
isLink: true,
|
||||
@ -714,24 +709,37 @@ export class DriveService {
|
||||
this.driveFilesRepository.delete(file.id);
|
||||
}
|
||||
|
||||
// 統計を更新
|
||||
this.driveChart.update(file, false);
|
||||
this.perUserDriveChart.update(file, false);
|
||||
if (file.userHost !== null) {
|
||||
this.instanceChart.updateDrive(file, false);
|
||||
if (file.userHost == null) {
|
||||
// ローカルユーザーのみ
|
||||
this.perUserDriveChart.update(file, false);
|
||||
} else {
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateDrive(file, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async deleteObjectStorageFile(key: string) {
|
||||
const meta = await this.metaService.fetch();
|
||||
try {
|
||||
const param = {
|
||||
Bucket: meta.objectStorageBucket,
|
||||
Key: key,
|
||||
} as DeleteObjectCommandInput;
|
||||
|
||||
const s3 = this.s3Service.getS3(meta);
|
||||
|
||||
await s3.deleteObject({
|
||||
Bucket: meta.objectStorageBucket!,
|
||||
Key: key,
|
||||
}).promise();
|
||||
await this.s3Service.delete(meta, param);
|
||||
} catch (err: any) {
|
||||
if (err.name === 'NoSuchKey') {
|
||||
this.deleteLogger.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err as Error);
|
||||
return;
|
||||
} else {
|
||||
throw new Error(`Failed to delete the file from the object storage with the given key: ${key}`, {
|
||||
cause: err,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@ -749,7 +757,7 @@ export class DriveService {
|
||||
}: UploadFromUrlArgs): Promise<DriveFile> {
|
||||
// Create temp file
|
||||
const [path, cleanup] = await createTemp();
|
||||
|
||||
|
||||
try {
|
||||
// write content at URL to temp file
|
||||
const { filename: name } = await this.downloadService.downloadUrl(url, path);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { InstancesRepository } from '@/models/index.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 { DI } from '@/di-symbols.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
@ -9,7 +9,7 @@ import { bindThis } from '@/decorators.js';
|
||||
|
||||
@Injectable()
|
||||
export class FederatedInstanceService {
|
||||
private cache: Cache<Instance>;
|
||||
private cache: MemoryKVCache<Instance>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.instancesRepository)
|
||||
@ -18,7 +18,7 @@ export class FederatedInstanceService {
|
||||
private utilityService: UtilityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
this.cache = new Cache<Instance>(1000 * 60 * 60);
|
||||
this.cache = new MemoryKVCache<Instance>(1000 * 60 * 60);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -16,7 +16,7 @@ import type {
|
||||
UserListStreamTypes,
|
||||
UserStreamTypes,
|
||||
} 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 type { Config } from '@/config.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ulid } from 'ulid';
|
||||
import { DI } from '@/di-symbols.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 { genMeidg } from '@/misc/id/meidg.js';
|
||||
import { genObjectId } from '@/misc/id/object-id.js';
|
||||
@ -32,4 +32,17 @@ export class IdService {
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,15 +15,28 @@ export type IImageStream = {
|
||||
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 = {
|
||||
quality: 85,
|
||||
quality: 77,
|
||||
alphaQuality: 95,
|
||||
lossless: false,
|
||||
nearLossless: false,
|
||||
smartSubsample: true,
|
||||
mixed: true,
|
||||
effort: 2,
|
||||
};
|
||||
|
||||
export const avifDefault: sharp.AvifOptions = {
|
||||
quality: 60,
|
||||
lossless: false,
|
||||
effort: 2,
|
||||
};
|
||||
|
||||
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
|
||||
* with resize, remove metadata, resolve orientation, stop animation
|
||||
@ -78,29 +61,22 @@ export class ImageProcessingService {
|
||||
|
||||
@bindThis
|
||||
public async convertSharpToWebp(sharp: sharp.Sharp, width: number, height: number, options: sharp.WebpOptions = webpDefault): Promise<IImage> {
|
||||
const data = await sharp
|
||||
.resize(width, height, {
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
.rotate()
|
||||
.webp(options)
|
||||
.toBuffer();
|
||||
const result = this.convertSharpToWebpStream(sharp, width, height, options);
|
||||
|
||||
return {
|
||||
data,
|
||||
ext: 'webp',
|
||||
type: 'image/webp',
|
||||
data: await result.data.toBuffer(),
|
||||
ext: result.ext,
|
||||
type: result.type,
|
||||
};
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@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
|
||||
.resize(width, height, {
|
||||
fit: 'inside',
|
||||
@ -115,13 +91,56 @@ export class ImageProcessingService {
|
||||
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
|
||||
* with resize, remove metadata, resolve orientation, stop animation
|
||||
*/
|
||||
@bindThis
|
||||
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
|
||||
|
@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { IsNull } from 'typeorm';
|
||||
import type { LocalUser } from '@/models/entities/User.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 { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
@ -11,7 +11,7 @@ const ACTOR_USERNAME = 'instance.actor' as const;
|
||||
|
||||
@Injectable()
|
||||
export class InstanceActorService {
|
||||
private cache: Cache<LocalUser>;
|
||||
private cache: MemoryCache<LocalUser>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
@ -19,12 +19,12 @@ export class InstanceActorService {
|
||||
|
||||
private createSystemUserService: CreateSystemUserService,
|
||||
) {
|
||||
this.cache = new Cache<LocalUser>(Infinity);
|
||||
this.cache = new MemoryCache<LocalUser>(Infinity);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getInstanceActor(): Promise<LocalUser> {
|
||||
const cached = this.cache.get(null);
|
||||
const cached = this.cache.get();
|
||||
if (cached) return cached;
|
||||
|
||||
const user = await this.usersRepository.findOneBy({
|
||||
@ -33,11 +33,11 @@ export class InstanceActorService {
|
||||
}) as LocalUser | undefined;
|
||||
|
||||
if (user) {
|
||||
this.cache.set(null, user);
|
||||
this.cache.set(user);
|
||||
return user;
|
||||
} else {
|
||||
const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME) as LocalUser;
|
||||
this.cache.set(null, created);
|
||||
this.cache.set(created);
|
||||
return created;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { setImmediate } from 'node:timers/promises';
|
||||
import * as mfm from 'mfm-js';
|
||||
import { In, DataSource } from 'typeorm';
|
||||
import Redis from 'ioredis';
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import { extractMentions } from '@/misc/extract-mentions.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 type { Channel } from '@/models/entities/Channel.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 { RelayService } from '@/core/RelayService.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 ActiveUsersChart from '@/core/chart/charts/active-users.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 { HashtagService } from '@/core/HashtagService.js';
|
||||
import { AntennaService } from '@/core/AntennaService.js';
|
||||
@ -44,8 +45,9 @@ import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.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';
|
||||
|
||||
@ -59,7 +61,7 @@ class NotificationManager {
|
||||
|
||||
constructor(
|
||||
private mutingsRepository: MutingsRepository,
|
||||
private createNotificationService: CreateNotificationService,
|
||||
private notificationService: NotificationService,
|
||||
notifier: { id: User['id']; },
|
||||
note: Note,
|
||||
) {
|
||||
@ -100,7 +102,7 @@ class NotificationManager {
|
||||
|
||||
// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
|
||||
if (!mentioneesMutedUserIds.includes(this.notifier.id)) {
|
||||
this.createNotificationService.createNotification(x.target, x.reason, {
|
||||
this.notificationService.createNotification(x.target, x.reason, {
|
||||
notifierId: this.notifier.id,
|
||||
noteId: this.note.id,
|
||||
});
|
||||
@ -149,6 +151,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@ -182,7 +187,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
private globalEventService: GlobalEventService,
|
||||
private queueService: QueueService,
|
||||
private noteReadService: NoteReadService,
|
||||
private createNotificationService: CreateNotificationService,
|
||||
private notificationService: NotificationService,
|
||||
private relayService: RelayService,
|
||||
private federatedInstanceService: FederatedInstanceService,
|
||||
private hashtagService: HashtagService,
|
||||
@ -192,11 +197,12 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
private apDeliverManagerService: ApDeliverManagerService,
|
||||
private apRendererService: ApRendererService,
|
||||
private roleService: RoleService,
|
||||
private metaService: MetaService,
|
||||
private notesChart: NotesChart,
|
||||
private perUserNotesChart: PerUserNotesChart,
|
||||
private activeUsersChart: ActiveUsersChart,
|
||||
private instanceChart: InstanceChart,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
@bindThis
|
||||
public async create(user: {
|
||||
@ -230,7 +236,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
if (data.channel != null) data.localOnly = true;
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
@ -317,6 +325,14 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
|
||||
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(
|
||||
() => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!),
|
||||
() => { /* aborted, ignore this */ },
|
||||
@ -387,7 +403,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
// 投稿を作成
|
||||
try {
|
||||
if (insert.hasPoll) {
|
||||
// Start transaction
|
||||
// Start transaction
|
||||
await this.db.transaction(async transactionalEntityManager => {
|
||||
await transactionalEntityManager.insert(Note, insert);
|
||||
|
||||
@ -410,7 +426,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
|
||||
return insert;
|
||||
} catch (e) {
|
||||
// duplicate key error
|
||||
// duplicate key error
|
||||
if (isDuplicateKeyValueError(e)) {
|
||||
const err = new Error('Duplicated note');
|
||||
err.name = 'duplicated';
|
||||
@ -431,15 +447,20 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
createdAt: User['createdAt'];
|
||||
isBot: User['isBot'];
|
||||
}, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) {
|
||||
// 統計を更新
|
||||
const meta = await this.metaService.fetch();
|
||||
|
||||
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
|
||||
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.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);
|
||||
|
||||
// Word mute
|
||||
mutedWordsCache.fetch(null, () => this.userProfilesRepository.find({
|
||||
mutedWordsCache.fetch(() => this.userProfilesRepository.find({
|
||||
where: {
|
||||
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);
|
||||
|
||||
|
@ -16,6 +16,7 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
|
||||
@Injectable()
|
||||
export class NoteDeleteService {
|
||||
@ -39,6 +40,7 @@ export class NoteDeleteService {
|
||||
private federatedInstanceService: FederatedInstanceService,
|
||||
private apRendererService: ApRendererService,
|
||||
private apDeliverManagerService: ApDeliverManagerService,
|
||||
private metaService: MetaService,
|
||||
private notesChart: NotesChart,
|
||||
private perUserNotesChart: PerUserNotesChart,
|
||||
private instanceChart: InstanceChart,
|
||||
@ -95,14 +97,19 @@ export class NoteDeleteService {
|
||||
}
|
||||
//#endregion
|
||||
|
||||
// 統計を更新
|
||||
const meta = await this.metaService.fetch();
|
||||
|
||||
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)) {
|
||||
this.federatedInstanceService.fetch(user.host).then(i => {
|
||||
this.federatedInstanceService.fetch(user.host).then(async i => {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -4,11 +4,11 @@ import { In, IsNull, Not } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { User } from '@/models/entities/User.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 { IdService } from '@/core/IdService.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 { bindThis } from '@/decorators.js';
|
||||
import { NotificationService } from './NotificationService.js';
|
||||
@ -38,9 +38,6 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||
@Inject(DI.channelFollowingsRepository)
|
||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||
|
||||
@Inject(DI.antennaNotesRepository)
|
||||
private antennaNotesRepository: AntennaNotesRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private idService: IdService,
|
||||
private globalEventService: GlobalEventService,
|
||||
@ -121,7 +118,6 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||
const readMentions: (Note | Packed<'Note'>)[] = [];
|
||||
const readSpecifiedNotes: (Note | Packed<'Note'>)[] = [];
|
||||
const readChannelNotes: (Note | Packed<'Note'>)[] = [];
|
||||
const readAntennaNotes: (Note | Packed<'Note'>)[] = [];
|
||||
|
||||
for (const note of notes) {
|
||||
if (note.mentions && note.mentions.includes(userId)) {
|
||||
@ -133,14 +129,6 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||
if (note.channelId && followingChannels.has(note.channelId)) {
|
||||
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)) {
|
||||
@ -181,39 +169,6 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 { 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 { Notification } from '@/models/entities/Notification.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { GlobalEventService } from './GlobalEventService.js';
|
||||
import { PushNotificationService } from './PushNotificationService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.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()
|
||||
export class NotificationService {
|
||||
constructor(
|
||||
@Inject(DI.notificationsRepository)
|
||||
private notificationsRepository: NotificationsRepository,
|
||||
export class NotificationService implements OnApplicationShutdown {
|
||||
#shutdownController = new AbortController();
|
||||
|
||||
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 idService: IdService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private pushNotificationService: PushNotificationService,
|
||||
private cacheService: CacheService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async readNotification(
|
||||
public async readAllNotification(
|
||||
userId: User['id'],
|
||||
notificationIds: Notification['id'][],
|
||||
) {
|
||||
if (notificationIds.length === 0) return;
|
||||
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
|
||||
|
||||
const latestNotificationIdsRes = await this.redisClient.xrevrange(
|
||||
`notificationTimeline:${userId}`,
|
||||
'+',
|
||||
'-',
|
||||
'COUNT', 1);
|
||||
const latestNotificationId = latestNotificationIdsRes[0]?.[0];
|
||||
|
||||
// Update documents
|
||||
const result = await this.notificationsRepository.update({
|
||||
notifieeId: userId,
|
||||
id: In(notificationIds),
|
||||
isRead: false,
|
||||
}, {
|
||||
isRead: true,
|
||||
});
|
||||
if (latestNotificationId == null) return;
|
||||
|
||||
if (result.affected === 0) return;
|
||||
this.redisClient.set(`latestReadNotification:${userId}`, latestNotificationId);
|
||||
|
||||
if (!await this.userEntityService.getHasUnreadNotification(userId)) return this.postReadAllNotifications(userId);
|
||||
else return this.postReadNotifications(userId, notificationIds);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async readNotificationByQuery(
|
||||
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);
|
||||
if (latestReadNotificationId == null || (latestReadNotificationId < latestNotificationId)) {
|
||||
return this.postReadAllNotifications(userId);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private postReadAllNotifications(userId: User['id']) {
|
||||
this.globalEventService.publishMainStream(userId, 'readAllNotifications');
|
||||
return this.pushNotificationService.pushNotification(userId, 'readAllNotifications', undefined);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private postReadNotifications(userId: User['id'], notificationIds: Notification['id'][]) {
|
||||
return this.pushNotificationService.pushNotification(userId, 'readNotifications', { notificationIds });
|
||||
public async createNotification(
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import push from 'web-push';
|
||||
import { DI } from '@/di-symbols.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 type { SwSubscriptionsRepository } from '@/models/index.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
@ -15,10 +15,6 @@ type PushNotificationsTypes = {
|
||||
antenna: { id: string, name: string };
|
||||
note: Packed<'Note'>;
|
||||
};
|
||||
'readNotifications': { notificationIds: string[] };
|
||||
'readAllNotifications': undefined;
|
||||
'readAntenna': { antennaId: string };
|
||||
'readAllAntennas': undefined;
|
||||
};
|
||||
|
||||
// Reduce length because push message servers have character limits
|
||||
@ -72,14 +68,6 @@ export class PushNotificationService {
|
||||
});
|
||||
|
||||
for (const subscription of subscriptions) {
|
||||
// Continue if sendReadMessage is false
|
||||
if ([
|
||||
'readNotifications',
|
||||
'readAllNotifications',
|
||||
'readAntenna',
|
||||
'readAllAntennas',
|
||||
].includes(type) && !subscription.sendReadMessage) continue;
|
||||
|
||||
const pushSubscription = {
|
||||
endpoint: subscription.endpoint,
|
||||
keys: {
|
||||
|
@ -26,7 +26,7 @@ export class QueueService {
|
||||
) {}
|
||||
|
||||
@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 (to == null) return null;
|
||||
|
||||
@ -36,6 +36,7 @@ export class QueueService {
|
||||
},
|
||||
content,
|
||||
to,
|
||||
isSharedInbox,
|
||||
};
|
||||
|
||||
return this.deliverQueue.add(data, {
|
||||
|
@ -9,7 +9,7 @@ import { IdService } from '@/core/IdService.js';
|
||||
import type { NoteReaction } from '@/models/entities/NoteReaction.js';
|
||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.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 { emojiRegex } from '@/misc/emoji-regex.js';
|
||||
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
|
||||
@ -21,6 +21,8 @@ import { bindThis } from '@/decorators.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
|
||||
const FALLBACK = '❤';
|
||||
|
||||
const legacies: Record<string, string> = {
|
||||
'like': '👍',
|
||||
'love': '❤', // ここに記述する場合は異体字セレクタを入れない
|
||||
@ -79,7 +81,7 @@ export class ReactionService {
|
||||
private globalEventService: GlobalEventService,
|
||||
private apRendererService: ApRendererService,
|
||||
private apDeliverManagerService: ApDeliverManagerService,
|
||||
private createNotificationService: CreateNotificationService,
|
||||
private notificationService: NotificationService,
|
||||
private perUserReactionsChart: PerUserReactionsChart,
|
||||
) {
|
||||
}
|
||||
@ -93,19 +95,19 @@ export class ReactionService {
|
||||
throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// check visibility
|
||||
if (!await this.noteEntityService.isVisibleForMe(note, user.id)) {
|
||||
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
|
||||
}
|
||||
|
||||
|
||||
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) {
|
||||
reaction = '❤️';
|
||||
} else {
|
||||
// TODO: cache
|
||||
reaction = await this.toDbReaction(reaction, user.host);
|
||||
}
|
||||
|
||||
|
||||
const record: NoteReaction = {
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
@ -113,7 +115,7 @@ export class ReactionService {
|
||||
userId: user.id,
|
||||
reaction,
|
||||
};
|
||||
|
||||
|
||||
// Create reaction
|
||||
try {
|
||||
await this.noteReactionsRepository.insert(record);
|
||||
@ -123,7 +125,7 @@ export class ReactionService {
|
||||
noteId: note.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
|
||||
if (exists.reaction !== reaction) {
|
||||
// 別のリアクションがすでにされていたら置き換える
|
||||
await this.delete(user, note);
|
||||
@ -136,7 +138,7 @@ export class ReactionService {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Increment reactions count
|
||||
const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;
|
||||
await this.notesRepository.createQueryBuilder().update()
|
||||
@ -146,12 +148,16 @@ export class ReactionService {
|
||||
})
|
||||
.where('id = :id', { id: note.id })
|
||||
.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 emoji = await this.emojisRepository.findOne({
|
||||
where: {
|
||||
name: decodedReaction.name,
|
||||
@ -159,7 +165,7 @@ export class ReactionService {
|
||||
},
|
||||
select: ['name', 'host', 'originalUrl', 'publicUrl'],
|
||||
});
|
||||
|
||||
|
||||
this.globalEventService.publishNoteStream(note.id, 'reacted', {
|
||||
reaction: decodedReaction.reaction,
|
||||
emoji: emoji != null ? {
|
||||
@ -169,16 +175,16 @@ export class ReactionService {
|
||||
} : null,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
|
||||
// リアクションされたユーザーがローカルユーザーなら通知を作成
|
||||
if (note.userHost === null) {
|
||||
this.createNotificationService.createNotification(note.userId, 'reaction', {
|
||||
this.notificationService.createNotification(note.userId, 'reaction', {
|
||||
notifierId: user.id,
|
||||
noteId: note.id,
|
||||
reaction: reaction,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
//#region 配信
|
||||
if (this.userEntityService.isLocalUser(user) && !note.localOnly) {
|
||||
const content = this.apRendererService.addContext(await this.apRendererService.renderLike(record, note));
|
||||
@ -187,7 +193,7 @@ export class ReactionService {
|
||||
const reactee = await this.usersRepository.findOneBy({ id: note.userId });
|
||||
dm.addDirectRecipe(reactee as RemoteUser);
|
||||
}
|
||||
|
||||
|
||||
if (['public', 'home', 'followers'].includes(note.visibility)) {
|
||||
dm.addFollowersRecipe();
|
||||
} else if (note.visibility === 'specified') {
|
||||
@ -196,7 +202,7 @@ export class ReactionService {
|
||||
dm.addDirectRecipe(u as RemoteUser);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
dm.execute();
|
||||
}
|
||||
//#endregion
|
||||
@ -209,18 +215,18 @@ export class ReactionService {
|
||||
noteId: note.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
|
||||
if (exist == null) {
|
||||
throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted');
|
||||
}
|
||||
|
||||
|
||||
// Delete reaction
|
||||
const result = await this.noteReactionsRepository.delete(exist.id);
|
||||
|
||||
|
||||
if (result.affected !== 1) {
|
||||
throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted');
|
||||
}
|
||||
|
||||
|
||||
// Decrement reactions count
|
||||
const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`;
|
||||
await this.notesRepository.createQueryBuilder().update()
|
||||
@ -229,14 +235,14 @@ export class ReactionService {
|
||||
})
|
||||
.where('id = :id', { id: note.id })
|
||||
.execute();
|
||||
|
||||
|
||||
if (!user.isBot) this.notesRepository.decrement({ id: note.id }, 'score', 1);
|
||||
|
||||
|
||||
this.globalEventService.publishNoteStream(note.id, 'unreacted', {
|
||||
reaction: this.decodeReaction(exist.reaction).reaction,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
|
||||
//#region 配信
|
||||
if (this.userEntityService.isLocalUser(user) && !note.localOnly) {
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(await this.apRendererService.renderLike(exist, note), user));
|
||||
@ -250,12 +256,6 @@ export class ReactionService {
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getFallbackReaction(): Promise<string> {
|
||||
const meta = await this.metaService.fetch();
|
||||
return meta.useStarForReactionFallback ? '⭐' : '👍';
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public convertLegacyReactions(reactions: Record<string, number>) {
|
||||
@ -290,7 +290,7 @@ export class ReactionService {
|
||||
|
||||
@bindThis
|
||||
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);
|
||||
|
||||
@ -300,7 +300,7 @@ export class ReactionService {
|
||||
// Unicode絵文字
|
||||
const match = emojiRegex.exec(reaction);
|
||||
if (match) {
|
||||
// 合字を含む1つの絵文字
|
||||
// 合字を含む1つの絵文字
|
||||
const unicode = match[0];
|
||||
|
||||
// 異体字セレクタ除去
|
||||
@ -318,7 +318,7 @@ export class ReactionService {
|
||||
if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
|
||||
}
|
||||
|
||||
return await this.getFallbackReaction();
|
||||
return FALLBACK;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -3,7 +3,7 @@ import { IsNull } from 'typeorm';
|
||||
import type { LocalUser, User } from '@/models/entities/User.js';
|
||||
import type { RelaysRepository, UsersRepository } from '@/models/index.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 { QueueService } from '@/core/QueueService.js';
|
||||
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
|
||||
@ -16,7 +16,7 @@ const ACTOR_USERNAME = 'relay.actor' as const;
|
||||
|
||||
@Injectable()
|
||||
export class RelayService {
|
||||
private relaysCache: Cache<Relay[]>;
|
||||
private relaysCache: MemoryCache<Relay[]>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
@ -30,7 +30,7 @@ export class RelayService {
|
||||
private createSystemUserService: CreateSystemUserService,
|
||||
private apRendererService: ApRendererService,
|
||||
) {
|
||||
this.relaysCache = new Cache<Relay[]>(1000 * 60 * 10);
|
||||
this.relaysCache = new MemoryCache<Relay[]>(1000 * 60 * 10);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@ -57,7 +57,7 @@ export class RelayService {
|
||||
const relayActor = await this.getRelayActor();
|
||||
const follow = await this.apRendererService.renderFollowRelay(relay, relayActor);
|
||||
const activity = this.apRendererService.addContext(follow);
|
||||
this.queueService.deliver(relayActor, activity, relay.inbox);
|
||||
this.queueService.deliver(relayActor, activity, relay.inbox, false);
|
||||
|
||||
return relay;
|
||||
}
|
||||
@ -76,7 +76,7 @@ export class RelayService {
|
||||
const follow = this.apRendererService.renderFollowRelay(relay, relayActor);
|
||||
const undo = this.apRendererService.renderUndo(follow, relayActor);
|
||||
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);
|
||||
}
|
||||
@ -109,7 +109,7 @@ export class RelayService {
|
||||
public async deliverToRelays(user: { id: User['id']; host: null; }, activity: any): Promise<void> {
|
||||
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',
|
||||
}));
|
||||
if (relays.length === 0) return;
|
||||
@ -120,7 +120,7 @@ export class RelayService {
|
||||
const signed = await this.apRendererService.attachLdSignature(copy, user);
|
||||
|
||||
for (const relay of relays) {
|
||||
this.queueService.deliver(user, signed, relay.inbox);
|
||||
this.queueService.deliver(user, signed, relay.inbox, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,12 +2,12 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import { In } from 'typeorm';
|
||||
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 { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.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 { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||
@ -21,6 +21,7 @@ export type RolePolicies = {
|
||||
canPublicNote: boolean;
|
||||
canInvite: boolean;
|
||||
canManageCustomEmojis: boolean;
|
||||
canSearchNotes: boolean;
|
||||
canHideAds: boolean;
|
||||
driveCapacityMb: number;
|
||||
pinLimit: number;
|
||||
@ -40,6 +41,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||
canPublicNote: true,
|
||||
canInvite: false,
|
||||
canManageCustomEmojis: false,
|
||||
canSearchNotes: false,
|
||||
canHideAds: false,
|
||||
driveCapacityMb: 100,
|
||||
pinLimit: 5,
|
||||
@ -55,8 +57,8 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||
|
||||
@Injectable()
|
||||
export class RoleService implements OnApplicationShutdown {
|
||||
private rolesCache: Cache<Role[]>;
|
||||
private roleAssignmentByUserIdCache: Cache<RoleAssignment[]>;
|
||||
private rolesCache: MemoryCache<Role[]>;
|
||||
private roleAssignmentByUserIdCache: MemoryKVCache<RoleAssignment[]>;
|
||||
|
||||
public static AlreadyAssignedError = class extends Error {};
|
||||
public static NotAssignedError = class extends Error {};
|
||||
@ -75,15 +77,15 @@ export class RoleService implements OnApplicationShutdown {
|
||||
private roleAssignmentsRepository: RoleAssignmentsRepository,
|
||||
|
||||
private metaService: MetaService,
|
||||
private userCacheService: UserCacheService,
|
||||
private cacheService: CacheService,
|
||||
private userEntityService: UserEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
//this.onMessage = this.onMessage.bind(this);
|
||||
|
||||
this.rolesCache = new Cache<Role[]>(Infinity);
|
||||
this.roleAssignmentByUserIdCache = new Cache<RoleAssignment[]>(Infinity);
|
||||
this.rolesCache = new MemoryCache<Role[]>(Infinity);
|
||||
this.roleAssignmentByUserIdCache = new MemoryKVCache<RoleAssignment[]>(Infinity);
|
||||
|
||||
this.redisSubscriber.on('message', this.onMessage);
|
||||
}
|
||||
@ -96,7 +98,7 @@ export class RoleService implements OnApplicationShutdown {
|
||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'roleCreated': {
|
||||
const cached = this.rolesCache.get(null);
|
||||
const cached = this.rolesCache.get();
|
||||
if (cached) {
|
||||
cached.push({
|
||||
...body,
|
||||
@ -108,7 +110,7 @@ export class RoleService implements OnApplicationShutdown {
|
||||
break;
|
||||
}
|
||||
case 'roleUpdated': {
|
||||
const cached = this.rolesCache.get(null);
|
||||
const cached = this.rolesCache.get();
|
||||
if (cached) {
|
||||
const i = cached.findIndex(x => x.id === body.id);
|
||||
if (i > -1) {
|
||||
@ -123,9 +125,9 @@ export class RoleService implements OnApplicationShutdown {
|
||||
break;
|
||||
}
|
||||
case 'roleDeleted': {
|
||||
const cached = this.rolesCache.get(null);
|
||||
const cached = this.rolesCache.get();
|
||||
if (cached) {
|
||||
this.rolesCache.set(null, cached.filter(x => x.id !== body.id));
|
||||
this.rolesCache.set(cached.filter(x => x.id !== body.id));
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -190,6 +192,12 @@ export class RoleService implements OnApplicationShutdown {
|
||||
case 'followingMoreThanOrEq': {
|
||||
return user.followingCount >= value.value;
|
||||
}
|
||||
case 'notesLessThanOrEq': {
|
||||
return user.notesCount <= value.value;
|
||||
}
|
||||
case 'notesMoreThanOrEq': {
|
||||
return user.notesCount >= value.value;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@ -206,9 +214,9 @@ export class RoleService implements OnApplicationShutdown {
|
||||
// 期限切れのロールを除外
|
||||
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
|
||||
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 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));
|
||||
return [...assignedRoles, ...matchedCondRoles];
|
||||
}
|
||||
@ -223,11 +231,11 @@ export class RoleService implements OnApplicationShutdown {
|
||||
// 期限切れのロールを除外
|
||||
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
|
||||
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 badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional'));
|
||||
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));
|
||||
return [...assignedBadgeRoles, ...matchedBadgeCondRoles];
|
||||
} else {
|
||||
@ -264,6 +272,7 @@ export class RoleService implements OnApplicationShutdown {
|
||||
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
|
||||
canInvite: calc('canInvite', 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)),
|
||||
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
|
||||
pinLimit: calc('pinLimit', vs => Math.max(...vs)),
|
||||
@ -292,7 +301,7 @@ export class RoleService implements OnApplicationShutdown {
|
||||
|
||||
@bindThis
|
||||
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 assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
|
||||
roleId: In(moderatorRoles.map(r => r.id)),
|
||||
@ -312,7 +321,7 @@ export class RoleService implements OnApplicationShutdown {
|
||||
|
||||
@bindThis
|
||||
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 assigns = administratorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
|
||||
roleId: In(administratorRoles.map(r => r.id)),
|
||||
|
@ -1,11 +1,16 @@
|
||||
import { URL } from 'node:url';
|
||||
import * as http from 'node:http';
|
||||
import * as https from 'node:https';
|
||||
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 type { Config } from '@/config.js';
|
||||
import type { Meta } from '@/models/entities/Meta.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { DeleteObjectCommandInput, PutObjectCommandInput } from '@aws-sdk/client-s3';
|
||||
|
||||
@Injectable()
|
||||
export class S3Service {
|
||||
@ -18,23 +23,47 @@ export class S3Service {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getS3(meta: Meta) {
|
||||
const u = meta.objectStorageEndpoint != null
|
||||
? `${meta.objectStorageUseSSL ? 'https://' : 'http://'}${meta.objectStorageEndpoint}`
|
||||
: `${meta.objectStorageUseSSL ? 'https://' : 'http://'}example.net`;
|
||||
|
||||
return new S3({
|
||||
endpoint: meta.objectStorageEndpoint ?? undefined,
|
||||
accessKeyId: meta.objectStorageAccessKey!,
|
||||
secretAccessKey: meta.objectStorageSecretKey!,
|
||||
region: meta.objectStorageRegion ?? undefined,
|
||||
sslEnabled: meta.objectStorageUseSSL,
|
||||
s3ForcePathStyle: !meta.objectStorageEndpoint // AWS with endPoint omitted
|
||||
? false
|
||||
: meta.objectStorageS3ForcePathStyle,
|
||||
httpOptions: {
|
||||
agent: this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy),
|
||||
},
|
||||
public getS3Client(meta: Meta): S3Client {
|
||||
const u = meta.objectStorageEndpoint
|
||||
? `${meta.objectStorageUseSSL ? 'https' : 'http'}://${meta.objectStorageEndpoint}`
|
||||
: `${meta.objectStorageUseSSL ? 'https' : 'http'}://example.net`; // dummy url to select http(s) agent
|
||||
|
||||
const agent = this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy);
|
||||
const handlerOption: NodeHttpHandlerOptions = {};
|
||||
if (meta.objectStorageUseSSL) {
|
||||
handlerOption.httpsAgent = agent as https.Agent;
|
||||
} else {
|
||||
handlerOption.httpAgent = agent as http.Agent;
|
||||
}
|
||||
|
||||
return new S3Client({
|
||||
endpoint: meta.objectStorageEndpoint ? u : undefined,
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
@ -90,7 +90,7 @@ export class SignupService {
|
||||
cipher: undefined,
|
||||
passphrase: undefined,
|
||||
},
|
||||
} as any, (err, publicKey, privateKey) =>
|
||||
}, (err, publicKey, privateKey) =>
|
||||
err ? rej(err) : res([publicKey, privateKey]),
|
||||
));
|
||||
|
||||
|
@ -15,7 +15,7 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { WebhookService } from '@/core/WebhookService.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';
|
||||
|
||||
@Injectable()
|
||||
@ -23,7 +23,7 @@ export class UserBlockingService implements OnApplicationShutdown {
|
||||
private logger: Logger;
|
||||
|
||||
// キーがユーザーIDで、値がそのユーザーがブロックしているユーザーのIDのリストなキャッシュ
|
||||
private blockingsByUserIdCache: Cache<User['id'][]>;
|
||||
private blockingsByUserIdCache: MemoryKVCache<User['id'][]>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redisSubscriber)
|
||||
@ -58,7 +58,7 @@ export class UserBlockingService implements OnApplicationShutdown {
|
||||
) {
|
||||
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);
|
||||
}
|
||||
@ -118,7 +118,7 @@ export class UserBlockingService implements OnApplicationShutdown {
|
||||
|
||||
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
|
||||
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送信
|
||||
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||
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送信
|
||||
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));
|
||||
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送信
|
||||
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||
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送信
|
||||
if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) {
|
||||
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
|
||||
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,17 +6,18 @@ import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { IdService } from '@/core/IdService.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 { FederatedInstanceService } from '@/core/FederatedInstanceService.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 type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import Logger from '../logger.js';
|
||||
|
||||
const logger = new Logger('following/create');
|
||||
@ -57,7 +58,8 @@ export class UserFollowingService {
|
||||
private idService: IdService,
|
||||
private queueService: QueueService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private createNotificationService: CreateNotificationService,
|
||||
private metaService: MetaService,
|
||||
private notificationService: NotificationService,
|
||||
private federatedInstanceService: FederatedInstanceService,
|
||||
private webhookService: WebhookService,
|
||||
private apRendererService: ApRendererService,
|
||||
@ -82,7 +84,7 @@ export class UserFollowingService {
|
||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocked) {
|
||||
// リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。
|
||||
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;
|
||||
} 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)) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -145,15 +147,15 @@ export class UserFollowingService {
|
||||
},
|
||||
): Promise<void> {
|
||||
if (follower.id === followee.id) return;
|
||||
|
||||
|
||||
let alreadyFollowed = false as boolean;
|
||||
|
||||
|
||||
await this.followingsRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
|
||||
|
||||
// 非正規化
|
||||
followerHost: follower.host,
|
||||
followerInbox: this.userEntityService.isRemoteUser(follower) ? follower.inbox : null,
|
||||
@ -169,51 +171,55 @@ export class UserFollowingService {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const req = await this.followRequestsRepository.findOneBy({
|
||||
followeeId: followee.id,
|
||||
followerId: follower.id,
|
||||
});
|
||||
|
||||
|
||||
if (req) {
|
||||
await this.followRequestsRepository.delete({
|
||||
followeeId: followee.id,
|
||||
followerId: follower.id,
|
||||
});
|
||||
|
||||
|
||||
// 通知を作成
|
||||
this.createNotificationService.createNotification(follower.id, 'followRequestAccepted', {
|
||||
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
|
||||
notifierId: followee.id,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (alreadyFollowed) return;
|
||||
|
||||
this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id });
|
||||
|
||||
|
||||
//#region Increment counts
|
||||
await Promise.all([
|
||||
this.usersRepository.increment({ id: follower.id }, 'followingCount', 1),
|
||||
this.usersRepository.increment({ id: followee.id }, 'followersCount', 1),
|
||||
]);
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region Update instance stats
|
||||
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.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)) {
|
||||
this.federatedInstanceService.fetch(followee.host).then(i => {
|
||||
this.federatedInstanceService.fetch(followee.host).then(async i => {
|
||||
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
|
||||
|
||||
|
||||
this.perUserFollowingChart.update(follower, followee, true);
|
||||
|
||||
|
||||
// Publish follow event
|
||||
if (this.userEntityService.isLocalUser(follower)) {
|
||||
this.userEntityService.pack(followee.id, follower, {
|
||||
@ -221,7 +227,7 @@ export class UserFollowingService {
|
||||
}).then(async packed => {
|
||||
this.globalEventService.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
|
||||
this.globalEventService.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
|
||||
|
||||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
|
||||
for (const webhook of webhooks) {
|
||||
this.queueService.webhookDeliver(webhook, 'follow', {
|
||||
@ -230,12 +236,12 @@ export class UserFollowingService {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Publish followed event
|
||||
if (this.userEntityService.isLocalUser(followee)) {
|
||||
this.userEntityService.pack(follower.id, followee).then(async packed => {
|
||||
this.globalEventService.publishMainStream(followee.id, 'followed', packed);
|
||||
|
||||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed'));
|
||||
for (const webhook of webhooks) {
|
||||
this.queueService.webhookDeliver(webhook, 'followed', {
|
||||
@ -243,9 +249,9 @@ export class UserFollowingService {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 通知を作成
|
||||
this.createNotificationService.createNotification(followee.id, 'follow', {
|
||||
this.notificationService.createNotification(followee.id, 'follow', {
|
||||
notifierId: follower.id,
|
||||
});
|
||||
}
|
||||
@ -265,16 +271,16 @@ export class UserFollowingService {
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
});
|
||||
|
||||
|
||||
if (following == null) {
|
||||
logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
await this.followingsRepository.delete(following.id);
|
||||
|
||||
|
||||
this.decrementFollowing(follower, followee);
|
||||
|
||||
|
||||
// Publish unfollow event
|
||||
if (!silent && this.userEntityService.isLocalUser(follower)) {
|
||||
this.userEntityService.pack(followee.id, follower, {
|
||||
@ -282,7 +288,7 @@ export class UserFollowingService {
|
||||
}).then(async packed => {
|
||||
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
|
||||
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
|
||||
|
||||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||
for (const webhook of webhooks) {
|
||||
this.queueService.webhookDeliver(webhook, 'unfollow', {
|
||||
@ -291,47 +297,51 @@ export class UserFollowingService {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||
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)) {
|
||||
// local user has null host
|
||||
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
|
||||
private async decrementFollowing(
|
||||
follower: {id: User['id']; host: User['host']; },
|
||||
follower: { id: User['id']; host: User['host']; },
|
||||
followee: { id: User['id']; host: User['host']; },
|
||||
): Promise<void> {
|
||||
this.globalEventService.publishInternalEvent('unfollow', { followerId: follower.id, followeeId: followee.id });
|
||||
|
||||
|
||||
//#region Decrement following / followers counts
|
||||
await Promise.all([
|
||||
this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1),
|
||||
this.usersRepository.decrement({ id: followee.id }, 'followersCount', 1),
|
||||
]);
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region Update instance stats
|
||||
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.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)) {
|
||||
this.federatedInstanceService.fetch(followee.host).then(i => {
|
||||
this.federatedInstanceService.fetch(followee.host).then(async i => {
|
||||
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
|
||||
|
||||
|
||||
this.perUserFollowingChart.update(follower, followee, false);
|
||||
}
|
||||
|
||||
@ -346,23 +356,23 @@ export class UserFollowingService {
|
||||
requestId?: string,
|
||||
): Promise<void> {
|
||||
if (follower.id === followee.id) return;
|
||||
|
||||
|
||||
// check blocking
|
||||
const [blocking, blocked] = await Promise.all([
|
||||
this.userBlockingService.checkBlocked(follower.id, followee.id),
|
||||
this.userBlockingService.checkBlocked(followee.id, follower.id),
|
||||
]);
|
||||
|
||||
|
||||
if (blocking) throw new Error('blocking');
|
||||
if (blocked) throw new Error('blocked');
|
||||
|
||||
|
||||
const followRequest = await this.followRequestsRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
requestId,
|
||||
|
||||
|
||||
// 非正規化
|
||||
followerHost: follower.host,
|
||||
followerInbox: this.userEntityService.isRemoteUser(follower) ? follower.inbox : undefined,
|
||||
@ -371,25 +381,25 @@ export class UserFollowingService {
|
||||
followeeInbox: this.userEntityService.isRemoteUser(followee) ? followee.inbox : undefined,
|
||||
followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : undefined,
|
||||
}).then(x => this.followRequestsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
|
||||
// Publish receiveRequest event
|
||||
if (this.userEntityService.isLocalUser(followee)) {
|
||||
this.userEntityService.pack(follower.id, followee).then(packed => this.globalEventService.publishMainStream(followee.id, 'receiveFollowRequest', packed));
|
||||
|
||||
|
||||
this.userEntityService.pack(followee.id, followee, {
|
||||
detail: true,
|
||||
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
|
||||
|
||||
|
||||
// 通知を作成
|
||||
this.createNotificationService.createNotification(followee.id, 'receiveFollowRequest', {
|
||||
this.notificationService.createNotification(followee.id, 'receiveFollowRequest', {
|
||||
notifierId: follower.id,
|
||||
followRequestId: followRequest.id,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -404,26 +414,26 @@ export class UserFollowingService {
|
||||
): Promise<void> {
|
||||
if (this.userEntityService.isRemoteUser(followee)) {
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
|
||||
|
||||
|
||||
if (this.userEntityService.isLocalUser(follower)) { // 本来このチェックは不要だけどTSに怒られるので
|
||||
this.queueService.deliver(follower, content, followee.inbox);
|
||||
this.queueService.deliver(follower, content, followee.inbox, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const request = await this.followRequestsRepository.findOneBy({
|
||||
followeeId: followee.id,
|
||||
followerId: follower.id,
|
||||
});
|
||||
|
||||
|
||||
if (request == null) {
|
||||
throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found');
|
||||
}
|
||||
|
||||
|
||||
await this.followRequestsRepository.delete({
|
||||
followeeId: followee.id,
|
||||
followerId: follower.id,
|
||||
});
|
||||
|
||||
|
||||
this.userEntityService.pack(followee.id, followee, {
|
||||
detail: true,
|
||||
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
|
||||
@ -440,18 +450,18 @@ export class UserFollowingService {
|
||||
followeeId: followee.id,
|
||||
followerId: follower.id,
|
||||
});
|
||||
|
||||
|
||||
if (request == null) {
|
||||
throw new IdentifiableError('8884c2dd-5795-4ac9-b27e-6a01d38190f9', 'No follow request.');
|
||||
}
|
||||
|
||||
|
||||
await this.insertFollowingDoc(followee, follower);
|
||||
|
||||
|
||||
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));
|
||||
this.queueService.deliver(followee, content, follower.inbox);
|
||||
this.queueService.deliver(followee, content, follower.inbox, false);
|
||||
}
|
||||
|
||||
|
||||
this.userEntityService.pack(followee.id, followee, {
|
||||
detail: true,
|
||||
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
|
||||
@ -466,13 +476,13 @@ export class UserFollowingService {
|
||||
const requests = await this.followRequestsRepository.findBy({
|
||||
followeeId: user.id,
|
||||
});
|
||||
|
||||
|
||||
for (const request of requests) {
|
||||
const follower = await this.usersRepository.findOneByOrFail({ id: request.followerId });
|
||||
this.acceptFollowRequest(user, follower);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* API following/request/reject
|
||||
*/
|
||||
@ -557,7 +567,7 @@ export class UserFollowingService {
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,20 +1,20 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { User } from '@/models/entities/User.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 { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserKeypairStoreService {
|
||||
private cache: Cache<UserKeypair>;
|
||||
private cache: MemoryKVCache<UserKeypair>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.userKeypairsRepository)
|
||||
private userKeypairsRepository: UserKeypairsRepository,
|
||||
) {
|
||||
this.cache = new Cache<UserKeypair>(Infinity);
|
||||
this.cache = new MemoryKVCache<UserKeypair>(Infinity);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -54,7 +54,7 @@ export class UserSuspendService {
|
||||
}
|
||||
|
||||
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) {
|
||||
this.queueService.deliver(user as any, content, inbox);
|
||||
this.queueService.deliver(user as any, content, inbox, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
cleanup();
|
||||
}
|
||||
|
@ -3,9 +3,9 @@ import escapeRegexp from 'escape-regexp';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.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 { UserCacheService } from '@/core/UserCacheService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import type { Note } from '@/models/entities/Note.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RemoteUser, User } from '@/models/entities/User.js';
|
||||
@ -31,8 +31,8 @@ export type UriParseResult = {
|
||||
|
||||
@Injectable()
|
||||
export class ApDbResolverService {
|
||||
private publicKeyCache: Cache<UserPublickey | null>;
|
||||
private publicKeyByUserIdCache: Cache<UserPublickey | null>;
|
||||
private publicKeyCache: MemoryKVCache<UserPublickey | null>;
|
||||
private publicKeyByUserIdCache: MemoryKVCache<UserPublickey | null>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
@ -47,11 +47,11 @@ export class ApDbResolverService {
|
||||
@Inject(DI.userPublickeysRepository)
|
||||
private userPublickeysRepository: UserPublickeysRepository,
|
||||
|
||||
private userCacheService: UserCacheService,
|
||||
private cacheService: CacheService,
|
||||
private apPersonService: ApPersonService,
|
||||
) {
|
||||
this.publicKeyCache = new Cache<UserPublickey | null>(Infinity);
|
||||
this.publicKeyByUserIdCache = new Cache<UserPublickey | null>(Infinity);
|
||||
this.publicKeyCache = new MemoryKVCache<UserPublickey | null>(Infinity);
|
||||
this.publicKeyByUserIdCache = new MemoryKVCache<UserPublickey | null>(Infinity);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@ -107,11 +107,11 @@ export class ApDbResolverService {
|
||||
if (parsed.local) {
|
||||
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,
|
||||
}).then(x => x ?? undefined)) ?? null;
|
||||
} 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,
|
||||
}));
|
||||
}
|
||||
@ -138,7 +138,7 @@ export class ApDbResolverService {
|
||||
if (key == null) return null;
|
||||
|
||||
return {
|
||||
user: await this.userCacheService.findById(key.userId) as RemoteUser,
|
||||
user: await this.cacheService.findUserById(key.userId) as RemoteUser,
|
||||
key,
|
||||
};
|
||||
}
|
||||
|
@ -157,7 +157,8 @@ class DeliverManager {
|
||||
public async execute() {
|
||||
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
|
||||
@ -185,7 +186,7 @@ class DeliverManager {
|
||||
|
||||
for (const following of followers) {
|
||||
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
|
||||
&& recipe.to.inbox != null,
|
||||
)
|
||||
.forEach(recipe => inboxes.add(recipe.to.inbox!));
|
||||
.forEach(recipe => inboxes.set(recipe.to.inbox!, false));
|
||||
|
||||
// deliver
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -140,7 +140,7 @@ export class ApInboxService {
|
||||
} else if (isFlag(activity)) {
|
||||
await this.flag(actor, activity);
|
||||
} else {
|
||||
this.logger.warn(`unrecognized activity type: ${(activity as any).type}`);
|
||||
this.logger.warn(`unrecognized activity type: ${activity.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -91,6 +91,9 @@ export class ApRendererService {
|
||||
} else if (note.visibility === 'home') {
|
||||
to = [`${attributedTo}/followers`];
|
||||
cc = ['https://www.w3.org/ns/activitystreams#Public'];
|
||||
} else if (note.visibility === 'followers') {
|
||||
to = [`${attributedTo}/followers`];
|
||||
cc = [];
|
||||
} else {
|
||||
throw new Error('renderAnnounce: cannot render non-public note');
|
||||
}
|
||||
@ -116,7 +119,7 @@ export class ApRendererService {
|
||||
if (block.blockee?.uri == null) {
|
||||
throw new Error('renderBlock: missing blockee uri');
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
type: 'Block',
|
||||
id: `${this.config.url}/blocks/${block.id}`,
|
||||
@ -134,10 +137,10 @@ export class ApRendererService {
|
||||
published: note.createdAt.toISOString(),
|
||||
object,
|
||||
} as ICreate;
|
||||
|
||||
|
||||
if (object.to) activity.to = object.to;
|
||||
if (object.cc) activity.cc = object.cc;
|
||||
|
||||
|
||||
return activity;
|
||||
}
|
||||
|
||||
@ -155,7 +158,7 @@ export class ApRendererService {
|
||||
public renderDocument(file: DriveFile): IApDocument {
|
||||
return {
|
||||
type: 'Document',
|
||||
mediaType: file.type,
|
||||
mediaType: file.webpublicType ?? file.type,
|
||||
url: this.driveFileEntityService.getPublicUrl(file),
|
||||
name: file.comment,
|
||||
};
|
||||
@ -297,16 +300,16 @@ export class ApRendererService {
|
||||
const items = await this.driveFilesRepository.findBy({ id: In(ids) });
|
||||
return ids.map(id => items.find(item => item.id === id)).filter(item => item != null) as DriveFile[];
|
||||
};
|
||||
|
||||
|
||||
let inReplyTo;
|
||||
let inReplyToNote: Note | null;
|
||||
|
||||
|
||||
if (note.replyId) {
|
||||
inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId });
|
||||
|
||||
|
||||
if (inReplyToNote != null) {
|
||||
const inReplyToUser = await this.usersRepository.findOneBy({ id: inReplyToNote.userId });
|
||||
|
||||
|
||||
if (inReplyToUser != null) {
|
||||
if (inReplyToNote.uri) {
|
||||
inReplyTo = inReplyToNote.uri;
|
||||
@ -322,24 +325,24 @@ export class ApRendererService {
|
||||
} else {
|
||||
inReplyTo = null;
|
||||
}
|
||||
|
||||
|
||||
let quote;
|
||||
|
||||
|
||||
if (note.renoteId) {
|
||||
const renote = await this.notesRepository.findOneBy({ id: note.renoteId });
|
||||
|
||||
|
||||
if (renote) {
|
||||
quote = renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const attributedTo = `${this.config.url}/users/${note.userId}`;
|
||||
|
||||
|
||||
const mentions = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri);
|
||||
|
||||
|
||||
let to: string[] = [];
|
||||
let cc: string[] = [];
|
||||
|
||||
|
||||
if (note.visibility === 'public') {
|
||||
to = ['https://www.w3.org/ns/activitystreams#Public'];
|
||||
cc = [`${attributedTo}/followers`].concat(mentions);
|
||||
@ -352,44 +355,44 @@ export class ApRendererService {
|
||||
} else {
|
||||
to = mentions;
|
||||
}
|
||||
|
||||
|
||||
const mentionedUsers = note.mentions.length > 0 ? await this.usersRepository.findBy({
|
||||
id: In(note.mentions),
|
||||
}) : [];
|
||||
|
||||
|
||||
const hashtagTags = (note.tags ?? []).map(tag => this.renderHashtag(tag));
|
||||
const mentionTags = mentionedUsers.map(u => this.renderMention(u));
|
||||
|
||||
|
||||
const files = await getPromisedFiles(note.fileIds);
|
||||
|
||||
|
||||
const text = note.text ?? '';
|
||||
let poll: Poll | null = null;
|
||||
|
||||
|
||||
if (note.hasPoll) {
|
||||
poll = await this.pollsRepository.findOneBy({ noteId: note.id });
|
||||
}
|
||||
|
||||
|
||||
let apText = text;
|
||||
|
||||
|
||||
if (quote) {
|
||||
apText += `\n\nRE: ${quote}`;
|
||||
}
|
||||
|
||||
|
||||
const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
|
||||
|
||||
|
||||
const content = this.apMfmService.getNoteHtml(Object.assign({}, note, {
|
||||
text: apText,
|
||||
}));
|
||||
|
||||
|
||||
const emojis = await this.getEmojis(note.emojis);
|
||||
const apemojis = emojis.map(emoji => this.renderEmoji(emoji));
|
||||
|
||||
|
||||
const tag = [
|
||||
...hashtagTags,
|
||||
...mentionTags,
|
||||
...apemojis,
|
||||
];
|
||||
|
||||
|
||||
const asPoll = poll ? {
|
||||
type: 'Question',
|
||||
content: this.apMfmService.getNoteHtml(Object.assign({}, note, {
|
||||
@ -601,7 +604,7 @@ export class ApRendererService {
|
||||
if (typeof x === 'object' && x.id == null) {
|
||||
x.id = `${this.config.url}/${uuid()}`;
|
||||
}
|
||||
|
||||
|
||||
return Object.assign({
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
@ -634,18 +637,18 @@ export class ApRendererService {
|
||||
],
|
||||
}, x as T & { id: string; });
|
||||
}
|
||||
|
||||
|
||||
@bindThis
|
||||
public async attachLdSignature(activity: any, user: { id: User['id']; host: null; }): Promise<IActivity> {
|
||||
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
|
||||
|
||||
|
||||
const ldSignature = this.ldSignatureService.use();
|
||||
ldSignature.debug = false;
|
||||
activity = await ldSignature.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`);
|
||||
|
||||
|
||||
return activity;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Render OrderedCollectionPage
|
||||
* @param id URL of self
|
||||
@ -686,11 +689,11 @@ export class ApRendererService {
|
||||
type: 'OrderedCollection',
|
||||
totalItems,
|
||||
};
|
||||
|
||||
|
||||
if (first) page.first = first;
|
||||
if (last) page.last = last;
|
||||
if (orderedItems) page.orderedItems = orderedItems;
|
||||
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
|
@ -124,7 +124,7 @@ export class ApNoteService {
|
||||
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)}`);
|
||||
|
||||
@ -180,7 +180,7 @@ export class ApNoteService {
|
||||
const reply: Note | null = note.inReplyTo
|
||||
? await this.resolveNote(note.inReplyTo, resolver).then(x => {
|
||||
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');
|
||||
} else {
|
||||
return x;
|
||||
|
@ -8,7 +8,7 @@ import type { Config } from '@/config.js';
|
||||
import type { RemoteUser } from '@/models/entities/User.js';
|
||||
import { User } from '@/models/entities/User.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 { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.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 { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
|
||||
import { extractApHashtags } from './tag.js';
|
||||
import type { OnModuleInit } from '@nestjs/common';
|
||||
@ -50,9 +51,10 @@ export class ApPersonService implements OnModuleInit {
|
||||
private userEntityService: UserEntityService;
|
||||
private idService: IdService;
|
||||
private globalEventService: GlobalEventService;
|
||||
private metaService: MetaService;
|
||||
private federatedInstanceService: FederatedInstanceService;
|
||||
private fetchInstanceMetadataService: FetchInstanceMetadataService;
|
||||
private userCacheService: UserCacheService;
|
||||
private cacheService: CacheService;
|
||||
private apResolverService: ApResolverService;
|
||||
private apNoteService: ApNoteService;
|
||||
private apImageService: ApImageService;
|
||||
@ -92,9 +94,10 @@ export class ApPersonService implements OnModuleInit {
|
||||
//private userEntityService: UserEntityService,
|
||||
//private idService: IdService,
|
||||
//private globalEventService: GlobalEventService,
|
||||
//private metaService: MetaService,
|
||||
//private federatedInstanceService: FederatedInstanceService,
|
||||
//private fetchInstanceMetadataService: FetchInstanceMetadataService,
|
||||
//private userCacheService: UserCacheService,
|
||||
//private cacheService: CacheService,
|
||||
//private apResolverService: ApResolverService,
|
||||
//private apNoteService: ApNoteService,
|
||||
//private apImageService: ApImageService,
|
||||
@ -112,9 +115,10 @@ export class ApPersonService implements OnModuleInit {
|
||||
this.userEntityService = this.moduleRef.get('UserEntityService');
|
||||
this.idService = this.moduleRef.get('IdService');
|
||||
this.globalEventService = this.moduleRef.get('GlobalEventService');
|
||||
this.metaService = this.moduleRef.get('MetaService');
|
||||
this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
|
||||
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.apNoteService = this.moduleRef.get('ApNoteService');
|
||||
this.apImageService = this.moduleRef.get('ApImageService');
|
||||
@ -164,6 +168,9 @@ export class ApPersonService implements OnModuleInit {
|
||||
throw new Error('invalid Actor: wrong name');
|
||||
}
|
||||
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 (!(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> {
|
||||
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;
|
||||
|
||||
// URIがこのサーバーを指しているならデータベースからフェッチ
|
||||
if (uri.startsWith(this.config.url + '/')) {
|
||||
const id = uri.split('/').pop();
|
||||
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;
|
||||
}
|
||||
|
||||
@ -215,7 +222,7 @@ export class ApPersonService implements OnModuleInit {
|
||||
const exist = await this.usersRepository.findOneBy({ uri });
|
||||
|
||||
if (exist) {
|
||||
this.userCacheService.uriPersonCache.set(uri, exist);
|
||||
this.cacheService.uriPersonCache.set(uri, exist);
|
||||
return exist;
|
||||
}
|
||||
//#endregion
|
||||
@ -324,10 +331,12 @@ export class ApPersonService implements OnModuleInit {
|
||||
}
|
||||
|
||||
// Register host
|
||||
this.federatedInstanceService.fetch(host).then(i => {
|
||||
this.federatedInstanceService.fetch(host).then(async i => {
|
||||
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
|
||||
this.instanceChart.newUser(i.host);
|
||||
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
this.instanceChart.newUser(i.host);
|
||||
}
|
||||
});
|
||||
|
||||
this.usersChart.update(user!, true);
|
||||
|
@ -195,7 +195,8 @@ export const isPropertyValue = (object: IObject): object is IApPropertyValue =>
|
||||
object &&
|
||||
getApType(object) === 'PropertyValue' &&
|
||||
typeof object.name === 'string' &&
|
||||
typeof (object as any).value === 'string';
|
||||
'value' in object &&
|
||||
typeof object.value === 'string';
|
||||
|
||||
export interface IApMention extends IObject {
|
||||
type: 'Mention';
|
||||
|
@ -3,15 +3,15 @@ import Chart from '../../core.js';
|
||||
export const name = 'activeUsers';
|
||||
|
||||
export const schema = {
|
||||
'readWrite': { intersection: ['read', 'write'], range: 'small' },
|
||||
'read': { uniqueIncrement: true, range: 'small' },
|
||||
'write': { uniqueIncrement: true, range: 'small' },
|
||||
'registeredWithinWeek': { uniqueIncrement: true, range: 'small' },
|
||||
'registeredWithinMonth': { uniqueIncrement: true, range: 'small' },
|
||||
'registeredWithinYear': { uniqueIncrement: true, range: 'small' },
|
||||
'registeredOutsideWeek': { uniqueIncrement: true, range: 'small' },
|
||||
'registeredOutsideMonth': { uniqueIncrement: true, range: 'small' },
|
||||
'registeredOutsideYear': { uniqueIncrement: true, range: 'small' },
|
||||
'readWrite': { intersection: ['read', 'write'] },
|
||||
'read': { uniqueIncrement: true },
|
||||
'write': { uniqueIncrement: true },
|
||||
'registeredWithinWeek': { uniqueIncrement: true },
|
||||
'registeredWithinMonth': { uniqueIncrement: true },
|
||||
'registeredWithinYear': { uniqueIncrement: true },
|
||||
'registeredOutsideWeek': { uniqueIncrement: true },
|
||||
'registeredOutsideMonth': { uniqueIncrement: true },
|
||||
'registeredOutsideYear': { uniqueIncrement: true },
|
||||
} as const;
|
||||
|
||||
export const entity = Chart.schemaToEntity(name, schema);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user