Compare commits

...

140 Commits

Author SHA1 Message Date
syuilo
92f872f3e4 Merge branch 'develop' into chat 2025-03-23 21:24:35 +09:00
syuilo
0471e457fe fix(frontend): fix broken styles 2025-03-23 21:23:52 +09:00
syuilo
a77c10bbdd wip 2025-03-23 21:19:54 +09:00
syuilo
9b9ff81201 wip 2025-03-23 18:34:25 +09:00
syuilo
b98d3cbc29 wip 2025-03-23 16:51:35 +09:00
syuilo
9e23531adc wip 2025-03-23 14:44:49 +09:00
syuilo
ba026cfcd5 wip 2025-03-23 12:09:12 +09:00
syuilo
b043455152 wip 2025-03-23 11:41:31 +09:00
syuilo
ff4568e1d6 wip 2025-03-23 11:20:01 +09:00
syuilo
92f7532eb3 wip 2025-03-23 10:58:46 +09:00
syuilo
f6737149e0 Update misskey-js.api.md 2025-03-23 10:03:51 +09:00
syuilo
67dfd08a02 clean up 2025-03-23 10:01:37 +09:00
syuilo
a3c204e266 Update home.vue 2025-03-23 09:58:47 +09:00
syuilo
f3bb4d6603 wip 2025-03-23 09:53:02 +09:00
syuilo
50435a3f68 Update CHANGELOG.md 2025-03-23 09:27:59 +09:00
syuilo
a94a5fe14d wip 2025-03-23 08:31:32 +09:00
syuilo
53386fb5fc wip 2025-03-23 08:08:27 +09:00
syuilo
798a50da0e wip 2025-03-22 20:53:06 +09:00
syuilo
34760459b2 wip 2025-03-22 19:49:59 +09:00
syuilo
260d35e2f0 Update CHANGELOG.md 2025-03-22 18:37:41 +09:00
syuilo
3ff9d9f4fd Update CHANGELOG.md 2025-03-22 18:34:56 +09:00
syuilo
27991a3bc8 Update CHANGELOG.md 2025-03-22 18:28:51 +09:00
syuilo
7e826ce5d6 Merge branch 'develop' into chat 2025-03-22 18:25:57 +09:00
syuilo
b5f86e5210 refactor(frontend): refactor page styles 2025-03-22 18:25:45 +09:00
syuilo
55dc1d5305 wip 2025-03-22 18:23:52 +09:00
syuilo
acac58e59c wip 2025-03-22 16:23:16 +09:00
syuilo
16cde5568d Update CHANGELOG.md 2025-03-22 15:12:24 +09:00
syuilo
bf07796b6b Update CHANGELOG.md 2025-03-22 15:08:19 +09:00
syuilo
381126bdc7 wip 2025-03-22 10:56:49 +09:00
syuilo
3e200c0f66 wip 2025-03-22 10:30:18 +09:00
syuilo
37ee6ec9a8 wip 2025-03-22 09:20:50 +09:00
syuilo
202c1b2910 Merge branch 'develop' into chat 2025-03-22 08:16:29 +09:00
syuilo
08b131ec33 refactor(backend): better prop name 2025-03-22 08:16:15 +09:00
syuilo
1312fe34c1 Update CHANGELOG.md 2025-03-22 08:12:32 +09:00
syuilo
f6c5639841 Merge branch 'develop' into chat 2025-03-22 08:05:29 +09:00
github-actions[bot]
97563910fa Bump version to 2025.3.2-beta.9 2025-03-21 12:05:52 +00:00
syuilo
96a7c4a568 fix(frontend): チャンネルのフッターが表示されない問題を修正 2025-03-21 21:04:38 +09:00
syuilo
fee6f9fcc2 Update def.ts 2025-03-21 21:04:17 +09:00
renovate[bot]
50724b6ab8 fix(deps): update [frontend] update dependencies (#15625)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-21 19:41:26 +09:00
renovate[bot]
e61263cff0 fix(deps): update [backend] update dependencies (#15596)
* fix(deps): update [backend] update dependencies

* fix(backend/types): ensure to use nodejs-provided stream api

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com>
2025-03-21 19:38:11 +09:00
syuilo
2eef19d95d wip 2025-03-21 18:46:25 +09:00
syuilo
3c6f3992b0 Merge branch 'develop' into chat 2025-03-21 09:13:46 +09:00
github-actions[bot]
d073fe6b02 Bump version to 2025.3.2-beta.8 2025-03-20 12:18:50 +00:00
syuilo
ce858a676b refactor(frontend): use PageWithHeader instead of MkStickyContainer+MkPageHeader combination 2025-03-20 21:04:37 +09:00
syuilo
733a391d86 follow up of 1fd87bd2e4
コミットし忘れ
2025-03-20 20:29:46 +09:00
syuilo
0e25a0fb81 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2025-03-20 20:28:29 +09:00
syuilo
1fd87bd2e4 fix(frontend): prevent transition glitch of MkHorizontalSwipe 2025-03-20 20:28:16 +09:00
github-actions[bot]
ebc54b1f82 Bump version to 2025.3.2-beta.7 2025-03-20 10:07:37 +00:00
syuilo
6015254e59 lint fixes 2025-03-20 19:00:09 +09:00
syuilo
c02f0b3b33 Update eslint.config.js 2025-03-20 18:59:10 +09:00
syuilo
abddd40c09 enhance(frontend): 通常のRouterViewにTransitionを追加 2025-03-20 18:55:32 +09:00
syuilo
ce81855d8e Merge branch 'develop' into chat 2025-03-20 16:40:21 +09:00
かっこかり
a865a949b5 fix(frontend): MkRoleSelectDialogでのpopupの使い方が誤っているのを修正 (#15683) 2025-03-20 16:36:37 +09:00
syuilo
0007723405 fix lint 2025-03-20 16:34:50 +09:00
syuilo
71188b3463 fix lint 2025-03-20 16:10:38 +09:00
syuilo
7f534a41a6 fix lint 2025-03-20 16:07:52 +09:00
syuilo
f25963e2c2 Update eslint.config.js 2025-03-20 16:06:32 +09:00
syuilo
dfab6b1b8d lint(frontend): force window prefix 2025-03-20 15:44:06 +09:00
syuilo
fac59d75e2 lint(frontend): relax id-denylist rule 2025-03-20 15:43:35 +09:00
syuilo
fd3e28812e clean up console 2025-03-20 15:15:46 +09:00
syuilo
6a90b7e04b add todo 2025-03-20 15:09:50 +09:00
syuilo
8d8414687a enhance(frontend): improve preference manager stability 2025-03-20 14:57:14 +09:00
syuilo
0c682dd027 🎨 2025-03-20 13:34:57 +09:00
syuilo
3399c786a8 refactor(frontend): refactor components 2025-03-20 13:33:01 +09:00
syuilo
64cf101fe7 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2025-03-20 13:16:23 +09:00
syuilo
8b6d90b7a4 🎨 2025-03-20 13:16:08 +09:00
github-actions[bot]
070749bdc8 Bump version to 2025.3.2-beta.6 2025-03-20 04:03:25 +00:00
syuilo
161706c5e2 New Crowdin updates (#15680)
* New translations ja-jp.yml (Czech)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Russian)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Japanese, Kansai)

* New translations ja-jp.yml (Romanian)

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Arabic)

* New translations ja-jp.yml (Czech)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (Greek)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Dutch)

* New translations ja-jp.yml (Norwegian)

* New translations ja-jp.yml (Polish)

* New translations ja-jp.yml (Slovak)

* New translations ja-jp.yml (Swedish)

* New translations ja-jp.yml (Ukrainian)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Vietnamese)

* New translations ja-jp.yml (Indonesian)

* New translations ja-jp.yml (Bengali)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Uzbek)

* New translations ja-jp.yml (Lao)

* New translations ja-jp.yml (Korean (Gyeongsang))
2025-03-20 13:01:20 +09:00
syuilo
596e517f77 fix(frontend): minimum uiが表示できない 2025-03-20 12:58:18 +09:00
syuilo
91670caca0 enhance(frontend): tweak install-extensions behaviour 2025-03-20 12:58:01 +09:00
syuilo
fccaadacf0 lint(frontend): improve id-denylist rule 2025-03-20 12:47:51 +09:00
syuilo
282caa0b7e 🎨 for install-extensions 2025-03-20 12:36:48 +09:00
syuilo
9529867630 fix(frontend): dev buildでpreferencesのタブ同期が不必要に行われるのを修正 2025-03-20 12:02:50 +09:00
github-actions[bot]
d06fadd095 Bump version to 2025.3.2-beta.5 2025-03-20 00:06:04 +00:00
zyoshoka
9dd13f364b fix(backend): mismatch in emojis param of test WebHook payload (#15675)
* fix(backend): mismatch in `emojis` param of test WebHook payload

* fix: test

* fix: type
2025-03-20 09:00:58 +09:00
syuilo
b067d4dcd6 follow up of 7b323031b7 2025-03-20 08:59:54 +09:00
syuilo
3d1deb1a7b Update index.d.ts 2025-03-20 08:54:13 +09:00
syuilo
2e16f5cd74 Merge branch 'develop' into chat 2025-03-20 08:54:08 +09:00
syuilo
acac759d87 fix(frontend): モバイルレイアウト時にホームを押しても最上部へスクロールされない
Fix #15679
2025-03-20 08:35:45 +09:00
syuilo
4e1a17cf0d Update ja-JP.yml 2025-03-20 08:30:21 +09:00
syuilo
b37622fa64 🎨 2025-03-19 20:54:07 +09:00
syuilo
c17a104de6 fix(frontend): router view stacking有効時にinstall-extensionsが動かない 2025-03-19 20:53:48 +09:00
syuilo
e56d57c6e6 Merge branch 'develop' into chat 2025-03-19 20:36:29 +09:00
syuilo
4ab9f66356 Update deep-equal.ts 2025-03-19 20:32:15 +09:00
syuilo
e3c38cf1ac Update home.vue 2025-03-19 19:57:03 +09:00
zyoshoka
aed95a765d chore(storybook): fix storybook build (#15678) 2025-03-19 10:52:05 +00:00
syuilo
5618688d0e Update home.vue 2025-03-19 19:40:48 +09:00
syuilo
ab55a2487c Merge branch 'develop' into chat 2025-03-19 19:37:31 +09:00
Sayamame-beans
71841e365e Enhance: 2段階認証時のリカバリーコードのファイル名にサーバーURLを含めるように (#15677)
* enhance(frontend): include server hostname and port in 2fa recovery code filename

* chore(frontend): fix mistake(use `@` for indicate server hostname)

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-03-19 19:34:57 +09:00
github-actions[bot]
3b20279e20 Bump version to 2025.3.2-beta.4 2025-03-19 10:27:14 +00:00
Yuri Lee
21dc7aebe7 Fix: Don't delete remote emoji when import zip (#15674)
* Fix: Don't delete remote emoji when import zip

* Update packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts

Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com>

---------

Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
2025-03-19 10:23:50 +00:00
syuilo
aff03708a7 New Crowdin updates (#15667)
* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Catalan)
2025-03-19 19:18:14 +09:00
syuilo
69ad3cf89b Update CHANGELOG.md 2025-03-19 19:17:33 +09:00
syuilo
7b323031b7 refactor(frontend): use useTemplateRef for DOM referencing 2025-03-19 18:46:03 +09:00
syuilo
81ac71f7e5 refactor(frontend): router refactoring 2025-03-19 18:06:22 +09:00
syuilo
2c76018b7f better import paths 2025-03-19 17:27:18 +09:00
syuilo
2dc2d2e4fe refactor 2025-03-19 16:04:01 +09:00
syuilo
409cd4fbd3 refactor(frontend): router refactoring 2025-03-19 15:54:30 +09:00
syuilo
7d4045e8b4 refactor(frontend): router refactoring 2025-03-19 15:24:56 +09:00
syuilo
bdc72e5817 clean up 2025-03-19 15:17:41 +09:00
syuilo
ea1003177a Update room.vue 2025-03-19 09:49:15 +09:00
syuilo
83c7d74e11 Merge branch 'develop' into chat 2025-03-19 09:34:56 +09:00
syuilo
11378b17c5 🎨 2025-03-19 09:31:01 +09:00
syuilo
62bf0d53d3 🎨 2025-03-18 22:21:28 +09:00
syuilo
05391f59a5 enhance(frontend): improve StackingRouterView 2025-03-18 20:55:19 +09:00
syuilo
d609f41f61 🎨 2025-03-18 17:31:25 +09:00
syuilo
0a295e1bb0 🎨 2025-03-18 15:23:50 +09:00
syuilo
6b5cf2e229 wip 2025-03-18 10:27:30 +09:00
syuilo
86f2ababd1 wip 2025-03-17 18:59:22 +09:00
syuilo
0c81f8a28a Create 1742203321812-chat.js 2025-03-17 18:26:52 +09:00
syuilo
6503559013 Update types.ts 2025-03-17 18:21:22 +09:00
syuilo
50ec9b9071 wip 2025-03-17 18:19:48 +09:00
syuilo
06bd615334 wip 2025-03-17 18:18:04 +09:00
syuilo
30be29a785 wip 2025-03-17 18:00:31 +09:00
syuilo
9e5fb89408 wip 2025-03-17 15:38:18 +09:00
syuilo
a90b3b7e67 wip 2025-03-17 15:21:00 +09:00
syuilo
24bbbe96a1 Merge branch 'develop' into chat 2025-03-17 14:48:37 +09:00
syuilo
474155b677 follow up of 6c8f21b608 2025-03-17 13:27:47 +09:00
syuilo
6c8f21b608 fix(backend): 連合無しモードでも外部から照会可能だった問題を修正 2025-03-17 13:21:09 +09:00
ikasoba
7d5daa4b00 fix (#15671) 2025-03-17 03:40:10 +00:00
syuilo
855d8d0a9f wip 2025-03-17 12:32:55 +09:00
syuilo
1ce81f243e Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2025-03-16 19:04:16 +09:00
syuilo
a773f2976d refactor(frontend): signinRequired -> ensureSignin 2025-03-16 19:04:14 +09:00
github-actions[bot]
467c68fb98 Bump version to 2025.3.2-beta.3 2025-03-16 08:51:58 +00:00
syuilo
22b0ace8b4 enhance(frontend): 投稿フォームの設定メニューを改良 (改)
This reverts commit a814395127.
2025-03-16 17:48:16 +09:00
syuilo
a814395127 Revert "enhance(frontend): 投稿フォームの設定メニューを改良 (#14804)"
This reverts commit ce6b2448ce.
2025-03-16 17:21:20 +09:00
syuilo
81a0cbd294 chore(frontend): use toast to show message when call copyToClipboard 2025-03-16 15:04:38 +09:00
syuilo
32844e4775 🎨 2025-03-16 14:56:27 +09:00
github-actions[bot]
fbd9f47182 Bump version to 2025.3.2-beta.2 2025-03-16 05:18:29 +00:00
syuilo
1c9e25470a refactor 2025-03-16 14:15:09 +09:00
かっこかり
ce6b2448ce enhance(frontend): 投稿フォームの設定メニューを改良 (#14804)
* enhance(frontend): 投稿フォームの設定メニューを改良

* Update Changelog

* indent

* MkMenuのitemを切り出して共通化

* remove unused expose

* fix: ドロワーなどのOptionが当たらない問題を修正

* 他のpopupMenuの項目選択時と挙動をあわせる

* チュートリアルで詰む問題を修正

* Revert "MkMenuのitemを切り出して共通化"

This reverts commit ce3679798c.

* enhance: slotで共通化

* Update MkPostFormOtherMenu.vue

* remove duplicated locale key

* refactor: メニューの定義をMkPostForm側で行うように

* Update CHANGELOG.md

* [ci skip] Update MkPostFormOtherMenu.vue

* Update MkPostForm.vue

* Update CHANGELOG.md

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-03-16 05:05:58 +00:00
syuilo
7d44b47fdf New Crowdin updates (#15662)
* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Simplified)
2025-03-16 13:59:22 +09:00
syuilo
dca42fd6e6 enhance(frontend): improve ux for touch devices 2025-03-16 13:59:08 +09:00
syuilo
43153311c6 🎨 2025-03-16 13:43:47 +09:00
syuilo
2b23c7e7f5 chore(frontend): remove duplicated warn 2025-03-16 13:30:08 +09:00
syuilo
2d4e85b466 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2025-03-16 13:03:04 +09:00
syuilo
7bfada9792 enhance: remove bull-board support 2025-03-16 13:03:02 +09:00
lqvp
4fef9c670a fix(i18n): 通知タイプのcreateTokenが抜けていたのを修正 (#15663) 2025-03-16 02:27:41 +00:00
syuilo
9599261fc3 fix(frontend): fix settings-search-index of webhook 2025-03-16 11:02:54 +09:00
syuilo
c2940fd77c enhance(frontend): improve usability on touch device 2025-03-16 10:58:06 +09:00
510 changed files with 11151 additions and 4964 deletions

View File

@@ -24,9 +24,6 @@ updates:
aws-sdk: aws-sdk:
patterns: patterns:
- "@aws-sdk/*" - "@aws-sdk/*"
bull-board:
patterns:
- "@bull-board/*"
nestjs: nestjs:
patterns: patterns:
- "@nestjs/*" - "@nestjs/*"

View File

@@ -50,8 +50,8 @@ jobs:
- run: pnpm i --frozen-lockfile - run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml - name: Check pnpm-lock.yaml
run: git diff --exit-code pnpm-lock.yaml run: git diff --exit-code pnpm-lock.yaml
- name: Build misskey-js - name: Build dependent packages
run: pnpm --filter misskey-js build run: pnpm -F misskey-js -F misskey-bubble-game -F misskey-reversi build
- name: Build storybook - name: Build storybook
run: pnpm --filter frontend build-storybook run: pnpm --filter frontend build-storybook
- name: Publish to Chromatic - name: Publish to Chromatic

View File

@@ -1,23 +1,51 @@
## 2025.3.2 ## 2025.3.2
### General ### General
- - Feat: チャットがリニューアルして復活しました
- 既存のDM機能よりも便利で効率的になっています
- チャットを受け付ける相手を制限できます
- チャット機能を開放するかどうかをロールで制御できます
- Enhance: セキュリティを強化するため、ジョブキューのダッシュボード(bull-board)統合が削除されました。
- Misskeyネイティブでダッシュボードを実装予定です
### Client ### Client
- Feat: 設定の管理が強化されました - Feat: 設定の管理が強化されました
- 自動でバックアップされるように - 内部処理が一新され、安定性とパフォーマンスが向上しました
- 全てのクライアント設定がエクスポート(バックアップ)/インポート対象に含まれるようになりました
- プラグイン、テーマ、クライアントに追加されたすべてのアカウント情報も含まれるようになりました
- 自動で設定データをサーバーにバックアップできるように
- 設定→設定のプロファイル→自動バックアップ で有効にできます
- 新しいデバイスからログインしたり、ブラウザから設定データが消えてしまったときに自動で復元されます(復元をスキップすることも可能)
- 任意の設定項目をデバイス間で同期できるように - 任意の設定項目をデバイス間で同期できるように
- 設定項目の「...」メニュー→「デバイス間で同期」
- 同期をオンにした際にサーバーに保存された値とローカルの値が競合する場合はどちらを優先するか選択できます
- 任意の設定項目を初期値にリセットできるように
- 設定項目の「...」メニュー→「初期値にリセット」
- アカウントごとに設定値が分離される設定とそうでないクライアント設定が混在していた(かつ分離するかどうかを設定不可だった)のを、基本的に一律でクライアント全体に適用されるようにし、個別でアカウントごとに異なる設定を行えるように
- 設定項目の「...」メニュー→「アカウントで上書き」をオンにすることで、設定値をそのアカウントでだけ適用するようにできます
- ログアウトすると設定データもブラウザから消去されるようになりプライバシーが向上しました
- 再度ログインすればサーバーのバックアップから設定データを復元可能です
- エクスポートした設定データを他のサーバーでインポートして適用すること(設定の持ち運び)が可能になりました
- Feat: 画面を重ねて表示するオプションを実装(実験的)
- 設定 → その他 → 実験的機能 → Enable stacking router view
- Enhance: プラグインの管理が強化されました - Enhance: プラグインの管理が強化されました
- インストール/アンインストール/設定の変更時にリロード不要になりました - インストール/アンインストール/設定の変更時にリロード不要になりました
- Enhance: ログアウト時、ブラウザに保存されたWebクライアントのデータを全て消去するように - Enhance: ログアウト時、ブラウザに保存されたWebクライアントのデータを全て消去するように
- Enhance: CWの注釈テキストが入力されていない場合, Postボタンを非アクティブに - Enhance: CWの注釈テキストが入力されていない場合, Postボタンを非アクティブに
- Enhance: CWを無効にした場合, 注釈テキストが最大入力文字数を超えていても投稿できるように - Enhance: CWを無効にした場合, 注釈テキストが最大入力文字数を超えていても投稿できるように
- Enhance: テーマ設定画面のデザインを改善 - Enhance: テーマ設定画面のデザインを改善
- Enhance: 投稿フォームの設定メニューを改良
- 投稿フォームをリセットできるように
- 文字数カウントを復活
- Enhance: 2段階認証時のリカバリーコードのファイル名にサーバーURLを含めるように
- Fix: テーマ切り替え時に一部の色が変わらない問題を修正 - Fix: テーマ切り替え時に一部の色が変わらない問題を修正
### Server ### Server
- Enhance 全体的なパフォーマンス向上
- Fix: プロフィール追加情報で無効なURLに入力された場合に照会エラーを出るのを修正 - Fix: プロフィール追加情報で無効なURLに入力された場合に照会エラーを出るのを修正
- Fix: ActivityPubリクエストURLチェック実装は仕様に従っていないのを修正 - Fix: ActivityPubリクエストURLチェック実装は仕様に従っていないのを修正
- Fix: 連合無しモードでも外部から照会可能だった問題を修正
- Fix: テスト用WebHookのペイロードの`emojis`パラメータが実際のものと異なる問題を修正
## 2025.3.1 ## 2025.3.1

View File

@@ -273,7 +273,6 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド
query?: Record<string, string>; query?: Record<string, string>;
loginRequired?: boolean; loginRequired?: boolean;
hash?: string; hash?: string;
globalCacheKey?: string;
children?: RouteDef[]; children?: RouteDef[];
} }
``` ```

View File

@@ -1013,6 +1013,7 @@ flip: "اقلب"
lastNDays: "آخر {n} أيام" lastNDays: "آخر {n} أيام"
surrender: "ألغِ" surrender: "ألغِ"
postForm: "أنشئ ملاحظة" postForm: "أنشئ ملاحظة"
information: "عن"
_delivery: _delivery:
stop: "مُعلّق" stop: "مُعلّق"
_initialAccountSetting: _initialAccountSetting:

View File

@@ -853,6 +853,7 @@ renotes: "রিনোট"
sourceCode: "সোর্স কোড" sourceCode: "সোর্স কোড"
flip: "উল্টান" flip: "উল্টান"
postForm: "নোট লিখুন" postForm: "নোট লিখুন"
information: "আপনার সম্পর্কে"
_delivery: _delivery:
stop: "স্থগিত করা হয়েছে" stop: "স্থগিত করা হয়েছে"
_type: _type:

View File

@@ -698,6 +698,7 @@ userSaysSomethingAbout: "{name} està parlant sobre \"{word}\""
makeActive: "Activar" makeActive: "Activar"
display: "Veure" display: "Veure"
copy: "Copiar" copy: "Copiar"
copiedToClipboard: "Copiat al porta papers"
metrics: "Mètriques" metrics: "Mètriques"
overview: "Visió General" overview: "Visió General"
logs: "Registres" logs: "Registres"
@@ -1139,7 +1140,7 @@ channelArchiveConfirmDescription: "Un Canal arxivat no apareixerà a la llista d
thisChannelArchived: "Aquest Canal ha sigut arxivat." thisChannelArchived: "Aquest Canal ha sigut arxivat."
displayOfNote: "Mostrar notes" displayOfNote: "Mostrar notes"
initialAccountSetting: "Configuració del perfil" initialAccountSetting: "Configuració del perfil"
youFollowing: "Seguint" youFollowing: "Segueixes "
preventAiLearning: "Descartar l'ús d'aprenentatge automàtic (IA Generativa)" preventAiLearning: "Descartar l'ús d'aprenentatge automàtic (IA Generativa)"
preventAiLearningDescription: "Demanar els indexadors no fer servir els texts, imatges, etc. en cap conjunt de dades per alimentar l'aprenentatge automàtic (IA Predictiva/ Generativa). Això s'aconsegueix afegint la etiqueta \"noai\" com a resposta HTML al contingut corresponent. Prevenir aquest ús totalment pot ser que no sigui aconseguit, ja que molts indexadors poden obviar aquesta etiqueta." preventAiLearningDescription: "Demanar els indexadors no fer servir els texts, imatges, etc. en cap conjunt de dades per alimentar l'aprenentatge automàtic (IA Predictiva/ Generativa). Això s'aconsegueix afegint la etiqueta \"noai\" com a resposta HTML al contingut corresponent. Prevenir aquest ús totalment pot ser que no sigui aconseguit, ja que molts indexadors poden obviar aquesta etiqueta."
options: "Opcions" options: "Opcions"
@@ -1190,7 +1191,7 @@ pastAnnouncements: "Informes passats"
youHaveUnreadAnnouncements: "Tens informes per llegir." youHaveUnreadAnnouncements: "Tens informes per llegir."
useSecurityKey: "Segueix les instruccions del teu navegador O dispositiu per fer servir el teu passkey." useSecurityKey: "Segueix les instruccions del teu navegador O dispositiu per fer servir el teu passkey."
replies: "Respostes" replies: "Respostes"
renotes: "Impulsar" renotes: "Impulsos"
loadReplies: "Mostrar les respostes" loadReplies: "Mostrar les respostes"
loadConversation: "Mostrar la conversació " loadConversation: "Mostrar la conversació "
pinnedList: "Llista fixada" pinnedList: "Llista fixada"
@@ -1326,7 +1327,19 @@ restore: "Restaurar "
syncBetweenDevices: "Sincronització entre dispositius" syncBetweenDevices: "Sincronització entre dispositius"
preferenceSyncConflictTitle: "Els valors de la configuració ja existeixen al dispositiu" preferenceSyncConflictTitle: "Els valors de la configuració ja existeixen al dispositiu"
preferenceSyncConflictText: "Un element de la configuració amb sincronització activada desa els seus valors al servidor, però s'ha trobat un valor a la configuració desat al servidor per aquest element de la configuració. Quin valor us sobreescriure?" preferenceSyncConflictText: "Un element de la configuració amb sincronització activada desa els seus valors al servidor, però s'ha trobat un valor a la configuració desat al servidor per aquest element de la configuració. Quin valor us sobreescriure?"
preferenceSyncConflictChoiceServer: "Valors de configuració del servidor"
preferenceSyncConflictChoiceDevice: "Punts d'ajustos del dispositiu "
preferenceSyncConflictChoiceCancel: "Cancel·lar l'activació de la sincronització "
paste: "Pegar"
emojiPalette: "Calaix d'emojis"
postForm: "Formulari de publicació" postForm: "Formulari de publicació"
textCount: "Nombre de caràcters "
information: "Informació"
_emojiPalette:
palettes: "Calaixos d'emojis"
enableSyncBetweenDevicesForPalettes: "Activa la sincronització dels calaixos d'emojis entre dispositius"
paletteForMain: "Calaix d'emojis principal"
paletteForReaction: "Calaix d'emojis per reaccions"
_settings: _settings:
driveBanner: "Pots gestionar i configurar el Disc, comprovar el seu ús i establir una configuració per a la càrrega d'arxius." driveBanner: "Pots gestionar i configurar el Disc, comprovar el seu ús i establir una configuració per a la càrrega d'arxius."
pluginBanner: "Els complements poden fer-se servir per ampliar les funcionalitats del client. Els complements poden instal·lar-se, configurar-se individualment i gestionar-se." pluginBanner: "Els complements poden fer-se servir per ampliar les funcionalitats del client. Els complements poden instal·lar-se, configurar-se individualment i gestionar-se."
@@ -1344,6 +1357,9 @@ _settings:
preferencesBanner: "Pots configurar el comportament general del client segons les teves preferències." preferencesBanner: "Pots configurar el comportament general del client segons les teves preferències."
appearanceBanner: "Pots configurar les preferències relacionades amb la visualització i l'aspecte del client segons el teu parer." appearanceBanner: "Pots configurar les preferències relacionades amb la visualització i l'aspecte del client segons el teu parer."
soundsBanner: "Configuració dels sons que reproduirà el client." soundsBanner: "Configuració dels sons que reproduirà el client."
timelineAndNote: "Línia de temps i nota"
makeEveryTextElementsSelectable: "Fes que tots els elements del text siguin seleccionables"
makeEveryTextElementsSelectable_description: "L'activació pot reduir la usabilitat en determinades ocasions."
_preferencesProfile: _preferencesProfile:
profileName: "Nom del perfil" profileName: "Nom del perfil"
profileNameDescription: "Estableix un nom que identifiqui aquest dispositiu." profileNameDescription: "Estableix un nom que identifiqui aquest dispositiu."
@@ -2056,7 +2072,7 @@ _theme:
hashtag: "Etiqueta" hashtag: "Etiqueta"
mention: "Menció" mention: "Menció"
mentionMe: "Mencions (jo)" mentionMe: "Mencions (jo)"
renote: "Renotar" renote: "Impulsar"
modalBg: "Fons del modal" modalBg: "Fons del modal"
divider: "Divisor" divider: "Divisor"
scrollbarHandle: "Maneta de la barra de desplaçament" scrollbarHandle: "Maneta de la barra de desplaçament"
@@ -2498,7 +2514,7 @@ _notification:
follow: "Segueix-me" follow: "Segueix-me"
mention: "Menció" mention: "Menció"
reply: "Respostes" reply: "Respostes"
renote: "Impulsar" renote: "Impulsos"
quote: "Citar" quote: "Citar"
reaction: "Reaccions" reaction: "Reaccions"
pollEnded: "Enquesta terminada" pollEnded: "Enquesta terminada"
@@ -2508,12 +2524,13 @@ _notification:
achievementEarned: "Assoliment desbloquejat" achievementEarned: "Assoliment desbloquejat"
exportCompleted: "Exportació completada" exportCompleted: "Exportació completada"
login: "Iniciar sessió" login: "Iniciar sessió"
createToken: "Creació de tokens d'accés "
test: "Prova la notificació" test: "Prova la notificació"
app: "Notificacions d'aplicacions" app: "Notificacions d'aplicacions"
_actions: _actions:
followBack: "També et segueix" followBack: "També et segueix"
reply: "Respondre" reply: "Respondre"
renote: "Impulsos" renote: "Impulsar"
_deck: _deck:
alwaysShowMainColumn: "Mostrar sempre la columna principal" alwaysShowMainColumn: "Mostrar sempre la columna principal"
columnAlign: "Alinea les columnes" columnAlign: "Alinea les columnes"
@@ -2657,10 +2674,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "Assegura't que qui distribueix aquest recurs és fiable abans d'instal·lar-ho." checkVendorBeforeInstall: "Assegura't que qui distribueix aquest recurs és fiable abans d'instal·lar-ho."
_plugin: _plugin:
title: "Vols instal·lar aquest afegit?" title: "Vols instal·lar aquest afegit?"
metaTitle: "Informació de l'afegit "
_theme: _theme:
title: "Vols instal·lar aquest tema?" title: "Vols instal·lar aquest tema?"
metaTitle: "Informació del tema"
_meta: _meta:
base: "Paleta de colors base" base: "Paleta de colors base"
_vendorInfo: _vendorInfo:

View File

@@ -169,6 +169,9 @@ addAccount: "Přidat účet"
reloadAccountsList: "Obnovit list účtů" reloadAccountsList: "Obnovit list účtů"
loginFailed: "Přihlášení se nezdařilo." loginFailed: "Přihlášení se nezdařilo."
showOnRemote: "Více na původním profilu" showOnRemote: "Více na původním profilu"
continueOnRemote: "Pokračujte na původní profil"
chooseServerOnMisskeyHub: "Vyberete si server z Misskey Hubu"
inputHostName: "Zadejte doménu"
general: "Obecně" general: "Obecně"
wallpaper: "Obrázek na pozadí" wallpaper: "Obrázek na pozadí"
setWallpaper: "Nastavení obrázku na pozadí" setWallpaper: "Nastavení obrázku na pozadí"
@@ -193,6 +196,7 @@ perHour: "za hodinu"
perDay: "za den" perDay: "za den"
stopActivityDelivery: "Přestat zasílat aktivitu" stopActivityDelivery: "Přestat zasílat aktivitu"
blockThisInstance: "Blokovat tuto instanci" blockThisInstance: "Blokovat tuto instanci"
silenceThisInstance: "Utišit tuto instanci"
operations: "Operace" operations: "Operace"
software: "Software" software: "Software"
version: "Verze" version: "Verze"
@@ -1099,6 +1103,7 @@ flip: "Otočit"
lastNDays: "Posledních {n} dnů" lastNDays: "Posledních {n} dnů"
surrender: "Zrušit" surrender: "Zrušit"
postForm: "Formulář pro odeslání" postForm: "Formulář pro odeslání"
information: "Informace"
_delivery: _delivery:
stop: "Suspendováno" stop: "Suspendováno"
_type: _type:

View File

@@ -49,7 +49,7 @@ pin: "An dein Profil anheften"
unpin: "Von deinem Profil lösen" unpin: "Von deinem Profil lösen"
copyContent: "Inhalt kopieren" copyContent: "Inhalt kopieren"
copyLink: "Link kopieren" copyLink: "Link kopieren"
copyRemoteLink: "Renote-Link kopieren" copyRemoteLink: "Remote-Link kopieren"
copyLinkRenote: "Renote-Link kopieren" copyLinkRenote: "Renote-Link kopieren"
delete: "Löschen" delete: "Löschen"
deleteAndEdit: "Löschen und Bearbeiten" deleteAndEdit: "Löschen und Bearbeiten"
@@ -1309,6 +1309,7 @@ availableRoles: "Verfügbare Rollen"
federationSpecified: "Dieser Server arbeitet mit Whitelist-Föderation. Er kann nicht mit anderen als den vom Administrator angegebenen Servern interagieren." federationSpecified: "Dieser Server arbeitet mit Whitelist-Föderation. Er kann nicht mit anderen als den vom Administrator angegebenen Servern interagieren."
federationDisabled: "Föderation ist auf diesem Server deaktiviert. Es ist nicht möglich, mit Benutzern auf anderen Servern zu interagieren." federationDisabled: "Föderation ist auf diesem Server deaktiviert. Es ist nicht möglich, mit Benutzern auf anderen Servern zu interagieren."
postForm: "Notizfenster" postForm: "Notizfenster"
information: "Über"
_settings: _settings:
webhook: "Webhook" webhook: "Webhook"
_accountSettings: _accountSettings:
@@ -2524,10 +2525,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "Überprüfe vor Installation die Vertrauenswürdigkeit des Vertreibers." checkVendorBeforeInstall: "Überprüfe vor Installation die Vertrauenswürdigkeit des Vertreibers."
_plugin: _plugin:
title: "Möchtest du dieses Plugin installieren?" title: "Möchtest du dieses Plugin installieren?"
metaTitle: "Plugininformation"
_theme: _theme:
title: "Möchten du dieses Farbschema installieren?" title: "Möchten du dieses Farbschema installieren?"
metaTitle: "Farbschemainfo"
_meta: _meta:
base: "Farbschemavorlage" base: "Farbschemavorlage"
_vendorInfo: _vendorInfo:

View File

@@ -289,6 +289,7 @@ icon: "Εικονίδιο"
replies: "Απάντηση" replies: "Απάντηση"
renotes: "Κοινοποίηση σημειώματος" renotes: "Κοινοποίηση σημειώματος"
postForm: "Φόρμα δημοσίευσης" postForm: "Φόρμα δημοσίευσης"
information: "Πληροφορίες"
_email: _email:
_follow: _follow:
title: "Έχετε ένα νέο ακόλουθο" title: "Έχετε ένα νέο ακόλουθο"

View File

@@ -698,6 +698,7 @@ userSaysSomethingAbout: "{name} said something about \"{word}\""
makeActive: "Activate" makeActive: "Activate"
display: "Display" display: "Display"
copy: "Copy" copy: "Copy"
copiedToClipboard: "Copied to clipboard"
metrics: "Metrics" metrics: "Metrics"
overview: "Overview" overview: "Overview"
logs: "Logs" logs: "Logs"
@@ -1316,14 +1317,29 @@ unmarkAsSensitiveConfirm: "Do you want to remove the sensitive designation for t
preferences: "Preferences" preferences: "Preferences"
accessibility: "Accessibility" accessibility: "Accessibility"
preferencesProfile: "Preferences profile" preferencesProfile: "Preferences profile"
copyPreferenceId: "Copy the proference ID" copyPreferenceId: "Copy the preference ID"
resetToDefaultValue: "Revert to default" resetToDefaultValue: "Revert to default"
overrideByAccount: "Override by the account" overrideByAccount: "Override by the account"
untitled: "Untitled" untitled: "Untitled"
noName: "No name" noName: "No name"
skip: "Skip" skip: "Skip"
restore: "Restore" restore: "Restore"
syncBetweenDevices: "Sync between devices"
preferenceSyncConflictTitle: "The configured value exists on the server."
preferenceSyncConflictText: "The sync enabled settings will save their values to the server. However, there are existing values on the server. Which set of values would you like to overwrite?"
preferenceSyncConflictChoiceServer: "Configured value on server"
preferenceSyncConflictChoiceDevice: "Configured value on device"
preferenceSyncConflictChoiceCancel: "Cancel enabling sync"
paste: "Paste"
emojiPalette: "Emoji palette"
postForm: "Posting form" postForm: "Posting form"
textCount: "Character count"
information: "About"
_emojiPalette:
palettes: "Palette"
enableSyncBetweenDevicesForPalettes: "Enable palette sync between devices"
paletteForMain: "Main palette"
paletteForReaction: "Reaction palette"
_settings: _settings:
driveBanner: "You can manage and configure the drive, check usage, and configure file upload settings." driveBanner: "You can manage and configure the drive, check usage, and configure file upload settings."
pluginBanner: "You can extend client features with plugins. You can install plugins, configure and manage individually." pluginBanner: "You can extend client features with plugins. You can install plugins, configure and manage individually."
@@ -1341,6 +1357,9 @@ _settings:
preferencesBanner: "You can configure the overall behavior of the client according to your preferences." preferencesBanner: "You can configure the overall behavior of the client according to your preferences."
appearanceBanner: "You can configure the appearance and display settings for the client according to your preferences." appearanceBanner: "You can configure the appearance and display settings for the client according to your preferences."
soundsBanner: "You can configure the sound settings for playback in the client." soundsBanner: "You can configure the sound settings for playback in the client."
timelineAndNote: "Timeline and note"
makeEveryTextElementsSelectable: "Make all text elements selectable"
makeEveryTextElementsSelectable_description: "Enabling this may reduce usability in some situations."
_preferencesProfile: _preferencesProfile:
profileName: "Profile name" profileName: "Profile name"
profileNameDescription: "Set a name that identifies this device." profileNameDescription: "Set a name that identifies this device."
@@ -2505,6 +2524,7 @@ _notification:
achievementEarned: "Achievement unlocked" achievementEarned: "Achievement unlocked"
exportCompleted: "The export has been completed" exportCompleted: "The export has been completed"
login: "Sign In" login: "Sign In"
createToken: "Create access token"
test: "Notification test" test: "Notification test"
app: "Notifications from linked apps" app: "Notifications from linked apps"
_actions: _actions:
@@ -2532,6 +2552,7 @@ _deck:
useSimpleUiForNonRootPages: "Use simple UI for navigated pages" useSimpleUiForNonRootPages: "Use simple UI for navigated pages"
usedAsMinWidthWhenFlexible: "Minimum width will be used for this when the \"Auto-adjust width\" option is enabled" usedAsMinWidthWhenFlexible: "Minimum width will be used for this when the \"Auto-adjust width\" option is enabled"
flexible: "Auto-adjust width" flexible: "Auto-adjust width"
enableSyncBetweenDevicesForProfiles: "Enable profile information sync between devices"
_columns: _columns:
main: "Main" main: "Main"
widgets: "Widgets" widgets: "Widgets"
@@ -2653,10 +2674,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "Make sure the distributor of this resource is trustworthy before installation." checkVendorBeforeInstall: "Make sure the distributor of this resource is trustworthy before installation."
_plugin: _plugin:
title: "Do you want to install this plugin?" title: "Do you want to install this plugin?"
metaTitle: "Plugin information"
_theme: _theme:
title: "Do you want to install this theme?" title: "Do you want to install this theme?"
metaTitle: "Theme information"
_meta: _meta:
base: "Base color scheme" base: "Base color scheme"
_vendorInfo: _vendorInfo:
@@ -2857,8 +2876,8 @@ _selfXssPrevention:
description2: "If you do not understand exactly what you are trying to paste, %cstop working right now and close this window." description2: "If you do not understand exactly what you are trying to paste, %cstop working right now and close this window."
description3: "For more information, please refer to this. {link}" description3: "For more information, please refer to this. {link}"
_followRequest: _followRequest:
recieved: "Received application" recieved: "Received request"
sent: "Sent application" sent: "Sent request"
_remoteLookupErrors: _remoteLookupErrors:
_federationNotAllowed: _federationNotAllowed:
title: "Unable to communicate with this server" title: "Unable to communicate with this server"

View File

@@ -1300,6 +1300,7 @@ target: "Para"
federationSpecified: "Este servidor opera en una federación de listas blancas. No puede interactuar con otros servidores que no sean los especificados por el administrador." federationSpecified: "Este servidor opera en una federación de listas blancas. No puede interactuar con otros servidores que no sean los especificados por el administrador."
federationDisabled: "La federación está desactivada en este servidor. No puede interactuar con usuarios de otros servidores" federationDisabled: "La federación está desactivada en este servidor. No puede interactuar con usuarios de otros servidores"
postForm: "Formulario" postForm: "Formulario"
information: "Información"
_settings: _settings:
webhook: "Webhook" webhook: "Webhook"
_accountSettings: _accountSettings:
@@ -2521,10 +2522,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "Asegúrate de que el distribuidor de este recurso es de confianza antes de proceder a la instalación." checkVendorBeforeInstall: "Asegúrate de que el distribuidor de este recurso es de confianza antes de proceder a la instalación."
_plugin: _plugin:
title: "¿Quieres instalar este plugin?" title: "¿Quieres instalar este plugin?"
metaTitle: "Información del plugin"
_theme: _theme:
title: "¿Quieres instalar este tema?" title: "¿Quieres instalar este tema?"
metaTitle: "Información del tema"
_meta: _meta:
base: "Esquema de color base" base: "Esquema de color base"
_vendorInfo: _vendorInfo:

View File

@@ -1278,6 +1278,7 @@ lockdown: "Verrouiller"
pleaseSelectAccount: "Sélectionner un compte" pleaseSelectAccount: "Sélectionner un compte"
availableRoles: "Rôles disponibles" availableRoles: "Rôles disponibles"
postForm: "Formulaire de publication" postForm: "Formulaire de publication"
information: "Informations"
_abuseUserReport: _abuseUserReport:
forward: "Transférer" forward: "Transférer"
forwardDescription: "Transférer le signalement vers une instance distante en tant qu'anonyme." forwardDescription: "Transférer le signalement vers une instance distante en tant qu'anonyme."
@@ -2295,10 +2296,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "Veuillez confirmer que le distributeur est fiable avant l'installation." checkVendorBeforeInstall: "Veuillez confirmer que le distributeur est fiable avant l'installation."
_plugin: _plugin:
title: "Voulez-vous installer cette extension ?" title: "Voulez-vous installer cette extension ?"
metaTitle: "Informations sur l'extension"
_theme: _theme:
title: "Voulez-vous installer ce thème ?" title: "Voulez-vous installer ce thème ?"
metaTitle: "Informations sur le thème"
_meta: _meta:
base: "Palette de couleurs de base" base: "Palette de couleurs de base"
_vendorInfo: _vendorInfo:

View File

@@ -1262,6 +1262,7 @@ modified: "Diubah"
thereAreNChanges: "Ada {n} perubahan" thereAreNChanges: "Ada {n} perubahan"
prohibitedWordsForNameOfUser: "Kata yang dilarang untuk nama pengguna" prohibitedWordsForNameOfUser: "Kata yang dilarang untuk nama pengguna"
postForm: "Buat catatan" postForm: "Buat catatan"
information: "Informasi"
_settings: _settings:
webhook: "Webhook" webhook: "Webhook"
_abuseUserReport: _abuseUserReport:
@@ -2492,10 +2493,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "Pastikan sumber dari sumber daya ini terpercaya sebelum melakukan pemasangan." checkVendorBeforeInstall: "Pastikan sumber dari sumber daya ini terpercaya sebelum melakukan pemasangan."
_plugin: _plugin:
title: "Apakah kamu ingin memasang plugin ini?" title: "Apakah kamu ingin memasang plugin ini?"
metaTitle: "Informasi plugin"
_theme: _theme:
title: "Apakah kamu ingin memasang tema ini?" title: "Apakah kamu ingin memasang tema ini?"
metaTitle: "Informasi tema"
_meta: _meta:
base: "Skema warna dasar" base: "Skema warna dasar"
_vendorInfo: _vendorInfo:

197
locales/index.d.ts vendored
View File

@@ -1174,10 +1174,6 @@ export interface Locale extends ILocale {
* 保存しました * 保存しました
*/ */
"saved": string; "saved": string;
/**
* チャット
*/
"messaging": string;
/** /**
* アップロード * アップロード
*/ */
@@ -1227,9 +1223,9 @@ export interface Locale extends ILocale {
*/ */
"noMoreHistory": string; "noMoreHistory": string;
/** /**
* チャットを * チャットを始める
*/ */
"startMessaging": string; "startChat": string;
/** /**
* {n}人が読みました * {n}人が読みました
*/ */
@@ -1982,14 +1978,6 @@ export interface Locale extends ILocale {
* クリップボードのテキストが長いです。テキストファイルとして添付しますか? * クリップボードのテキストが長いです。テキストファイルとして添付しますか?
*/ */
"attachAsFileQuestion": string; "attachAsFileQuestion": string;
/**
* まだチャットはありません
*/
"noMessagesYet": string;
/**
* 新しいメッセージがあります
*/
"newMessageExists": string;
/** /**
* メッセージに添付できるファイルはひとつです * メッセージに添付できるファイルはひとつです
*/ */
@@ -2810,6 +2798,10 @@ export interface Locale extends ILocale {
* コピー * コピー
*/ */
"copy": string; "copy": string;
/**
* クリップボードにコピーされました
*/
"copiedToClipboard": string;
/** /**
* メトリクス * メトリクス
*/ */
@@ -5346,6 +5338,151 @@ export interface Locale extends ILocale {
* 投稿フォーム * 投稿フォーム
*/ */
"postForm": string; "postForm": string;
/**
* 文字数
*/
"textCount": string;
/**
* 情報
*/
"information": string;
/**
* チャット
*/
"chat": string;
"_chat": {
/**
* まだメッセージはありません
*/
"noMessagesYet": string;
/**
* 新しいメッセージ
*/
"newMessage": string;
/**
* 個人チャット
*/
"individualChat": string;
/**
* 特定ユーザーとの一対一のチャットができます。
*/
"individualChat_description": string;
/**
* ルームチャット
*/
"roomChat": string;
/**
* 複数人でのチャットができます。
* また、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。
*/
"roomChat_description": string;
/**
* ルームを作成
*/
"createRoom": string;
/**
* ユーザーを招待してチャットを始めましょう
*/
"inviteUserToChat": string;
/**
* 作成したルーム
*/
"yourRooms": string;
/**
* 参加中のルーム
*/
"joiningRooms": string;
/**
* 招待
*/
"invitations": string;
/**
* 招待はありません
*/
"noInvitations": string;
/**
* 履歴
*/
"history": string;
/**
* 履歴はありません
*/
"noHistory": string;
/**
* ルームはありません
*/
"noRooms": string;
/**
* ユーザーを招待
*/
"inviteUser": string;
/**
* 参加
*/
"join": string;
/**
* 無視
*/
"ignore": string;
/**
* このユーザーとのチャットを開始できません
*/
"cannotChatWithTheUser": string;
/**
* チャットが使えない状態になっているか、相手がチャットを開放していません。
*/
"cannotChatWithTheUser_description": string;
/**
* チャットする
*/
"chatWithThisUser": string;
/**
* このユーザーはフォロワーからのみチャットを受け付けています。
*/
"thisUserAllowsChatOnlyFromFollowers": string;
/**
* このユーザーはフォローしているユーザーからのみチャットを受け付けています。
*/
"thisUserAllowsChatOnlyFromFollowing": string;
/**
* このユーザーは相互フォローのユーザーからのみチャットを受け付けています。
*/
"thisUserAllowsChatOnlyFromMutualFollowing": string;
/**
* このユーザーは誰からもチャットを受け付けていません。
*/
"thisUserNotAllowedChatAnyone": string;
/**
* チャットを許可する相手
*/
"chatAllowedUsers": string;
/**
* 自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。
*/
"chatAllowedUsers_note": string;
"_chatAllowedUsers": {
/**
* 誰でも
*/
"everyone": string;
/**
* 自分のフォロワーのみ
*/
"followers": string;
/**
* 自分がフォローしているユーザーのみ
*/
"following": string;
/**
* 相互フォローのユーザーのみ
*/
"mutual": string;
/**
* 誰も許可しない
*/
"none": string;
};
};
"_emojiPalette": { "_emojiPalette": {
/** /**
* パレット * パレット
@@ -5433,6 +5570,14 @@ export interface Locale extends ILocale {
* タイムラインとノート * タイムラインとノート
*/ */
"timelineAndNote": string; "timelineAndNote": string;
/**
* 全てのテキスト要素を選択可能にする
*/
"makeEveryTextElementsSelectable": string;
/**
* 有効にすると、一部のシチュエーションでのユーザビリティが低下する場合があります。
*/
"makeEveryTextElementsSelectable_description": string;
}; };
"_preferencesProfile": { "_preferencesProfile": {
/** /**
@@ -7265,6 +7410,10 @@ export interface Locale extends ILocale {
* リストのインポートを許可 * リストのインポートを許可
*/ */
"canImportUserLists": string; "canImportUserLists": string;
/**
* チャットを許可
*/
"canChat": string;
}; };
"_condition": { "_condition": {
/** /**
@@ -8706,6 +8855,14 @@ export interface Locale extends ILocale {
* 違反を報告する * 違反を報告する
*/ */
"write:report-abuse": string; "write:report-abuse": string;
/**
* チャットを操作する
*/
"write:chat": string;
/**
* チャットを閲覧する
*/
"read:chat": string;
}; };
"_auth": { "_auth": {
/** /**
@@ -9777,6 +9934,10 @@ export interface Locale extends ILocale {
* ログイン * ログイン
*/ */
"login": string; "login": string;
/**
* アクセストークンの作成
*/
"createToken": string;
/** /**
* 通知のテスト * 通知のテスト
*/ */
@@ -10339,20 +10500,12 @@ export interface Locale extends ILocale {
* このプラグインをインストールしますか? * このプラグインをインストールしますか?
*/ */
"title": string; "title": string;
/**
* プラグイン情報
*/
"metaTitle": string;
}; };
"_theme": { "_theme": {
/** /**
* このテーマをインストールしますか? * このテーマをインストールしますか?
*/ */
"title": string; "title": string;
/**
* テーマ情報
*/
"metaTitle": string;
}; };
"_meta": { "_meta": {
/** /**

View File

@@ -698,6 +698,7 @@ userSaysSomethingAbout: "{name} ha Notato a riguardo di \"{word}\""
makeActive: "Attiva" makeActive: "Attiva"
display: "Visualizza" display: "Visualizza"
copy: "Copia" copy: "Copia"
copiedToClipboard: "Copiato negli appunti"
metrics: "Statistiche" metrics: "Statistiche"
overview: "Anteprima" overview: "Anteprima"
logs: "Log" logs: "Log"
@@ -973,7 +974,7 @@ check: "Verifica"
driveCapOverrideLabel: "Modificare la capienza del Drive per questo profilo" driveCapOverrideLabel: "Modificare la capienza del Drive per questo profilo"
driveCapOverrideCaption: "Se viene specificato meno di 0, viene annullato." driveCapOverrideCaption: "Se viene specificato meno di 0, viene annullato."
requireAdminForView: "Per visualizzarli, è necessario aver effettuato l'accesso con un profilo amministratore." requireAdminForView: "Per visualizzarli, è necessario aver effettuato l'accesso con un profilo amministratore."
isSystemAccount: "Questi profili vengono creati e gestiti automaticamente dal sistema" isSystemAccount: "Si tratta di un profilo creato e gestito automaticamente dal sistema."
typeToConfirm: "Digita {x} per continuare" typeToConfirm: "Digita {x} per continuare"
deleteAccount: "Eliminazione profilo" deleteAccount: "Eliminazione profilo"
document: "Documentazione" document: "Documentazione"
@@ -1323,7 +1324,22 @@ untitled: "Senza titolo"
noName: "Senza nome" noName: "Senza nome"
skip: "Salta" skip: "Salta"
restore: "Ripristina" restore: "Ripristina"
syncBetweenDevices: "Sincronizzazione tra i dispositivi"
preferenceSyncConflictTitle: "Sul server esiste già il valore impostato"
preferenceSyncConflictText: "Le impostazione sincronizzata salverà il valore sul server. Però, bada che esiste già un valore sul server. Quale vorresti sovrascrivere?"
preferenceSyncConflictChoiceServer: "Valore del server"
preferenceSyncConflictChoiceDevice: "Valore del dispositivo"
preferenceSyncConflictChoiceCancel: "Annulla la sincronizzazione"
paste: "Incolla"
emojiPalette: "Tavolozza emoji"
postForm: "Finestra di pubblicazione" postForm: "Finestra di pubblicazione"
textCount: "Il numero di caratteri"
information: "Informazioni"
_emojiPalette:
palettes: "Tavolozza"
enableSyncBetweenDevicesForPalettes: "Attiva la sincronizzazione tra dispositivi"
paletteForMain: "Tavolozza principale"
paletteForReaction: "Tavolozza per reazioni"
_settings: _settings:
driveBanner: "Permette di gestire e configurare il Drive, controllare il consumo di spazio e configurare il caricamento dei file." driveBanner: "Permette di gestire e configurare il Drive, controllare il consumo di spazio e configurare il caricamento dei file."
pluginBanner: "Consentono di migliorare le funzionalità. Le estensioni si possono configurare e gestire singolarmente." pluginBanner: "Consentono di migliorare le funzionalità. Le estensioni si possono configurare e gestire singolarmente."
@@ -1341,6 +1357,9 @@ _settings:
preferencesBanner: "Puoi personalizzare il comportamento del tuo dispositivo." preferencesBanner: "Puoi personalizzare il comportamento del tuo dispositivo."
appearanceBanner: "Puoi personalizzare l'aspetto nel dispositivo, in base alle tue preferenze." appearanceBanner: "Puoi personalizzare l'aspetto nel dispositivo, in base alle tue preferenze."
soundsBanner: "Puoi personalizzare i suoni emessi dagli eventi sul tuo dispositivo." soundsBanner: "Puoi personalizzare i suoni emessi dagli eventi sul tuo dispositivo."
timelineAndNote: "Note e Timeline"
makeEveryTextElementsSelectable: "Imposta ogni elemento come selezionabile"
makeEveryTextElementsSelectable_description: "Potrebbe ridurre l'usabilità in alcune situazioni."
_preferencesProfile: _preferencesProfile:
profileName: "Nome del profilo" profileName: "Nome del profilo"
profileNameDescription: "Impostare il nome che indentifica questo dispositivo." profileNameDescription: "Impostare il nome che indentifica questo dispositivo."
@@ -2505,6 +2524,7 @@ _notification:
achievementEarned: "Risultato raggiunto" achievementEarned: "Risultato raggiunto"
exportCompleted: "Esportazione completata" exportCompleted: "Esportazione completata"
login: "Accessi" login: "Accessi"
createToken: "Creare un token di accesso"
test: "Notifiche di test" test: "Notifiche di test"
app: "Notifiche da applicazioni" app: "Notifiche da applicazioni"
_actions: _actions:
@@ -2532,6 +2552,7 @@ _deck:
useSimpleUiForNonRootPages: "Visualizza sotto pagine con interfaccia web semplice" useSimpleUiForNonRootPages: "Visualizza sotto pagine con interfaccia web semplice"
usedAsMinWidthWhenFlexible: "Se \"larghezza flessibile\" è abilitato, questa diventa la larghezza minima" usedAsMinWidthWhenFlexible: "Se \"larghezza flessibile\" è abilitato, questa diventa la larghezza minima"
flexible: "Larghezza flessibile" flexible: "Larghezza flessibile"
enableSyncBetweenDevicesForProfiles: "Abilita la sincronizzazione delle informazioni profilo tra dispositivi"
_columns: _columns:
main: "Principale" main: "Principale"
widgets: "Riquadri" widgets: "Riquadri"
@@ -2653,10 +2674,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "Prima di installare, assicurati che la fonte sia affidabile." checkVendorBeforeInstall: "Prima di installare, assicurati che la fonte sia affidabile."
_plugin: _plugin:
title: "Vuoi davvero installare questo componente aggiuntivo?" title: "Vuoi davvero installare questo componente aggiuntivo?"
metaTitle: "Informazioni sul componente aggiuntivo"
_theme: _theme:
title: "Vuoi davvero installare questa variazione grafica?" title: "Vuoi davvero installare questa variazione grafica?"
metaTitle: "Informazioni sulla variazione grafica"
_meta: _meta:
base: "Combinazione base di colori" base: "Combinazione base di colori"
_vendorInfo: _vendorInfo:

View File

@@ -289,7 +289,6 @@ deleteAreYouSure: "「{x}」を削除しますか?"
resetAreYouSure: "リセットしますか?" resetAreYouSure: "リセットしますか?"
areYouSure: "よろしいですか?" areYouSure: "よろしいですか?"
saved: "保存しました" saved: "保存しました"
messaging: "チャット"
upload: "アップロード" upload: "アップロード"
keepOriginalUploading: "オリジナル画像を保持" keepOriginalUploading: "オリジナル画像を保持"
keepOriginalUploadingDescription: "画像をアップロードする時にオリジナル版を保持します。オフにするとアップロード時にブラウザでWeb公開用画像を生成します。" keepOriginalUploadingDescription: "画像をアップロードする時にオリジナル版を保持します。オフにするとアップロード時にブラウザでWeb公開用画像を生成します。"
@@ -302,7 +301,7 @@ uploadFromUrlMayTakeTime: "アップロードが完了するまで時間がか
explore: "みつける" explore: "みつける"
messageRead: "既読" messageRead: "既読"
noMoreHistory: "これより過去の履歴はありません" noMoreHistory: "これより過去の履歴はありません"
startMessaging: "チャットを始" startChat: "チャットを始める"
nUsersRead: "{n}人が読みました" nUsersRead: "{n}人が読みました"
agreeTo: "{0}に同意" agreeTo: "{0}に同意"
agree: "同意する" agree: "同意する"
@@ -491,8 +490,6 @@ noteOf: "{user}のノート"
quoteAttached: "引用付き" quoteAttached: "引用付き"
quoteQuestion: "引用として添付しますか?" quoteQuestion: "引用として添付しますか?"
attachAsFileQuestion: "クリップボードのテキストが長いです。テキストファイルとして添付しますか?" attachAsFileQuestion: "クリップボードのテキストが長いです。テキストファイルとして添付しますか?"
noMessagesYet: "まだチャットはありません"
newMessageExists: "新しいメッセージがあります"
onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです" onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです"
signinRequired: "続行する前に、登録またはログインが必要です" signinRequired: "続行する前に、登録またはログインが必要です"
signinOrContinueOnRemote: "続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります" signinOrContinueOnRemote: "続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります"
@@ -698,6 +695,7 @@ userSaysSomethingAbout: "{name}が「{word}」について何かを言いまし
makeActive: "アクティブにする" makeActive: "アクティブにする"
display: "表示" display: "表示"
copy: "コピー" copy: "コピー"
copiedToClipboard: "クリップボードにコピーされました"
metrics: "メトリクス" metrics: "メトリクス"
overview: "概要" overview: "概要"
logs: "ログ" logs: "ログ"
@@ -1332,6 +1330,44 @@ preferenceSyncConflictChoiceCancel: "同期の有効化をキャンセル"
paste: "ペースト" paste: "ペースト"
emojiPalette: "絵文字パレット" emojiPalette: "絵文字パレット"
postForm: "投稿フォーム" postForm: "投稿フォーム"
textCount: "文字数"
information: "情報"
chat: "チャット"
_chat:
noMessagesYet: "まだメッセージはありません"
newMessage: "新しいメッセージ"
individualChat: "個人チャット"
individualChat_description: "特定ユーザーとの一対一のチャットができます。"
roomChat: "ルームチャット"
roomChat_description: "複数人でのチャットができます。\nまた、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。"
createRoom: "ルームを作成"
inviteUserToChat: "ユーザーを招待してチャットを始めましょう"
yourRooms: "作成したルーム"
joiningRooms: "参加中のルーム"
invitations: "招待"
noInvitations: "招待はありません"
history: "履歴"
noHistory: "履歴はありません"
noRooms: "ルームはありません"
inviteUser: "ユーザーを招待"
join: "参加"
ignore: "無視"
cannotChatWithTheUser: "このユーザーとのチャットを開始できません"
cannotChatWithTheUser_description: "チャットが使えない状態になっているか、相手がチャットを開放していません。"
chatWithThisUser: "チャットする"
thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみチャットを受け付けています。"
thisUserAllowsChatOnlyFromFollowing: "このユーザーはフォローしているユーザーからのみチャットを受け付けています。"
thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみチャットを受け付けています。"
thisUserNotAllowedChatAnyone: "このユーザーは誰からもチャットを受け付けていません。"
chatAllowedUsers: "チャットを許可する相手"
chatAllowedUsers_note: "自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。"
_chatAllowedUsers:
everyone: "誰でも"
followers: "自分のフォロワーのみ"
following: "自分がフォローしているユーザーのみ"
mutual: "相互フォローのユーザーのみ"
none: "誰も許可しない"
_emojiPalette: _emojiPalette:
palettes: "パレット" palettes: "パレット"
@@ -1357,6 +1393,8 @@ _settings:
appearanceBanner: "好みに応じた、クライアントの見た目・表示方法に関する設定が行えます。" appearanceBanner: "好みに応じた、クライアントの見た目・表示方法に関する設定が行えます。"
soundsBanner: "クライアントで再生するサウンドの設定が行えます。" soundsBanner: "クライアントで再生するサウンドの設定が行えます。"
timelineAndNote: "タイムラインとノート" timelineAndNote: "タイムラインとノート"
makeEveryTextElementsSelectable: "全てのテキスト要素を選択可能にする"
makeEveryTextElementsSelectable_description: "有効にすると、一部のシチュエーションでのユーザビリティが低下する場合があります。"
_preferencesProfile: _preferencesProfile:
profileName: "プロファイル名" profileName: "プロファイル名"
@@ -1881,6 +1919,7 @@ _role:
canImportFollowing: "フォローのインポートを許可" canImportFollowing: "フォローのインポートを許可"
canImportMuting: "ミュートのインポートを許可" canImportMuting: "ミュートのインポートを許可"
canImportUserLists: "リストのインポートを許可" canImportUserLists: "リストのインポートを許可"
canChat: "チャットを許可"
_condition: _condition:
roleAssignedTo: "マニュアルロールにアサイン済み" roleAssignedTo: "マニュアルロールにアサイン済み"
isLocal: "ローカルユーザー" isLocal: "ローカルユーザー"
@@ -2286,6 +2325,8 @@ _permissions:
"read:clip-favorite": "クリップのいいねを見る" "read:clip-favorite": "クリップのいいねを見る"
"read:federation": "連合に関する情報を取得する" "read:federation": "連合に関する情報を取得する"
"write:report-abuse": "違反を報告する" "write:report-abuse": "違反を報告する"
"write:chat": "チャットを操作する"
"read:chat": "チャットを閲覧する"
_auth: _auth:
shareAccessTitle: "アプリへのアクセス許可" shareAccessTitle: "アプリへのアクセス許可"
@@ -2584,6 +2625,7 @@ _notification:
achievementEarned: "実績の獲得" achievementEarned: "実績の獲得"
exportCompleted: "エクスポートが完了した" exportCompleted: "エクスポートが完了した"
login: "ログイン" login: "ログイン"
createToken: "アクセストークンの作成"
test: "通知のテスト" test: "通知のテスト"
app: "連携アプリからの通知" app: "連携アプリからの通知"
@@ -2744,10 +2786,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "配布元が信頼できるかを確認した上でインストールしてください。" checkVendorBeforeInstall: "配布元が信頼できるかを確認した上でインストールしてください。"
_plugin: _plugin:
title: "このプラグインをインストールしますか?" title: "このプラグインをインストールしますか?"
metaTitle: "プラグイン情報"
_theme: _theme:
title: "このテーマをインストールしますか?" title: "このテーマをインストールしますか?"
metaTitle: "テーマ情報"
_meta: _meta:
base: "基本のカラースキーム" base: "基本のカラースキーム"
_vendorInfo: _vendorInfo:

View File

@@ -1312,6 +1312,7 @@ federationDisabled: "このサーバーは連合が無効化されてるで。
confirmOnReact: "ツッコむときに確認とる" confirmOnReact: "ツッコむときに確認とる"
reactAreYouSure: "\" {emoji} \" でツッコむ?" reactAreYouSure: "\" {emoji} \" でツッコむ?"
postForm: "投稿フォーム" postForm: "投稿フォーム"
information: "情報"
_settings: _settings:
webhook: "Webhook" webhook: "Webhook"
_accountSettings: _accountSettings:
@@ -2610,10 +2611,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "配ってるとこが信頼できるか確認した上でインストールしてな。" checkVendorBeforeInstall: "配ってるとこが信頼できるか確認した上でインストールしてな。"
_plugin: _plugin:
title: "このプラグイン、インストールする?" title: "このプラグイン、インストールする?"
metaTitle: "プラグイン情報"
_theme: _theme:
title: "このテーマインストールする?" title: "このテーマインストールする?"
metaTitle: "テーマ情報"
_meta: _meta:
base: "" base: ""
_vendorInfo: _vendorInfo:

View File

@@ -655,6 +655,7 @@ replies: "답하기"
renotes: "리노트" renotes: "리노트"
attach: "옇기" attach: "옇기"
surrender: "아이예" surrender: "아이예"
information: "정보"
_delivery: _delivery:
stop: "고만 보내예" stop: "고만 보내예"
_type: _type:

View File

@@ -1330,6 +1330,7 @@ preferenceSyncConflictChoiceDevice: "장치 설정값"
paste: "붙여넣기" paste: "붙여넣기"
emojiPalette: "이모지 팔레트" emojiPalette: "이모지 팔레트"
postForm: "글 입력란" postForm: "글 입력란"
information: "정보"
_emojiPalette: _emojiPalette:
palettes: "팔레트" palettes: "팔레트"
paletteForMain: "메인으로 사용할 팔레트" paletteForMain: "메인으로 사용할 팔레트"
@@ -2650,10 +2651,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "제공자를 신뢰할 수 있는 경우에만 설치하십시오." checkVendorBeforeInstall: "제공자를 신뢰할 수 있는 경우에만 설치하십시오."
_plugin: _plugin:
title: "이 플러그인을 설치하시겠습니까?" title: "이 플러그인을 설치하시겠습니까?"
metaTitle: "플러그인 정보"
_theme: _theme:
title: "이 테마를 설치하시겠습니까?" title: "이 테마를 설치하시겠습니까?"
metaTitle: "테마 정보"
_meta: _meta:
base: "기본 컬러 스키마" base: "기본 컬러 스키마"
_vendorInfo: _vendorInfo:

View File

@@ -394,6 +394,7 @@ searchByGoogle: "ຄົ້ນຫາ"
file: "ໄຟລ໌" file: "ໄຟລ໌"
replies: "ຕອບ​ກັບ" replies: "ຕອບ​ກັບ"
renotes: "Renote" renotes: "Renote"
information: "ກ່ຽວກັບ"
_delivery: _delivery:
stop: "ໂຈະ" stop: "ໂຈະ"
_type: _type:

View File

@@ -462,6 +462,7 @@ loggedInAsBot: "Momenteel als bot ingelogd"
icon: "Avatar" icon: "Avatar"
replies: "Antwoord" replies: "Antwoord"
renotes: "Herdelen" renotes: "Herdelen"
information: "Over"
_delivery: _delivery:
stop: "Opgeschort" stop: "Opgeschort"
_type: _type:

View File

@@ -463,6 +463,7 @@ icon: "Avatar"
replies: "Svar" replies: "Svar"
renotes: "Renote" renotes: "Renote"
surrender: "Avbryt" surrender: "Avbryt"
information: "Informasjon"
_delivery: _delivery:
stop: "Suspendert" stop: "Suspendert"
_initialAccountSetting: _initialAccountSetting:

View File

@@ -1045,6 +1045,7 @@ lastNDays: "W ciągu ostatnich {n} dni"
surrender: "Odrzuć" surrender: "Odrzuć"
gameRetry: "Spróbuj ponownie" gameRetry: "Spróbuj ponownie"
postForm: "Formularz tworzenia wpisu" postForm: "Formularz tworzenia wpisu"
information: "Informacje"
_delivery: _delivery:
stop: "Zawieszono" stop: "Zawieszono"
_type: _type:

View File

@@ -1302,6 +1302,7 @@ pleaseSelectAccount: "Selecione uma conta"
availableRoles: "Cargos disponíveis" availableRoles: "Cargos disponíveis"
acknowledgeNotesAndEnable: "Ative após compreender as precauções." acknowledgeNotesAndEnable: "Ative após compreender as precauções."
postForm: "Campo de postagem" postForm: "Campo de postagem"
information: "Informações"
_settings: _settings:
webhook: "Webhook" webhook: "Webhook"
_accountSettings: _accountSettings:
@@ -2598,10 +2599,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "Tenha certeza de que o distribuidor desse recurso é confiável antes da instalação." checkVendorBeforeInstall: "Tenha certeza de que o distribuidor desse recurso é confiável antes da instalação."
_plugin: _plugin:
title: "Deseja instalar esse plugin?" title: "Deseja instalar esse plugin?"
metaTitle: "Informações do plugin"
_theme: _theme:
title: "Deseja instalar esse tema?" title: "Deseja instalar esse tema?"
metaTitle: "Informações do tema"
_meta: _meta:
base: "Paleta de cores base" base: "Paleta de cores base"
_vendorInfo: _vendorInfo:

View File

@@ -646,6 +646,7 @@ show: "Arată"
icon: "Avatar" icon: "Avatar"
replies: "Răspunde" replies: "Răspunde"
renotes: "Re-notează" renotes: "Re-notează"
information: "Despre"
_delivery: _delivery:
stop: "Suspendat" stop: "Suspendat"
_type: _type:

View File

@@ -1182,6 +1182,7 @@ alwaysConfirmFollow: "Всегда подтверждать подписку"
inquiry: "Связаться" inquiry: "Связаться"
messageToFollower: "Сообщение подписчикам" messageToFollower: "Сообщение подписчикам"
postForm: "Форма отправки" postForm: "Форма отправки"
information: "Описание"
_settings: _settings:
webhook: "Вебхук" webhook: "Вебхук"
_delivery: _delivery:

View File

@@ -918,6 +918,7 @@ sourceCode: "Zdrojový kód"
flip: "Preklopiť" flip: "Preklopiť"
lastNDays: "Posledných {n} dní" lastNDays: "Posledných {n} dní"
postForm: "Napísať poznámku" postForm: "Napísať poznámku"
information: "Informácie"
_delivery: _delivery:
stop: "Zmrazené" stop: "Zmrazené"
_type: _type:

View File

@@ -562,6 +562,7 @@ inquiry: "Kontakt"
tryAgain: "Försök igen senare" tryAgain: "Försök igen senare"
signinWithPasskey: "Logga in med nyckel" signinWithPasskey: "Logga in med nyckel"
unknownWebAuthnKey: "Okänd nyckel" unknownWebAuthnKey: "Okänd nyckel"
information: "Om"
_delivery: _delivery:
stop: "Suspenderad" stop: "Suspenderad"
_type: _type:

View File

@@ -1293,6 +1293,7 @@ prohibitedWordsForNameOfUserDescription: "หากมีสตริงใด
yourNameContainsProhibitedWords: "ชื่อของคุณนั้นมีคำที่ต้องห้าม" yourNameContainsProhibitedWords: "ชื่อของคุณนั้นมีคำที่ต้องห้าม"
yourNameContainsProhibitedWordsDescription: "ถ้าหากคุณต้องการใช้ชื่อนี้ กรุณาติดต่อผู้ดูแลระบบของเซิร์ฟเวอร์นะค่ะ" yourNameContainsProhibitedWordsDescription: "ถ้าหากคุณต้องการใช้ชื่อนี้ กรุณาติดต่อผู้ดูแลระบบของเซิร์ฟเวอร์นะค่ะ"
postForm: "แบบฟอร์มการโพสต์" postForm: "แบบฟอร์มการโพสต์"
information: "เกี่ยวกับ"
_settings: _settings:
webhook: "Webhook" webhook: "Webhook"
_abuseUserReport: _abuseUserReport:
@@ -2572,10 +2573,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "โปรดตรวจสอบให้แน่ใจว่าแหล่งแจกหน่ายมีความน่าเชื่อถือก่อนทำการติดตั้ง" checkVendorBeforeInstall: "โปรดตรวจสอบให้แน่ใจว่าแหล่งแจกหน่ายมีความน่าเชื่อถือก่อนทำการติดตั้ง"
_plugin: _plugin:
title: "ต้องการติดตั้งปลั๊กอินนี้ใช่ไหม?" title: "ต้องการติดตั้งปลั๊กอินนี้ใช่ไหม?"
metaTitle: "ข้อมูลส่วนเสริม"
_theme: _theme:
title: "ต้องการติดตั้งธีมนี้ใช่ไหม?" title: "ต้องการติดตั้งธีมนี้ใช่ไหม?"
metaTitle: "ข้อมูลธีม"
_meta: _meta:
base: "โทนสีพื้นฐาน" base: "โทนสีพื้นฐาน"
_vendorInfo: _vendorInfo:

View File

@@ -910,6 +910,7 @@ sourceCode: "Вихідний код"
flip: "Перевернути" flip: "Перевернути"
lastNDays: "Останні {n} днів" lastNDays: "Останні {n} днів"
postForm: "Створення нотатки" postForm: "Створення нотатки"
information: "Інформація"
_delivery: _delivery:
stop: "Призупинено" stop: "Призупинено"
_type: _type:

View File

@@ -841,6 +841,7 @@ icon: "Avatar"
replies: "Javob berish" replies: "Javob berish"
renotes: "Qayta qayd etish" renotes: "Qayta qayd etish"
flip: "Teskari" flip: "Teskari"
information: "Haqida"
_delivery: _delivery:
stop: "To'xtatilgan" stop: "To'xtatilgan"
_type: _type:

View File

@@ -1120,6 +1120,7 @@ cwNotationRequired: "Nếu \"Ẩn nội dung\" được bật thì cần phải
lastNDays: "{n} ngày trước" lastNDays: "{n} ngày trước"
surrender: "Từ chối" surrender: "Từ chối"
postForm: "Mẫu đăng" postForm: "Mẫu đăng"
information: "Giới thiệu"
_delivery: _delivery:
stop: "Đã vô hiệu hóa" stop: "Đã vô hiệu hóa"
_type: _type:

View File

@@ -698,6 +698,7 @@ userSaysSomethingAbout: "{name} 说了关于「{word}」的什么"
makeActive: "启用" makeActive: "启用"
display: "显示" display: "显示"
copy: "复制" copy: "复制"
copiedToClipboard: "已复制到剪贴板"
metrics: "指标" metrics: "指标"
overview: "概览" overview: "概览"
logs: "日志" logs: "日志"
@@ -746,7 +747,7 @@ confirmToUnclipAlreadyClippedNote: "本帖已包含在便签 \"{name}\" 里。
public: "公开" public: "公开"
private: "私密" private: "私密"
i18nInfo: "Misskey 已经被志愿者们翻译成了各种语言。如果你也有兴趣,可以通过 {link} 帮助翻译。" i18nInfo: "Misskey 已经被志愿者们翻译成了各种语言。如果你也有兴趣,可以通过 {link} 帮助翻译。"
manageAccessTokens: "管理 Access Tokens" manageAccessTokens: "管理访问令牌"
accountInfo: "账户信息" accountInfo: "账户信息"
notesCount: "帖子数量" notesCount: "帖子数量"
repliesCount: "回复数量" repliesCount: "回复数量"
@@ -1332,6 +1333,8 @@ preferenceSyncConflictChoiceCancel: "取消同步"
paste: "粘贴" paste: "粘贴"
emojiPalette: "表情符号调色板" emojiPalette: "表情符号调色板"
postForm: "投稿窗口" postForm: "投稿窗口"
textCount: "字数"
information: "关于"
_emojiPalette: _emojiPalette:
palettes: "调色板" palettes: "调色板"
enableSyncBetweenDevicesForPalettes: "启用调色板的设备间同步" enableSyncBetweenDevicesForPalettes: "启用调色板的设备间同步"
@@ -1339,13 +1342,24 @@ _emojiPalette:
paletteForReaction: "回应用调色板" paletteForReaction: "回应用调色板"
_settings: _settings:
driveBanner: "可在此管理和设置网盘、确认使用量及配置上传文件的设置。" driveBanner: "可在此管理和设置网盘、确认使用量及配置上传文件的设置。"
pluginBanner: "使用插件可以扩展客户端的功能。可以在此安装、单独管理插件。"
notificationsBanner: "可在此设置从服务器接收的通知的种类和范围,以及推送通知的设置。"
api: "API" api: "API"
webhook: "Webhook" webhook: "Webhook"
serviceConnection: "连接服务"
serviceConnectionBanner: "可在此管理用于连接外部应用或服务的访问令牌及 Webhook。"
accountData: "账户数据"
accountDataBanner: "可在此导入或导出帐户数据的存档。"
muteAndBlockBanner: "可在此设置隐藏内容,或限制指定用户能进行的操作。"
accessibilityBanner: "可在此设置客户端的显示及动态效果等辅助设置。"
privacyBanner: "可在此设置如内容可见性、可发现性、批准关注请求等账户隐私设置。" privacyBanner: "可在此设置如内容可见性、可发现性、批准关注请求等账户隐私设置。"
securityBanner: "可在此设置如密码、登入方式、验证器、Passkey 等账户安全性设置。" securityBanner: "可在此设置如密码、登入方式、验证器、Passkey 等账户安全性设置。"
preferencesBanner: "可在此设置客户端的整体运作行为。" preferencesBanner: "可在此设置客户端的整体运作行为。"
appearanceBanner: "可在此设置客户端的外观及显示方式。" appearanceBanner: "可在此设置客户端的外观及显示方式。"
soundsBanner: "可在此设置客户端播放的声音。" soundsBanner: "可在此设置客户端播放的声音。"
timelineAndNote: "时间线和帖子"
makeEveryTextElementsSelectable: "使所有的文字均可选择"
makeEveryTextElementsSelectable_description: "若开启,在某些情况下可能降低用户体验。"
_preferencesProfile: _preferencesProfile:
profileName: "配置名" profileName: "配置名"
profileNameDescription: "请指定用于识别此设备的名称" profileNameDescription: "请指定用于识别此设备的名称"
@@ -2510,6 +2524,7 @@ _notification:
achievementEarned: "取得的成就" achievementEarned: "取得的成就"
exportCompleted: "已完成导出" exportCompleted: "已完成导出"
login: "登录" login: "登录"
createToken: "创建访问令牌"
test: "测试通知" test: "测试通知"
app: "关联应用的通知" app: "关联应用的通知"
_actions: _actions:
@@ -2659,10 +2674,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "请在安装前确保来源可靠" checkVendorBeforeInstall: "请在安装前确保来源可靠"
_plugin: _plugin:
title: "要安装此插件吗?" title: "要安装此插件吗?"
metaTitle: "插件信息"
_theme: _theme:
title: "要安装此主题吗?" title: "要安装此主题吗?"
metaTitle: "主题信息"
_meta: _meta:
base: "基本配色方案" base: "基本配色方案"
_vendorInfo: _vendorInfo:

View File

@@ -698,6 +698,7 @@ userSaysSomethingAbout: "{name} 說了一些關於「{word}」的話"
makeActive: "啟用" makeActive: "啟用"
display: "檢視" display: "檢視"
copy: "複製" copy: "複製"
copiedToClipboard: "已複製到剪貼簿"
metrics: "指標" metrics: "指標"
overview: "概覽" overview: "概覽"
logs: "日誌" logs: "日誌"
@@ -1332,6 +1333,8 @@ preferenceSyncConflictChoiceCancel: "取消啟用同步"
paste: "貼上" paste: "貼上"
emojiPalette: "表情符號調色盤" emojiPalette: "表情符號調色盤"
postForm: "發文視窗" postForm: "發文視窗"
textCount: "字數"
information: "關於"
_emojiPalette: _emojiPalette:
palettes: "調色盤" palettes: "調色盤"
enableSyncBetweenDevicesForPalettes: "啟用裝置與裝置之間的調色盤同步化" enableSyncBetweenDevicesForPalettes: "啟用裝置與裝置之間的調色盤同步化"
@@ -1355,6 +1358,8 @@ _settings:
appearanceBanner: "您可以根據喜好設定與用戶端外觀和顯示方式相關的設定。" appearanceBanner: "您可以根據喜好設定與用戶端外觀和顯示方式相關的設定。"
soundsBanner: "您可以調整用戶端播放的聲音設定。" soundsBanner: "您可以調整用戶端播放的聲音設定。"
timelineAndNote: "時間軸及貼文" timelineAndNote: "時間軸及貼文"
makeEveryTextElementsSelectable: "允許選取所有文字"
makeEveryTextElementsSelectable_description: "啟用此功能後,可能會在某些情境下降低可用性。"
_preferencesProfile: _preferencesProfile:
profileName: "設定檔案名稱" profileName: "設定檔案名稱"
profileNameDescription: "設定一個名稱來識別此裝置。" profileNameDescription: "設定一個名稱來識別此裝置。"
@@ -2519,6 +2524,7 @@ _notification:
achievementEarned: "獲得成就" achievementEarned: "獲得成就"
exportCompleted: "已完成匯出。" exportCompleted: "已完成匯出。"
login: "登入" login: "登入"
createToken: "建立存取權杖"
test: "通知測試" test: "通知測試"
app: "應用程式通知" app: "應用程式通知"
_actions: _actions:
@@ -2668,10 +2674,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "安裝前請確認提供者是可信賴的。" checkVendorBeforeInstall: "安裝前請確認提供者是可信賴的。"
_plugin: _plugin:
title: "要安裝此外掛嘛?" title: "要安裝此外掛嘛?"
metaTitle: "外掛資訊"
_theme: _theme:
title: "要安裝此佈景主題嗎?" title: "要安裝此佈景主題嗎?"
metaTitle: "佈景主題資訊"
_meta: _meta:
base: "基本配色方案" base: "基本配色方案"
_vendorInfo: _vendorInfo:

View File

@@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2025.3.2-beta.1", "version": "2025.3.2-beta.9",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@@ -0,0 +1,46 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class Chat1742203321812 {
name = 'Chat1742203321812'
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "chat_room" ("id" character varying(32) NOT NULL, "name" character varying(256) NOT NULL, "ownerId" character varying(32) NOT NULL, CONSTRAINT "PK_8aa3a52cf74c96469f0ef9fbe3e" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_f0d8ad64243fa2ca2800da0dfd" ON "chat_room" ("ownerId") `);
await queryRunner.query(`CREATE TABLE "chat_message" ("id" character varying(32) NOT NULL, "fromUserId" character varying(32) NOT NULL, "toUserId" character varying(32), "toRoomId" character varying(32), "text" character varying(4096), "uri" character varying(512), "reads" character varying(32) array NOT NULL DEFAULT '{}', "fileId" character varying(32), "reactions" character varying(1024) array NOT NULL DEFAULT '{}', CONSTRAINT "PK_3cc0d85193aade457d3077dd06b" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_79a26e7a4d9afa5e4fc05f134e" ON "chat_message" ("fromUserId") `);
await queryRunner.query(`CREATE INDEX "IDX_25e097b51d7622c249452c6f75" ON "chat_message" ("toUserId") `);
await queryRunner.query(`CREATE INDEX "IDX_f006b8a76efd1abf9f221c175c" ON "chat_message" ("toRoomId") `);
await queryRunner.query(`CREATE TABLE "chat_room_membership" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "roomId" character varying(32) NOT NULL, CONSTRAINT "PK_2bd59c741e571b283c048beb69a" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_d99c5279460fb77ef58c596ce5" ON "chat_room_membership" ("userId") `);
await queryRunner.query(`CREATE INDEX "IDX_c25143ebab714e930aeca1c0e8" ON "chat_room_membership" ("roomId") `);
await queryRunner.query(`ALTER TABLE "chat_room" ADD CONSTRAINT "FK_f0d8ad64243fa2ca2800da0dfd6" FOREIGN KEY ("ownerId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "chat_message" ADD CONSTRAINT "FK_79a26e7a4d9afa5e4fc05f134ed" FOREIGN KEY ("fromUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "chat_message" ADD CONSTRAINT "FK_25e097b51d7622c249452c6f757" FOREIGN KEY ("toUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "chat_message" ADD CONSTRAINT "FK_f006b8a76efd1abf9f221c175ce" FOREIGN KEY ("toRoomId") REFERENCES "chat_room"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "chat_message" ADD CONSTRAINT "FK_fd0f9a4879430239715ad4f8e2a" FOREIGN KEY ("fileId") REFERENCES "drive_file"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "chat_room_membership" ADD CONSTRAINT "FK_d99c5279460fb77ef58c596ce51" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "chat_room_membership" ADD CONSTRAINT "FK_c25143ebab714e930aeca1c0e8d" FOREIGN KEY ("roomId") REFERENCES "chat_room"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "chat_room_membership" DROP CONSTRAINT "FK_c25143ebab714e930aeca1c0e8d"`);
await queryRunner.query(`ALTER TABLE "chat_room_membership" DROP CONSTRAINT "FK_d99c5279460fb77ef58c596ce51"`);
await queryRunner.query(`ALTER TABLE "chat_message" DROP CONSTRAINT "FK_fd0f9a4879430239715ad4f8e2a"`);
await queryRunner.query(`ALTER TABLE "chat_message" DROP CONSTRAINT "FK_f006b8a76efd1abf9f221c175ce"`);
await queryRunner.query(`ALTER TABLE "chat_message" DROP CONSTRAINT "FK_25e097b51d7622c249452c6f757"`);
await queryRunner.query(`ALTER TABLE "chat_message" DROP CONSTRAINT "FK_79a26e7a4d9afa5e4fc05f134ed"`);
await queryRunner.query(`ALTER TABLE "chat_room" DROP CONSTRAINT "FK_f0d8ad64243fa2ca2800da0dfd6"`);
await queryRunner.query(`DROP INDEX "public"."IDX_c25143ebab714e930aeca1c0e8"`);
await queryRunner.query(`DROP INDEX "public"."IDX_d99c5279460fb77ef58c596ce5"`);
await queryRunner.query(`DROP TABLE "chat_room_membership"`);
await queryRunner.query(`DROP INDEX "public"."IDX_f006b8a76efd1abf9f221c175c"`);
await queryRunner.query(`DROP INDEX "public"."IDX_25e097b51d7622c249452c6f75"`);
await queryRunner.query(`DROP INDEX "public"."IDX_79a26e7a4d9afa5e4fc05f134e"`);
await queryRunner.query(`DROP TABLE "chat_message"`);
await queryRunner.query(`DROP INDEX "public"."IDX_f0d8ad64243fa2ca2800da0dfd"`);
await queryRunner.query(`DROP TABLE "chat_room"`);
}
}

View File

@@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class Chat21742608337548 {
name = 'Chat21742608337548'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" ADD "chatScope" character varying(128) NOT NULL DEFAULT 'mutual'`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_185b6b5afa707b5d36d1ce3144" ON "chat_room_membership" ("userId", "roomId") `);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_185b6b5afa707b5d36d1ce3144"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "chatScope"`);
}
}

View File

@@ -0,0 +1,26 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class Chat31742617546147 {
name = 'Chat31742617546147'
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "chat_approval" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "otherId" character varying(32) NOT NULL, CONSTRAINT "PK_fbbb95d60acf5c85388345b5f5d" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_530257863e1381a7f2f1d3282f" ON "chat_approval" ("userId") `);
await queryRunner.query(`CREATE INDEX "IDX_b1d46037f23d170da5c05fdf75" ON "chat_approval" ("otherId") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_12c4768a2f706fc267f2078903" ON "chat_approval" ("userId", "otherId") `);
await queryRunner.query(`ALTER TABLE "chat_approval" ADD CONSTRAINT "FK_530257863e1381a7f2f1d3282fe" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "chat_approval" ADD CONSTRAINT "FK_b1d46037f23d170da5c05fdf755" FOREIGN KEY ("otherId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "chat_approval" DROP CONSTRAINT "FK_b1d46037f23d170da5c05fdf755"`);
await queryRunner.query(`ALTER TABLE "chat_approval" DROP CONSTRAINT "FK_530257863e1381a7f2f1d3282fe"`);
await queryRunner.query(`DROP INDEX "public"."IDX_12c4768a2f706fc267f2078903"`);
await queryRunner.query(`DROP INDEX "public"."IDX_b1d46037f23d170da5c05fdf75"`);
await queryRunner.query(`DROP INDEX "public"."IDX_530257863e1381a7f2f1d3282f"`);
await queryRunner.query(`DROP TABLE "chat_approval"`);
}
}

View File

@@ -0,0 +1,26 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class Chat41742707840715 {
name = 'Chat41742707840715'
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "chat_room_invitation" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "roomId" character varying(32) NOT NULL, CONSTRAINT "PK_9d489521a312dd28225672de2dc" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_8552bb38e7ed038c5bdd398a38" ON "chat_room_invitation" ("userId") `);
await queryRunner.query(`CREATE INDEX "IDX_5f265075b215fc390a57523b12" ON "chat_room_invitation" ("roomId") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_044f2a7962b8ee5bbfaa02e8a3" ON "chat_room_invitation" ("userId", "roomId") `);
await queryRunner.query(`ALTER TABLE "chat_room_invitation" ADD CONSTRAINT "FK_8552bb38e7ed038c5bdd398a384" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "chat_room_invitation" ADD CONSTRAINT "FK_5f265075b215fc390a57523b12a" FOREIGN KEY ("roomId") REFERENCES "chat_room"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "chat_room_invitation" DROP CONSTRAINT "FK_5f265075b215fc390a57523b12a"`);
await queryRunner.query(`ALTER TABLE "chat_room_invitation" DROP CONSTRAINT "FK_8552bb38e7ed038c5bdd398a384"`);
await queryRunner.query(`DROP INDEX "public"."IDX_044f2a7962b8ee5bbfaa02e8a3"`);
await queryRunner.query(`DROP INDEX "public"."IDX_5f265075b215fc390a57523b12"`);
await queryRunner.query(`DROP INDEX "public"."IDX_8552bb38e7ed038c5bdd398a38"`);
await queryRunner.query(`DROP TABLE "chat_room_invitation"`);
}
}

View File

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

View File

@@ -37,17 +37,17 @@
}, },
"optionalDependencies": { "optionalDependencies": {
"@swc/core-android-arm64": "1.3.11", "@swc/core-android-arm64": "1.3.11",
"@swc/core-darwin-arm64": "1.10.16", "@swc/core-darwin-arm64": "1.11.11",
"@swc/core-darwin-x64": "1.10.16", "@swc/core-darwin-x64": "1.11.11",
"@swc/core-freebsd-x64": "1.3.11", "@swc/core-freebsd-x64": "1.3.11",
"@swc/core-linux-arm-gnueabihf": "1.10.16", "@swc/core-linux-arm-gnueabihf": "1.11.11",
"@swc/core-linux-arm64-gnu": "1.10.16", "@swc/core-linux-arm64-gnu": "1.11.11",
"@swc/core-linux-arm64-musl": "1.10.16", "@swc/core-linux-arm64-musl": "1.11.11",
"@swc/core-linux-x64-gnu": "1.10.16", "@swc/core-linux-x64-gnu": "1.11.11",
"@swc/core-linux-x64-musl": "1.10.16", "@swc/core-linux-x64-musl": "1.11.11",
"@swc/core-win32-arm64-msvc": "1.10.16", "@swc/core-win32-arm64-msvc": "1.11.11",
"@swc/core-win32-ia32-msvc": "1.10.16", "@swc/core-win32-ia32-msvc": "1.11.11",
"@swc/core-win32-x64-msvc": "1.10.16", "@swc/core-win32-x64-msvc": "1.11.11",
"@tensorflow/tfjs": "4.22.0", "@tensorflow/tfjs": "4.22.0",
"@tensorflow/tfjs-node": "4.22.0", "@tensorflow/tfjs-node": "4.22.0",
"bufferutil": "4.0.9", "bufferutil": "4.0.9",
@@ -67,26 +67,23 @@
"utf-8-validate": "6.0.5" "utf-8-validate": "6.0.5"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "3.749.0", "@aws-sdk/client-s3": "3.772.0",
"@aws-sdk/lib-storage": "3.749.0", "@aws-sdk/lib-storage": "3.772.0",
"@bull-board/api": "6.7.7",
"@bull-board/fastify": "6.7.7",
"@bull-board/ui": "6.7.7",
"@discordapp/twemoji": "15.1.0", "@discordapp/twemoji": "15.1.0",
"@fastify/accepts": "5.0.2", "@fastify/accepts": "5.0.2",
"@fastify/cookie": "11.0.2", "@fastify/cookie": "11.0.2",
"@fastify/cors": "10.0.2", "@fastify/cors": "10.1.0",
"@fastify/express": "4.0.2", "@fastify/express": "4.0.2",
"@fastify/http-proxy": "10.0.2", "@fastify/http-proxy": "10.0.2",
"@fastify/multipart": "9.0.3", "@fastify/multipart": "9.0.3",
"@fastify/static": "8.1.0", "@fastify/static": "8.1.1",
"@fastify/view": "10.0.2", "@fastify/view": "10.0.2",
"@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.2.0", "@misskey-dev/summaly": "5.2.0",
"@napi-rs/canvas": "0.1.67", "@napi-rs/canvas": "0.1.68",
"@nestjs/common": "11.0.9", "@nestjs/common": "11.0.12",
"@nestjs/core": "11.0.9", "@nestjs/core": "11.0.12",
"@nestjs/testing": "11.0.9", "@nestjs/testing": "11.0.12",
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@sentry/node": "8.55.0", "@sentry/node": "8.55.0",
"@sentry/profiling-node": "8.55.0", "@sentry/profiling-node": "8.55.0",
@@ -94,7 +91,7 @@
"@sinonjs/fake-timers": "11.3.1", "@sinonjs/fake-timers": "11.3.1",
"@smithy/node-http-handler": "2.5.0", "@smithy/node-http-handler": "2.5.0",
"@swc/cli": "0.6.0", "@swc/cli": "0.6.0",
"@swc/core": "1.10.16", "@swc/core": "1.11.11",
"@twemoji/parser": "15.1.1", "@twemoji/parser": "15.1.1",
"accepts": "1.3.8", "accepts": "1.3.8",
"ajv": "8.17.1", "ajv": "8.17.1",
@@ -103,7 +100,7 @@
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"body-parser": "1.20.3", "body-parser": "1.20.3",
"bullmq": "5.41.1", "bullmq": "5.44.1",
"cacheable-lookup": "7.0.0", "cacheable-lookup": "7.0.0",
"cbor": "9.0.2", "cbor": "9.0.2",
"chalk": "5.4.1", "chalk": "5.4.1",
@@ -125,7 +122,7 @@
"hpagent": "1.2.0", "hpagent": "1.2.0",
"htmlescape": "1.1.1", "htmlescape": "1.1.1",
"http-link-header": "1.1.3", "http-link-header": "1.1.3",
"ioredis": "5.5.0", "ioredis": "5.6.0",
"ip-cidr": "4.0.2", "ip-cidr": "4.0.2",
"ipaddr.js": "2.2.0", "ipaddr.js": "2.2.0",
"is-svg": "5.1.0", "is-svg": "5.1.0",
@@ -134,26 +131,26 @@
"json5": "2.2.3", "json5": "2.2.3",
"jsonld": "8.3.3", "jsonld": "8.3.3",
"jsrsasign": "11.1.0", "jsrsasign": "11.1.0",
"juice": "11.0.0", "juice": "11.0.1",
"meilisearch": "0.48.2", "meilisearch": "0.49.0",
"mfm-js": "0.24.0", "mfm-js": "0.24.0",
"microformats-parser": "2.0.2", "microformats-parser": "2.0.2",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"misskey-js": "workspace:*", "misskey-js": "workspace:*",
"misskey-reversi": "workspace:*", "misskey-reversi": "workspace:*",
"ms": "3.0.0-canary.1", "ms": "3.0.0-canary.1",
"nanoid": "5.1.0", "nanoid": "5.1.5",
"nested-property": "4.0.0", "nested-property": "4.0.0",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"nodemailer": "6.10.0", "nodemailer": "6.10.0",
"nsfwjs": "4.2.0", "nsfwjs": "4.2.0",
"oauth": "0.10.0", "oauth": "0.10.2",
"oauth2orize": "1.12.0", "oauth2orize": "1.12.0",
"oauth2orize-pkce": "0.1.2", "oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14", "os-utils": "0.0.14",
"otpauth": "9.3.6", "otpauth": "9.3.6",
"parse5": "7.2.1", "parse5": "7.2.1",
"pg": "8.13.3", "pg": "8.14.1",
"pkce-challenge": "4.1.0", "pkce-challenge": "4.1.0",
"probe-image-size": "7.2.3", "probe-image-size": "7.2.3",
"promise-limit": "2.7.0", "promise-limit": "2.7.0",
@@ -166,8 +163,8 @@
"reflect-metadata": "0.2.2", "reflect-metadata": "0.2.2",
"rename": "1.0.4", "rename": "1.0.4",
"rss-parser": "3.13.0", "rss-parser": "3.13.0",
"rxjs": "7.8.1", "rxjs": "7.8.2",
"sanitize-html": "2.14.0", "sanitize-html": "2.15.0",
"secure-json-parse": "3.0.2", "secure-json-parse": "3.0.2",
"sharp": "0.33.5", "sharp": "0.33.5",
"slacc": "0.0.10", "slacc": "0.0.10",
@@ -176,14 +173,14 @@
"systeminformation": "5.25.11", "systeminformation": "5.25.11",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tmp": "0.2.3", "tmp": "0.2.3",
"tsc-alias": "1.8.10", "tsc-alias": "1.8.11",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"typeorm": "0.3.20", "typeorm": "0.3.21",
"typescript": "5.7.3", "typescript": "5.8.2",
"ulid": "2.3.0", "ulid": "2.4.0",
"vary": "1.1.2", "vary": "1.1.2",
"web-push": "3.6.7", "web-push": "3.6.7",
"ws": "8.18.0", "ws": "8.18.1",
"xev": "3.0.2" "xev": "3.0.2"
}, },
"devDependencies": { "devDependencies": {
@@ -207,7 +204,7 @@
"@types/jsrsasign": "10.5.15", "@types/jsrsasign": "10.5.15",
"@types/mime-types": "2.1.4", "@types/mime-types": "2.1.4",
"@types/ms": "0.7.34", "@types/ms": "0.7.34",
"@types/node": "22.13.4", "@types/node": "22.13.10",
"@types/nodemailer": "6.4.17", "@types/nodemailer": "6.4.17",
"@types/oauth": "0.9.6", "@types/oauth": "0.9.6",
"@types/oauth2orize": "1.11.5", "@types/oauth2orize": "1.11.5",
@@ -226,9 +223,9 @@
"@types/tmp": "0.2.6", "@types/tmp": "0.2.6",
"@types/vary": "1.1.3", "@types/vary": "1.1.3",
"@types/web-push": "3.6.4", "@types/web-push": "3.6.4",
"@types/ws": "8.5.14", "@types/ws": "8.18.0",
"@typescript-eslint/eslint-plugin": "8.24.0", "@typescript-eslint/eslint-plugin": "8.27.0",
"@typescript-eslint/parser": "8.24.0", "@typescript-eslint/parser": "8.27.0",
"aws-sdk-client-mock": "4.1.0", "aws-sdk-client-mock": "4.1.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint-plugin-import": "2.31.0", "eslint-plugin-import": "2.31.0",

View File

@@ -0,0 +1,602 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import { Brackets } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { QueueService } from '@/core/QueueService.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { PushNotificationService } from '@/core/PushNotificationService.js';
import { bindThis } from '@/decorators.js';
import type { ChatApprovalsRepository, ChatMessagesRepository, ChatRoomInvitationsRepository, ChatRoomMembershipsRepository, ChatRoomsRepository, MiChatMessage, MiChatRoom, MiChatRoomMembership, MiDriveFile, MiUser, MutingsRepository, UsersRepository } from '@/models/_.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import { QueryService } from '@/core/QueryService.js';
import { RoleService } from '@/core/RoleService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
import { MiChatRoomInvitation } from '@/models/ChatRoomInvitation.js';
import { Packed } from '@/misc/json-schema.js';
const MAX_ROOM_MEMBERS = 30;
@Injectable()
export class ChatService {
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.chatMessagesRepository)
private chatMessagesRepository: ChatMessagesRepository,
@Inject(DI.chatApprovalsRepository)
private chatApprovalsRepository: ChatApprovalsRepository,
@Inject(DI.chatRoomsRepository)
private chatRoomsRepository: ChatRoomsRepository,
@Inject(DI.chatRoomInvitationsRepository)
private chatRoomInvitationsRepository: ChatRoomInvitationsRepository,
@Inject(DI.chatRoomMembershipsRepository)
private chatRoomMembershipsRepository: ChatRoomMembershipsRepository,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
private userEntityService: UserEntityService,
private chatEntityService: ChatEntityService,
private idService: IdService,
private globalEventService: GlobalEventService,
private apRendererService: ApRendererService,
private queueService: QueueService,
private pushNotificationService: PushNotificationService,
private userBlockingService: UserBlockingService,
private queryService: QueryService,
private roleService: RoleService,
private userFollowingService: UserFollowingService,
) {
}
@bindThis
public async createMessageToUser(fromUser: { id: MiUser['id']; host: MiUser['host']; }, toUser: MiUser, params: {
text?: string | null;
file?: MiDriveFile | null;
uri?: string | null;
}): Promise<Packed<'ChatMessageLite'>> {
if (fromUser.id === toUser.id) {
throw new Error('yourself');
}
const approvals = await this.chatApprovalsRepository.createQueryBuilder('approval')
.where(new Brackets(qb => { // 自分が相手を許可しているか
qb.where('approval.userId = :fromUserId', { fromUserId: fromUser.id })
.andWhere('approval.otherId = :toUserId', { toUserId: toUser.id });
}))
.orWhere(new Brackets(qb => { // 相手が自分を許可しているか
qb.where('approval.userId = :toUserId', { toUserId: toUser.id })
.andWhere('approval.otherId = :fromUserId', { fromUserId: fromUser.id });
}))
.take(2)
.getMany();
const otherApprovedMe = approvals.some(approval => approval.userId === toUser.id);
const iApprovedOther = approvals.some(approval => approval.userId === fromUser.id);
if (!otherApprovedMe) {
if (toUser.chatScope === 'none') {
throw new Error('recipient is cannot chat (none)');
} else if (toUser.chatScope === 'followers') {
const isFollower = await this.userFollowingService.isFollowing(fromUser.id, toUser.id);
if (!isFollower) {
throw new Error('recipient is cannot chat (followers)');
}
} else if (toUser.chatScope === 'following') {
const isFollowing = await this.userFollowingService.isFollowing(toUser.id, fromUser.id);
if (!isFollowing) {
throw new Error('recipient is cannot chat (following)');
}
} else if (toUser.chatScope === 'mutual') {
const isMutual = await this.userFollowingService.isMutual(fromUser.id, toUser.id);
if (!isMutual) {
throw new Error('recipient is cannot chat (mutual)');
}
}
}
if (!(await this.roleService.getUserPolicies(toUser.id)).canChat) {
throw new Error('recipient is cannot chat (policy)');
}
const blocked = await this.userBlockingService.checkBlocked(toUser.id, fromUser.id);
if (blocked) {
throw new Error('blocked');
}
const message = {
id: this.idService.gen(),
fromUserId: fromUser.id,
toUserId: toUser.id,
text: params.text ? params.text.trim() : null,
fileId: params.file ? params.file.id : null,
reads: [],
uri: params.uri ?? null,
} satisfies Partial<MiChatMessage>;
const inserted = await this.chatMessagesRepository.insertOne(message);
// 相手を許可しておく
if (!iApprovedOther) {
this.chatApprovalsRepository.insertOne({
id: this.idService.gen(),
userId: fromUser.id,
otherId: toUser.id,
});
}
const packedMessage = await this.chatEntityService.packMessageLite(inserted);
if (this.userEntityService.isLocalUser(toUser)) {
const redisPipeline = this.redisClient.pipeline();
redisPipeline.set(`newUserChatMessageExists:${toUser.id}:${fromUser.id}`, message.id);
redisPipeline.sadd(`newChatMessagesExists:${toUser.id}`, `user:${fromUser.id}`);
redisPipeline.exec();
}
if (this.userEntityService.isLocalUser(fromUser)) {
// 自分のストリーム
this.globalEventService.publishChatUserStream(fromUser.id, toUser.id, 'message', packedMessage);
}
if (this.userEntityService.isLocalUser(toUser)) {
// 相手のストリーム
this.globalEventService.publishChatUserStream(toUser.id, fromUser.id, 'message', packedMessage);
}
// 3秒経っても既読にならなかったらイベント発行
if (this.userEntityService.isLocalUser(toUser)) {
setTimeout(async () => {
const marker = await this.redisClient.get(`newUserChatMessageExists:${toUser.id}:${fromUser.id}`);
if (marker == null) return; // 既読
const packedMessageForTo = await this.chatEntityService.packMessageDetailed(inserted, toUser);
this.globalEventService.publishMainStream(toUser.id, 'newChatMessage', packedMessageForTo);
this.pushNotificationService.pushNotification(toUser.id, 'newChatMessage', packedMessageForTo);
}, 3000);
}
return packedMessage;
}
@bindThis
public async createMessageToRoom(fromUser: { id: MiUser['id']; host: MiUser['host']; }, toRoom: MiChatRoom, params: {
text?: string | null;
file?: MiDriveFile | null;
uri?: string | null;
}): Promise<Packed<'ChatMessageLite'>> {
const memberships = await this.chatRoomMembershipsRepository.findBy({ roomId: toRoom.id });
if (toRoom.ownerId !== fromUser.id && !memberships.some(member => member.userId === fromUser.id)) {
throw new Error('you are not a member of the room');
}
const message = {
id: this.idService.gen(),
fromUserId: fromUser.id,
toRoomId: toRoom.id,
text: params.text ? params.text.trim() : null,
fileId: params.file ? params.file.id : null,
reads: [],
uri: params.uri ?? null,
} satisfies Partial<MiChatMessage>;
const inserted = await this.chatMessagesRepository.insertOne(message);
const packedMessage = await this.chatEntityService.packMessageLiteForRoom(inserted);
this.globalEventService.publishChatRoomStream(toRoom.id, 'message', packedMessage);
const redisPipeline = this.redisClient.pipeline();
for (const membership of memberships) {
redisPipeline.set(`newRoomChatMessageExists:${membership.userId}:${toRoom.id}`, message.id);
redisPipeline.sadd(`newChatMessagesExists:${membership.userId}`, `room:${toRoom.id}`);
}
redisPipeline.exec();
// 3秒経っても既読にならなかったらイベント発行
setTimeout(async () => {
const redisPipeline = this.redisClient.pipeline();
for (const membership of memberships) {
redisPipeline.get(`newRoomChatMessageExists:${membership.userId}:${toRoom.id}`);
}
const markers = await redisPipeline.exec();
if (markers.every(marker => marker[1] == null)) return;
const packedMessageForTo = await this.chatEntityService.packMessageDetailed(inserted);
for (let i = 0; i < memberships.length; i++) {
const marker = markers[i][1];
if (marker == null) continue;
this.globalEventService.publishMainStream(memberships[i].userId, 'newChatMessage', packedMessageForTo);
this.pushNotificationService.pushNotification(memberships[i].userId, 'newChatMessage', packedMessageForTo);
}
}, 3000);
return packedMessage;
}
@bindThis
public async readUserChatMessage(
readerId: MiUser['id'],
senderId: MiUser['id'],
): Promise<void> {
const redisPipeline = this.redisClient.pipeline();
redisPipeline.del(`newUserChatMessageExists:${readerId}:${senderId}`);
redisPipeline.srem(`newChatMessagesExists:${readerId}`, `user:${senderId}`);
await redisPipeline.exec();
}
@bindThis
public async readRoomChatMessage(
readerId: MiUser['id'],
roomId: MiChatRoom['id'],
): Promise<void> {
const redisPipeline = this.redisClient.pipeline();
redisPipeline.del(`newRoomChatMessageExists:${readerId}:${roomId}`);
redisPipeline.srem(`newChatMessagesExists:${readerId}`, `room:${roomId}`);
await redisPipeline.exec();
}
@bindThis
public findMessageById(messageId: MiChatMessage['id']) {
return this.chatMessagesRepository.findOneBy({ id: messageId });
}
@bindThis
public findMyMessageById(userId: MiUser['id'], messageId: MiChatMessage['id']) {
return this.chatMessagesRepository.findOneBy({ id: messageId, fromUserId: userId });
}
@bindThis
public async deleteMessage(message: MiChatMessage) {
await this.chatMessagesRepository.delete(message.id);
if (message.toUserId) {
const [fromUser, toUser] = await Promise.all([
this.usersRepository.findOneByOrFail({ id: message.fromUserId }),
this.usersRepository.findOneByOrFail({ id: message.toUserId }),
]);
if (this.userEntityService.isLocalUser(fromUser)) this.globalEventService.publishChatUserStream(message.fromUserId, message.toUserId, 'deleted', message.id);
if (this.userEntityService.isLocalUser(toUser)) this.globalEventService.publishChatUserStream(message.toUserId, message.fromUserId, 'deleted', message.id);
if (this.userEntityService.isLocalUser(fromUser) && this.userEntityService.isRemoteUser(toUser)) {
//const activity = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${message.id}`), fromUser));
//this.queueService.deliver(fromUser, activity, toUser.inbox);
}
} else if (message.toRoomId) {
this.globalEventService.publishChatRoomStream(message.toRoomId, 'deleted', message.id);
}
}
@bindThis
public async userTimeline(meId: MiUser['id'], otherId: MiUser['id'], limit: number, sinceId?: MiChatMessage['id'] | null, untilId?: MiChatMessage['id'] | null) {
const query = this.queryService.makePaginationQuery(this.chatMessagesRepository.createQueryBuilder('message'), sinceId, untilId)
.andWhere(new Brackets(qb => {
qb
.where(new Brackets(qb => {
qb
.where('message.fromUserId = :meId')
.andWhere('message.toUserId = :otherId');
}))
.orWhere(new Brackets(qb => {
qb
.where('message.fromUserId = :otherId')
.andWhere('message.toUserId = :meId');
}));
}))
.setParameter('meId', meId)
.setParameter('otherId', otherId);
const messages = await query.take(limit).getMany();
return messages;
}
@bindThis
public async roomTimeline(roomId: MiChatRoom['id'], limit: number, sinceId?: MiChatMessage['id'] | null, untilId?: MiChatMessage['id'] | null) {
const query = this.queryService.makePaginationQuery(this.chatMessagesRepository.createQueryBuilder('message'), sinceId, untilId)
.where('message.toRoomId = :roomId', { roomId });
const messages = await query.take(limit).getMany();
return messages;
}
@bindThis
public async userHistory(meId: MiUser['id'], limit: number): Promise<MiChatMessage[]> {
const history: MiChatMessage[] = [];
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
.select('muting.muteeId')
.where('muting.muterId = :muterId', { muterId: meId });
for (let i = 0; i < limit; i++) {
const found = history.map(m => (m.fromUserId === meId) ? m.toUserId! : m.fromUserId!);
const query = this.chatMessagesRepository.createQueryBuilder('message')
.orderBy('message.id', 'DESC')
.where(new Brackets(qb => {
qb
.where('message.fromUserId = :meId', { meId: meId })
.orWhere('message.toUserId = :meId', { meId: meId });
}))
.andWhere('message.toRoomId IS NULL')
.andWhere(`message.fromUserId NOT IN (${ mutingQuery.getQuery() })`)
.andWhere(`message.toUserId NOT IN (${ mutingQuery.getQuery() })`);
if (found.length > 0) {
query.andWhere('message.fromUserId NOT IN (:...found)', { found: found });
query.andWhere('message.toUserId NOT IN (:...found)', { found: found });
}
query.setParameters(mutingQuery.getParameters());
const message = await query.getOne();
if (message) {
history.push(message);
} else {
break;
}
}
return history;
}
@bindThis
public async roomHistory(meId: MiUser['id'], limit: number): Promise<MiChatMessage[]> {
// TODO: 一回のクエリにまとめられるかも
const [memberRoomIds, ownedRoomIds] = await Promise.all([
this.chatRoomMembershipsRepository.findBy({
userId: meId,
}).then(xs => xs.map(x => x.roomId)),
this.chatRoomsRepository.findBy({
ownerId: meId,
}).then(xs => xs.map(x => x.id)),
]);
const roomIds = memberRoomIds.concat(ownedRoomIds);
if (memberRoomIds.length === 0 && ownedRoomIds.length === 0) {
return [];
}
const history: MiChatMessage[] = [];
for (let i = 0; i < limit; i++) {
const found = history.map(m => m.toRoomId!);
const query = this.chatMessagesRepository.createQueryBuilder('message')
.orderBy('message.id', 'DESC')
.where('message.toRoomId IN (:...roomIds)', { roomIds });
if (found.length > 0) {
query.andWhere('message.toRoomId NOT IN (:...found)', { found: found });
}
const message = await query.getOne();
if (message) {
history.push(message);
} else {
break;
}
}
return history;
}
@bindThis
public async getUserReadStateMap(userId: MiUser['id'], otherIds: MiUser['id'][]) {
const readStateMap: Record<MiUser['id'], boolean> = {};
const redisPipeline = this.redisClient.pipeline();
for (const otherId of otherIds) {
redisPipeline.get(`newUserChatMessageExists:${userId}:${otherId}`);
}
const markers = await redisPipeline.exec();
for (let i = 0; i < otherIds.length; i++) {
const marker = markers[i][1];
readStateMap[otherIds[i]] = marker == null;
}
return readStateMap;
}
@bindThis
public async getRoomReadStateMap(userId: MiUser['id'], roomIds: MiChatRoom['id'][]) {
const readStateMap: Record<MiChatRoom['id'], boolean> = {};
const redisPipeline = this.redisClient.pipeline();
for (const roomId of roomIds) {
redisPipeline.get(`newRoomChatMessageExists:${userId}:${roomId}`);
}
const markers = await redisPipeline.exec();
for (let i = 0; i < roomIds.length; i++) {
const marker = markers[i][1];
readStateMap[roomIds[i]] = marker == null;
}
return readStateMap;
}
@bindThis
public async hasUnreadMessages(userId: MiUser['id']) {
const card = await this.redisClient.scard(`newChatMessagesExists:${userId}`);
return card > 0;
}
@bindThis
public async createRoom(owner: MiUser, name: string) {
const room = {
id: this.idService.gen(),
name: name,
ownerId: owner.id,
} satisfies Partial<MiChatRoom>;
const created = await this.chatRoomsRepository.insertOne(room);
return created;
}
@bindThis
public async deleteRoom(room: MiChatRoom) {
await this.chatRoomsRepository.delete(room.id);
}
@bindThis
public async findMyRoomById(ownerId: MiUser['id'], roomId: MiChatRoom['id']) {
return this.chatRoomsRepository.findOneBy({ id: roomId, ownerId: ownerId });
}
@bindThis
public async findRoomById(roomId: MiChatRoom['id']) {
return this.chatRoomsRepository.findOneBy({ id: roomId });
}
@bindThis
public async isRoomMember(roomId: MiChatRoom['id'], userId: MiUser['id']) {
const membership = await this.chatRoomMembershipsRepository.findOneBy({ roomId, userId });
return membership != null;
}
@bindThis
public async createRoomInvitation(inviterId: MiUser['id'], roomId: MiChatRoom['id'], inviteeId: MiUser['id']) {
if (inviterId === inviteeId) {
throw new Error('yourself');
}
const room = await this.chatRoomsRepository.findOneByOrFail({ id: roomId, ownerId: inviterId });
const existingInvitation = await this.chatRoomInvitationsRepository.findOneBy({ roomId, userId: inviteeId });
if (existingInvitation) {
throw new Error('already invited');
}
const membershipsCount = await this.chatRoomMembershipsRepository.countBy({ roomId });
if (membershipsCount >= MAX_ROOM_MEMBERS) {
throw new Error('room is full');
}
// TODO: cehck block
const invitation = {
id: this.idService.gen(),
roomId: room.id,
userId: inviteeId,
} satisfies Partial<MiChatRoomInvitation>;
const created = await this.chatRoomInvitationsRepository.insertOne(invitation);
return created;
}
@bindThis
public async getOwnedRoomsWithPagination(ownerId: MiUser['id'], limit: number, sinceId?: MiChatRoom['id'] | null, untilId?: MiChatRoom['id'] | null) {
const query = this.queryService.makePaginationQuery(this.chatRoomsRepository.createQueryBuilder('room'), sinceId, untilId)
.where('room.ownerId = :ownerId', { ownerId });
const rooms = await query.take(limit).getMany();
return rooms;
}
@bindThis
public async getReceivedRoomInvitationsWithPagination(userId: MiUser['id'], limit: number, sinceId?: MiChatRoomInvitation['id'] | null, untilId?: MiChatRoomInvitation['id'] | null) {
const query = this.queryService.makePaginationQuery(this.chatRoomInvitationsRepository.createQueryBuilder('invitation'), sinceId, untilId)
.where('invitation.userId = :userId', { userId })
.andWhere('invitation.ignored = FALSE');
const invitations = await query.take(limit).getMany();
return invitations;
}
@bindThis
public async joinToRoom(userId: MiUser['id'], roomId: MiChatRoom['id']) {
const invitation = await this.chatRoomInvitationsRepository.findOneByOrFail({ roomId, userId });
const membershipsCount = await this.chatRoomMembershipsRepository.countBy({ roomId });
if (membershipsCount >= MAX_ROOM_MEMBERS) {
throw new Error('room is full');
}
const membership = {
id: this.idService.gen(),
roomId: roomId,
userId: userId,
} satisfies Partial<MiChatRoomMembership>;
// TODO: transaction
await this.chatRoomMembershipsRepository.insertOne(membership);
await this.chatRoomInvitationsRepository.delete(invitation.id);
}
@bindThis
public async ignoreRoomInvitation(userId: MiUser['id'], roomId: MiChatRoom['id']) {
const invitation = await this.chatRoomInvitationsRepository.findOneByOrFail({ roomId, userId });
await this.chatRoomInvitationsRepository.update(invitation.id, { ignored: true });
}
@bindThis
public async leaveRoom(userId: MiUser['id'], roomId: MiChatRoom['id']) {
const membership = await this.chatRoomMembershipsRepository.findOneByOrFail({ roomId, userId });
await this.chatRoomMembershipsRepository.delete(membership.id);
}
@bindThis
public async updateRoom(room: MiChatRoom, params: {
name?: string;
}): Promise<MiChatRoom> {
return this.chatRoomsRepository.createQueryBuilder().update()
.set(params)
.where('id = :id', { id: room.id })
.returning('*')
.execute()
.then((response) => {
return response.raw[0];
});
}
@bindThis
public async getRoomMembershipsWithPagination(roomId: MiChatRoom['id'], limit: number, sinceId?: MiChatRoomMembership['id'] | null, untilId?: MiChatRoomMembership['id'] | null) {
const query = this.queryService.makePaginationQuery(this.chatRoomMembershipsRepository.createQueryBuilder('membership'), sinceId, untilId)
.where('membership.roomId = :roomId', { roomId });
const memberships = await query.take(limit).getMany();
return memberships;
}
}

View File

@@ -44,7 +44,6 @@ import { ModerationLogService } from './ModerationLogService.js';
import { NoteCreateService } from './NoteCreateService.js'; import { NoteCreateService } from './NoteCreateService.js';
import { NoteDeleteService } from './NoteDeleteService.js'; import { NoteDeleteService } from './NoteDeleteService.js';
import { NotePiningService } from './NotePiningService.js'; import { NotePiningService } from './NotePiningService.js';
import { NoteReadService } from './NoteReadService.js';
import { NotificationService } from './NotificationService.js'; import { NotificationService } from './NotificationService.js';
import { PollService } from './PollService.js'; import { PollService } from './PollService.js';
import { PushNotificationService } from './PushNotificationService.js'; import { PushNotificationService } from './PushNotificationService.js';
@@ -75,6 +74,7 @@ import { ClipService } from './ClipService.js';
import { FeaturedService } from './FeaturedService.js'; import { FeaturedService } from './FeaturedService.js';
import { FanoutTimelineService } from './FanoutTimelineService.js'; import { FanoutTimelineService } from './FanoutTimelineService.js';
import { ChannelFollowingService } from './ChannelFollowingService.js'; import { ChannelFollowingService } from './ChannelFollowingService.js';
import { ChatService } from './ChatService.js';
import { RegistryApiService } from './RegistryApiService.js'; import { RegistryApiService } from './RegistryApiService.js';
import { ReversiService } from './ReversiService.js'; import { ReversiService } from './ReversiService.js';
@@ -100,6 +100,7 @@ import { AppEntityService } from './entities/AppEntityService.js';
import { AuthSessionEntityService } from './entities/AuthSessionEntityService.js'; import { AuthSessionEntityService } from './entities/AuthSessionEntityService.js';
import { BlockingEntityService } from './entities/BlockingEntityService.js'; import { BlockingEntityService } from './entities/BlockingEntityService.js';
import { ChannelEntityService } from './entities/ChannelEntityService.js'; import { ChannelEntityService } from './entities/ChannelEntityService.js';
import { ChatEntityService } from './entities/ChatEntityService.js';
import { ClipEntityService } from './entities/ClipEntityService.js'; import { ClipEntityService } from './entities/ClipEntityService.js';
import { DriveFileEntityService } from './entities/DriveFileEntityService.js'; import { DriveFileEntityService } from './entities/DriveFileEntityService.js';
import { DriveFolderEntityService } from './entities/DriveFolderEntityService.js'; import { DriveFolderEntityService } from './entities/DriveFolderEntityService.js';
@@ -184,7 +185,6 @@ const $ModerationLogService: Provider = { provide: 'ModerationLogService', useEx
const $NoteCreateService: Provider = { provide: 'NoteCreateService', useExisting: NoteCreateService }; const $NoteCreateService: Provider = { provide: 'NoteCreateService', useExisting: NoteCreateService };
const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useExisting: NoteDeleteService }; const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useExisting: NoteDeleteService };
const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting: NotePiningService }; const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting: NotePiningService };
const $NoteReadService: Provider = { provide: 'NoteReadService', useExisting: NoteReadService };
const $NotificationService: Provider = { provide: 'NotificationService', useExisting: NotificationService }; const $NotificationService: Provider = { provide: 'NotificationService', useExisting: NotificationService };
const $PollService: Provider = { provide: 'PollService', useExisting: PollService }; const $PollService: Provider = { provide: 'PollService', useExisting: PollService };
const $SystemAccountService: Provider = { provide: 'SystemAccountService', useExisting: SystemAccountService }; const $SystemAccountService: Provider = { provide: 'SystemAccountService', useExisting: SystemAccountService };
@@ -221,6 +221,7 @@ const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: Fe
const $FanoutTimelineService: Provider = { provide: 'FanoutTimelineService', useExisting: FanoutTimelineService }; const $FanoutTimelineService: Provider = { provide: 'FanoutTimelineService', useExisting: FanoutTimelineService };
const $FanoutTimelineEndpointService: Provider = { provide: 'FanoutTimelineEndpointService', useExisting: FanoutTimelineEndpointService }; const $FanoutTimelineEndpointService: Provider = { provide: 'FanoutTimelineEndpointService', useExisting: FanoutTimelineEndpointService };
const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService }; const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService };
const $ChatService: Provider = { provide: 'ChatService', useExisting: ChatService };
const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService }; const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService };
const $ReversiService: Provider = { provide: 'ReversiService', useExisting: ReversiService }; const $ReversiService: Provider = { provide: 'ReversiService', useExisting: ReversiService };
@@ -247,6 +248,7 @@ const $AppEntityService: Provider = { provide: 'AppEntityService', useExisting:
const $AuthSessionEntityService: Provider = { provide: 'AuthSessionEntityService', useExisting: AuthSessionEntityService }; const $AuthSessionEntityService: Provider = { provide: 'AuthSessionEntityService', useExisting: AuthSessionEntityService };
const $BlockingEntityService: Provider = { provide: 'BlockingEntityService', useExisting: BlockingEntityService }; const $BlockingEntityService: Provider = { provide: 'BlockingEntityService', useExisting: BlockingEntityService };
const $ChannelEntityService: Provider = { provide: 'ChannelEntityService', useExisting: ChannelEntityService }; const $ChannelEntityService: Provider = { provide: 'ChannelEntityService', useExisting: ChannelEntityService };
const $ChatEntityService: Provider = { provide: 'ChatEntityService', useExisting: ChatEntityService };
const $ClipEntityService: Provider = { provide: 'ClipEntityService', useExisting: ClipEntityService }; const $ClipEntityService: Provider = { provide: 'ClipEntityService', useExisting: ClipEntityService };
const $DriveFileEntityService: Provider = { provide: 'DriveFileEntityService', useExisting: DriveFileEntityService }; const $DriveFileEntityService: Provider = { provide: 'DriveFileEntityService', useExisting: DriveFileEntityService };
const $DriveFolderEntityService: Provider = { provide: 'DriveFolderEntityService', useExisting: DriveFolderEntityService }; const $DriveFolderEntityService: Provider = { provide: 'DriveFolderEntityService', useExisting: DriveFolderEntityService };
@@ -333,7 +335,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
NoteCreateService, NoteCreateService,
NoteDeleteService, NoteDeleteService,
NotePiningService, NotePiningService,
NoteReadService,
NotificationService, NotificationService,
PollService, PollService,
SystemAccountService, SystemAccountService,
@@ -370,6 +371,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
FanoutTimelineService, FanoutTimelineService,
FanoutTimelineEndpointService, FanoutTimelineEndpointService,
ChannelFollowingService, ChannelFollowingService,
ChatService,
RegistryApiService, RegistryApiService,
ReversiService, ReversiService,
@@ -396,6 +398,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AuthSessionEntityService, AuthSessionEntityService,
BlockingEntityService, BlockingEntityService,
ChannelEntityService, ChannelEntityService,
ChatEntityService,
ClipEntityService, ClipEntityService,
DriveFileEntityService, DriveFileEntityService,
DriveFolderEntityService, DriveFolderEntityService,
@@ -478,7 +481,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$NoteCreateService, $NoteCreateService,
$NoteDeleteService, $NoteDeleteService,
$NotePiningService, $NotePiningService,
$NoteReadService,
$NotificationService, $NotificationService,
$PollService, $PollService,
$SystemAccountService, $SystemAccountService,
@@ -515,6 +517,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$FanoutTimelineService, $FanoutTimelineService,
$FanoutTimelineEndpointService, $FanoutTimelineEndpointService,
$ChannelFollowingService, $ChannelFollowingService,
$ChatService,
$RegistryApiService, $RegistryApiService,
$ReversiService, $ReversiService,
@@ -541,6 +544,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AuthSessionEntityService, $AuthSessionEntityService,
$BlockingEntityService, $BlockingEntityService,
$ChannelEntityService, $ChannelEntityService,
$ChatEntityService,
$ClipEntityService, $ClipEntityService,
$DriveFileEntityService, $DriveFileEntityService,
$DriveFolderEntityService, $DriveFolderEntityService,
@@ -624,7 +628,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
NoteCreateService, NoteCreateService,
NoteDeleteService, NoteDeleteService,
NotePiningService, NotePiningService,
NoteReadService,
NotificationService, NotificationService,
PollService, PollService,
SystemAccountService, SystemAccountService,
@@ -661,6 +664,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
FanoutTimelineService, FanoutTimelineService,
FanoutTimelineEndpointService, FanoutTimelineEndpointService,
ChannelFollowingService, ChannelFollowingService,
ChatService,
RegistryApiService, RegistryApiService,
ReversiService, ReversiService,
@@ -686,6 +690,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AuthSessionEntityService, AuthSessionEntityService,
BlockingEntityService, BlockingEntityService,
ChannelEntityService, ChannelEntityService,
ChatEntityService,
ClipEntityService, ClipEntityService,
DriveFileEntityService, DriveFileEntityService,
DriveFolderEntityService, DriveFolderEntityService,
@@ -768,7 +773,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$NoteCreateService, $NoteCreateService,
$NoteDeleteService, $NoteDeleteService,
$NotePiningService, $NotePiningService,
$NoteReadService,
$NotificationService, $NotificationService,
$PollService, $PollService,
$SystemAccountService, $SystemAccountService,
@@ -804,6 +808,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$FanoutTimelineService, $FanoutTimelineService,
$FanoutTimelineEndpointService, $FanoutTimelineEndpointService,
$ChannelFollowingService, $ChannelFollowingService,
$ChatService,
$RegistryApiService, $RegistryApiService,
$ReversiService, $ReversiService,
@@ -829,6 +834,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AuthSessionEntityService, $AuthSessionEntityService,
$BlockingEntityService, $BlockingEntityService,
$ChannelEntityService, $ChannelEntityService,
$ChatEntityService,
$ClipEntityService, $ClipEntityService,
$DriveFileEntityService, $DriveFileEntityService,
$DriveFolderEntityService, $DriveFolderEntityService,

View File

@@ -20,7 +20,7 @@ import type { MiPage } from '@/models/Page.js';
import type { MiWebhook } from '@/models/Webhook.js'; import type { MiWebhook } from '@/models/Webhook.js';
import type { MiSystemWebhook } from '@/models/SystemWebhook.js'; import type { MiSystemWebhook } from '@/models/SystemWebhook.js';
import type { MiMeta } from '@/models/Meta.js'; import type { MiMeta } from '@/models/Meta.js';
import { MiAvatarDecoration, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js'; import { MiAvatarDecoration, MiChatRoom, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
@@ -72,12 +72,8 @@ export interface MainEventTypes {
readAllNotifications: undefined; readAllNotifications: undefined;
notificationFlushed: undefined; notificationFlushed: undefined;
unreadNotification: Packed<'Notification'>; unreadNotification: Packed<'Notification'>;
unreadMention: MiNote['id'];
readAllUnreadMentions: undefined;
unreadSpecifiedNote: MiNote['id'];
readAllUnreadSpecifiedNotes: undefined;
readAllAntennas: undefined;
unreadAntenna: MiAntenna; unreadAntenna: MiAntenna;
newChatMessage: Packed<'ChatMessage'>;
readAllAnnouncements: undefined; readAllAnnouncements: undefined;
myTokenRegenerated: undefined; myTokenRegenerated: undefined;
signin: { signin: {
@@ -163,6 +159,11 @@ export interface AdminEventTypes {
}; };
} }
export interface ChatEventTypes {
message: Packed<'ChatMessage'>;
deleted: Packed<'ChatMessage'>['id'];
}
export interface ReversiEventTypes { export interface ReversiEventTypes {
matched: { matched: {
game: Packed<'ReversiGameDetailed'>; game: Packed<'ReversiGameDetailed'>;
@@ -202,7 +203,7 @@ export interface ReversiGameEventTypes {
type Events<T extends object> = { [K in keyof T]: { type: K; body: T[K]; } }; type Events<T extends object> = { [K in keyof T]: { type: K; body: T[K]; } };
type EventUnionFromDictionary< type EventUnionFromDictionary<
T extends object, T extends object,
U = Events<T> U = Events<T>,
> = U[keyof U]; > = U[keyof U];
type SerializedAll<T> = { type SerializedAll<T> = {
@@ -295,6 +296,14 @@ export type GlobalEvents = {
name: 'notesStream'; name: 'notesStream';
payload: Serialized<Packed<'Note'>>; payload: Serialized<Packed<'Note'>>;
}; };
chat: {
name: `chatUserStream:${MiUser['id']}-${MiUser['id']}`;
payload: EventTypesToEventPayload<ChatEventTypes>;
};
chatRoom: {
name: `chatRoomStream:${MiChatRoom['id']}`;
payload: EventTypesToEventPayload<ChatEventTypes>;
};
reversi: { reversi: {
name: `reversiStream:${MiUser['id']}`; name: `reversiStream:${MiUser['id']}`;
payload: EventTypesToEventPayload<ReversiEventTypes>; payload: EventTypesToEventPayload<ReversiEventTypes>;
@@ -393,6 +402,16 @@ export class GlobalEventService {
this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value); this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value);
} }
@bindThis
public publishChatUserStream<K extends keyof ChatEventTypes>(fromUserId: MiUser['id'], toUserId: MiUser['id'], type: K, value?: ChatEventTypes[K]): void {
this.publish(`chatUserStream:${fromUserId}-${toUserId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
public publishChatRoomStream<K extends keyof ChatEventTypes>(toRoomId: MiChatRoom['id'], type: K, value?: ChatEventTypes[K]): void {
this.publish(`chatRoomStream:${toRoomId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis @bindThis
public publishReversiStream<K extends keyof ReversiEventTypes>(userId: MiUser['id'], type: K, value?: ReversiEventTypes[K]): void { public publishReversiStream<K extends keyof ReversiEventTypes>(userId: MiUser['id'], type: K, value?: ReversiEventTypes[K]): void {
this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value); this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value);

View File

@@ -42,7 +42,6 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
import { NoteReadService } from '@/core/NoteReadService.js';
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
@@ -199,7 +198,6 @@ export class NoteCreateService implements OnApplicationShutdown {
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private queueService: QueueService, private queueService: QueueService,
private fanoutTimelineService: FanoutTimelineService, private fanoutTimelineService: FanoutTimelineService,
private noteReadService: NoteReadService,
private notificationService: NotificationService, private notificationService: NotificationService,
private relayService: RelayService, private relayService: RelayService,
private federatedInstanceService: FederatedInstanceService, private federatedInstanceService: FederatedInstanceService,
@@ -582,31 +580,6 @@ export class NoteCreateService implements OnApplicationShutdown {
if (!silent) { if (!silent) {
if (this.userEntityService.isLocalUser(user)) this.activeUsersChart.write(user); if (this.userEntityService.isLocalUser(user)) this.activeUsersChart.write(user);
// 未読通知を作成
if (data.visibility === 'specified') {
if (data.visibleUsers == null) throw new Error('invalid param');
for (const u of data.visibleUsers) {
// ローカルユーザーのみ
if (!this.userEntityService.isLocalUser(u)) continue;
this.noteReadService.insertNoteUnread(u.id, note, {
isSpecified: true,
isMentioned: false,
});
}
} else {
for (const u of mentionedUsers) {
// ローカルユーザーのみ
if (!this.userEntityService.isLocalUser(u)) continue;
this.noteReadService.insertNoteUnread(u.id, note, {
isSpecified: false,
isMentioned: true,
});
}
}
// Pack the note // Pack the note
const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true, withReactionAndUserPairCache: true }); const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true, withReactionAndUserPairCache: true });

View File

@@ -1,143 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { setTimeout } from 'node:timers/promises';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { MiUser } from '@/models/User.js';
import type { Packed } from '@/misc/json-schema.js';
import type { MiNote } from '@/models/Note.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { trackPromise } from '@/misc/promise-tracker.js';
@Injectable()
export class NoteReadService implements OnApplicationShutdown {
#shutdownController = new AbortController();
constructor(
@Inject(DI.noteUnreadsRepository)
private noteUnreadsRepository: NoteUnreadsRepository,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
@Inject(DI.noteThreadMutingsRepository)
private noteThreadMutingsRepository: NoteThreadMutingsRepository,
private idService: IdService,
private globalEventService: GlobalEventService,
) {
}
@bindThis
public async insertNoteUnread(userId: MiUser['id'], note: MiNote, params: {
// NOTE: isSpecifiedがtrueならisMentionedは必ずfalse
isSpecified: boolean;
isMentioned: boolean;
}): Promise<void> {
//#region ミュートしているなら無視
const mute = await this.mutingsRepository.findBy({
muterId: userId,
});
if (mute.map(m => m.muteeId).includes(note.userId)) return;
//#endregion
// スレッドミュート
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
where: {
userId: userId,
threadId: note.threadId ?? note.id,
},
});
if (isThreadMuted) return;
const unread = {
id: this.idService.gen(),
noteId: note.id,
userId: userId,
isSpecified: params.isSpecified,
isMentioned: params.isMentioned,
noteUserId: note.userId,
};
await this.noteUnreadsRepository.insert(unread);
// 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
const exist = await this.noteUnreadsRepository.exists({ where: { id: unread.id } });
if (!exist) return;
if (params.isMentioned) {
this.globalEventService.publishMainStream(userId, 'unreadMention', note.id);
}
if (params.isSpecified) {
this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
}
}, () => { /* aborted, ignore it */ });
}
@bindThis
public async read(
userId: MiUser['id'],
notes: (MiNote | Packed<'Note'>)[],
): Promise<void> {
if (notes.length === 0) return;
const noteIds = new Set<MiNote['id']>();
for (const note of notes) {
if (note.mentions && note.mentions.includes(userId)) {
noteIds.add(note.id);
} else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) {
noteIds.add(note.id);
}
}
if (noteIds.size === 0) return;
// Remove the record
await this.noteUnreadsRepository.delete({
userId: userId,
noteId: In(Array.from(noteIds)),
});
// TODO: ↓まとめてクエリしたい
trackPromise(this.noteUnreadsRepository.countBy({
userId: userId,
isMentioned: true,
}).then(mentionsCount => {
if (mentionsCount === 0) {
// 全て既読になったイベントを発行
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
}
}));
trackPromise(this.noteUnreadsRepository.countBy({
userId: userId,
isSpecified: true,
}).then(specifiedCount => {
if (specifiedCount === 0) {
// 全て既読になったイベントを発行
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
}
}));
}
@bindThis
public dispose(): void {
this.#shutdownController.abort();
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
}

View File

@@ -63,6 +63,7 @@ export type RolePolicies = {
canImportFollowing: boolean; canImportFollowing: boolean;
canImportMuting: boolean; canImportMuting: boolean;
canImportUserLists: boolean; canImportUserLists: boolean;
canChat: boolean;
}; };
export const DEFAULT_POLICIES: RolePolicies = { export const DEFAULT_POLICIES: RolePolicies = {
@@ -97,6 +98,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
canImportFollowing: true, canImportFollowing: true,
canImportMuting: true, canImportMuting: true,
canImportUserLists: true, canImportUserLists: true,
canChat: true,
}; };
@Injectable() @Injectable()
@@ -400,6 +402,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
canImportFollowing: calc('canImportFollowing', vs => vs.some(v => v === true)), canImportFollowing: calc('canImportFollowing', vs => vs.some(v => v === true)),
canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)), canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)),
canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)), canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)),
canChat: calc('canChat', vs => vs.some(v => v === true)),
}; };
} }

View File

@@ -5,7 +5,7 @@
import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core'; import { ModuleRef } from '@nestjs/core';
import { IsNull } from 'typeorm'; import { Brackets, IsNull } from 'typeorm';
import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js'; import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
@@ -736,4 +736,30 @@ export class UserFollowingService implements OnModuleInit {
.where('following.followerId = :followerId', { followerId: userId }) .where('following.followerId = :followerId', { followerId: userId })
.getMany(); .getMany();
} }
@bindThis
public isFollowing(followerId: MiUser['id'], followeeId: MiUser['id']) {
return this.followingsRepository.exists({
where: {
followerId,
followeeId,
},
});
}
@bindThis
public async isMutual(aUserId: MiUser['id'], bUserId: MiUser['id']) {
const count = await this.followingsRepository.createQueryBuilder('following')
.where(new Brackets(qb => {
qb.where('following.followerId = :aUserId', { aUserId })
.andWhere('following.followeeId = :bUserId', { bUserId });
}))
.orWhere(new Brackets(qb => {
qb.where('following.followerId = :bUserId', { bUserId })
.andWhere('following.followeeId = :aUserId', { aUserId });
}))
.getCount();
return count === 2;
}
} }

View File

@@ -7,42 +7,16 @@ import { Injectable } from '@nestjs/common';
import { MiAbuseUserReport, MiNote, MiUser, MiWebhook } from '@/models/_.js'; import { MiAbuseUserReport, MiNote, MiUser, MiWebhook } from '@/models/_.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js'; import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
import { AbuseReportPayload, SystemWebhookPayload, SystemWebhookService } from '@/core/SystemWebhookService.js'; import { type AbuseReportPayload, SystemWebhookPayload, SystemWebhookService } from '@/core/SystemWebhookService.js';
import { Packed } from '@/misc/json-schema.js'; import { type Packed } from '@/misc/json-schema.js';
import { type WebhookEventTypes } from '@/models/Webhook.js'; import { type WebhookEventTypes } from '@/models/Webhook.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { type UserWebhookPayload, UserWebhookService } from '@/core/UserWebhookService.js'; import { type UserWebhookPayload, UserWebhookService } from '@/core/UserWebhookService.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js'; import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
const oneDayMillis = 24 * 60 * 60 * 1000; const oneDayMillis = 24 * 60 * 60 * 1000;
function generateAbuseReport(override?: Partial<MiAbuseUserReport>): AbuseReportPayload {
const result: MiAbuseUserReport = {
id: 'dummy-abuse-report1',
targetUserId: 'dummy-target-user',
targetUser: null,
reporterId: 'dummy-reporter-user',
reporter: null,
assigneeId: null,
assignee: null,
resolved: false,
forwarded: false,
comment: 'This is a dummy report for testing purposes.',
targetUserHost: null,
reporterHost: null,
resolvedAs: null,
moderationNote: 'foo',
...override,
};
return {
...result,
targetUser: result.targetUser ? toPackedUserLite(result.targetUser) : null,
reporter: result.reporter ? toPackedUserLite(result.reporter) : null,
assignee: result.assignee ? toPackedUserLite(result.assignee) : null,
};
}
function generateDummyUser(override?: Partial<MiUser>): MiUser { function generateDummyUser(override?: Partial<MiUser>): MiUser {
return { return {
id: 'dummy-user-1', id: 'dummy-user-1',
@@ -134,124 +108,6 @@ function generateDummyNote(override?: Partial<MiNote>): MiNote {
}; };
} }
function toPackedNote(note: MiNote, detail = true, override?: Packed<'Note'>): Packed<'Note'> {
return {
id: note.id,
createdAt: new Date().toISOString(),
deletedAt: null,
text: note.text,
cw: note.cw,
userId: note.userId,
user: toPackedUserLite(note.user ?? generateDummyUser()),
replyId: note.replyId,
renoteId: note.renoteId,
isHidden: false,
visibility: note.visibility,
mentions: note.mentions,
visibleUserIds: note.visibleUserIds,
fileIds: note.fileIds,
files: [],
tags: note.tags,
poll: null,
emojis: note.emojis,
channelId: note.channelId,
channel: note.channel,
localOnly: note.localOnly,
reactionAcceptance: note.reactionAcceptance,
reactionEmojis: {},
reactions: {},
reactionCount: 0,
renoteCount: note.renoteCount,
repliesCount: note.repliesCount,
uri: note.uri ?? undefined,
url: note.url ?? undefined,
reactionAndUserPairCache: note.reactionAndUserPairCache,
...(detail ? {
clippedCount: note.clippedCount,
reply: note.reply ? toPackedNote(note.reply, false) : null,
renote: note.renote ? toPackedNote(note.renote, true) : null,
myReaction: null,
} : {}),
...override,
};
}
function toPackedUserLite(user: MiUser, override?: Packed<'UserLite'>): Packed<'UserLite'> {
return {
id: user.id,
name: user.name,
username: user.username,
host: user.host,
avatarUrl: user.avatarUrl,
avatarBlurhash: user.avatarBlurhash,
avatarDecorations: user.avatarDecorations.map(it => ({
id: it.id,
angle: it.angle,
flipH: it.flipH,
url: 'https://example.com/dummy-image001.png',
offsetX: it.offsetX,
offsetY: it.offsetY,
})),
isBot: user.isBot,
isCat: user.isCat,
emojis: user.emojis,
onlineStatus: 'active',
badgeRoles: [],
...override,
};
}
function toPackedUserDetailedNotMe(user: MiUser, override?: Packed<'UserDetailedNotMe'>): Packed<'UserDetailedNotMe'> {
return {
...toPackedUserLite(user),
url: null,
uri: null,
movedTo: null,
alsoKnownAs: [],
createdAt: new Date().toISOString(),
updatedAt: user.updatedAt?.toISOString() ?? null,
lastFetchedAt: user.lastFetchedAt?.toISOString() ?? null,
bannerUrl: user.bannerUrl,
bannerBlurhash: user.bannerBlurhash,
isLocked: user.isLocked,
isSilenced: false,
isSuspended: user.isSuspended,
description: null,
location: null,
birthday: null,
lang: null,
fields: [],
verifiedLinks: [],
followersCount: user.followersCount,
followingCount: user.followingCount,
notesCount: user.notesCount,
pinnedNoteIds: [],
pinnedNotes: [],
pinnedPageId: null,
pinnedPage: null,
publicReactions: true,
followersVisibility: 'public',
followingVisibility: 'public',
twoFactorEnabled: false,
usePasswordLessLogin: false,
securityKeys: false,
roles: [],
memo: null,
moderationNote: undefined,
isFollowing: false,
isFollowed: false,
hasPendingFollowRequestFromYou: false,
hasPendingFollowRequestToYou: false,
isBlocking: false,
isBlocked: false,
isMuted: false,
isRenoteMuted: false,
notify: 'none',
withReplies: true,
...override,
};
}
const dummyUser1 = generateDummyUser(); const dummyUser1 = generateDummyUser();
const dummyUser2 = generateDummyUser({ const dummyUser2 = generateDummyUser({
id: 'dummy-user-2', id: 'dummy-user-2',
@@ -284,6 +140,7 @@ export class WebhookTestService {
}; };
constructor( constructor(
private customEmojiService: CustomEmojiService,
private userWebhookService: UserWebhookService, private userWebhookService: UserWebhookService,
private systemWebhookService: SystemWebhookService, private systemWebhookService: SystemWebhookService,
private queueService: QueueService, private queueService: QueueService,
@@ -354,31 +211,31 @@ export class WebhookTestService {
switch (params.type) { switch (params.type) {
case 'note': { case 'note': {
send('note', { note: toPackedNote(dummyNote1) }); send('note', { note: await this.toPackedNote(dummyNote1) });
break; break;
} }
case 'reply': { case 'reply': {
send('reply', { note: toPackedNote(dummyReply1) }); send('reply', { note: await this.toPackedNote(dummyReply1) });
break; break;
} }
case 'renote': { case 'renote': {
send('renote', { note: toPackedNote(dummyRenote1) }); send('renote', { note: await this.toPackedNote(dummyRenote1) });
break; break;
} }
case 'mention': { case 'mention': {
send('mention', { note: toPackedNote(dummyMention1) }); send('mention', { note: await this.toPackedNote(dummyMention1) });
break; break;
} }
case 'follow': { case 'follow': {
send('follow', { user: toPackedUserDetailedNotMe(dummyUser1) }); send('follow', { user: await this.toPackedUserDetailedNotMe(dummyUser1) });
break; break;
} }
case 'followed': { case 'followed': {
send('followed', { user: toPackedUserLite(dummyUser2) }); send('followed', { user: await this.toPackedUserLite(dummyUser2) });
break; break;
} }
case 'unfollow': { case 'unfollow': {
send('unfollow', { user: toPackedUserDetailedNotMe(dummyUser3) }); send('unfollow', { user: await this.toPackedUserDetailedNotMe(dummyUser3) });
break; break;
} }
// まだ実装されていない (#9485) // まだ実装されていない (#9485)
@@ -427,7 +284,7 @@ export class WebhookTestService {
switch (params.type) { switch (params.type) {
case 'abuseReport': { case 'abuseReport': {
send('abuseReport', generateAbuseReport({ send('abuseReport', await this.generateAbuseReport({
targetUserId: dummyUser1.id, targetUserId: dummyUser1.id,
targetUser: dummyUser1, targetUser: dummyUser1,
reporterId: dummyUser2.id, reporterId: dummyUser2.id,
@@ -436,7 +293,7 @@ export class WebhookTestService {
break; break;
} }
case 'abuseReportResolved': { case 'abuseReportResolved': {
send('abuseReportResolved', generateAbuseReport({ send('abuseReportResolved', await this.generateAbuseReport({
targetUserId: dummyUser1.id, targetUserId: dummyUser1.id,
targetUser: dummyUser1, targetUser: dummyUser1,
reporterId: dummyUser2.id, reporterId: dummyUser2.id,
@@ -448,7 +305,7 @@ export class WebhookTestService {
break; break;
} }
case 'userCreated': { case 'userCreated': {
send('userCreated', toPackedUserLite(dummyUser1)); send('userCreated', await this.toPackedUserLite(dummyUser1));
break; break;
} }
case 'inactiveModeratorsWarning': { case 'inactiveModeratorsWarning': {
@@ -474,4 +331,153 @@ export class WebhookTestService {
} }
} }
} }
@bindThis
private async generateAbuseReport(override?: Partial<MiAbuseUserReport>): Promise<AbuseReportPayload> {
const result: MiAbuseUserReport = {
id: 'dummy-abuse-report1',
targetUserId: 'dummy-target-user',
targetUser: null,
reporterId: 'dummy-reporter-user',
reporter: null,
assigneeId: null,
assignee: null,
resolved: false,
forwarded: false,
comment: 'This is a dummy report for testing purposes.',
targetUserHost: null,
reporterHost: null,
resolvedAs: null,
moderationNote: 'foo',
...override,
};
return {
...result,
targetUser: result.targetUser ? await this.toPackedUserLite(result.targetUser) : null,
reporter: result.reporter ? await this.toPackedUserLite(result.reporter) : null,
assignee: result.assignee ? await this.toPackedUserLite(result.assignee) : null,
};
}
@bindThis
private async toPackedNote(note: MiNote, detail = true, override?: Packed<'Note'>): Promise<Packed<'Note'>> {
return {
id: note.id,
createdAt: new Date().toISOString(),
deletedAt: null,
text: note.text,
cw: note.cw,
userId: note.userId,
user: await this.toPackedUserLite(note.user ?? generateDummyUser()),
replyId: note.replyId,
renoteId: note.renoteId,
isHidden: false,
visibility: note.visibility,
mentions: note.mentions,
visibleUserIds: note.visibleUserIds,
fileIds: note.fileIds,
files: [],
tags: note.tags,
poll: null,
emojis: await this.customEmojiService.populateEmojis(note.emojis, note.userHost),
channelId: note.channelId,
channel: note.channel,
localOnly: note.localOnly,
reactionAcceptance: note.reactionAcceptance,
reactionEmojis: {},
reactions: {},
reactionCount: 0,
renoteCount: note.renoteCount,
repliesCount: note.repliesCount,
uri: note.uri ?? undefined,
url: note.url ?? undefined,
reactionAndUserPairCache: note.reactionAndUserPairCache,
...(detail ? {
clippedCount: note.clippedCount,
reply: note.reply ? await this.toPackedNote(note.reply, false) : null,
renote: note.renote ? await this.toPackedNote(note.renote, true) : null,
myReaction: null,
} : {}),
...override,
};
}
@bindThis
private async toPackedUserLite(user: MiUser, override?: Packed<'UserLite'>): Promise<Packed<'UserLite'>> {
return {
id: user.id,
name: user.name,
username: user.username,
host: user.host,
avatarUrl: user.avatarUrl,
avatarBlurhash: user.avatarBlurhash,
avatarDecorations: user.avatarDecorations.map(it => ({
id: it.id,
angle: it.angle,
flipH: it.flipH,
url: 'https://example.com/dummy-image001.png',
offsetX: it.offsetX,
offsetY: it.offsetY,
})),
isBot: user.isBot,
isCat: user.isCat,
emojis: await this.customEmojiService.populateEmojis(user.emojis, user.host),
onlineStatus: 'active',
badgeRoles: [],
...override,
};
}
@bindThis
private async toPackedUserDetailedNotMe(user: MiUser, override?: Packed<'UserDetailedNotMe'>): Promise<Packed<'UserDetailedNotMe'>> {
return {
...await this.toPackedUserLite(user),
url: null,
uri: null,
movedTo: null,
alsoKnownAs: [],
createdAt: new Date().toISOString(),
updatedAt: user.updatedAt?.toISOString() ?? null,
lastFetchedAt: user.lastFetchedAt?.toISOString() ?? null,
bannerUrl: user.bannerUrl,
bannerBlurhash: user.bannerBlurhash,
isLocked: user.isLocked,
isSilenced: false,
isSuspended: user.isSuspended,
description: null,
location: null,
birthday: null,
lang: null,
fields: [],
verifiedLinks: [],
followersCount: user.followersCount,
followingCount: user.followingCount,
notesCount: user.notesCount,
pinnedNoteIds: [],
pinnedNotes: [],
pinnedPageId: null,
pinnedPage: null,
publicReactions: true,
followersVisibility: 'public',
followingVisibility: 'public',
twoFactorEnabled: false,
usePasswordLessLogin: false,
securityKeys: false,
roles: [],
memo: null,
moderationNote: undefined,
isFollowing: false,
isFollowed: false,
hasPendingFollowRequestFromYou: false,
hasPendingFollowRequestToYou: false,
isBlocking: false,
isBlocked: false,
isMuted: false,
isRenoteMuted: false,
notify: 'none',
withReplies: true,
...override,
};
}
} }

View File

@@ -0,0 +1,282 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { MiUser, ChatMessagesRepository, MiChatMessage, ChatRoomsRepository, MiChatRoom, MiChatRoomInvitation, ChatRoomInvitationsRepository, MiChatRoomMembership, ChatRoomMembershipsRepository } from '@/models/_.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/json-schema.js';
import type { } from '@/models/Blocking.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
import { UserEntityService } from './UserEntityService.js';
import { DriveFileEntityService } from './DriveFileEntityService.js';
@Injectable()
export class ChatEntityService {
constructor(
@Inject(DI.chatMessagesRepository)
private chatMessagesRepository: ChatMessagesRepository,
@Inject(DI.chatRoomsRepository)
private chatRoomsRepository: ChatRoomsRepository,
@Inject(DI.chatRoomInvitationsRepository)
private chatRoomInvitationsRepository: ChatRoomInvitationsRepository,
@Inject(DI.chatRoomMembershipsRepository)
private chatRoomMembershipsRepository: ChatRoomMembershipsRepository,
private userEntityService: UserEntityService,
private driveFileEntityService: DriveFileEntityService,
private idService: IdService,
) {
}
@bindThis
public async packMessageDetailed(
src: MiChatMessage['id'] | MiChatMessage,
me?: { id: MiUser['id'] },
options?: {
_hint_?: {
packedFiles: Map<MiChatMessage['fileId'], Packed<'DriveFile'> | null>;
packedUsers: Map<MiChatMessage['id'], Packed<'UserLite'>>;
packedRooms: Map<MiChatMessage['toRoomId'], Packed<'ChatRoom'> | null>;
};
},
): Promise<Packed<'ChatMessage'>> {
const packedUsers = options?._hint_?.packedUsers;
const packedFiles = options?._hint_?.packedFiles;
const packedRooms = options?._hint_?.packedRooms;
const message = typeof src === 'object' ? src : await this.chatMessagesRepository.findOneByOrFail({ id: src });
return {
id: message.id,
createdAt: this.idService.parse(message.id).date.toISOString(),
text: message.text,
fromUserId: message.fromUserId,
fromUser: packedUsers?.get(message.fromUserId) ?? await this.userEntityService.pack(message.fromUser ?? message.fromUserId, me),
toUserId: message.toUserId,
toUser: message.toUserId ? (packedUsers?.get(message.toUserId) ?? await this.userEntityService.pack(message.toUser ?? message.toUserId, me)) : undefined,
toRoomId: message.toRoomId,
toRoom: message.toRoomId ? (packedRooms?.get(message.toRoomId) ?? await this.packRoom(message.toRoom ?? message.toRoomId, me)) : undefined,
fileId: message.fileId,
file: message.file ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file)) : null,
};
}
@bindThis
public async packMessagesDetailed(
messages: MiChatMessage[],
me: { id: MiUser['id'] },
) {
if (messages.length === 0) return [];
const excludeMe = (x: MiUser | string) => {
if (typeof x === 'string') {
return x !== me.id;
} else {
return x.id !== me.id;
}
};
const users = [
...messages.map((m) => m.fromUser ?? m.fromUserId).filter(excludeMe),
...messages.map((m) => m.toUser ?? m.toUserId).filter(x => x != null).filter(excludeMe),
];
const [packedUsers, packedFiles] = await Promise.all([
this.userEntityService.packMany(users, me)
.then(users => new Map(users.map(u => [u.id, u]))),
this.driveFileEntityService.packMany(messages.map(m => m.file).filter(x => x != null)),
]);
return Promise.all(messages.map(message => this.packMessageDetailed(message, me, { _hint_: { packedUsers, packedFiles } })));
}
@bindThis
public async packMessageLite(
src: MiChatMessage['id'] | MiChatMessage,
options?: {
_hint_?: {
packedFiles: Map<MiChatMessage['fileId'], Packed<'DriveFile'> | null>;
};
},
): Promise<Packed<'ChatMessageLite'>> {
const packedFiles = options?._hint_?.packedFiles;
const message = typeof src === 'object' ? src : await this.chatMessagesRepository.findOneByOrFail({ id: src });
return {
id: message.id,
createdAt: this.idService.parse(message.id).date.toISOString(),
text: message.text,
fromUserId: message.fromUserId,
toUserId: message.toUserId,
fileId: message.fileId,
file: message.file ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file)) : null,
};
}
@bindThis
public async packMessagesLite(
messages: MiChatMessage[],
) {
if (messages.length === 0) return [];
const [packedFiles] = await Promise.all([
this.driveFileEntityService.packMany(messages.map(m => m.file).filter(x => x != null)),
]);
return Promise.all(messages.map(message => this.packMessageLite(message, { _hint_: { packedFiles } })));
}
@bindThis
public async packMessageLiteForRoom(
src: MiChatMessage['id'] | MiChatMessage,
options?: {
_hint_?: {
packedFiles: Map<MiChatMessage['fileId'], Packed<'DriveFile'> | null>;
packedUsers: Map<MiChatMessage['id'], Packed<'UserLite'>>;
};
},
): Promise<Packed<'ChatMessageLite'>> {
const packedFiles = options?._hint_?.packedFiles;
const packedUsers = options?._hint_?.packedUsers;
const message = typeof src === 'object' ? src : await this.chatMessagesRepository.findOneByOrFail({ id: src });
return {
id: message.id,
createdAt: this.idService.parse(message.id).date.toISOString(),
text: message.text,
fromUserId: message.fromUserId,
fromUser: packedUsers?.get(message.fromUserId) ?? await this.userEntityService.pack(message.fromUser ?? message.fromUserId),
toRoomId: message.toRoomId,
fileId: message.fileId,
file: message.file ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file)) : null,
};
}
@bindThis
public async packMessagesLiteForRoom(
messages: MiChatMessage[],
) {
if (messages.length === 0) return [];
const [packedUsers, packedFiles] = await Promise.all([
this.userEntityService.packMany(messages.map(x => x.fromUser ?? x.fromUserId))
.then(users => new Map(users.map(u => [u.id, u]))),
this.driveFileEntityService.packMany(messages.map(m => m.file).filter(x => x != null)),
]);
return Promise.all(messages.map(message => this.packMessageLiteForRoom(message, { _hint_: { packedFiles, packedUsers } })));
}
@bindThis
public async packRoom(
src: MiChatRoom['id'] | MiChatRoom,
me?: { id: MiUser['id'] },
options?: {
_hint_?: {
packedOwners: Map<MiChatRoom['id'], Packed<'UserLite'>>;
};
},
): Promise<Packed<'ChatRoom'>> {
const room = typeof src === 'object' ? src : await this.chatRoomsRepository.findOneByOrFail({ id: src });
return {
id: room.id,
createdAt: this.idService.parse(room.id).date.toISOString(),
name: room.name,
ownerId: room.ownerId,
owner: options?._hint_?.packedOwners.get(room.ownerId) ?? await this.userEntityService.pack(room.owner ?? room.ownerId, me),
};
}
@bindThis
public async packRooms(
rooms: MiChatRoom[],
me: { id: MiUser['id'] },
) {
if (rooms.length === 0) return [];
const owners = rooms.map(x => x.owner ?? x.ownerId);
const packedOwners = await this.userEntityService.packMany(owners, me)
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(rooms.map(room => this.packRoom(room, me, { _hint_: { packedOwners } })));
}
@bindThis
public async packRoomInvitation(
src: MiChatRoomInvitation['id'] | MiChatRoomInvitation,
me: { id: MiUser['id'] },
options?: {
_hint_?: {
packedRooms: Map<MiChatRoomInvitation['roomId'], Packed<'ChatRoom'>>;
packedUsers: Map<MiChatRoomInvitation['id'], Packed<'UserLite'>>;
};
},
): Promise<Packed<'ChatRoomInvitation'>> {
const invitation = typeof src === 'object' ? src : await this.chatRoomInvitationsRepository.findOneByOrFail({ id: src });
return {
id: invitation.id,
createdAt: this.idService.parse(invitation.id).date.toISOString(),
roomId: invitation.roomId,
room: options?._hint_?.packedRooms.get(invitation.roomId) ?? await this.packRoom(invitation.room ?? invitation.roomId, me),
userId: invitation.userId,
user: options?._hint_?.packedUsers.get(invitation.userId) ?? await this.userEntityService.pack(invitation.user ?? invitation.userId, me),
};
}
@bindThis
public async packRoomInvitations(
invitations: MiChatRoomInvitation[],
me: { id: MiUser['id'] },
) {
if (invitations.length === 0) return [];
return Promise.all(invitations.map(invitation => this.packRoomInvitation(invitation, me)));
}
@bindThis
public async packRoomMembership(
src: MiChatRoomMembership['id'] | MiChatRoomMembership,
me: { id: MiUser['id'] },
options?: {
_hint_?: {
packedUsers: Map<MiChatRoomMembership['id'], Packed<'UserLite'>>;
};
},
): Promise<Packed<'ChatRoomMembership'>> {
const membership = typeof src === 'object' ? src : await this.chatRoomMembershipsRepository.findOneByOrFail({ id: src });
return {
id: membership.id,
createdAt: this.idService.parse(membership.id).date.toISOString(),
userId: membership.userId,
user: options?._hint_?.packedUsers.get(membership.userId) ?? await this.userEntityService.pack(membership.user ?? membership.userId, me),
};
}
@bindThis
public async packRoomMemberships(
memberships: MiChatRoomMembership[],
me: { id: MiUser['id'] },
) {
if (memberships.length === 0) return [];
const users = memberships.map(x => x.user ?? x.userId);
const packedUsers = await this.userEntityService.packMany(users, me)
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(memberships.map(membership => this.packRoomMembership(membership, me, { _hint_: { packedUsers } })));
}
}

View File

@@ -32,7 +32,6 @@ import type {
MiUserNotePining, MiUserNotePining,
MiUserProfile, MiUserProfile,
MutingsRepository, MutingsRepository,
NoteUnreadsRepository,
RenoteMutingsRepository, RenoteMutingsRepository,
UserMemoRepository, UserMemoRepository,
UserNotePiningsRepository, UserNotePiningsRepository,
@@ -48,9 +47,9 @@ import { IdService } from '@/core/IdService.js';
import type { AnnouncementService } from '@/core/AnnouncementService.js'; import type { AnnouncementService } from '@/core/AnnouncementService.js';
import type { CustomEmojiService } from '@/core/CustomEmojiService.js'; import type { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
import { ChatService } from '@/core/ChatService.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
import type { NoteEntityService } from './NoteEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js';
import type { DriveFileEntityService } from './DriveFileEntityService.js';
import type { PageEntityService } from './PageEntityService.js'; import type { PageEntityService } from './PageEntityService.js';
const Ajv = _Ajv.default; const Ajv = _Ajv.default;
@@ -94,6 +93,7 @@ export class UserEntityService implements OnModuleInit {
private federatedInstanceService: FederatedInstanceService; private federatedInstanceService: FederatedInstanceService;
private idService: IdService; private idService: IdService;
private avatarDecorationService: AvatarDecorationService; private avatarDecorationService: AvatarDecorationService;
private chatService: ChatService;
constructor( constructor(
private moduleRef: ModuleRef, private moduleRef: ModuleRef,
@@ -128,9 +128,6 @@ export class UserEntityService implements OnModuleInit {
@Inject(DI.renoteMutingsRepository) @Inject(DI.renoteMutingsRepository)
private renoteMutingsRepository: RenoteMutingsRepository, private renoteMutingsRepository: RenoteMutingsRepository,
@Inject(DI.noteUnreadsRepository)
private noteUnreadsRepository: NoteUnreadsRepository,
@Inject(DI.userNotePiningsRepository) @Inject(DI.userNotePiningsRepository)
private userNotePiningsRepository: UserNotePiningsRepository, private userNotePiningsRepository: UserNotePiningsRepository,
@@ -152,6 +149,7 @@ export class UserEntityService implements OnModuleInit {
this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService'); this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
this.idService = this.moduleRef.get('IdService'); this.idService = this.moduleRef.get('IdService');
this.avatarDecorationService = this.moduleRef.get('AvatarDecorationService'); this.avatarDecorationService = this.moduleRef.get('AvatarDecorationService');
this.chatService = this.moduleRef.get('ChatService');
} }
//#region Validators //#region Validators
@@ -558,6 +556,7 @@ export class UserEntityService implements OnModuleInit {
publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964 publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964
followersVisibility: profile!.followersVisibility, followersVisibility: profile!.followersVisibility,
followingVisibility: profile!.followingVisibility, followingVisibility: profile!.followingVisibility,
chatScope: user.chatScope,
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({ roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
id: role.id, id: role.id,
name: role.name, name: role.name,
@@ -598,14 +597,9 @@ export class UserEntityService implements OnModuleInit {
isDeleted: user.isDeleted, isDeleted: user.isDeleted,
twoFactorBackupCodesStock: profile?.twoFactorBackupSecret?.length === 5 ? 'full' : (profile?.twoFactorBackupSecret?.length ?? 0) > 0 ? 'partial' : 'none', twoFactorBackupCodesStock: profile?.twoFactorBackupSecret?.length === 5 ? 'full' : (profile?.twoFactorBackupSecret?.length ?? 0) > 0 ? 'partial' : 'none',
hideOnlineStatus: user.hideOnlineStatus, hideOnlineStatus: user.hideOnlineStatus,
hasUnreadSpecifiedNotes: this.noteUnreadsRepository.count({ hasUnreadSpecifiedNotes: false, // 後方互換性のため
where: { userId: user.id, isSpecified: true }, hasUnreadMentions: false, // 後方互換性のため
take: 1, hasUnreadChatMessages: this.chatService.hasUnreadMessages(user.id),
}).then(count => count > 0),
hasUnreadMentions: this.noteUnreadsRepository.count({
where: { userId: user.id, isMentioned: true },
take: 1,
}).then(count => count > 0),
hasUnreadAnnouncement: unreadAnnouncements!.length > 0, hasUnreadAnnouncement: unreadAnnouncements!.length > 0,
unreadAnnouncements, unreadAnnouncements,
hasUnreadAntenna: this.getHasUnreadAntenna(user.id), hasUnreadAntenna: this.getHasUnreadAntenna(user.id),

View File

@@ -24,7 +24,6 @@ export const DI = {
noteFavoritesRepository: Symbol('noteFavoritesRepository'), noteFavoritesRepository: Symbol('noteFavoritesRepository'),
noteThreadMutingsRepository: Symbol('noteThreadMutingsRepository'), noteThreadMutingsRepository: Symbol('noteThreadMutingsRepository'),
noteReactionsRepository: Symbol('noteReactionsRepository'), noteReactionsRepository: Symbol('noteReactionsRepository'),
noteUnreadsRepository: Symbol('noteUnreadsRepository'),
pollsRepository: Symbol('pollsRepository'), pollsRepository: Symbol('pollsRepository'),
pollVotesRepository: Symbol('pollVotesRepository'), pollVotesRepository: Symbol('pollVotesRepository'),
userProfilesRepository: Symbol('userProfilesRepository'), userProfilesRepository: Symbol('userProfilesRepository'),
@@ -83,6 +82,11 @@ export const DI = {
flashsRepository: Symbol('flashsRepository'), flashsRepository: Symbol('flashsRepository'),
flashLikesRepository: Symbol('flashLikesRepository'), flashLikesRepository: Symbol('flashLikesRepository'),
userMemosRepository: Symbol('userMemosRepository'), userMemosRepository: Symbol('userMemosRepository'),
chatMessagesRepository: Symbol('chatMessagesRepository'),
chatApprovalsRepository: Symbol('chatApprovalsRepository'),
chatRoomsRepository: Symbol('chatRoomsRepository'),
chatRoomMembershipsRepository: Symbol('chatRoomMembershipsRepository'),
chatRoomInvitationsRepository: Symbol('chatRoomInvitationsRepository'),
bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'), bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'),
reversiGamesRepository: Symbol('reversiGamesRepository'), reversiGamesRepository: Symbol('reversiGamesRepository'),
//#endregion //#endregion

View File

@@ -4,6 +4,7 @@
*/ */
import * as fs from 'node:fs/promises'; import * as fs from 'node:fs/promises';
import { WritableStream } from 'node:stream/web';
import type { PathLike } from 'node:fs'; import type { PathLike } from 'node:fs';
/** /**

View File

@@ -63,6 +63,10 @@ import {
} from '@/models/json-schema/meta.js'; } from '@/models/json-schema/meta.js';
import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js'; import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js'; import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js';
import { packedChatMessageSchema, packedChatMessageLiteSchema } from '@/models/json-schema/chat-message.js';
import { packedChatRoomSchema } from '@/models/json-schema/chat-room.js';
import { packedChatRoomInvitationSchema } from '@/models/json-schema/chat-room-invitation.js';
import { packedChatRoomMembershipSchema } from '@/models/json-schema/chat-room-membership.js';
export const refs = { export const refs = {
UserLite: packedUserLiteSchema, UserLite: packedUserLiteSchema,
@@ -120,6 +124,11 @@ export const refs = {
MetaDetailed: packedMetaDetailedSchema, MetaDetailed: packedMetaDetailedSchema,
SystemWebhook: packedSystemWebhookSchema, SystemWebhook: packedSystemWebhookSchema,
AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema, AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,
ChatMessage: packedChatMessageSchema,
ChatMessageLite: packedChatMessageLiteSchema,
ChatRoom: packedChatRoomSchema,
ChatRoomInvitation: packedChatRoomInvitationSchema,
ChatRoomMembership: packedChatRoomMembershipSchema,
}; };
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>; export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
@@ -166,15 +175,16 @@ export interface Schema extends OfSchema {
readonly maximum?: number; readonly maximum?: number;
readonly minimum?: number; readonly minimum?: number;
readonly pattern?: string; readonly pattern?: string;
readonly additionalProperties?: Schema | boolean;
} }
type RequiredPropertyNames<s extends Obj> = { type RequiredPropertyNames<s extends Obj> = {
[K in keyof s]: [K in keyof s]:
// K is not optional // K is not optional
s[K]['optional'] extends false ? K : s[K]['optional'] extends false ? K :
// K has default value // K has default value
s[K]['default'] extends null | string | number | boolean | Record<string, unknown> ? K : s[K]['default'] extends null | string | number | boolean | Record<string, unknown> ? K :
never never
}[keyof s]; }[keyof s];
export type Obj = Record<string, Schema>; export type Obj = Record<string, Schema>;
@@ -213,11 +223,18 @@ type ObjectSchemaTypeDef<p extends Schema> =
p['anyOf'] extends ReadonlyArray<Schema> ? p['anyOf'][number]['required'] extends ReadonlyArray<keyof p['properties']> ? p['anyOf'] extends ReadonlyArray<Schema> ? p['anyOf'][number]['required'] extends ReadonlyArray<keyof p['properties']> ?
UnionObjType<p['properties'], NonNullable<p['anyOf'][number]['required']>> & ObjType<p['properties'], NonNullable<p['required']>> UnionObjType<p['properties'], NonNullable<p['anyOf'][number]['required']>> & ObjType<p['properties'], NonNullable<p['required']>>
: never : never
: ObjType<p['properties'], NonNullable<p['required']>> : ObjType<p['properties'], NonNullable<p['required']>>
: :
p['anyOf'] extends ReadonlyArray<Schema> ? never : // see CONTRIBUTING.md p['anyOf'] extends ReadonlyArray<Schema> ? never : // see CONTRIBUTING.md
p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> : p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> :
any; p['additionalProperties'] extends true ? Record<string, any> :
p['additionalProperties'] extends Schema ?
p['additionalProperties'] extends infer AdditionalProperties ?
AdditionalProperties extends Schema ?
Record<string, SchemaType<AdditionalProperties>> :
never :
never :
any;
type ObjectSchemaType<p extends Schema> = NullOrUndefined<p, ObjectSchemaTypeDef<p>>; type ObjectSchemaType<p extends Schema> = NullOrUndefined<p, ObjectSchemaTypeDef<p>>;
@@ -227,30 +244,30 @@ export type SchemaTypeDef<p extends Schema> =
p['type'] extends 'number' ? number : p['type'] extends 'number' ? number :
p['type'] extends 'string' ? ( p['type'] extends 'string' ? (
p['enum'] extends readonly (string | null)[] ? p['enum'] extends readonly (string | null)[] ?
p['enum'][number] : p['enum'][number] :
p['format'] extends 'date-time' ? string : // Dateにする p['format'] extends 'date-time' ? string : // Dateにする
string string
) : ) :
p['type'] extends 'boolean' ? boolean : p['type'] extends 'boolean' ? boolean :
p['type'] extends 'object' ? ObjectSchemaTypeDef<p> : p['type'] extends 'object' ? ObjectSchemaTypeDef<p> :
p['type'] extends 'array' ? ( p['type'] extends 'array' ? (
p['items'] extends OfSchema ? ( p['items'] extends OfSchema ? (
p['items']['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<NonNullable<p['items']['anyOf']>>[] : p['items']['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<NonNullable<p['items']['anyOf']>>[] :
p['items']['oneOf'] extends ReadonlyArray<Schema> ? ArrayUnion<UnionSchemaType<NonNullable<p['items']['oneOf']>>> : p['items']['oneOf'] extends ReadonlyArray<Schema> ? ArrayUnion<UnionSchemaType<NonNullable<p['items']['oneOf']>>> :
p['items']['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<NonNullable<p['items']['allOf']>>>[] : p['items']['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<NonNullable<p['items']['allOf']>>>[] :
never never
) :
p['prefixItems'] extends ReadonlyArray<Schema> ? (
p['items'] extends NonNullable<Schema> ? [...ArrayToTuple<p['prefixItems']>, ...SchemaType<p['items']>[]] :
p['items'] extends false ? ArrayToTuple<p['prefixItems']> :
p['unevaluatedItems'] extends false ? ArrayToTuple<p['prefixItems']> :
[...ArrayToTuple<p['prefixItems']>, ...unknown[]]
) :
p['items'] extends NonNullable<Schema> ? SchemaType<p['items']>[] :
any[]
) : ) :
p['prefixItems'] extends ReadonlyArray<Schema> ? ( p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> & PartialIntersection<UnionSchemaType<p['anyOf']>> :
p['items'] extends NonNullable<Schema> ? [...ArrayToTuple<p['prefixItems']>, ...SchemaType<p['items']>[]] : p['oneOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['oneOf']> :
p['items'] extends false ? ArrayToTuple<p['prefixItems']> : any;
p['unevaluatedItems'] extends false ? ArrayToTuple<p['prefixItems']> :
[...ArrayToTuple<p['prefixItems']>, ...unknown[]]
) :
p['items'] extends NonNullable<Schema> ? SchemaType<p['items']>[] :
any[]
) :
p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> & PartialIntersection<UnionSchemaType<p['anyOf']>> :
p['oneOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['oneOf']> :
any;
export type SchemaType<p extends Schema> = NullOrUndefined<p, SchemaTypeDef<p>>; export type SchemaType<p extends Schema> = NullOrUndefined<p, SchemaTypeDef<p>>;

View File

@@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { id } from './util/id.js';
import { MiUser } from './User.js';
@Entity('chat_approval')
@Index(['userId', 'otherId'], { unique: true })
export class MiChatApproval {
@PrimaryColumn(id())
public id: string;
@Index()
@Column({
...id(),
})
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
public user: MiUser | null;
@Index()
@Column({
...id(),
})
public otherId: MiUser['id'];
@ManyToOne(type => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
public other: MiUser | null;
}

View File

@@ -0,0 +1,85 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { id } from './util/id.js';
import { MiUser } from './User.js';
import { MiDriveFile } from './DriveFile.js';
import { MiChatRoom } from './ChatRoom.js';
@Entity('chat_message')
export class MiChatMessage {
@PrimaryColumn(id())
public id: string;
@Index()
@Column({
...id(),
})
public fromUserId: MiUser['id'];
@ManyToOne(type => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
public fromUser: MiUser | null;
@Index()
@Column({
...id(), nullable: true,
})
public toUserId: MiUser['id'] | null;
@ManyToOne(type => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
public toUser: MiUser | null;
@Index()
@Column({
...id(), nullable: true,
})
public toRoomId: MiChatRoom['id'] | null;
@ManyToOne(type => MiChatRoom, {
onDelete: 'CASCADE',
})
@JoinColumn()
public toRoom: MiChatRoom | null;
@Column('varchar', {
length: 4096, nullable: true,
})
public text: string | null;
@Column('varchar', {
length: 512, nullable: true,
})
public uri: string | null;
@Column({
...id(),
array: true, default: '{}',
})
public reads: MiUser['id'][];
@Column({
...id(),
nullable: true,
})
public fileId: MiDriveFile['id'] | null;
@ManyToOne(type => MiDriveFile, {
onDelete: 'SET NULL',
})
@JoinColumn()
public file: MiDriveFile | null;
@Column('varchar', {
length: 1024, array: true, default: '{}',
})
public reactions: string[];
}

View File

@@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { id } from './util/id.js';
import { MiUser } from './User.js';
@Entity('chat_room')
export class MiChatRoom {
@PrimaryColumn(id())
public id: string;
@Column('varchar', {
length: 256,
})
public name: string;
@Index()
@Column({
...id(),
})
public ownerId: MiUser['id'];
@ManyToOne(type => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
public owner: MiUser | null;
}

View File

@@ -0,0 +1,45 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { id } from './util/id.js';
import { MiUser } from './User.js';
import { MiChatRoom } from './ChatRoom.js';
@Entity('chat_room_invitation')
@Index(['userId', 'roomId'], { unique: true })
export class MiChatRoomInvitation {
@PrimaryColumn(id())
public id: string;
@Index()
@Column({
...id(),
})
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
public user: MiUser | null;
@Index()
@Column({
...id(),
})
public roomId: MiChatRoom['id'];
@ManyToOne(type => MiChatRoom, {
onDelete: 'CASCADE',
})
@JoinColumn()
public room: MiChatRoom | null;
@Column('boolean', {
default: false,
})
public ignored: boolean;
}

View File

@@ -0,0 +1,40 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { id } from './util/id.js';
import { MiUser } from './User.js';
import { MiChatRoom } from './ChatRoom.js';
@Entity('chat_room_membership')
@Index(['userId', 'roomId'], { unique: true })
export class MiChatRoomMembership {
@PrimaryColumn(id())
public id: string;
@Index()
@Column({
...id(),
})
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
public user: MiUser | null;
@Index()
@Column({
...id(),
})
public roomId: MiChatRoom['id'];
@ManyToOne(type => MiChatRoom, {
onDelete: 'CASCADE',
})
@JoinColumn()
public room: MiChatRoom | null;
}

View File

@@ -1,68 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { id } from './util/id.js';
import { MiUser } from './User.js';
import { MiNote } from './Note.js';
import type { MiChannel } from './Channel.js';
@Entity('note_unread')
@Index(['userId', 'noteId'], { unique: true })
export class MiNoteUnread {
@PrimaryColumn(id())
public id: string;
@Index()
@Column(id())
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
public user: MiUser | null;
@Index()
@Column(id())
public noteId: MiNote['id'];
@ManyToOne(type => MiNote, {
onDelete: 'CASCADE',
})
@JoinColumn()
public note: MiNote | null;
/**
* メンションか否か
*/
@Index()
@Column('boolean')
public isMentioned: boolean;
/**
* ダイレクト投稿か否か
*/
@Index()
@Column('boolean')
public isSpecified: boolean;
//#region Denormalized fields
@Index()
@Column({
...id(),
comment: '[Denormalized]',
})
public noteUserId: MiUser['id'];
@Index()
@Column({
...id(),
nullable: true,
comment: '[Denormalized]',
})
public noteChannelId: MiChannel['id'] | null;
//#endregion
}

View File

@@ -42,7 +42,6 @@ import {
MiNoteFavorite, MiNoteFavorite,
MiNoteReaction, MiNoteReaction,
MiNoteThreadMuting, MiNoteThreadMuting,
MiNoteUnread,
MiPage, MiPage,
MiPageLike, MiPageLike,
MiPasswordResetRequest, MiPasswordResetRequest,
@@ -78,6 +77,11 @@ import {
MiUserPublickey, MiUserPublickey,
MiUserSecurityKey, MiUserSecurityKey,
MiWebhook, MiWebhook,
MiChatMessage,
MiChatRoom,
MiChatRoomMembership,
MiChatRoomInvitation,
MiChatApproval,
} from './_.js'; } from './_.js';
import type { Provider } from '@nestjs/common'; import type { Provider } from '@nestjs/common';
import type { DataSource } from 'typeorm'; import type { DataSource } from 'typeorm';
@@ -136,12 +140,6 @@ const $noteReactionsRepository: Provider = {
inject: [DI.db], inject: [DI.db],
}; };
const $noteUnreadsRepository: Provider = {
provide: DI.noteUnreadsRepository,
useFactory: (db: DataSource) => db.getRepository(MiNoteUnread).extend(miRepository as MiRepository<MiNoteUnread>),
inject: [DI.db],
};
const $pollsRepository: Provider = { const $pollsRepository: Provider = {
provide: DI.pollsRepository, provide: DI.pollsRepository,
useFactory: (db: DataSource) => db.getRepository(MiPoll).extend(miRepository as MiRepository<MiPoll>), useFactory: (db: DataSource) => db.getRepository(MiPoll).extend(miRepository as MiRepository<MiPoll>),
@@ -288,7 +286,7 @@ const $swSubscriptionsRepository: Provider = {
const $systemAccountsRepository: Provider = { const $systemAccountsRepository: Provider = {
provide: DI.systemAccountsRepository, provide: DI.systemAccountsRepository,
useFactory: (db: DataSource) => db.getRepository(MiSystemAccount), useFactory: (db: DataSource) => db.getRepository(MiSystemAccount).extend(miRepository as MiRepository<MiSystemAccount>),
inject: [DI.db], inject: [DI.db],
}; };
@@ -306,7 +304,7 @@ const $abuseUserReportsRepository: Provider = {
const $abuseReportNotificationRecipientRepository: Provider = { const $abuseReportNotificationRecipientRepository: Provider = {
provide: DI.abuseReportNotificationRecipientRepository, provide: DI.abuseReportNotificationRecipientRepository,
useFactory: (db: DataSource) => db.getRepository(MiAbuseReportNotificationRecipient), useFactory: (db: DataSource) => db.getRepository(MiAbuseReportNotificationRecipient).extend(miRepository as MiRepository<MiAbuseReportNotificationRecipient>),
inject: [DI.db], inject: [DI.db],
}; };
@@ -438,7 +436,7 @@ const $webhooksRepository: Provider = {
const $systemWebhooksRepository: Provider = { const $systemWebhooksRepository: Provider = {
provide: DI.systemWebhooksRepository, provide: DI.systemWebhooksRepository,
useFactory: (db: DataSource) => db.getRepository(MiSystemWebhook), useFactory: (db: DataSource) => db.getRepository(MiSystemWebhook).extend(miRepository as MiRepository<MiSystemWebhook>),
inject: [DI.db], inject: [DI.db],
}; };
@@ -490,6 +488,36 @@ const $userMemosRepository: Provider = {
inject: [DI.db], inject: [DI.db],
}; };
const $chatMessagesRepository: Provider = {
provide: DI.chatMessagesRepository,
useFactory: (db: DataSource) => db.getRepository(MiChatMessage).extend(miRepository as MiRepository<MiChatMessage>),
inject: [DI.db],
};
const $chatRoomsRepository: Provider = {
provide: DI.chatRoomsRepository,
useFactory: (db: DataSource) => db.getRepository(MiChatRoom).extend(miRepository as MiRepository<MiChatRoom>),
inject: [DI.db],
};
const $chatRoomMembershipsRepository: Provider = {
provide: DI.chatRoomMembershipsRepository,
useFactory: (db: DataSource) => db.getRepository(MiChatRoomMembership).extend(miRepository as MiRepository<MiChatRoomMembership>),
inject: [DI.db],
};
const $chatRoomInvitationsRepository: Provider = {
provide: DI.chatRoomInvitationsRepository,
useFactory: (db: DataSource) => db.getRepository(MiChatRoomInvitation).extend(miRepository as MiRepository<MiChatRoomInvitation>),
inject: [DI.db],
};
const $chatApprovalsRepository: Provider = {
provide: DI.chatApprovalsRepository,
useFactory: (db: DataSource) => db.getRepository(MiChatApproval).extend(miRepository as MiRepository<MiChatApproval>),
inject: [DI.db],
};
const $bubbleGameRecordsRepository: Provider = { const $bubbleGameRecordsRepository: Provider = {
provide: DI.bubbleGameRecordsRepository, provide: DI.bubbleGameRecordsRepository,
useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord).extend(miRepository as MiRepository<MiBubbleGameRecord>), useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord).extend(miRepository as MiRepository<MiBubbleGameRecord>),
@@ -514,7 +542,6 @@ const $reversiGamesRepository: Provider = {
$noteFavoritesRepository, $noteFavoritesRepository,
$noteThreadMutingsRepository, $noteThreadMutingsRepository,
$noteReactionsRepository, $noteReactionsRepository,
$noteUnreadsRepository,
$pollsRepository, $pollsRepository,
$pollVotesRepository, $pollVotesRepository,
$userProfilesRepository, $userProfilesRepository,
@@ -573,6 +600,11 @@ const $reversiGamesRepository: Provider = {
$flashsRepository, $flashsRepository,
$flashLikesRepository, $flashLikesRepository,
$userMemosRepository, $userMemosRepository,
$chatMessagesRepository,
$chatRoomsRepository,
$chatRoomMembershipsRepository,
$chatRoomInvitationsRepository,
$chatApprovalsRepository,
$bubbleGameRecordsRepository, $bubbleGameRecordsRepository,
$reversiGamesRepository, $reversiGamesRepository,
], ],
@@ -586,7 +618,6 @@ const $reversiGamesRepository: Provider = {
$noteFavoritesRepository, $noteFavoritesRepository,
$noteThreadMutingsRepository, $noteThreadMutingsRepository,
$noteReactionsRepository, $noteReactionsRepository,
$noteUnreadsRepository,
$pollsRepository, $pollsRepository,
$pollVotesRepository, $pollVotesRepository,
$userProfilesRepository, $userProfilesRepository,
@@ -645,6 +676,11 @@ const $reversiGamesRepository: Provider = {
$flashsRepository, $flashsRepository,
$flashLikesRepository, $flashLikesRepository,
$userMemosRepository, $userMemosRepository,
$chatMessagesRepository,
$chatRoomsRepository,
$chatRoomMembershipsRepository,
$chatRoomInvitationsRepository,
$chatApprovalsRepository,
$bubbleGameRecordsRepository, $bubbleGameRecordsRepository,
$reversiGamesRepository, $reversiGamesRepository,
], ],

View File

@@ -225,6 +225,17 @@ export class MiUser {
}) })
public emojis: string[]; public emojis: string[];
// チャットを許可する相手
// everyone: 誰からでも
// followers: フォロワーのみ
// following: フォローしているユーザーのみ
// mutual: 相互フォローのみ
// none: 誰からも受け付けない
@Column('varchar', {
length: 128, default: 'mutual',
})
public chatScope: 'everyone' | 'followers' | 'following' | 'mutual' | 'none';
@Index() @Index()
@Column('varchar', { @Column('varchar', {
length: 128, nullable: true, length: 128, nullable: true,

View File

@@ -3,13 +3,10 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { FindOneOptions, InsertQueryBuilder, ObjectLiteral, Repository, SelectQueryBuilder, TypeORMError } from 'typeorm'; import { FindOneOptions, InsertQueryBuilder, ObjectLiteral, Repository, SelectQueryBuilder } from 'typeorm';
import { DriverUtils } from 'typeorm/driver/DriverUtils.js';
import { RelationCountLoader } from 'typeorm/query-builder/relation-count/RelationCountLoader.js'; import { RelationCountLoader } from 'typeorm/query-builder/relation-count/RelationCountLoader.js';
import { RelationIdLoader } from 'typeorm/query-builder/relation-id/RelationIdLoader.js'; import { RelationIdLoader } from 'typeorm/query-builder/relation-id/RelationIdLoader.js';
import { RawSqlResultsToEntityTransformer } from 'typeorm/query-builder/transformer/RawSqlResultsToEntityTransformer.js'; import { RawSqlResultsToEntityTransformer } from 'typeorm/query-builder/transformer/RawSqlResultsToEntityTransformer.js';
import { ObjectUtils } from 'typeorm/util/ObjectUtils.js';
import { OrmUtils } from 'typeorm/util/OrmUtils.js';
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js'; import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js';
import { MiAccessToken } from '@/models/AccessToken.js'; import { MiAccessToken } from '@/models/AccessToken.js';
@@ -43,7 +40,6 @@ import { MiNote } from '@/models/Note.js';
import { MiNoteFavorite } from '@/models/NoteFavorite.js'; import { MiNoteFavorite } from '@/models/NoteFavorite.js';
import { MiNoteReaction } from '@/models/NoteReaction.js'; import { MiNoteReaction } from '@/models/NoteReaction.js';
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js'; import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
import { MiNoteUnread } from '@/models/NoteUnread.js';
import { MiPage } from '@/models/Page.js'; import { MiPage } from '@/models/Page.js';
import { MiPageLike } from '@/models/PageLike.js'; import { MiPageLike } from '@/models/PageLike.js';
import { MiPasswordResetRequest } from '@/models/PasswordResetRequest.js'; import { MiPasswordResetRequest } from '@/models/PasswordResetRequest.js';
@@ -78,6 +74,11 @@ import { MiRoleAssignment } from '@/models/RoleAssignment.js';
import { MiFlash } from '@/models/Flash.js'; import { MiFlash } from '@/models/Flash.js';
import { MiFlashLike } from '@/models/FlashLike.js'; import { MiFlashLike } from '@/models/FlashLike.js';
import { MiUserListFavorite } from '@/models/UserListFavorite.js'; import { MiUserListFavorite } from '@/models/UserListFavorite.js';
import { MiChatMessage } from '@/models/ChatMessage.js';
import { MiChatRoom } from '@/models/ChatRoom.js';
import { MiChatRoomMembership } from '@/models/ChatRoomMembership.js';
import { MiChatRoomInvitation } from '@/models/ChatRoomInvitation.js';
import { MiChatApproval } from '@/models/ChatApproval.js';
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
import { MiReversiGame } from '@/models/ReversiGame.js'; import { MiReversiGame } from '@/models/ReversiGame.js';
import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js'; import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
@@ -159,7 +160,6 @@ export {
MiNoteFavorite, MiNoteFavorite,
MiNoteReaction, MiNoteReaction,
MiNoteThreadMuting, MiNoteThreadMuting,
MiNoteUnread,
MiPage, MiPage,
MiPageLike, MiPageLike,
MiPasswordResetRequest, MiPasswordResetRequest,
@@ -194,6 +194,11 @@ export {
MiFlash, MiFlash,
MiFlashLike, MiFlashLike,
MiUserMemo, MiUserMemo,
MiChatMessage,
MiChatRoom,
MiChatRoomMembership,
MiChatRoomInvitation,
MiChatApproval,
MiBubbleGameRecord, MiBubbleGameRecord,
MiReversiGame, MiReversiGame,
}; };
@@ -231,7 +236,6 @@ export type NotesRepository = Repository<MiNote> & MiRepository<MiNote>;
export type NoteFavoritesRepository = Repository<MiNoteFavorite> & MiRepository<MiNoteFavorite>; export type NoteFavoritesRepository = Repository<MiNoteFavorite> & MiRepository<MiNoteFavorite>;
export type NoteReactionsRepository = Repository<MiNoteReaction> & MiRepository<MiNoteReaction>; export type NoteReactionsRepository = Repository<MiNoteReaction> & MiRepository<MiNoteReaction>;
export type NoteThreadMutingsRepository = Repository<MiNoteThreadMuting> & MiRepository<MiNoteThreadMuting>; export type NoteThreadMutingsRepository = Repository<MiNoteThreadMuting> & MiRepository<MiNoteThreadMuting>;
export type NoteUnreadsRepository = Repository<MiNoteUnread> & MiRepository<MiNoteUnread>;
export type PagesRepository = Repository<MiPage> & MiRepository<MiPage>; export type PagesRepository = Repository<MiPage> & MiRepository<MiPage>;
export type PageLikesRepository = Repository<MiPageLike> & MiRepository<MiPageLike>; export type PageLikesRepository = Repository<MiPageLike> & MiRepository<MiPageLike>;
export type PasswordResetRequestsRepository = Repository<MiPasswordResetRequest> & MiRepository<MiPasswordResetRequest>; export type PasswordResetRequestsRepository = Repository<MiPasswordResetRequest> & MiRepository<MiPasswordResetRequest>;
@@ -266,5 +270,10 @@ export type RoleAssignmentsRepository = Repository<MiRoleAssignment> & MiReposit
export type FlashsRepository = Repository<MiFlash> & MiRepository<MiFlash>; export type FlashsRepository = Repository<MiFlash> & MiRepository<MiFlash>;
export type FlashLikesRepository = Repository<MiFlashLike> & MiRepository<MiFlashLike>; export type FlashLikesRepository = Repository<MiFlashLike> & MiRepository<MiFlashLike>;
export type UserMemoRepository = Repository<MiUserMemo> & MiRepository<MiUserMemo>; export type UserMemoRepository = Repository<MiUserMemo> & MiRepository<MiUserMemo>;
export type ChatMessagesRepository = Repository<MiChatMessage> & MiRepository<MiChatMessage>;
export type ChatRoomsRepository = Repository<MiChatRoom> & MiRepository<MiChatRoom>;
export type ChatRoomMembershipsRepository = Repository<MiChatRoomMembership> & MiRepository<MiChatRoomMembership>;
export type ChatRoomInvitationsRepository = Repository<MiChatRoomInvitation> & MiRepository<MiChatRoomInvitation>;
export type ChatApprovalsRepository = Repository<MiChatApproval> & MiRepository<MiChatApproval>;
export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord> & MiRepository<MiBubbleGameRecord>; export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord> & MiRepository<MiBubbleGameRecord>;
export type ReversiGamesRepository = Repository<MiReversiGame> & MiRepository<MiReversiGame>; export type ReversiGamesRepository = Repository<MiReversiGame> & MiRepository<MiReversiGame>;

View File

@@ -0,0 +1,103 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const packedChatMessageSchema = {
type: 'object',
properties: {
id: {
type: 'string',
optional: false, nullable: false,
},
createdAt: {
type: 'string',
format: 'date-time',
optional: false, nullable: false,
},
fromUserId: {
type: 'string',
optional: false, nullable: false,
},
fromUser: {
type: 'object',
optional: false, nullable: false,
ref: 'UserLite',
},
toUserId: {
type: 'string',
optional: true, nullable: true,
},
toUser: {
type: 'object',
optional: true, nullable: true,
ref: 'UserLite',
},
toRoomId: {
type: 'string',
optional: true, nullable: true,
},
toRoom: {
type: 'object',
optional: true, nullable: true,
ref: 'ChatRoom',
},
text: {
type: 'string',
optional: true, nullable: true,
},
fileId: {
type: 'string',
optional: true, nullable: true,
},
file: {
type: 'object',
optional: true, nullable: true,
ref: 'DriveFile',
},
isRead: {
type: 'boolean',
optional: true, nullable: false,
},
},
} as const;
export const packedChatMessageLiteSchema = {
type: 'object',
properties: {
id: {
type: 'string',
optional: false, nullable: false,
},
createdAt: {
type: 'string',
format: 'date-time',
optional: false, nullable: false,
},
fromUserId: {
type: 'string',
optional: false, nullable: false,
},
toUserId: {
type: 'string',
optional: true, nullable: true,
},
toRoomId: {
type: 'string',
optional: true, nullable: true,
},
text: {
type: 'string',
optional: true, nullable: true,
},
fileId: {
type: 'string',
optional: true, nullable: true,
},
file: {
type: 'object',
optional: true, nullable: true,
ref: 'DriveFile',
},
},
} as const;

View File

@@ -0,0 +1,37 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const packedChatRoomInvitationSchema = {
type: 'object',
properties: {
id: {
type: 'string',
optional: false, nullable: false,
},
createdAt: {
type: 'string',
format: 'date-time',
optional: false, nullable: false,
},
userId: {
type: 'string',
optional: false, nullable: false,
},
user: {
type: 'object',
optional: false, nullable: false,
ref: 'UserLite',
},
roomId: {
type: 'string',
optional: false, nullable: false,
},
room: {
type: 'object',
optional: false, nullable: false,
ref: 'ChatRoom',
},
},
} as const;

View File

@@ -0,0 +1,28 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const packedChatRoomMembershipSchema = {
type: 'object',
properties: {
id: {
type: 'string',
optional: false, nullable: false,
},
createdAt: {
type: 'string',
format: 'date-time',
optional: false, nullable: false,
},
userId: {
type: 'string',
optional: false, nullable: false,
},
user: {
type: 'object',
optional: false, nullable: false,
ref: 'UserLite',
},
},
} as const;

View File

@@ -0,0 +1,32 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const packedChatRoomSchema = {
type: 'object',
properties: {
id: {
type: 'string',
optional: false, nullable: false,
},
createdAt: {
type: 'string',
format: 'date-time',
optional: false, nullable: false,
},
ownerId: {
type: 'string',
optional: false, nullable: false,
},
owner: {
type: 'object',
optional: false, nullable: false,
ref: 'UserLite',
},
name: {
type: 'string',
optional: false, nullable: false,
},
},
} as const;

View File

@@ -292,6 +292,10 @@ export const packedRolePoliciesSchema = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
canChat: {
type: 'boolean',
optional: false, nullable: false,
},
}, },
} as const; } as const;

View File

@@ -358,6 +358,11 @@ export const packedUserDetailedNotMeOnlySchema = {
nullable: false, optional: false, nullable: false, optional: false,
enum: ['public', 'followers', 'private'], enum: ['public', 'followers', 'private'],
}, },
chatScope: {
type: 'string',
nullable: false, optional: false,
enum: ['everyone', 'following', 'followers', 'mutual', 'none'],
},
roles: { roles: {
type: 'array', type: 'array',
nullable: false, optional: false, nullable: false, optional: false,
@@ -540,6 +545,10 @@ export const packedMeDetailedOnlySchema = {
type: 'boolean', type: 'boolean',
nullable: false, optional: false, nullable: false, optional: false,
}, },
hasUnreadChatMessages: {
type: 'boolean',
nullable: false, optional: false,
},
hasUnreadNotification: { hasUnreadNotification: {
type: 'boolean', type: 'boolean',
nullable: false, optional: false, nullable: false, optional: false,

View File

@@ -8,6 +8,9 @@ import pg from 'pg';
import { DataSource, Logger } from 'typeorm'; import { DataSource, Logger } from 'typeorm';
import * as highlight from 'cli-highlight'; import * as highlight from 'cli-highlight';
import { entities as charts } from '@/core/chart/entities.js'; import { entities as charts } from '@/core/chart/entities.js';
import { Config } from '@/config.js';
import MisskeyLogger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js'; import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js';
@@ -42,7 +45,6 @@ import { MiNote } from '@/models/Note.js';
import { MiNoteFavorite } from '@/models/NoteFavorite.js'; import { MiNoteFavorite } from '@/models/NoteFavorite.js';
import { MiNoteReaction } from '@/models/NoteReaction.js'; import { MiNoteReaction } from '@/models/NoteReaction.js';
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js'; import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
import { MiNoteUnread } from '@/models/NoteUnread.js';
import { MiPage } from '@/models/Page.js'; import { MiPage } from '@/models/Page.js';
import { MiPageLike } from '@/models/PageLike.js'; import { MiPageLike } from '@/models/PageLike.js';
import { MiPasswordResetRequest } from '@/models/PasswordResetRequest.js'; import { MiPasswordResetRequest } from '@/models/PasswordResetRequest.js';
@@ -76,13 +78,14 @@ import { MiRoleAssignment } from '@/models/RoleAssignment.js';
import { MiFlash } from '@/models/Flash.js'; import { MiFlash } from '@/models/Flash.js';
import { MiFlashLike } from '@/models/FlashLike.js'; import { MiFlashLike } from '@/models/FlashLike.js';
import { MiUserMemo } from '@/models/UserMemo.js'; import { MiUserMemo } from '@/models/UserMemo.js';
import { MiChatMessage } from '@/models/ChatMessage.js';
import { MiChatRoom } from '@/models/ChatRoom.js';
import { MiChatRoomMembership } from '@/models/ChatRoomMembership.js';
import { MiChatRoomInvitation } from '@/models/ChatRoomInvitation.js';
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
import { MiReversiGame } from '@/models/ReversiGame.js'; import { MiReversiGame } from '@/models/ReversiGame.js';
import { MiChatApproval } from '@/models/ChatApproval.js';
import { Config } from '@/config.js'; import { MiSystemAccount } from '@/models/SystemAccount.js';
import MisskeyLogger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { MiSystemAccount } from './models/SystemAccount.js';
pg.types.setTypeParser(20, Number); pg.types.setTypeParser(20, Number);
@@ -195,7 +198,6 @@ export const entities = [
MiNoteFavorite, MiNoteFavorite,
MiNoteReaction, MiNoteReaction,
MiNoteThreadMuting, MiNoteThreadMuting,
MiNoteUnread,
MiPage, MiPage,
MiPageLike, MiPageLike,
MiGalleryPost, MiGalleryPost,
@@ -236,6 +238,11 @@ export const entities = [
MiFlash, MiFlash,
MiFlashLike, MiFlashLike,
MiUserMemo, MiUserMemo,
MiChatMessage,
MiChatRoom,
MiChatRoomMembership,
MiChatRoomInvitation,
MiChatApproval,
MiBubbleGameRecord, MiBubbleGameRecord,
MiReversiGame, MiReversiGame,
...charts, ...charts,

View File

@@ -6,6 +6,7 @@
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { ZipReader } from 'slacc'; import { ZipReader } from 'slacc';
import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { EmojisRepository, DriveFilesRepository } from '@/models/_.js'; import type { EmojisRepository, DriveFilesRepository } from '@/models/_.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
@@ -86,6 +87,7 @@ export class ImportCustomEmojisProcessorService {
const emojiPath = outputPath + '/' + record.fileName; const emojiPath = outputPath + '/' + record.fileName;
await this.emojisRepository.delete({ await this.emojisRepository.delete({
name: emojiInfo.name, name: emojiInfo.name,
host: IsNull(),
}); });
try { try {

View File

@@ -13,7 +13,7 @@ import accepts from 'accepts';
import vary from 'vary'; import vary from 'vary';
import secureJson from 'secure-json-parse'; import secureJson from 'secure-json-parse';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { FollowingsRepository, NotesRepository, EmojisRepository, NoteReactionsRepository, UserProfilesRepository, UserNotePiningsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js'; import type { FollowingsRepository, NotesRepository, EmojisRepository, NoteReactionsRepository, UserProfilesRepository, UserNotePiningsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js';
import * as url from '@/misc/prelude/url.js'; import * as url from '@/misc/prelude/url.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
@@ -42,6 +42,9 @@ export class ActivityPubServerService {
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@@ -102,6 +105,11 @@ export class ActivityPubServerService {
@bindThis @bindThis
private inbox(request: FastifyRequest, reply: FastifyReply) { private inbox(request: FastifyRequest, reply: FastifyReply) {
if (this.meta.federation === 'none') {
reply.code(403);
return;
}
let signature; let signature;
try { try {
@@ -173,6 +181,11 @@ export class ActivityPubServerService {
request: FastifyRequest<{ Params: { user: string; }; Querystring: { cursor?: string; page?: string; }; }>, request: FastifyRequest<{ Params: { user: string; }; Querystring: { cursor?: string; page?: string; }; }>,
reply: FastifyReply, reply: FastifyReply,
) { ) {
if (this.meta.federation === 'none') {
reply.code(403);
return;
}
const userId = request.params.user; const userId = request.params.user;
const cursor = request.query.cursor; const cursor = request.query.cursor;
@@ -265,6 +278,11 @@ export class ActivityPubServerService {
request: FastifyRequest<{ Params: { user: string; }; Querystring: { cursor?: string; page?: string; }; }>, request: FastifyRequest<{ Params: { user: string; }; Querystring: { cursor?: string; page?: string; }; }>,
reply: FastifyReply, reply: FastifyReply,
) { ) {
if (this.meta.federation === 'none') {
reply.code(403);
return;
}
const userId = request.params.user; const userId = request.params.user;
const cursor = request.query.cursor; const cursor = request.query.cursor;
@@ -354,6 +372,11 @@ export class ActivityPubServerService {
@bindThis @bindThis
private async featured(request: FastifyRequest<{ Params: { user: string; }; }>, reply: FastifyReply) { private async featured(request: FastifyRequest<{ Params: { user: string; }; }>, reply: FastifyReply) {
if (this.meta.federation === 'none') {
reply.code(403);
return;
}
const userId = request.params.user; const userId = request.params.user;
const user = await this.usersRepository.findOneBy({ const user = await this.usersRepository.findOneBy({
@@ -398,6 +421,11 @@ export class ActivityPubServerService {
}>, }>,
reply: FastifyReply, reply: FastifyReply,
) { ) {
if (this.meta.federation === 'none') {
reply.code(403);
return;
}
const userId = request.params.user; const userId = request.params.user;
const sinceId = request.query.since_id; const sinceId = request.query.since_id;
@@ -482,6 +510,11 @@ export class ActivityPubServerService {
@bindThis @bindThis
private async userInfo(request: FastifyRequest, reply: FastifyReply, user: MiUser | null) { private async userInfo(request: FastifyRequest, reply: FastifyReply, user: MiUser | null) {
if (this.meta.federation === 'none') {
reply.code(403);
return;
}
if (user == null) { if (user == null) {
reply.code(404); reply.code(404);
return; return;
@@ -564,6 +597,11 @@ export class ActivityPubServerService {
fastify.get<{ Params: { note: string; } }>('/notes/:note', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { fastify.get<{ Params: { note: string; } }>('/notes/:note', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
vary(reply.raw, 'Accept'); vary(reply.raw, 'Accept');
if (this.meta.federation === 'none') {
reply.code(403);
return;
}
const note = await this.notesRepository.findOneBy({ const note = await this.notesRepository.findOneBy({
id: request.params.note, id: request.params.note,
visibility: In(['public', 'home']), visibility: In(['public', 'home']),
@@ -594,6 +632,11 @@ export class ActivityPubServerService {
fastify.get<{ Params: { note: string; } }>('/notes/:note/activity', async (request, reply) => { fastify.get<{ Params: { note: string; } }>('/notes/:note/activity', async (request, reply) => {
vary(reply.raw, 'Accept'); vary(reply.raw, 'Accept');
if (this.meta.federation === 'none') {
reply.code(403);
return;
}
const note = await this.notesRepository.findOneBy({ const note = await this.notesRepository.findOneBy({
id: request.params.note, id: request.params.note,
userHost: IsNull(), userHost: IsNull(),
@@ -634,6 +677,11 @@ export class ActivityPubServerService {
// publickey // publickey
fastify.get<{ Params: { user: string; } }>('/users/:user/publickey', async (request, reply) => { fastify.get<{ Params: { user: string; } }>('/users/:user/publickey', async (request, reply) => {
if (this.meta.federation === 'none') {
reply.code(403);
return;
}
const userId = request.params.user; const userId = request.params.user;
const user = await this.usersRepository.findOneBy({ const user = await this.usersRepository.findOneBy({
@@ -661,6 +709,11 @@ export class ActivityPubServerService {
fastify.get<{ Params: { user: string; } }>('/users/:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { fastify.get<{ Params: { user: string; } }>('/users/:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
vary(reply.raw, 'Accept'); vary(reply.raw, 'Accept');
if (this.meta.federation === 'none') {
reply.code(403);
return;
}
const userId = request.params.user; const userId = request.params.user;
const user = await this.usersRepository.findOneBy({ const user = await this.usersRepository.findOneBy({
@@ -674,6 +727,11 @@ export class ActivityPubServerService {
fastify.get<{ Params: { acct: string; } }>('/@:acct', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { fastify.get<{ Params: { acct: string; } }>('/@:acct', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
vary(reply.raw, 'Accept'); vary(reply.raw, 'Accept');
if (this.meta.federation === 'none') {
reply.code(403);
return;
}
const acct = Acct.parse(request.params.acct); const acct = Acct.parse(request.params.acct);
const user = await this.usersRepository.findOneBy({ const user = await this.usersRepository.findOneBy({
@@ -688,6 +746,11 @@ export class ActivityPubServerService {
// emoji // emoji
fastify.get<{ Params: { emoji: string; } }>('/emojis/:emoji', async (request, reply) => { fastify.get<{ Params: { emoji: string; } }>('/emojis/:emoji', async (request, reply) => {
if (this.meta.federation === 'none') {
reply.code(403);
return;
}
const emoji = await this.emojisRepository.findOneBy({ const emoji = await this.emojisRepository.findOneBy({
host: IsNull(), host: IsNull(),
name: request.params.emoji, name: request.params.emoji,
@@ -705,6 +768,11 @@ export class ActivityPubServerService {
// like // like
fastify.get<{ Params: { like: string; } }>('/likes/:like', async (request, reply) => { fastify.get<{ Params: { like: string; } }>('/likes/:like', async (request, reply) => {
if (this.meta.federation === 'none') {
reply.code(403);
return;
}
const reaction = await this.noteReactionsRepository.findOneBy({ id: request.params.like }); const reaction = await this.noteReactionsRepository.findOneBy({ id: request.params.like });
if (reaction == null) { if (reaction == null) {
@@ -726,6 +794,11 @@ export class ActivityPubServerService {
// follow // follow
fastify.get<{ Params: { follower: string; followee: string; } }>('/follows/:follower/:followee', async (request, reply) => { fastify.get<{ Params: { follower: string; followee: string; } }>('/follows/:follower/:followee', async (request, reply) => {
if (this.meta.federation === 'none') {
reply.code(403);
return;
}
// This may be used before the follow is completed, so we do not // This may be used before the follow is completed, so we do not
// check if the following exists. // check if the following exists.
@@ -752,6 +825,11 @@ export class ActivityPubServerService {
// follow // follow
fastify.get<{ Params: { followRequestId: string; } }>('/follows/:followRequestId', async (request, reply) => { fastify.get<{ Params: { followRequestId: string; } }>('/follows/:followRequestId', async (request, reply) => {
if (this.meta.federation === 'none') {
reply.code(403);
return;
}
// This may be used before the follow is completed, so we do not // This may be used before the follow is completed, so we do not
// check if the following exists and only check if the follow request exists. // check if the following exists and only check if the follow request exists.

View File

@@ -44,6 +44,8 @@ import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js';
import { ServerStatsChannelService } from './api/stream/channels/server-stats.js'; import { ServerStatsChannelService } from './api/stream/channels/server-stats.js';
import { UserListChannelService } from './api/stream/channels/user-list.js'; import { UserListChannelService } from './api/stream/channels/user-list.js';
import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js'; import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js';
import { ChatUserChannelService } from './api/stream/channels/chat-user.js';
import { ChatRoomChannelService } from './api/stream/channels/chat-room.js';
import { ReversiChannelService } from './api/stream/channels/reversi.js'; import { ReversiChannelService } from './api/stream/channels/reversi.js';
import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js'; import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js';
import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js'; import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
@@ -84,6 +86,8 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
GlobalTimelineChannelService, GlobalTimelineChannelService,
HashtagChannelService, HashtagChannelService,
RoleTimelineChannelService, RoleTimelineChannelService,
ChatUserChannelService,
ChatRoomChannelService,
ReversiChannelService, ReversiChannelService,
ReversiGameChannelService, ReversiGameChannelService,
HomeTimelineChannelService, HomeTimelineChannelService,

View File

@@ -8,7 +8,7 @@ import { IsNull } from 'typeorm';
import vary from 'vary'; import vary from 'vary';
import fastifyAccepts from '@fastify/accepts'; import fastifyAccepts from '@fastify/accepts';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { UsersRepository } from '@/models/_.js'; import type { MiMeta, UsersRepository } from '@/models/_.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { escapeAttribute, escapeValue } from '@/misc/prelude/xml.js'; import { escapeAttribute, escapeValue } from '@/misc/prelude/xml.js';
import type { MiUser } from '@/models/User.js'; import type { MiUser } from '@/models/User.js';
@@ -26,6 +26,9 @@ export class WellKnownServerService {
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@@ -66,6 +69,11 @@ export class WellKnownServerService {
}); });
fastify.get('/.well-known/host-meta', async (request, reply) => { fastify.get('/.well-known/host-meta', async (request, reply) => {
if (this.meta.federation === 'none') {
reply.code(403);
return;
}
reply.header('Content-Type', xrd); reply.header('Content-Type', xrd);
return XRD({ element: 'Link', attributes: { return XRD({ element: 'Link', attributes: {
rel: 'lrdd', rel: 'lrdd',
@@ -75,6 +83,11 @@ export class WellKnownServerService {
}); });
fastify.get('/.well-known/host-meta.json', async (request, reply) => { fastify.get('/.well-known/host-meta.json', async (request, reply) => {
if (this.meta.federation === 'none') {
reply.code(403);
return;
}
reply.header('Content-Type', 'application/json'); reply.header('Content-Type', 'application/json');
return { return {
links: [{ links: [{
@@ -86,6 +99,11 @@ export class WellKnownServerService {
}); });
fastify.get('/.well-known/nodeinfo', async (request, reply) => { fastify.get('/.well-known/nodeinfo', async (request, reply) => {
if (this.meta.federation === 'none') {
reply.code(403);
return;
}
return { links: this.nodeinfoServerService.getLinks() }; return { links: this.nodeinfoServerService.getLinks() };
}); });
@@ -99,6 +117,11 @@ fastify.get('/.well-known/change-password', async (request, reply) => {
*/ */
fastify.get<{ Querystring: { resource: string } }>(webFingerPath, async (request, reply) => { fastify.get<{ Querystring: { resource: string } }>(webFingerPath, async (request, reply) => {
if (this.meta.federation === 'none') {
reply.code(403);
return;
}
const fromId = (id: MiUser['id']): FindOptionsWhere<MiUser> => ({ const fromId = (id: MiUser['id']): FindOptionsWhere<MiUser> => ({
id, id,
host: IsNull(), host: IsNull(),

View File

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

View File

@@ -6,7 +6,6 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import cors from '@fastify/cors'; import cors from '@fastify/cors';
import multipart from '@fastify/multipart'; import multipart from '@fastify/multipart';
import fastifyCookie from '@fastify/cookie';
import { ModuleRef } from '@nestjs/core'; import { ModuleRef } from '@nestjs/core';
import { AuthenticationResponseJSON } from '@simplewebauthn/types'; import { AuthenticationResponseJSON } from '@simplewebauthn/types';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
@@ -57,8 +56,6 @@ export class ApiServerService {
}, },
}); });
fastify.register(fastifyCookie, {});
// Prevent cache // Prevent cache
fastify.addHook('onRequest', (request, reply, done) => { fastify.addHook('onRequest', (request, reply, done) => {
reply.header('Cache-Control', 'private, max-age=0, must-revalidate'); reply.header('Cache-Control', 'private, max-age=0, must-revalidate');

View File

@@ -9,7 +9,6 @@ import * as Redis from 'ioredis';
import * as WebSocket from 'ws'; import * as WebSocket from 'ws';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { UsersRepository, MiAccessToken } from '@/models/_.js'; import type { UsersRepository, MiAccessToken } from '@/models/_.js';
import { NoteReadService } from '@/core/NoteReadService.js';
import { NotificationService } from '@/core/NotificationService.js'; import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
@@ -35,7 +34,6 @@ export class StreamingApiServerService {
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
private cacheService: CacheService, private cacheService: CacheService,
private noteReadService: NoteReadService,
private authenticateService: AuthenticateService, private authenticateService: AuthenticateService,
private channelsService: ChannelsService, private channelsService: ChannelsService,
private notificationService: NotificationService, private notificationService: NotificationService,
@@ -96,7 +94,6 @@ export class StreamingApiServerService {
const stream = new MainStreamConnection( const stream = new MainStreamConnection(
this.channelsService, this.channelsService,
this.noteReadService,
this.notificationService, this.notificationService,
this.cacheService, this.cacheService,
this.channelFollowingService, this.channelFollowingService,

View File

@@ -263,7 +263,6 @@ export * as 'i/notifications-grouped' from './endpoints/i/notifications-grouped.
export * as 'i/page-likes' from './endpoints/i/page-likes.js'; export * as 'i/page-likes' from './endpoints/i/page-likes.js';
export * as 'i/pages' from './endpoints/i/pages.js'; export * as 'i/pages' from './endpoints/i/pages.js';
export * as 'i/pin' from './endpoints/i/pin.js'; export * as 'i/pin' from './endpoints/i/pin.js';
export * as 'i/read-all-unread-notes' from './endpoints/i/read-all-unread-notes.js';
export * as 'i/read-announcement' from './endpoints/i/read-announcement.js'; export * as 'i/read-announcement' from './endpoints/i/read-announcement.js';
export * as 'i/regenerate-token' from './endpoints/i/regenerate-token.js'; export * as 'i/regenerate-token' from './endpoints/i/regenerate-token.js';
export * as 'i/registry/get' from './endpoints/i/registry/get.js'; export * as 'i/registry/get' from './endpoints/i/registry/get.js';
@@ -397,4 +396,22 @@ export * as 'users/search' from './endpoints/users/search.js';
export * as 'users/search-by-username-and-host' from './endpoints/users/search-by-username-and-host.js'; export * as 'users/search-by-username-and-host' from './endpoints/users/search-by-username-and-host.js';
export * as 'users/show' from './endpoints/users/show.js'; export * as 'users/show' from './endpoints/users/show.js';
export * as 'users/update-memo' from './endpoints/users/update-memo.js'; export * as 'users/update-memo' from './endpoints/users/update-memo.js';
export * as 'chat/messages/create-to-user' from './endpoints/chat/messages/create-to-user.js';
export * as 'chat/messages/create-to-room' from './endpoints/chat/messages/create-to-room.js';
export * as 'chat/messages/delete' from './endpoints/chat/messages/delete.js';
export * as 'chat/messages/show' from './endpoints/chat/messages/show.js';
export * as 'chat/messages/user-timeline' from './endpoints/chat/messages/user-timeline.js';
export * as 'chat/messages/room-timeline' from './endpoints/chat/messages/room-timeline.js';
export * as 'chat/rooms/create' from './endpoints/chat/rooms/create.js';
export * as 'chat/rooms/delete' from './endpoints/chat/rooms/delete.js';
export * as 'chat/rooms/join' from './endpoints/chat/rooms/join.js';
export * as 'chat/rooms/leave' from './endpoints/chat/rooms/leave.js';
export * as 'chat/rooms/show' from './endpoints/chat/rooms/show.js';
export * as 'chat/rooms/owned' from './endpoints/chat/rooms/owned.js';
export * as 'chat/rooms/update' from './endpoints/chat/rooms/update.js';
export * as 'chat/rooms/members' from './endpoints/chat/rooms/members.js';
export * as 'chat/rooms/invitations/create' from './endpoints/chat/rooms/invitations/create.js';
export * as 'chat/rooms/invitations/ignore' from './endpoints/chat/rooms/invitations/ignore.js';
export * as 'chat/rooms/invitations/inbox' from './endpoints/chat/rooms/invitations/inbox.js';
export * as 'chat/history' from './endpoints/chat/history.js';
export * as 'v2/admin/emoji/list' from './endpoints/v2/admin/emoji/list.js'; export * as 'v2/admin/emoji/list' from './endpoints/v2/admin/emoji/list.js';

View File

@@ -39,7 +39,7 @@ interface IEndpointMetaBase {
*/ */
readonly requireAdmin?: boolean; readonly requireAdmin?: boolean;
readonly requireRolePolicy?: KeyOf<'RolePolicies'>; readonly requiredRolePolicy?: KeyOf<'RolePolicies'>;
/** /**
* 引っ越し済みのユーザーによるリクエストを禁止するか * 引っ越し済みのユーザーによるリクエストを禁止するか

View File

@@ -12,7 +12,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canManageAvatarDecorations', requiredRolePolicy: 'canManageAvatarDecorations',
kind: 'write:admin:avatar-decorations', kind: 'write:admin:avatar-decorations',
res: { res: {

View File

@@ -13,7 +13,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canManageAvatarDecorations', requiredRolePolicy: 'canManageAvatarDecorations',
kind: 'write:admin:avatar-decorations', kind: 'write:admin:avatar-decorations',
errors: { errors: {
}, },

View File

@@ -13,7 +13,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canManageAvatarDecorations', requiredRolePolicy: 'canManageAvatarDecorations',
kind: 'read:admin:avatar-decorations', kind: 'read:admin:avatar-decorations',
res: { res: {

View File

@@ -13,7 +13,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canManageAvatarDecorations', requiredRolePolicy: 'canManageAvatarDecorations',
kind: 'write:admin:avatar-decorations', kind: 'write:admin:avatar-decorations',
errors: { errors: {

View File

@@ -11,7 +11,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis', requiredRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji', kind: 'write:admin:emoji',
} as const; } as const;

View File

@@ -16,7 +16,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis', requiredRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji', kind: 'write:admin:emoji',
errors: { errors: {

View File

@@ -17,7 +17,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis', requiredRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji', kind: 'write:admin:emoji',
errors: { errors: {

View File

@@ -11,7 +11,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis', requiredRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji', kind: 'write:admin:emoji',
} as const; } as const;

View File

@@ -11,7 +11,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis', requiredRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji', kind: 'write:admin:emoji',
errors: { errors: {

View File

@@ -10,7 +10,7 @@ import { QueueService } from '@/core/QueueService.js';
export const meta = { export const meta = {
secure: true, secure: true,
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis', requiredRolePolicy: 'canManageCustomEmojis',
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@@ -16,7 +16,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis', requiredRolePolicy: 'canManageCustomEmojis',
kind: 'read:admin:emoji', kind: 'read:admin:emoji',
res: { res: {

View File

@@ -16,7 +16,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis', requiredRolePolicy: 'canManageCustomEmojis',
kind: 'read:admin:emoji', kind: 'read:admin:emoji',
res: { res: {

View File

@@ -11,7 +11,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis', requiredRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji', kind: 'write:admin:emoji',
} as const; } as const;

View File

@@ -11,7 +11,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis', requiredRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji', kind: 'write:admin:emoji',
} as const; } as const;

View File

@@ -11,7 +11,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis', requiredRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji', kind: 'write:admin:emoji',
} as const; } as const;

View File

@@ -11,7 +11,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis', requiredRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji', kind: 'write:admin:emoji',
} as const; } as const;

View File

@@ -14,7 +14,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis', requiredRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji', kind: 'write:admin:emoji',
errors: { errors: {

View File

@@ -8,7 +8,6 @@ import * as Redis from 'ioredis';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { NotesRepository, AntennasRepository } from '@/models/_.js'; import type { NotesRepository, AntennasRepository } from '@/models/_.js';
import { QueryService } from '@/core/QueryService.js'; import { QueryService } from '@/core/QueryService.js';
import { NoteReadService } from '@/core/NoteReadService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
@@ -59,9 +58,6 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.redisForTimelines)
private redisForTimelines: Redis.Redis,
@Inject(DI.notesRepository) @Inject(DI.notesRepository)
private notesRepository: NotesRepository, private notesRepository: NotesRepository,
@@ -71,7 +67,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private idService: IdService, private idService: IdService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private queryService: QueryService, private queryService: QueryService,
private noteReadService: NoteReadService,
private fanoutTimelineService: FanoutTimelineService, private fanoutTimelineService: FanoutTimelineService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
) { ) {
@@ -124,8 +119,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
notes.sort((a, b) => a.id > b.id ? -1 : 1); notes.sort((a, b) => a.id > b.id ? -1 : 1);
} }
this.noteReadService.read(me.id, notes);
return await this.noteEntityService.packMany(notes, me); return await this.noteEntityService.packMany(notes, me);
}); });
} }

View File

@@ -0,0 +1,73 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ChatService } from '@/core/ChatService.js';
import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
import { ApiError } from '@/server/api/error.js';
export const meta = {
tags: ['chat'],
requireCredential: true,
kind: 'read:chat',
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'ChatMessage',
},
},
errors: {
},
} as const;
export const paramDef = {
type: 'object',
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
room: { type: 'boolean', default: false },
},
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private chatEntityService: ChatEntityService,
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
const history = ps.room ? await this.chatService.roomHistory(me.id, ps.limit) : await this.chatService.userHistory(me.id, ps.limit);
const packedMessages = await this.chatEntityService.packMessagesDetailed(history, me);
if (ps.room) {
const roomIds = history.map(m => m.toRoomId!);
const readStateMap = await this.chatService.getRoomReadStateMap(me.id, roomIds);
for (const message of packedMessages) {
message.isRead = readStateMap[message.toRoomId!] ?? false;
}
} else {
const otherIds = history.map(m => m.fromUserId === me.id ? m.toUserId! : m.fromUserId!);
const readStateMap = await this.chatService.getUserReadStateMap(me.id, otherIds);
for (const message of packedMessages) {
const otherId = message.fromUserId === me.id ? message.toUserId! : message.fromUserId!;
message.isRead = readStateMap[otherId] ?? false;
}
}
return packedMessages;
});
}
}

View File

@@ -0,0 +1,105 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { GetterService } from '@/server/api/GetterService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js';
import { ChatService } from '@/core/ChatService.js';
import type { DriveFilesRepository, MiUser } from '@/models/_.js';
export const meta = {
tags: ['chat'],
requireCredential: true,
requiredRolePolicy: 'canChat',
prohibitMoved: true,
kind: 'write:chat',
limit: {
duration: ms('1hour'),
max: 500,
},
res: {
type: 'object',
optional: false, nullable: false,
ref: 'ChatMessageLite',
},
errors: {
noSuchRoom: {
message: 'No such room.',
code: 'NO_SUCH_ROOM',
id: '8098520d-2da5-4e8f-8ee1-df78b55a4ec6',
},
noSuchFile: {
message: 'No such file.',
code: 'NO_SUCH_FILE',
id: 'b6accbd3-1d7b-4d9f-bdb7-eb185bac06db',
},
contentRequired: {
message: 'Content required. You need to set text or fileId.',
code: 'CONTENT_REQUIRED',
id: '340517b7-6d04-42c0-bac1-37ee804e3594',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
text: { type: 'string', nullable: true, maxLength: 2000 },
fileId: { type: 'string', format: 'misskey:id' },
toRoomId: { type: 'string', format: 'misskey:id' },
},
required: ['toRoomId'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
private getterService: GetterService,
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
const room = await this.chatService.findRoomById(ps.toRoomId);
if (room == null) {
throw new ApiError(meta.errors.noSuchRoom);
}
let file = null;
if (ps.fileId != null) {
file = await this.driveFilesRepository.findOneBy({
id: ps.fileId,
userId: me.id,
});
if (file == null) {
throw new ApiError(meta.errors.noSuchFile);
}
}
// テキストが無いかつ添付ファイルも無かったらエラー
if (ps.text == null && file == null) {
throw new ApiError(meta.errors.contentRequired);
}
return await this.chatService.createMessageToRoom(me, room, {
text: ps.text,
file: file,
});
});
}
}

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