Compare commits

..

22 Commits

Author SHA1 Message Date
syuilo
407a965c1d Merge pull request #10932 from misskey-dev/develop
Release: 13.13.0
2023-06-05 19:47:08 +09:00
syuilo
de6348e8a0 Merge pull request #10833 from misskey-dev/develop
* refactor(frontend): use css modules

* feat: 投稿したコンテンツのAIによる学習を軽減するオプションを追加

Resolve #10819

* enhance(backend): publicReactionsをデフォルトtrueに

* 念のためnoimageaiもつける

* add X-Robots-Tag: noai

* Update ja-JP.yml

* fix(frontend): ブラーエフェクトを有効にしている状態で高負荷になる問題を修正

* enhance(backend): graceful shutdown for job queue and refactor

* fix(backend): テスト時は一部のサービスを停止

* fix test

* New Crowdin updates (#10815)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Korean)

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

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

* refactor

* bump

* refactor(frontend): use css module

* refactor(frontend): use css module

* delete unused component

* センシティブワードを正規表現、CWにも適用するように (#10688)

* cwにセンシティブが効いてない

* CWが無いときにTextを見るように

* 比較演算子間違えた

* とりあえずチェック

* 正規表現対応

* /test/giにも対応

* matchでしなくてもいいのでは感

* レビュー修正

* Update packages/backend/src/core/NoteCreateService.ts

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* Update packages/backend/src/core/NoteCreateService.ts

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* 修正

* wipかも

* wordsでスペース区切りのものできたかも

* なんか動いたかも

* test作成

* 文言の修正

* 修正

* note参照

---------

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* Update CHANGELOG.md

* New Crowdin updates (#10823)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* ci: fix typo

* fix(frontend): より明確な説明にしたのとtypo修正

* fix typo

* fix(frontend): カラーバーがリプライには表示されないのを修正

* fix(frontend): チャンネル内の検索ボックスが挙動不審な問題を修正

Fix #10793

* enhance(backend): ノートのハッシュタグもMeilisearchに突っ込むように

今後ハッシュタグ検索とか実装するときのため

* feat(frontend): ユーザー指定ノート検索

* fix(frontend): fix retention chart rendering

* Update about-misskey.vue

* meta: Remove @rinsuki from reviewer-lottery (#10830)

* New Crowdin updates (#10824)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

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

* New translations ja-JP.yml (English)

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

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

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

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

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

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

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Norwegian)

* New translations ja-JP.yml (Russian)

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

* New translations ja-JP.yml (Indonesian)

* New translations ja-JP.yml (Thai)

* enhance(frontend): アカウント初期設定ウィザードにプライバシー設定を追加

* Update CHANGELOG.md

* fix(backend): ひとつのMeilisearchサーバーを複数のMisskeyサーバーで使えない問題を修正

* fix MkUserSetupDialog.Privacy.vue

* ci: skip non-Japanese locale on TurboSnap

* ci: notify on changes for push events

* ci: fix missing branch

* Update basic.cy.js

* [ci skip] New Crowdin updates (#10834)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (German)

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

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

* New translations ja-JP.yml (Arabic)

* 🎨

* 🎨

* enhance(frontend): add retention line chart

* update deps

* refactor

* fix(frontend): Pageにおいて画像ブロックに画像を設定できない問題を修正

Fix #10837

---------

Co-authored-by: nenohi <kimutipartylove@gmail.com>
Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
Co-authored-by: rinsuki <428rinsuki+git@gmail.com>
2023-05-12 12:41:53 +09:00
syuilo
9ad57324db Merge pull request #10814 from misskey-dev/develop
Release: 13.12.1
2023-05-09 15:38:17 +09:00
syuilo
94690c835e Merge pull request #10774 from misskey-dev/develop
Release: 13.12.0
2023-05-09 09:17:34 +09:00
syuilo
c5d2dba28d Merge pull request #10608 from misskey-dev/develop
Release: 13.11.3
2023-04-13 12:18:07 +09:00
syuilo
272e0c874f Merge pull request #10606 from misskey-dev/EbiseLutica-patch-1
Update CHANGELOG.md
2023-04-13 08:35:14 +09:00
Ebise Lutica
d429f810a9 Update CHANGELOG.md 2023-04-13 00:31:22 +09:00
syuilo
75b28d6782 Merge pull request #10578 from misskey-dev/develop
Release: 13.11.2
2023-04-11 15:51:07 +09:00
syuilo
8b1362ab03 Merge pull request #10543 from misskey-dev/develop
Release: 13.11.1
2023-04-09 10:29:36 +09:00
syuilo
a096f621cf Merge pull request #10506 from misskey-dev/develop
13.11.0
2023-04-08 21:27:21 +09:00
syuilo
f54a9542bb Merge pull request #10402 from misskey-dev/develop
Release: 13.10.3
2023-03-25 08:36:41 +09:00
syuilo
a52bbc7c8d Merge pull request #10388 from misskey-dev/develop
Release: 13.10.2
2023-03-22 18:47:10 +09:00
syuilo
59768bdf3f Merge pull request #10383 from misskey-dev/develop
Release: 13.10.1
2023-03-22 16:30:36 +09:00
syuilo
1e67e9c661 Merge pull request #10342 from misskey-dev/develop
Release: 13.10.0
2023-03-22 09:55:38 +09:00
syuilo
ae517a99a7 Merge pull request #10218 from misskey-dev/develop
Release: 13.9.2
2023-03-06 11:54:12 +09:00
syuilo
b23a9b1a88 Merge pull request #10181 from misskey-dev/develop
Release: 13.9.1
2023-03-03 20:56:50 +09:00
syuilo
5bd68aa3e0 Merge pull request #10177 from misskey-dev/develop
Release: 13.9.0
2023-03-03 15:35:40 +09:00
syuilo
647ce174b3 Merge pull request #10112 from misskey-dev/develop
Release: 13.8.1
2023-02-26 20:57:13 +09:00
syuilo
02c8fd9de5 Merge pull request #10108 from misskey-dev/develop
* Add dialog to remove follower (#9718)

* update PULL_REQUEST_TEMPLATE

* 起動時にRedisの疎通確認を行う (#9832)

* 起動時にRedisの疎通確認を行う

* check:connectをstart内に移動

---------

Co-authored-by: tamaina <tamaina@hotmail.co.jp>

* Pass `--detectOpenHandles` to Jest (#9895)

Co-authored-by: tamaina <tamaina@hotmail.co.jp>

* enhance(client): MkUrlPreviewの閉じるボタンを見やすく (#9913)

Co-authored-by: tamaina <tamaina@hotmail.co.jp>

* test(backend): restore ap-request tests (#9997)

Co-authored-by: tamaina <tamaina@hotmail.co.jp>

* fix/refaftor(client): MkTime.vueの変更 (#10061)

* fix(client): MkTime.timeにstringでもDateでない値が入った場合、?を表示

* fix(client): MkTimeを改良

* numberを許容

* falsyな値もとる

* 不明

* ありません

* fix

* fix(server): notes/createで、fileIdsと見つかったファイルの数が異なる場合はエラーにする (#9911)

* fix(server): notes/createで、fileIdsと見つかったファイルの数が異なる場合はエラーにする

* NO_SUCH_FILE

* Update codecov.yml

* Update apple-touch-icon.png

* デプロイされているプレビュー環境がない場合はプレビュー環境を削除しないようにする (#10062)

* デプロイされているプレビュー環境がない場合はDestroy preview environmentを実行しないようにする

* CIがない場合の処理追加

* enhance(client): improve clip menu ux

* 未知のユーザーが deleteActor されたら処理をスキップする (#10067)

* fix(client): Android ChromeでPWAとしてインストールできない問題を修正 (#10069)

* fix(client): Android ChromeでPWAとしてインストールできない問題を修正

* 順番関係ある?

* Windows環境でswcを使うと正常にビルドができない問題の修正 (#10074)

* Update @swc/core to v1.3.36

* Update CHANGELOG.md

* Update CHANGELOG.md

* バックグラウンドで一定時間経過したらページネーションのアイテム更新をしない (#10053)

* 🎨

* feat: 2つの検索画面の統合 (#9949) (#10038)

* feat: 検索画面の UI を統一

* fix: エラーの修正

* add: changelog

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* enhance(client): ノートメニューからユーザーメニューを開けるように

Resolve #10019

* enhance(client): renoteした際の表示を改善

Resolve #10078

* Update CHANGELOG.md

* enhance(client): tweak contextmenu position calculation

* 🎨

* 🎨

* feat: in-channel featured note

Resolve #9938

* refactor(frontend): fix eslint error (#10084)

* Simplify search.vue (remove dead code) (#10088)

* Simplify search.vue

This is already handled by the code above it, no need to handle it twice

* Remove unused imports

* Update about-misskey.vue

* test(server): add validation test of api:notes/create (#10090)

* fix(server): notes/createのバリデーションが効いていない
Fix #10079

Co-Authored-By: mei23 <m@m544.net>

* anyOf内にバリデーションを書いても最初の一つしかチェックされない

* ✌️

* wip

* wip

* ✌️

* RequiredProp

* Revert "RequiredProp"

This reverts commit 7469390011.

* add api:notes/create

* fix lint

* text

* ✌️

* improve readability

---------

Co-authored-by: mei23 <m@m544.net>
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* New Crowdin updates (#10059)

* 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 (Italian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Slovak)

* New translations ja-JP.yml (Ukrainian)

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

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

* New translations ja-JP.yml (English)

* 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 (Japanese, Kansai)

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

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

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Ukrainian)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

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

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

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* enhance(client): improve user menu ux

* enhance(client): photoswipe 表示時に戻る操作をしても前の画面に戻らないように (#10098)

* enhance(client): photoswipe 表示時に戻る操作をしても前の画面に戻らないように

* add: changelog

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* enhance(client): メニューの「もっと」からインスタンス情報を見れるように

* [Fix] fixed an typo in error message (#10102)

* Update codecov.yml

* Update CHANGELOG.md

* fix(server): エラーのスタックトレースは返さないように

Fix #10064

* [chore]Editorconfig: ymlに加えてyamlファイルに対しても同じ規約を適用する (#10081)

* Added yaml file in addition to yml file, in editorconfig

* Applied editorconfig for pnpm-workspace.yaml

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* update deps

* ホームタイムラインの読み込みでクエリタイムアウトになるのを修正する (#10106)

* refactor

* New translations ja-JP.yml (French) (#10103)

* Update CHANGELOG.md

* 13.8.0

---------

Co-authored-by: atsuchan <83960488+atsu1125@users.noreply.github.com>
Co-authored-by: Masaya Suzuki <15100604+massongit@users.noreply.github.com>
Co-authored-by: tamaina <tamaina@hotmail.co.jp>
Co-authored-by: Kagami Sascha Rosylight <saschanaz@outlook.com>
Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com>
Co-authored-by: xianon <xianon@hotmail.co.jp>
Co-authored-by: kabo2468 <28654659+kabo2468@users.noreply.github.com>
Co-authored-by: YS <47836716+yszkst@users.noreply.github.com>
Co-authored-by: Khsmty <me@khsmty.com>
Co-authored-by: Soni L <EnderMoneyMod@gmail.com>
Co-authored-by: mei23 <m@m544.net>
Co-authored-by: daima3629 <52790780+daima3629@users.noreply.github.com>
Co-authored-by: Windymelt <1113940+windymelt@users.noreply.github.com>
2023-02-26 20:21:54 +09:00
syuilo
1ba49b614d Merge pull request #10058 from misskey-dev/develop
Release: 13.7.5
2023-02-24 13:06:55 +09:00
tamaina
40de14415c Release: 13.7.4
Merge pull request #10050 from misskey-dev/develop
2023-02-23 23:11:25 +09:00
tamaina
7c9330a02f Release: 13.7.3
Merge pull request #10048 from misskey-dev/develop
2023-02-23 22:15:56 +09:00
405 changed files with 5747 additions and 6314 deletions

View File

@@ -6,7 +6,7 @@
"features": { "features": {
"ghcr.io/devcontainers-contrib/features/pnpm:2": {}, "ghcr.io/devcontainers-contrib/features/pnpm:2": {},
"ghcr.io/devcontainers/features/node:1": { "ghcr.io/devcontainers/features/node:1": {
"version": "20.3.1" "version": "18.16.0"
} }
}, },
"forwardPorts": [3000], "forwardPorts": [3000],

View File

@@ -2,7 +2,7 @@ version: '3.8'
services: services:
app: app:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile

View File

@@ -6,10 +6,6 @@ indent_size = 2
charset = utf-8 charset = utf-8
insert_final_newline = true insert_final_newline = true
end_of_line = lf end_of_line = lf
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}] [*.{yml,yaml}]
indent_style = space indent_style = space

View File

@@ -39,22 +39,8 @@ Please include errors from the developer console and/or server log files if you
<!-- Tell us where on the platform it happens --> <!-- Tell us where on the platform it happens -->
<!-- DO NOT WRITE "latest". Please provide the specific version. --> <!-- DO NOT WRITE "latest". Please provide the specific version. -->
### 💻 Frontend Misskey version:
* Model and OS of the device(s): PostgreSQL version:
<!-- Example: MacBook Pro (14inch, 2021), macOS Ventura 13.4 --> Redis version:
* Browser: Your OS:
<!-- Example: Chrome 113.0.5672.126 --> Your browser:
* Server URL:
<!-- Example: misskey.io -->
* Misskey:
13.x.x
### 🛰 Backend (for server admin)
<!-- If you are using a managed service, put that after the version. -->
* Installation Method or Hosting Service: <!-- Example: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment -->
* Misskey: 13.x.x
* Node: 20.x.x
* PostgreSQL: 15.x.x
* Redis: 7.x.x
* OS and Architecture: <!-- Example: Ubuntu 22.04.2 LTS aarch64 -->

View File

@@ -37,7 +37,7 @@ jobs:
with: with:
version: 8 version: 8
run_install: false run_install: false
- name: Use Node.js 20.x - name: Use Node.js 18.x
uses: actions/setup-node@v3.6.0 uses: actions/setup-node@v3.6.0
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'

View File

@@ -13,7 +13,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [20.x] node-version: [18.x]
services: services:
postgres: postgres:

View File

@@ -13,7 +13,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [20.x] node-version: [18.x]
steps: steps:
- uses: actions/checkout@v3.3.0 - uses: actions/checkout@v3.3.0
@@ -51,7 +51,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
node-version: [20.x] node-version: [18.x]
browser: [chrome] browser: [chrome]
services: services:

View File

@@ -16,7 +16,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [20.x] node-version: [18.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/ # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps: steps:

View File

@@ -16,7 +16,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [20.x] node-version: [18.x]
steps: steps:
- uses: actions/checkout@v3.3.0 - uses: actions/checkout@v3.3.0

3
.gitignore vendored
View File

@@ -64,6 +64,3 @@ temp
*.blend3 *.blend3
*.blend4 *.blend4
*.blend5 *.blend5
# VSCode addon
.favorites.json

View File

@@ -1 +1 @@
20.3.1 18.16.0

View File

@@ -12,51 +12,6 @@
--> -->
## 13.x.x (unreleased)
### General
- identicon生成を無効にしてパフォーマンスを向上させることができるようになりました
- サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました
### Client
- deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように
- ドライブファイルのメニューで画像をクロップできるように
- 画像を動画と同様に簡単に隠せるように
- オリジナル画像を保持せずにアップロードする場合webpでアップロードされるように(Safari以外)
- 見たことのあるRenoteを省略して表示をオンのときに自分のnoteのrenoteを省略するように
- Fix: サーバーメトリクスが90度傾いている
- Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正
- Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正
- Fix: ZenUIでポップアップの表示位置がおかしい問題を修正
- Fix: ページ遷移でスクロール位置が保持されない問題を修正
### Server
- JSON.parse の回数を削減することで、ストリーミングのパフォーマンスを向上しました
- nsfwjs のモデルロードを排他することで、重複ロードによってメモリ使用量が増加しないように
- 連合の配送ジョブのパフォーマンスを向上ロック機構の見直し、Redisキャッシュの活用
- 全体的なDBクエリのパフォーマンスを向上
## 13.13.2
### General
- エラー時や項目が存在しないときなどのアイコン画像をサーバー管理者が設定できるように
- ロールが付与されているユーザーリストを非公開にできるように
- サーバーの負荷が非常に高いため、ユーザー統計表示機能を削除しました
### Client
- Fix: タブがバックグラウンドでもstreamが切断されないように
### Server
- Fix: キャッシュが溜まり続けないように
## 13.13.1
### Client
- Fix: タブがアクティブな間はstreamが切断されないように
### Server
- Fix: api/metaで`TypeError: JSON5.parse is not a function`エラーが発生する問題を修正
## 13.13.0 ## 13.13.0
### General ### General
@@ -133,12 +88,11 @@ Meilisearchの設定に`index`が必要になりました。値はMisskeyサー
## 13.12.0 ## 13.12.0
### NOTE ### NOTE
- Node.js 18.16.0以上が必要になりました - Node.js 18.6.0以上が必要になりました
### General ### General
- アカウントの引っ越し(フォロワー引き継ぎ)に対応 - アカウントの引っ越し(フォロワー引き継ぎ)に対応
- Meilisearchを全文検索に使用できるようになりました - Meilisearchを全文検索に使用できるようになりました
* 「フォロワーのみ」の投稿は検索結果に表示されません。
- 新規登録前に簡潔なルールをユーザーに表示できる、サーバールール機能を追加 - 新規登録前に簡潔なルールをユーザーに表示できる、サーバールール機能を追加
- ユーザーへの自分用メモ機能 - ユーザーへの自分用メモ機能
* ユーザーに対して、自分だけが見られるメモを追加できるようになりました。 * ユーザーに対して、自分だけが見られるメモを追加できるようになりました。

View File

@@ -106,7 +106,7 @@ If your language is not listed in Crowdin, please open an issue.
![Crowdin](https://d322cqt584bo4o.cloudfront.net/misskey/localized.svg) ![Crowdin](https://d322cqt584bo4o.cloudfront.net/misskey/localized.svg)
## Development ## Development
During development, it is useful to use the During development, it is useful to use the
``` ```
pnpm dev pnpm dev
@@ -150,7 +150,7 @@ Prepare DB/Redis for testing.
``` ```
docker compose -f packages/backend/test/docker-compose.yml up docker compose -f packages/backend/test/docker-compose.yml up
``` ```
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`. Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.
Run all test. Run all test.
``` ```

View File

@@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.4 # syntax = docker/dockerfile:1.4
ARG NODE_VERSION=20.3.1-bullseye ARG NODE_VERSION=18.16.0-bullseye
# build assets & compile TypeScript # build assets & compile TypeScript

View File

@@ -2,9 +2,9 @@
<a href="https://misskey-hub.net"> <a href="https://misskey-hub.net">
<img src="./assets/title_float.svg" alt="Misskey logo" style="border-radius:50%" width="400"/> <img src="./assets/title_float.svg" alt="Misskey logo" style="border-radius:50%" width="400"/>
</a> </a>
**🌎 **[Misskey](https://misskey-hub.net/)** is an open source, decentralized social media platform that's free forever! 🚀** **🌎 **[Misskey](https://misskey-hub.net/)** is an open source, decentralized social media platform that's free forever! 🚀**
--- ---
<a href="https://misskey-hub.net/instances.html"> <a href="https://misskey-hub.net/instances.html">
@@ -21,7 +21,7 @@
<a href="https://www.patreon.com/syuilo"> <a href="https://www.patreon.com/syuilo">
<img src="https://custom-icon-badges.herokuapp.com/badge/become_a-patron-F96854?logoColor=F96854&style=for-the-badge&logo=patreon&labelColor=363B40" alt="become a patron"/></a> <img src="https://custom-icon-badges.herokuapp.com/badge/become_a-patron-F96854?logoColor=F96854&style=for-the-badge&logo=patreon&labelColor=363B40" alt="become a patron"/></a>
--- ---
[![codecov](https://codecov.io/gh/misskey-dev/misskey/branch/develop/graph/badge.svg?token=R6IQZ3QJOL)](https://codecov.io/gh/misskey-dev/misskey) [![codecov](https://codecov.io/gh/misskey-dev/misskey/branch/develop/graph/badge.svg?token=R6IQZ3QJOL)](https://codecov.io/gh/misskey-dev/misskey)

View File

@@ -23,13 +23,13 @@
</rdf:RDF> </rdf:RDF>
</metadata> </metadata>
<style> <style>
#g8 { #g8 {
animation-name: floating; animation-name: floating;
animation-duration: 3s; animation-duration: 3s;
animation-iteration-count: infinite; animation-iteration-count: infinite;
animation-timing-function: ease-in-out; animation-timing-function: ease-in-out;
} }
@keyframes floating { @keyframes floating {
0% { transform: translate(0, 0px); } 0% { transform: translate(0, 0px); }
50% { transform: translate(0, -5px); } 50% { transform: translate(0, -5px); }

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -56,7 +56,7 @@ describe('After setup instance', () => {
cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click(); cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click();
cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled'); cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled');
cy.get('[data-cy-signup-rules-continue]').click(); cy.get('[data-cy-signup-rules-continue]').click();
cy.get('[data-cy-signup-submit]').should('be.disabled'); cy.get('[data-cy-signup-submit]').should('be.disabled');
cy.get('[data-cy-signup-username] input').type('alice'); cy.get('[data-cy-signup-username] input').type('alice');
cy.get('[data-cy-signup-submit]').should('be.disabled'); cy.get('[data-cy-signup-submit]').should('be.disabled');

View File

@@ -991,7 +991,7 @@ postToTheChannel: "In Kanal senden"
cannotBeChangedLater: "Kann später nicht mehr geändert werden." cannotBeChangedLater: "Kann später nicht mehr geändert werden."
reactionAcceptance: "Reaktionsannahme" reactionAcceptance: "Reaktionsannahme"
likeOnly: "Nur \"Gefällt mir\"" likeOnly: "Nur \"Gefällt mir\""
likeOnlyForRemote: "Alle (Nur \"Gefällt mir\" für fremde Instanzen)" likeOnlyForRemote: "Nur \"Gefällt mir\" für fremde Instanzen"
nonSensitiveOnly: "Keine Sensitiven" nonSensitiveOnly: "Keine Sensitiven"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "Keine Sensitiven (Nur \"Gefällt mir\" von fremden Instanzen)" nonSensitiveOnlyForLocalLikeOnlyForRemote: "Keine Sensitiven (Nur \"Gefällt mir\" von fremden Instanzen)"
rolesAssignedToMe: "Mir zugewiesene Rollen" rolesAssignedToMe: "Mir zugewiesene Rollen"
@@ -1062,7 +1062,6 @@ later: "Später"
goToMisskey: "Zu Misskey" goToMisskey: "Zu Misskey"
additionalEmojiDictionary: "Zusätzliche Emoji-Wörterbücher" additionalEmojiDictionary: "Zusätzliche Emoji-Wörterbücher"
installed: "Installiert" installed: "Installiert"
branding: "Branding"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "Dein Konto wurde erfolgreich erstellt!" accountCreated: "Dein Konto wurde erfolgreich erstellt!"
letsStartAccountSetup: "Lass uns nun dein Konto einrichten." letsStartAccountSetup: "Lass uns nun dein Konto einrichten."
@@ -1094,7 +1093,7 @@ _accountMigration:
migrationConfirm: "Dieses Konto wirklich zu {account} umziehen? Sobald der Umzug beginnt, kann er nicht rückgängig gemacht werden, und dieses Konto nicht wieder im ursprünglichen Zustand verwendet werden." migrationConfirm: "Dieses Konto wirklich zu {account} umziehen? Sobald der Umzug beginnt, kann er nicht rückgängig gemacht werden, und dieses Konto nicht wieder im ursprünglichen Zustand verwendet werden."
movedAndCannotBeUndone: "\nDieses Konto wurde migriert.\nDiese Aktion ist unwiderruflich." movedAndCannotBeUndone: "\nDieses Konto wurde migriert.\nDiese Aktion ist unwiderruflich."
postMigrationNote: "Dieses Konto wird 24 Stunden nach Abschluss der Migration allen Konten, denen es derzeit folgt, nicht mehr folgen.\n\nSowohl die Anzahl der Follower als auch die der Konten, denen dieses Konto folgt, wird dann auf Null gesetzt. Um zu vermeiden, dass Follower dieses Kontos dessen Beiträge, welche nur für Follower bestimmt sind, nicht mehr sehen können, werden sie diesem Konto jedoch weiterhin folgen." postMigrationNote: "Dieses Konto wird 24 Stunden nach Abschluss der Migration allen Konten, denen es derzeit folgt, nicht mehr folgen.\n\nSowohl die Anzahl der Follower als auch die der Konten, denen dieses Konto folgt, wird dann auf Null gesetzt. Um zu vermeiden, dass Follower dieses Kontos dessen Beiträge, welche nur für Follower bestimmt sind, nicht mehr sehen können, werden sie diesem Konto jedoch weiterhin folgen."
movedTo: "Neues Konto:" movedTo: "Umzugsziel:"
_achievements: _achievements:
earnedAt: "Freigeschaltet am" earnedAt: "Freigeschaltet am"
_types: _types:
@@ -1348,7 +1347,7 @@ _role:
condition: "Bedingung" condition: "Bedingung"
isConditionalRole: "Dies ist eine konditionale Rolle." isConditionalRole: "Dies ist eine konditionale Rolle."
isPublic: "Öffentliche Rolle" isPublic: "Öffentliche Rolle"
descriptionOfIsPublic: "Diese Rolle wird im Profil zugewiesener Benutzer angezeigt." descriptionOfIsPublic: "Ist dies aktiviert, so kann jeder die Liste der Benutzer, die dieser Rolle zugewiesen sind, einsehen. Zusätzlich wird diese Rolle im Profil zugewiesener Benutzer angezeigt."
options: "Optionen" options: "Optionen"
policies: "Richtlinien" policies: "Richtlinien"
baseRole: "Rollenvorlage" baseRole: "Rollenvorlage"
@@ -1357,8 +1356,8 @@ _role:
iconUrl: "Icon-URL" iconUrl: "Icon-URL"
asBadge: "Als Abzeichen anzeigen" asBadge: "Als Abzeichen anzeigen"
descriptionOfAsBadge: "Ist dies aktiviert, so wird das Icon dieser Rolle an der Seite der Namen von Benutzern mit dieser Rolle angezeigt." descriptionOfAsBadge: "Ist dies aktiviert, so wird das Icon dieser Rolle an der Seite der Namen von Benutzern mit dieser Rolle angezeigt."
isExplorable: "Benutzerliste veröffentlichen" isExplorable: "Rollenchronik veröffentlichen"
descriptionOfIsExplorable: "Ist dies aktiviert, so ist die Chronik dieser Rolle, sowie eine Liste der Benutzer mit dieser Rolle, frei zugänglich." descriptionOfIsExplorable: "Ist dies aktiviert, so ist die Rollenchronik dieser Rolle frei zugänglich. Die Chronik von Rollen, welche nicht öffentlich sind, wird auch bei Aktivierung nicht veröffentlicht."
displayOrder: "Position" displayOrder: "Position"
descriptionOfDisplayOrder: "Je höher die Nummer, desto höher die UI-Position." descriptionOfDisplayOrder: "Je höher die Nummer, desto höher die UI-Position."
canEditMembersByModerator: "Moderatoren können Benutzern diese Rolle zuweisen" canEditMembersByModerator: "Moderatoren können Benutzern diese Rolle zuweisen"

View File

@@ -991,7 +991,7 @@ postToTheChannel: "Post to channel"
cannotBeChangedLater: "This cannot be changed later." cannotBeChangedLater: "This cannot be changed later."
reactionAcceptance: "Reaction Acceptance" reactionAcceptance: "Reaction Acceptance"
likeOnly: "Only likes" likeOnly: "Only likes"
likeOnlyForRemote: "All (Only likes for remote instances)" likeOnlyForRemote: "Only likes for remote instances"
nonSensitiveOnly: "Non-sensitive only" nonSensitiveOnly: "Non-sensitive only"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "Non-sensitive only (Only likes from remote)" nonSensitiveOnlyForLocalLikeOnlyForRemote: "Non-sensitive only (Only likes from remote)"
rolesAssignedToMe: "Roles assigned to me" rolesAssignedToMe: "Roles assigned to me"
@@ -1062,7 +1062,6 @@ later: "Later"
goToMisskey: "To Misskey" goToMisskey: "To Misskey"
additionalEmojiDictionary: "Additional emoji dictionaries" additionalEmojiDictionary: "Additional emoji dictionaries"
installed: "Installed" installed: "Installed"
branding: "Branding"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "Your account was successfully created!" accountCreated: "Your account was successfully created!"
letsStartAccountSetup: "For starters, let's set up your profile." letsStartAccountSetup: "For starters, let's set up your profile."
@@ -1094,7 +1093,7 @@ _accountMigration:
migrationConfirm: "Really migrate this account to {account}? Once started, this process cannot be stopped or taken back, and you will not be able to use this account in its original state anymore." migrationConfirm: "Really migrate this account to {account}? Once started, this process cannot be stopped or taken back, and you will not be able to use this account in its original state anymore."
movedAndCannotBeUndone: "\nThis account has been migrated.\nMigration cannot be reversed." movedAndCannotBeUndone: "\nThis account has been migrated.\nMigration cannot be reversed."
postMigrationNote: "This account will unfollow all accounts it is currently following 24 hours after migration finishes.\nBoth the number of follows and followers will then become zero. To avoid your followers from being unable to see followers only posts of this account, they will however continue following this account." postMigrationNote: "This account will unfollow all accounts it is currently following 24 hours after migration finishes.\nBoth the number of follows and followers will then become zero. To avoid your followers from being unable to see followers only posts of this account, they will however continue following this account."
movedTo: "New account:" movedTo: "Account to move to:"
_achievements: _achievements:
earnedAt: "Unlocked at" earnedAt: "Unlocked at"
_types: _types:
@@ -1348,7 +1347,7 @@ _role:
condition: "Condition" condition: "Condition"
isConditionalRole: "This is a conditional role." isConditionalRole: "This is a conditional role."
isPublic: "Public role" isPublic: "Public role"
descriptionOfIsPublic: "This role will be displayed in the profiles of assigned users." descriptionOfIsPublic: "Anyone will be able to view a list of users assigned to this role. In addition, this role will be displayed in the profiles of assigned users."
options: "Options" options: "Options"
policies: "Policies" policies: "Policies"
baseRole: "Role template" baseRole: "Role template"
@@ -1357,8 +1356,8 @@ _role:
iconUrl: "Icon URL" iconUrl: "Icon URL"
asBadge: "Show as badge" asBadge: "Show as badge"
descriptionOfAsBadge: "This role's icon will be displayed next to the username of users with this role if turned on." descriptionOfAsBadge: "This role's icon will be displayed next to the username of users with this role if turned on."
isExplorable: "Make role explorable" isExplorable: "Role timeline is public"
descriptionOfIsExplorable: "This role's timeline and the list of users with this will be made public if enabled." descriptionOfIsExplorable: "This role's timeline will become publicly accessible if enabled. Timelines of non-public roles will not be made public even if set."
displayOrder: "Position" displayOrder: "Position"
descriptionOfDisplayOrder: "The higher the number, the higher its UI position." descriptionOfDisplayOrder: "The higher the number, the higher its UI position."
canEditMembersByModerator: "Allow moderators to edit the list of members for this role" canEditMembersByModerator: "Allow moderators to edit the list of members for this role"

11
locales/index.d.ts vendored
View File

@@ -139,10 +139,8 @@ export interface Locale {
"suspendConfirm": string; "suspendConfirm": string;
"unsuspendConfirm": string; "unsuspendConfirm": string;
"selectList": string; "selectList": string;
"editList": string;
"selectChannel": string; "selectChannel": string;
"selectAntenna": string; "selectAntenna": string;
"editAntenna": string;
"selectWidget": string; "selectWidget": string;
"editWidgets": string; "editWidgets": string;
"editWidgetsExit": string; "editWidgetsExit": string;
@@ -316,7 +314,7 @@ export interface Locale {
"rename": string; "rename": string;
"avatar": string; "avatar": string;
"banner": string; "banner": string;
"displayOfSensitiveMedia": string; "nsfw": string;
"whenServerDisconnected": string; "whenServerDisconnected": string;
"disconnectedFromServer": string; "disconnectedFromServer": string;
"reload": string; "reload": string;
@@ -1067,10 +1065,6 @@ export interface Locale {
"goToMisskey": string; "goToMisskey": string;
"additionalEmojiDictionary": string; "additionalEmojiDictionary": string;
"installed": string; "installed": string;
"branding": string;
"enableServerMachineStats": string;
"enableIdenticonGeneration": string;
"turnOffToImprovePerformance": string;
"_initialAccountSetting": { "_initialAccountSetting": {
"accountCreated": string; "accountCreated": string;
"letsStartAccountSetup": string; "letsStartAccountSetup": string;
@@ -1531,7 +1525,6 @@ export interface Locale {
"back": string; "back": string;
"reduceFrequencyOfThisAd": string; "reduceFrequencyOfThisAd": string;
"hide": string; "hide": string;
"timezoneinfo": string;
}; };
"_forgotPassword": { "_forgotPassword": {
"enterEmail": string; "enterEmail": string;
@@ -1593,7 +1586,7 @@ export interface Locale {
"morePatrons": string; "morePatrons": string;
"patrons": string; "patrons": string;
}; };
"_displayOfSensitiveMedia": { "_nsfw": {
"respect": string; "respect": string;
"ignore": string; "ignore": string;
"force": string; "force": string;

View File

@@ -112,7 +112,7 @@ pinnedNote: "ピン留めされたノート"
pinned: "ピン留め" pinned: "ピン留め"
you: "あなた" you: "あなた"
clickToShow: "クリックして表示" clickToShow: "クリックして表示"
sensitive: "センシティブ" sensitive: "閲覧注意"
add: "追加" add: "追加"
reaction: "リアクション" reaction: "リアクション"
reactions: "リアクション" reactions: "リアクション"
@@ -120,8 +120,8 @@ reactionSetting: "ピッカーに表示するリアクション"
reactionSettingDescription2: "ドラッグして並び替え、クリックして削除、+を押して追加します。" reactionSettingDescription2: "ドラッグして並び替え、クリックして削除、+を押して追加します。"
rememberNoteVisibility: "公開範囲を記憶する" rememberNoteVisibility: "公開範囲を記憶する"
attachCancel: "添付取り消し" attachCancel: "添付取り消し"
markAsSensitive: "センシティブとして設定" markAsSensitive: "閲覧注意にする"
unmarkAsSensitive: "センシティブを解除する" unmarkAsSensitive: "閲覧注意を解除する"
enterFileName: "ファイル名を入力" enterFileName: "ファイル名を入力"
mute: "ミュート" mute: "ミュート"
unmute: "ミュート解除" unmute: "ミュート解除"
@@ -136,10 +136,8 @@ unblockConfirm: "ブロック解除しますか?"
suspendConfirm: "凍結しますか?" suspendConfirm: "凍結しますか?"
unsuspendConfirm: "解凍しますか?" unsuspendConfirm: "解凍しますか?"
selectList: "リストを選択" selectList: "リストを選択"
editList: "リストを編集"
selectChannel: "チャンネルを選択" selectChannel: "チャンネルを選択"
selectAntenna: "アンテナを選択" selectAntenna: "アンテナを選択"
editAntenna: "アンテナを編集"
selectWidget: "ウィジェットを選択" selectWidget: "ウィジェットを選択"
editWidgets: "ウィジェットを編集" editWidgets: "ウィジェットを編集"
editWidgetsExit: "編集を終了" editWidgetsExit: "編集を終了"
@@ -313,7 +311,7 @@ copyUrl: "URLをコピー"
rename: "名前を変更" rename: "名前を変更"
avatar: "アイコン" avatar: "アイコン"
banner: "バナー" banner: "バナー"
displayOfSensitiveMedia: "センシティブなメディアの表示" nsfw: "閲覧注意"
whenServerDisconnected: "サーバーとの接続が失われたとき" whenServerDisconnected: "サーバーとの接続が失われたとき"
disconnectedFromServer: "サーバーから切断されました" disconnectedFromServer: "サーバーから切断されました"
reload: "リロード" reload: "リロード"
@@ -695,7 +693,7 @@ driveUsage: "ドライブ使用量"
noCrawle: "クローラーによるインデックスを拒否" noCrawle: "クローラーによるインデックスを拒否"
noCrawleDescription: "外部の検索エンジンにあなたのユーザーページ、ート、Pagesなどのコンテンツを登録(インデックス)しないよう要求します。" noCrawleDescription: "外部の検索エンジンにあなたのユーザーページ、ート、Pagesなどのコンテンツを登録(インデックス)しないよう要求します。"
lockedAccountInfo: "フォローを承認制にしても、ノートの公開範囲を「フォロワー」にしない限り、誰でもあなたのノートを見ることができます。" lockedAccountInfo: "フォローを承認制にしても、ノートの公開範囲を「フォロワー」にしない限り、誰でもあなたのノートを見ることができます。"
alwaysMarkSensitive: "デフォルトでメディアをセンシティブ設定にする" alwaysMarkSensitive: "デフォルトでメディアを閲覧注意にする"
loadRawImages: "添付画像のサムネイルをオリジナル画質にする" loadRawImages: "添付画像のサムネイルをオリジナル画質にする"
disableShowingAnimatedImages: "アニメーション画像を再生しない" disableShowingAnimatedImages: "アニメーション画像を再生しない"
verificationEmailSent: "確認のメールを送信しました。メールに記載されたリンクにアクセスして、設定を完了してください。" verificationEmailSent: "確認のメールを送信しました。メールに記載されたリンクにアクセスして、設定を完了してください。"
@@ -922,8 +920,8 @@ cannotUploadBecauseInappropriate: "不適切な内容を含む可能性がある
cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いためアップロードできません。" cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いためアップロードできません。"
cannotUploadBecauseExceedsFileSizeLimit: "ファイルサイズの制限を超えているためアップロードできません。" cannotUploadBecauseExceedsFileSizeLimit: "ファイルサイズの制限を超えているためアップロードできません。"
beta: "ベータ" beta: "ベータ"
enableAutoSensitive: "自動センシティブ判定" enableAutoSensitive: "自動NSFW判定"
enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにセンシティブフラグを設定します。この機能をオフにしても、サーバーによっては自動で設定されることがあります。" enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにNSFWフラグを設定します。この機能をオフにしても、サーバーによっては自動で設定されることがあります。"
activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかなどを判定しより積極的に行います。オフにすると単に文字列として正しいかどうかのみチェックされます。" activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかなどを判定しより積極的に行います。オフにすると単に文字列として正しいかどうかのみチェックされます。"
navbar: "ナビゲーションバー" navbar: "ナビゲーションバー"
shuffle: "シャッフル" shuffle: "シャッフル"
@@ -1064,10 +1062,6 @@ later: "あとで"
goToMisskey: "Misskeyへ" goToMisskey: "Misskeyへ"
additionalEmojiDictionary: "絵文字の追加辞書" additionalEmojiDictionary: "絵文字の追加辞書"
installed: "インストール済み" installed: "インストール済み"
branding: "ブランディング"
enableServerMachineStats: "サーバーのマシン情報を公開する"
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
turnOffToImprovePerformance: "オフにするとパフォーマンスが向上します。"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "アカウントの作成が完了しました!" accountCreated: "アカウントの作成が完了しました!"
@@ -1357,8 +1351,8 @@ _role:
conditional: "コンディショナル" conditional: "コンディショナル"
condition: "条件" condition: "条件"
isConditionalRole: "これはコンディショナルロールです。" isConditionalRole: "これはコンディショナルロールです。"
isPublic: "公開ロール" isPublic: "ロールを公開"
descriptionOfIsPublic: "ユーザーのプロフィールでこのロールが表示されます。" descriptionOfIsPublic: "ロールにアサインされたユーザーを誰でも見ることができます。また、ユーザーのプロフィールでこのロールが表示されます。"
options: "オプション" options: "オプション"
policies: "ポリシー" policies: "ポリシー"
baseRole: "ベースロール" baseRole: "ベースロール"
@@ -1367,8 +1361,8 @@ _role:
iconUrl: "アイコン画像のURL" iconUrl: "アイコン画像のURL"
asBadge: "バッジとして表示" asBadge: "バッジとして表示"
descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。" descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。"
isExplorable: "ユーザーを見つけやすくする" isExplorable: "ロールタイムラインを公開"
descriptionOfIsExplorable: "オンにすると、「みつける」でメンバー一覧が公開されるほか、ロールのタイムラインが利用可能になります。" descriptionOfIsExplorable: "オンにすると、ロールのタイムラインを公開します。ロールの公開がオフの場合、タイムラインの公開はされません。"
displayOrder: "表示順" displayOrder: "表示順"
descriptionOfDisplayOrder: "数値が大きいほどUI上で先頭に表示されます。" descriptionOfDisplayOrder: "数値が大きいほどUI上で先頭に表示されます。"
canEditMembersByModerator: "モデレーターのメンバー編集を許可" canEditMembersByModerator: "モデレーターのメンバー編集を許可"
@@ -1417,7 +1411,7 @@ _sensitiveMediaDetection:
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。" description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
sensitivity: "検出感度" sensitivity: "検出感度"
sensitivityDescription: "感度を低くすると、誤検知(偽陽性)が減ります。感度を高くすると、検知漏れ(偽陰性)が減ります。" sensitivityDescription: "感度を低くすると、誤検知(偽陽性)が減ります。感度を高くすると、検知漏れ(偽陰性)が減ります。"
setSensitiveFlagAutomatically: "センシティブフラグを設定する" setSensitiveFlagAutomatically: "NSFWフラグを設定する"
setSensitiveFlagAutomaticallyDescription: "この設定をオフにしても内部的に判定結果は保持されます。" setSensitiveFlagAutomaticallyDescription: "この設定をオフにしても内部的に判定結果は保持されます。"
analyzeVideos: "動画の解析を有効化" analyzeVideos: "動画の解析を有効化"
analyzeVideosDescription: "静止画に加えて動画も解析するようにします。サーバーの負荷が少し増えます。" analyzeVideosDescription: "静止画に加えて動画も解析するようにします。サーバーの負荷が少し増えます。"
@@ -1451,7 +1445,6 @@ _ad:
back: "戻る" back: "戻る"
reduceFrequencyOfThisAd: "この広告の表示頻度を下げる" reduceFrequencyOfThisAd: "この広告の表示頻度を下げる"
hide: "表示しない" hide: "表示しない"
timezoneinfo: "曜日はサーバーのタイムゾーンを元に指定されます。"
_forgotPassword: _forgotPassword:
enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。" enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。"
@@ -1511,9 +1504,9 @@ _aboutMisskey:
morePatrons: "他にも多くの方が支援してくれています。ありがとうございます🥰" morePatrons: "他にも多くの方が支援してくれています。ありがとうございます🥰"
patrons: "支援者" patrons: "支援者"
_displayOfSensitiveMedia: _nsfw:
respect: "センシティブ設定されたメディア隠す" respect: "閲覧注意のメディア隠す"
ignore: "センシティブ設定されたメディアを隠さない" ignore: "閲覧注意のメディアを隠さない"
force: "常にメディアを隠す" force: "常にメディアを隠す"
_instanceTicker: _instanceTicker:

View File

@@ -792,7 +792,6 @@ noMaintainerInformationWarning: "管理者情報が設定されてへんで"
noBotProtectionWarning: "Botプロテクションが設定されてへんで。" noBotProtectionWarning: "Botプロテクションが設定されてへんで。"
configure: "設定する" configure: "設定する"
postToGallery: "ギャラリーへ投稿" postToGallery: "ギャラリーへ投稿"
postToHashtag: "このハッシュタグで投稿"
gallery: "ギャラリー" gallery: "ギャラリー"
recentPosts: "最近の投稿" recentPosts: "最近の投稿"
popularPosts: "人気の投稿" popularPosts: "人気の投稿"
@@ -826,7 +825,6 @@ translatedFrom: "{x}から翻訳するで"
accountDeletionInProgress: "アカウント削除しとるで待っとってなー" accountDeletionInProgress: "アカウント削除しとるで待っとってなー"
usernameInfo: "サーバー上であんたのアカウントをあんたやと分かるようにするための名前やで。アルファベット(a~z, A~Z)、数字(0~9)、それとアンダーバー(_)が使って考えてな。この名前は後から変更することはできへんからちゃんと考えるんやで。" usernameInfo: "サーバー上であんたのアカウントをあんたやと分かるようにするための名前やで。アルファベット(a~z, A~Z)、数字(0~9)、それとアンダーバー(_)が使って考えてな。この名前は後から変更することはできへんからちゃんと考えるんやで。"
aiChanMode: "藍モードやで" aiChanMode: "藍モードやで"
devMode: "開発者モード"
keepCw: "CWを維持するで" keepCw: "CWを維持するで"
pubSub: "Pub/Subのアカウント" pubSub: "Pub/Subのアカウント"
lastCommunication: "直近の通信" lastCommunication: "直近の通信"
@@ -836,8 +834,6 @@ breakFollow: "フォロワーを解除するで"
breakFollowConfirm: "フォロワー解除してもええか?" breakFollowConfirm: "フォロワー解除してもええか?"
itsOn: "オンになっとるよ" itsOn: "オンになっとるよ"
itsOff: "オフになってるで" itsOff: "オフになってるで"
on: "オン"
off: "オフ"
emailRequiredForSignup: "アカウント登録にメールアドレスを必須にするで" emailRequiredForSignup: "アカウント登録にメールアドレスを必須にするで"
unread: "未読" unread: "未読"
filter: "フィルタ" filter: "フィルタ"
@@ -992,8 +988,6 @@ cannotBeChangedLater: "後からは変えられへんで。"
reactionAcceptance: "ツッコミの受け入れ" reactionAcceptance: "ツッコミの受け入れ"
likeOnly: "いいねだけ" likeOnly: "いいねだけ"
likeOnlyForRemote: "リモートからはいいねだけな" likeOnlyForRemote: "リモートからはいいねだけな"
nonSensitiveOnly: "センシティブじゃないやつだけ"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "センシティブじゃないやつだけ (リモートはいいねだけ)"
rolesAssignedToMe: "自分に割り当てられたロール" rolesAssignedToMe: "自分に割り当てられたロール"
resetPasswordConfirm: "パスワード作り直すんでええな?" resetPasswordConfirm: "パスワード作り直すんでええな?"
sensitiveWords: "けったいな単語" sensitiveWords: "けったいな単語"
@@ -1051,17 +1045,10 @@ preventAiLearning: "生成AIの学習に使わんといて"
preventAiLearningDescription: "他の文章生成AIとか画像生成AIに、投稿したートとか画像なんかを勝手に使わんように頼むで。具体的にはnoaiフラグをHTMLレスポンスに含めるんやけど、これ聞いてくれるんはAIの気分次第やから、使われる可能性もちょっとはあるな。" preventAiLearningDescription: "他の文章生成AIとか画像生成AIに、投稿したートとか画像なんかを勝手に使わんように頼むで。具体的にはnoaiフラグをHTMLレスポンスに含めるんやけど、これ聞いてくれるんはAIの気分次第やから、使われる可能性もちょっとはあるな。"
options: "オプション" options: "オプション"
specifyUser: "ユーザー指定" specifyUser: "ユーザー指定"
failedToPreviewUrl: "プレビューできへん"
update: "更新"
rolesThatCanBeUsedThisEmojiAsReaction: "ツッコミとして使えるロール" rolesThatCanBeUsedThisEmojiAsReaction: "ツッコミとして使えるロール"
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "ロールが一個も指定されてへんかったら、誰でもツッコミとして使えるで。" rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "ロールが一個も指定されてへんかったら、誰でもツッコミとして使えるで。"
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "ロールは公開ロールじゃないとアカンで。"
cancelReactionConfirm: "ツッコむんをやっぱやめるか?" cancelReactionConfirm: "ツッコむんをやっぱやめるか?"
changeReactionConfirm: "ツッコミを別のに変えるか?" changeReactionConfirm: "ツッコミを別のに変えるか?"
later: "あとで"
goToMisskey: "Misskeyへ"
additionalEmojiDictionary: "絵文字の追加辞書"
installed: "インストール済み"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "アカウント作り終わったで。" accountCreated: "アカウント作り終わったで。"
letsStartAccountSetup: "アカウントの初期設定をしよか。" letsStartAccountSetup: "アカウントの初期設定をしよか。"
@@ -1076,7 +1063,6 @@ _initialAccountSetting:
haveFun: "{name}、楽しんでな~" haveFun: "{name}、楽しんでな~"
ifYouNeedLearnMore: "{name}(Misskey)の使い方とかをよー知りたいんやったら{link}をみてな。" ifYouNeedLearnMore: "{name}(Misskey)の使い方とかをよー知りたいんやったら{link}をみてな。"
skipAreYouSure: "初期設定飛ばすか?" skipAreYouSure: "初期設定飛ばすか?"
laterAreYouSure: "初期設定あとでやり直すん?"
_serverRules: _serverRules:
description: "新規登録前に見せる、サーバーの簡潔なルールを設定すんで。内容は使うための決め事の要約とすることを推奨するわ。" description: "新規登録前に見せる、サーバーの簡潔なルールを設定すんで。内容は使うための決め事の要約とすることを推奨するわ。"
_accountMigration: _accountMigration:

View File

@@ -1,7 +1,6 @@
--- ---
_lang_: "Türkçe" _lang_: "Türkçe"
introMisskey: "Açık kaynaklı bir dağıtılmış mikroblog hizmeti olan Misskey'e hoş geldiniz.\nMisskey, neler olup bittiğini paylaşmak ve herkese sizden bahsetmek için \"notlar\" oluşturmanıza olanak tanıyan, açık kaynaklı, dağıtılmış bir mikroblog hizmetidir.\nHerkesin notlarına kendi tepkilerinizi hızlıca eklemek için \"Tepkiler\" özelliğini de kullanabilirsiniz👍.\nYeni bir dünyayı keşfedin🚀." introMisskey: "Açık kaynaklı bir dağıtılmış mikroblog hizmeti olan Misskey'e hoş geldiniz.\nMisskey, neler olup bittiğini paylaşmak ve herkese sizden bahsetmek için \"notlar\" oluşturmanıza olanak tanıyan, açık kaynaklı, dağıtılmış bir mikroblog hizmetidir.\nHerkesin notlarına kendi tepkilerinizi hızlıca eklemek için \"Tepkiler\" özelliğini de kullanabilirsiniz👍.\nYeni bir dünyayı keşfedin🚀."
poweredByMisskeyDescription: "name}Açık kaynak bir platform\n<b>Misskey</b>Dünya'nın en sunucularında biri。"
monthAndDay: "{month}Ay {day}Gün" monthAndDay: "{month}Ay {day}Gün"
search: "Arama" search: "Arama"
notifications: "Bildirim" notifications: "Bildirim"
@@ -14,9 +13,7 @@ cancel: "İptal"
enterUsername: "Kullanıcı adınızı giriniz" enterUsername: "Kullanıcı adınızı giriniz"
noNotes: "Notlar mevcut değil." noNotes: "Notlar mevcut değil."
noNotifications: "Bildirim bulunmuyor" noNotifications: "Bildirim bulunmuyor"
instance: "Sunucu"
settings: "Ayarlar" settings: "Ayarlar"
notificationSettings: "Bildirim Ayarları"
basicSettings: "Temel Ayarlar" basicSettings: "Temel Ayarlar"
otherSettings: "Diğer Ayarlar" otherSettings: "Diğer Ayarlar"
openInWindow: "Bir pencere ile aç" openInWindow: "Bir pencere ile aç"
@@ -24,11 +21,9 @@ profile: "Profil"
timeline: "Zaman çizelgesi" timeline: "Zaman çizelgesi"
noAccountDescription: "Bu kullanıcı henüz biyografisini yazmadı" noAccountDescription: "Bu kullanıcı henüz biyografisini yazmadı"
login: "Giriş Yap " login: "Giriş Yap "
loggingIn: "Oturum aç"
logout: ıkış Yap" logout: ıkış Yap"
signup: "Kayıt Ol" signup: "Kayıt Ol"
uploading: "Yükleniyor" uploading: "Yükleniyor"
save: "Kaydet"
users: "Kullanıcı" users: "Kullanıcı"
addUser: "Kullanıcı Ekle" addUser: "Kullanıcı Ekle"
favorite: "Favoriler" favorite: "Favoriler"
@@ -36,7 +31,6 @@ favorites: "Favoriler"
unfavorite: "Favorilerden Kaldır" unfavorite: "Favorilerden Kaldır"
favorited: "Favorilerime eklendi." favorited: "Favorilerime eklendi."
alreadyFavorited: "Zaten favorilerinizde kayıtlı." alreadyFavorited: "Zaten favorilerinizde kayıtlı."
cantFavorite: "Favorilere kayıt yapılamadı"
pin: "Sabitlenmiş" pin: "Sabitlenmiş"
unpin: "Sabitlemeyi kaldır" unpin: "Sabitlemeyi kaldır"
copyContent: "İçeriği kopyala" copyContent: "İçeriği kopyala"
@@ -46,88 +40,23 @@ deleteAndEdit: "Sil ve yeniden düzenle"
deleteAndEditConfirm: "Bu notu silip yeniden düzenlemek istiyor musunuz? Bu nota ilişkin tüm Tepkiler, Yeniden Notlar ve Yanıtlar da silinecektir." deleteAndEditConfirm: "Bu notu silip yeniden düzenlemek istiyor musunuz? Bu nota ilişkin tüm Tepkiler, Yeniden Notlar ve Yanıtlar da silinecektir."
addToList: "Listeye ekle" addToList: "Listeye ekle"
sendMessage: "Mesaj Gönder" sendMessage: "Mesaj Gönder"
copyRSS: "RSSKopyala"
copyUsername: "Kullanıcı Adını Kopyala" copyUsername: "Kullanıcı Adını Kopyala"
copyUserId: "KullanıcıyıKopyala"
copyNoteId: "Kimlik notunu kopyala"
searchUser: "Kullanıcıları ara" searchUser: "Kullanıcıları ara"
reply: "yanıt"
loadMore: "Devamını yükle"
showMore: "Devamını yükle"
lists: "Listeler"
noLists: "Liste yok"
note: "not"
notes: "notlar"
following: "takipçi"
followers: "takipçi"
followsYou: "seni takip ediyor"
createList: "Liste oluştur"
manageLists: "Yönetici Listeleri"
error: "hata"
follow: "takipçi"
followRequest: "Takip isteği"
followRequests: "Takip istekleri"
unfollow: "takip etmeyi bırak"
followRequestPending: "Bekleyen Takip Etme Talebi"
enterEmoji: "Emoji Giriniz"
renote: "vazgeçme"
unrenote: "not alma"
renoted: "yeniden adlandırılmış"
cantRenote: "Ayrılamama"
cantReRenote: "not alabilirmiyim"
quote: "alıntı"
pinnedNote: "Sabitlenen"
pinned: "Sabitlenmiş" pinned: "Sabitlenmiş"
you: "sen"
unmute: "sesi aç"
renoteMute: "sesi kapat"
renoteUnmute: "sesi açmayı iptal et"
block: "engelle"
unblock: "engellemeyi kaldır"
suspend: "askıya al"
unsuspend: "askıya alma"
blockConfirm: "Onayı engelle"
unblockConfirm: "engellemeyi kaldır onayla"
selectChannel: "Kanal seç"
flagAsBot: "Bot olarak işaretle"
instances: "Sunucu"
remove: "Sil" remove: "Sil"
pinnedNotes: "Sabitlenen"
userList: "Listeler"
smtpUser: "Kullanıcı Adı" smtpUser: "Kullanıcı Adı"
smtpPass: "Şifre" smtpPass: "Şifre"
user: "Kullanıcı" user: "Kullanıcı"
searchByGoogle: "Arama" searchByGoogle: "Arama"
_theme:
keys:
renote: "vazgeçme"
_sfx: _sfx:
note: "notlar"
notification: "Bildirim" notification: "Bildirim"
_widgets: _widgets:
profile: "Profil" profile: "Profil"
notifications: "Bildirim" notifications: "Bildirim"
timeline: "Zaman çizelgesi" timeline: "Zaman çizelgesi"
_cw:
show: "Devamını yükle"
_visibility:
followers: "takipçi"
_profile: _profile:
username: "Kullanıcı Adı" username: "Kullanıcı Adı"
_exportOrImport:
followingList: "takipçi"
blockingList: "engelle"
userLists: "Listeler"
_notification:
_types:
follow: "takipçi"
renote: "vazgeçme"
quote: "alıntı"
_actions:
reply: "yanıt"
renote: "vazgeçme"
_deck: _deck:
_columns: _columns:
notifications: "Bildirim" notifications: "Bildirim"
tl: "Zaman çizelgesi" tl: "Zaman çizelgesi"
list: "Listeler"

View File

@@ -1060,7 +1060,6 @@ cancelReactionConfirm: "要取消回应吗?"
changeReactionConfirm: "要更改回应吗?" changeReactionConfirm: "要更改回应吗?"
later: "一会再说" later: "一会再说"
goToMisskey: "去往Misskey" goToMisskey: "去往Misskey"
additionalEmojiDictionary: "表情符号追加字典"
installed: "已安装" installed: "已安装"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "账户创建完成了!" accountCreated: "账户创建完成了!"

View File

@@ -1062,7 +1062,6 @@ later: "稍後再說"
goToMisskey: "往Misskey" goToMisskey: "往Misskey"
additionalEmojiDictionary: "表情符號的附加辭典" additionalEmojiDictionary: "表情符號的附加辭典"
installed: "已安裝" installed: "已安裝"
branding: "品牌宣傳"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "帳戶已建立完成!" accountCreated: "帳戶已建立完成!"
letsStartAccountSetup: "來進行帳戶的初始設定吧。" letsStartAccountSetup: "來進行帳戶的初始設定吧。"

View File

@@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "13.14.0-beta.1", "version": "13.13.0",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -25,7 +25,7 @@
"migrateandstart": "pnpm migrate && pnpm start", "migrateandstart": "pnpm migrate && pnpm start",
"gulp": "pnpm exec gulp build", "gulp": "pnpm exec gulp build",
"watch": "pnpm dev", "watch": "pnpm dev",
"dev": "node ./scripts/dev.mjs", "dev": "node ./scripts/dev.js",
"lint": "pnpm -r lint", "lint": "pnpm -r lint",
"cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts", "cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts",
"cy:run": "pnpm cypress run", "cy:run": "pnpm cypress run",
@@ -44,23 +44,23 @@
"lodash": "4.17.21" "lodash": "4.17.21"
}, },
"dependencies": { "dependencies": {
"execa": "7.1.1", "execa": "5.1.1",
"gulp": "4.0.2", "gulp": "4.0.2",
"gulp-cssnano": "2.1.3", "gulp-cssnano": "2.1.3",
"gulp-rename": "2.0.0", "gulp-rename": "2.0.0",
"gulp-replace": "1.1.4", "gulp-replace": "1.1.4",
"gulp-terser": "2.1.0", "gulp-terser": "2.1.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"typescript": "5.1.6" "typescript": "5.1.3"
}, },
"devDependencies": { "devDependencies": {
"@types/gulp": "4.0.10", "@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1", "@types/gulp-rename": "2.0.1",
"@typescript-eslint/eslint-plugin": "5.61.0", "@typescript-eslint/eslint-plugin": "5.59.8",
"@typescript-eslint/parser": "5.61.0", "@typescript-eslint/parser": "5.59.8",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "12.17.0", "cypress": "12.13.0",
"eslint": "8.44.0", "eslint": "8.41.0",
"start-server-and-test": "2.0.0" "start-server-and-test": "2.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {

View File

@@ -17,7 +17,7 @@
"paths": { "paths": {
"@/*": ["*"] "@/*": ["*"]
}, },
"target": "es2022" "target": "es2021"
}, },
"minify": false "minify": false
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,9 +0,0 @@
export class ad1677054292210 {
name = 'ad1677054292210';
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "ad" ADD "dayOfWeek" integer NOT NULL Default 0`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "ad" DROP COLUMN "dayOfWeek"`);
}
}

View File

@@ -1,17 +0,0 @@
export class ErrorImageUrl1685973839966 {
name = 'ErrorImageUrl1685973839966'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "errorImageUrl"`);
await queryRunner.query(`ALTER TABLE "meta" ADD "serverErrorImageUrl" character varying(1024)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "notFoundImageUrl" character varying(1024)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "infoImageUrl" character varying(1024)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "infoImageUrl"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "notFoundImageUrl"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "serverErrorImageUrl"`);
await queryRunner.query(`ALTER TABLE "meta" ADD "errorImageUrl" character varying(1024) DEFAULT 'https://xn--931a.moe/aiart/yubitun.png'`);
}
}

View File

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

View File

@@ -3,9 +3,6 @@
"main": "./index.js", "main": "./index.js",
"private": true, "private": true,
"type": "module", "type": "module",
"engines": {
"node": ">=18.16.0"
},
"scripts": { "scripts": {
"start": "node ./built/index.js", "start": "node ./built/index.js",
"start:test": "NODE_ENV=test node ./built/index.js", "start:test": "NODE_ENV=test node ./built/index.js",
@@ -54,36 +51,35 @@
"utf-8-validate": "^6.0.3" "utf-8-validate": "^6.0.3"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "3.367.0", "@aws-sdk/client-s3": "3.321.1",
"@aws-sdk/lib-storage": "3.367.0", "@aws-sdk/lib-storage": "3.321.1",
"@aws-sdk/node-http-handler": "3.360.0", "@aws-sdk/node-http-handler": "3.321.1",
"@bull-board/api": "5.6.0", "@bull-board/api": "5.2.0",
"@bull-board/fastify": "5.6.0", "@bull-board/fastify": "5.2.0",
"@bull-board/ui": "5.6.0", "@bull-board/ui": "5.2.0",
"@discordapp/twemoji": "14.1.2", "@discordapp/twemoji": "14.1.2",
"@fastify/accepts": "4.2.0", "@fastify/accepts": "4.1.0",
"@fastify/cookie": "8.3.0", "@fastify/cookie": "8.3.0",
"@fastify/cors": "8.3.0", "@fastify/cors": "8.3.0",
"@fastify/http-proxy": "9.2.1", "@fastify/http-proxy": "9.1.0",
"@fastify/multipart": "7.7.0", "@fastify/multipart": "7.6.0",
"@fastify/static": "6.10.2", "@fastify/static": "6.10.2",
"@fastify/view": "8.0.0", "@fastify/view": "7.4.1",
"@nestjs/common": "10.0.5", "@nestjs/common": "9.4.2",
"@nestjs/core": "10.0.5", "@nestjs/core": "9.4.2",
"@nestjs/testing": "10.0.5", "@nestjs/testing": "9.4.2",
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.3.0", "@sinonjs/fake-timers": "10.2.0",
"@swc/cli": "0.1.62", "@swc/cli": "0.1.62",
"@swc/core": "1.3.68", "@swc/core": "1.3.61",
"accepts": "1.3.8", "accepts": "1.3.8",
"ajv": "8.12.0", "ajv": "8.12.0",
"archiver": "5.3.1", "archiver": "5.3.1",
"async-mutex": "^0.4.0",
"autwh": "0.1.0", "autwh": "0.1.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"bullmq": "4.2.0", "bullmq": "3.15.0",
"cacheable-lookup": "7.0.0", "cacheable-lookup": "6.1.0",
"cbor": "9.0.0", "cbor": "9.0.0",
"chalk": "5.2.0", "chalk": "5.2.0",
"chalk-template": "0.4.0", "chalk-template": "0.4.0",
@@ -94,24 +90,23 @@
"date-fns": "2.30.0", "date-fns": "2.30.0",
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"escape-regexp": "0.0.1", "escape-regexp": "0.0.1",
"fastify": "4.19.2", "fastify": "4.17.0",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "18.5.0", "file-type": "18.4.0",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
"form-data": "4.0.0", "form-data": "4.0.0",
"got": "13.0.0", "got": "12.6.0",
"happy-dom": "10.0.3", "happy-dom": "9.20.3",
"hpagent": "1.2.0", "hpagent": "1.2.0",
"ioredis": "5.3.2", "ioredis": "5.3.2",
"ip-cidr": "3.1.0", "ip-cidr": "3.1.0",
"ipaddr.js": "2.1.0", "is-svg": "4.3.2",
"is-svg": "5.0.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsdom": "22.1.0", "jsdom": "22.1.0",
"json5": "2.2.3", "json5": "2.2.3",
"jsonld": "8.2.0", "jsonld": "8.2.0",
"jsrsasign": "10.8.6", "jsrsasign": "10.8.6",
"meilisearch": "0.33.0", "meilisearch": "0.32.5",
"mfm-js": "0.23.3", "mfm-js": "0.23.3",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"misskey-js": "workspace:*", "misskey-js": "workspace:*",
@@ -122,9 +117,10 @@
"nsfwjs": "2.4.2", "nsfwjs": "2.4.2",
"oauth": "0.10.0", "oauth": "0.10.0",
"os-utils": "0.0.14", "os-utils": "0.0.14",
"otpauth": "9.1.3", "otpauth": "9.1.2",
"parse5": "7.1.2", "parse5": "7.1.2",
"pg": "8.11.1", "pg": "8.11.0",
"private-ip": "3.0.0",
"probe-image-size": "7.2.3", "probe-image-size": "7.2.3",
"promise-limit": "2.7.0", "promise-limit": "2.7.0",
"pug": "3.0.2", "pug": "3.0.2",
@@ -133,39 +129,41 @@
"qrcode": "1.5.3", "qrcode": "1.5.3",
"random-seed": "0.3.0", "random-seed": "0.3.0",
"ratelimiter": "3.4.1", "ratelimiter": "3.4.1",
"re2": "1.19.1", "re2": "1.19.0",
"redis-lock": "0.1.4", "redis-lock": "0.1.4",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"rename": "1.0.4", "rename": "1.0.4",
"rndstr": "1.0.0",
"rss-parser": "3.13.0", "rss-parser": "3.13.0",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"s-age": "1.1.2", "s-age": "1.1.2",
"sanitize-html": "2.11.0", "sanitize-html": "2.10.0",
"semver": "7.5.3", "seedrandom": "3.0.5",
"semver": "7.5.1",
"sharp": "0.32.1", "sharp": "0.32.1",
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp", "sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
"slacc": "0.0.9", "slacc": "0.0.9",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"summaly": "github:misskey-dev/summaly", "summaly": "github:misskey-dev/summaly",
"systeminformation": "5.18.6", "systeminformation": "5.17.16",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tmp": "0.2.1", "tmp": "0.2.1",
"tsc-alias": "1.8.7", "tsc-alias": "1.8.6",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0", "twemoji-parser": "14.0.0",
"typeorm": "0.3.17", "typeorm": "0.3.16",
"typescript": "5.1.6", "typescript": "5.1.3",
"ulid": "2.3.0", "ulid": "2.3.0",
"unzipper": "0.10.14", "unzipper": "0.10.14",
"uuid": "9.0.0", "uuid": "9.0.0",
"vary": "1.1.2", "vary": "1.1.2",
"web-push": "3.6.3", "web-push": "3.6.1",
"ws": "8.13.0", "ws": "8.13.0",
"xev": "3.0.2" "xev": "3.0.2"
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "29.6.1", "@jest/globals": "29.5.0",
"@swc/jest": "0.2.26", "@swc/jest": "0.2.26",
"@types/accepts": "1.3.5", "@types/accepts": "1.3.5",
"@types/archiver": "5.3.2", "@types/archiver": "5.3.2",
@@ -178,18 +176,17 @@
"@types/jest": "29.5.2", "@types/jest": "29.5.2",
"@types/js-yaml": "4.0.5", "@types/js-yaml": "4.0.5",
"@types/jsdom": "21.1.1", "@types/jsdom": "21.1.1",
"@types/jsonld": "1.5.9", "@types/jsonld": "1.5.8",
"@types/jsrsasign": "10.5.8", "@types/jsrsasign": "10.5.8",
"@types/mime-types": "2.1.1", "@types/mime-types": "2.1.1",
"@types/ms": "^0.7.31", "@types/node": "20.2.5",
"@types/node": "20.4.0",
"@types/node-fetch": "3.0.3", "@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.8", "@types/nodemailer": "6.4.8",
"@types/oauth": "0.9.1", "@types/oauth": "0.9.1",
"@types/pg": "8.10.2", "@types/pg": "8.10.1",
"@types/pug": "2.0.6", "@types/pug": "2.0.6",
"@types/punycode": "2.1.0", "@types/punycode": "2.1.0",
"@types/qrcode": "1.5.1", "@types/qrcode": "1.5.0",
"@types/random-seed": "0.3.3", "@types/random-seed": "0.3.3",
"@types/ratelimiter": "3.4.4", "@types/ratelimiter": "3.4.4",
"@types/redis": "4.0.11", "@types/redis": "4.0.11",
@@ -201,19 +198,19 @@
"@types/tinycolor2": "1.4.3", "@types/tinycolor2": "1.4.3",
"@types/tmp": "0.2.3", "@types/tmp": "0.2.3",
"@types/unzipper": "0.10.6", "@types/unzipper": "0.10.6",
"@types/uuid": "9.0.2", "@types/uuid": "9.0.1",
"@types/vary": "1.1.0", "@types/vary": "1.1.0",
"@types/web-push": "3.3.2", "@types/web-push": "3.3.2",
"@types/websocket": "1.0.5", "@types/websocket": "1.0.5",
"@types/ws": "8.5.5", "@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.61.0", "@typescript-eslint/eslint-plugin": "5.59.8",
"@typescript-eslint/parser": "5.61.0", "@typescript-eslint/parser": "5.59.8",
"aws-sdk-client-mock": "3.0.0", "aws-sdk-client-mock": "2.1.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.44.0", "eslint": "8.41.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
"execa": "7.1.1", "execa": "6.1.0",
"jest": "29.6.1", "jest": "29.5.0",
"jest-mock": "29.6.1" "jest-mock": "29.5.0"
} }
} }

View File

@@ -96,6 +96,12 @@ function showNodejsVersion(): void {
const nodejsLogger = bootLogger.createSubLogger('nodejs'); const nodejsLogger = bootLogger.createSubLogger('nodejs');
nodejsLogger.info(`Version ${process.version} detected.`); nodejsLogger.info(`Version ${process.version} detected.`);
const minVersion = fs.readFileSync(`${_dirname}/../../../../.node-version`, 'utf-8').trim();
if (semver.lt(process.version, minVersion)) {
nodejsLogger.error(`At least Node.js ${minVersion} required!`);
process.exit(1);
}
} }
function loadConfigBoot(): Config { function loadConfigBoot(): Config {

View File

@@ -295,7 +295,7 @@ export class AccountMoveService {
* dstユーザーのalsoKnownAsをfetchPersonしていき、本当にmovedToUrlをdstに指定するユーザーが存在するのかを調べる * dstユーザーのalsoKnownAsをfetchPersonしていき、本当にmovedToUrlをdstに指定するユーザーが存在するのかを調べる
* *
* @param dst movedToUrlを指定するユーザー * @param dst movedToUrlを指定するユーザー
* @param check * @param check
* @param instant checkがtrueであるユーザーが最初に見つかったら即座にreturnするかどうか * @param instant checkがtrueであるユーザーが最初に見つかったら即座にreturnするかどうか
* @returns Promise<LocalUser | RemoteUser | null> * @returns Promise<LocalUser | RemoteUser | null>
*/ */

View File

@@ -4,7 +4,6 @@ import { dirname } from 'node:path';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as nsfw from 'nsfwjs'; import * as nsfw from 'nsfwjs';
import si from 'systeminformation'; import si from 'systeminformation';
import { Mutex } from 'async-mutex';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@@ -18,7 +17,6 @@ let isSupportedCpu: undefined | boolean = undefined;
@Injectable() @Injectable()
export class AiService { export class AiService {
private model: nsfw.NSFWJS; private model: nsfw.NSFWJS;
private modelLoadMutex: Mutex = new Mutex();
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
@@ -33,22 +31,16 @@ export class AiService {
const cpuFlags = await this.getCpuFlags(); const cpuFlags = await this.getCpuFlags();
isSupportedCpu = REQUIRED_CPU_FLAGS.every(required => cpuFlags.includes(required)); isSupportedCpu = REQUIRED_CPU_FLAGS.every(required => cpuFlags.includes(required));
} }
if (!isSupportedCpu) { if (!isSupportedCpu) {
console.error('This CPU cannot use TensorFlow.'); console.error('This CPU cannot use TensorFlow.');
return null; return null;
} }
const tf = await import('@tensorflow/tfjs-node'); const tf = await import('@tensorflow/tfjs-node');
if (this.model == null) { if (this.model == null) this.model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { size: 299 });
await this.modelLoadMutex.runExclusive(async () => {
if (this.model == null) {
this.model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { size: 299 });
}
});
}
const buffer = await fs.promises.readFile(path); const buffer = await fs.promises.readFile(path);
const image = await tf.node.decodeImage(buffer, 3) as any; const image = await tf.node.decodeImage(buffer, 3) as any;
try { try {

View File

@@ -99,7 +99,7 @@ export class AntennaService implements OnApplicationShutdown {
'MAXLEN', '~', '200', 'MAXLEN', '~', '200',
'*', '*',
'note', note.id); 'note', note.id);
this.globalEventService.publishAntennaStream(antenna.id, 'note', note); this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
} }
@@ -112,16 +112,16 @@ export class AntennaService implements OnApplicationShutdown {
public async checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }): Promise<boolean> { public async checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }): Promise<boolean> {
if (note.visibility === 'specified') return false; if (note.visibility === 'specified') return false;
if (note.visibility === 'followers') return false; if (note.visibility === 'followers') return false;
if (!antenna.withReplies && note.replyId != null) return false; if (!antenna.withReplies && note.replyId != null) return false;
if (antenna.src === 'home') { if (antenna.src === 'home') {
// TODO // TODO
} else if (antenna.src === 'list') { } else if (antenna.src === 'list') {
const listUsers = (await this.userListJoiningsRepository.findBy({ const listUsers = (await this.userListJoiningsRepository.findBy({
userListId: antenna.userListId!, userListId: antenna.userListId!,
})).map(x => x.userId); })).map(x => x.userId);
if (!listUsers.includes(note.userId)) return false; if (!listUsers.includes(note.userId)) return false;
} else if (antenna.src === 'users') { } else if (antenna.src === 'users') {
const accts = antenna.users.map(x => { const accts = antenna.users.map(x => {
@@ -130,32 +130,32 @@ export class AntennaService implements OnApplicationShutdown {
}); });
if (!accts.includes(this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false; if (!accts.includes(this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false;
} }
const keywords = antenna.keywords const keywords = antenna.keywords
// Clean up // Clean up
.map(xs => xs.filter(x => x !== '')) .map(xs => xs.filter(x => x !== ''))
.filter(xs => xs.length > 0); .filter(xs => xs.length > 0);
if (keywords.length > 0) { if (keywords.length > 0) {
if (note.text == null && note.cw == null) return false; if (note.text == null && note.cw == null) return false;
const _text = (note.text ?? '') + '\n' + (note.cw ?? ''); const _text = (note.text ?? '') + '\n' + (note.cw ?? '');
const matched = keywords.some(and => const matched = keywords.some(and =>
and.every(keyword => and.every(keyword =>
antenna.caseSensitive antenna.caseSensitive
? _text.includes(keyword) ? _text.includes(keyword)
: _text.toLowerCase().includes(keyword.toLowerCase()), : _text.toLowerCase().includes(keyword.toLowerCase()),
)); ));
if (!matched) return false; if (!matched) return false;
} }
const excludeKeywords = antenna.excludeKeywords const excludeKeywords = antenna.excludeKeywords
// Clean up // Clean up
.map(xs => xs.filter(x => x !== '')) .map(xs => xs.filter(x => x !== ''))
.filter(xs => xs.length > 0); .filter(xs => xs.length > 0);
if (excludeKeywords.length > 0) { if (excludeKeywords.length > 0) {
if (note.text == null && note.cw == null) return false; if (note.text == null && note.cw == null) return false;
@@ -167,16 +167,16 @@ export class AntennaService implements OnApplicationShutdown {
? _text.includes(keyword) ? _text.includes(keyword)
: _text.toLowerCase().includes(keyword.toLowerCase()), : _text.toLowerCase().includes(keyword.toLowerCase()),
)); ));
if (matched) return false; if (matched) return false;
} }
if (antenna.withFile) { if (antenna.withFile) {
if (note.fileIds && note.fileIds.length === 0) return false; if (note.fileIds && note.fileIds.length === 0) return false;
} }
// TODO: eval expression // TODO: eval expression
return true; return true;
} }
@@ -188,7 +188,7 @@ export class AntennaService implements OnApplicationShutdown {
}); });
this.antennasFetched = true; this.antennasFetched = true;
} }
return this.antennas; return this.antennas;
} }

View File

@@ -32,6 +32,11 @@ export class AppLockService {
return this.lock(`ap-object:${uri}`, timeout); return this.lock(`ap-object:${uri}`, timeout);
} }
@bindThis
public getFetchInstanceMetadataLock(host: string, timeout = 30 * 1000): Promise<() => void> {
return this.lock(`instance:${host}`, timeout);
}
@bindThis @bindThis
public getChartInsertLock(lockKey: string, timeout = 30 * 1000): Promise<() => void> { public getChartInsertLock(lockKey: string, timeout = 30 * 1000): Promise<() => void> {
return this.lock(`chart-insert:${lockKey}`, timeout); return this.lock(`chart-insert:${lockKey}`, timeout);

View File

@@ -168,17 +168,6 @@ export class CacheService implements OnApplicationShutdown {
@bindThis @bindThis
public dispose(): void { public dispose(): void {
this.redisForSub.off('message', this.onMessage); this.redisForSub.off('message', this.onMessage);
this.userByIdCache.dispose();
this.localUserByNativeTokenCache.dispose();
this.localUserByIdCache.dispose();
this.uriPersonCache.dispose();
this.userProfileCache.dispose();
this.userMutingsCache.dispose();
this.userBlockingCache.dispose();
this.userBlockedCache.dispose();
this.renoteMutingsCache.dispose();
this.userFollowingsCache.dispose();
this.userFollowingChannelsCache.dispose();
} }
@bindThis @bindThis

View File

@@ -20,7 +20,7 @@ export class CaptchaService {
secret, secret,
response, response,
}); });
const res = await this.httpRequestService.send(url, { const res = await this.httpRequestService.send(url, {
method: 'POST', method: 'POST',
body: params.toString(), body: params.toString(),
@@ -28,14 +28,14 @@ export class CaptchaService {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
}, },
}, { throwErrorWhenResponseNotOk: false }); }, { throwErrorWhenResponseNotOk: false });
if (!res.ok) { if (!res.ok) {
throw new Error(`${res.status}`); throw new Error(`${res.status}`);
} }
return await res.json() as CaptchaResponse; return await res.json() as CaptchaResponse;
} }
@bindThis @bindThis
public async verifyRecaptcha(secret: string, response: string | null | undefined): Promise<void> { public async verifyRecaptcha(secret: string, response: string | null | undefined): Promise<void> {
if (response == null) { if (response == null) {
@@ -73,7 +73,7 @@ export class CaptchaService {
if (response == null) { if (response == null) {
throw new Error('turnstile-failed: no response provided'); throw new Error('turnstile-failed: no response provided');
} }
const result = await this.getCaptchaResponse('https://challenges.cloudflare.com/turnstile/v0/siteverify', secret, response).catch(err => { const result = await this.getCaptchaResponse('https://challenges.cloudflare.com/turnstile/v0/siteverify', secret, response).catch(err => {
throw new Error(`turnstile-request-failed: ${err}`); throw new Error(`turnstile-request-failed: ${err}`);
}); });

View File

@@ -25,27 +25,27 @@ export class CreateSystemUserService {
@bindThis @bindThis
public async createSystemUser(username: string): Promise<User> { public async createSystemUser(username: string): Promise<User> {
const password = uuid(); const password = uuid();
// Generate hash of password // Generate hash of password
const salt = await bcrypt.genSalt(8); const salt = await bcrypt.genSalt(8);
const hash = await bcrypt.hash(password, salt); const hash = await bcrypt.hash(password, salt);
// Generate secret // Generate secret
const secret = generateNativeUserToken(); const secret = generateNativeUserToken();
const keyPair = await genRsaKeyPair(4096); const keyPair = await genRsaKeyPair(4096);
let account!: User; let account!: User;
// Start transaction // Start transaction
await this.db.transaction(async transactionalEntityManager => { await this.db.transaction(async transactionalEntityManager => {
const exist = await transactionalEntityManager.findOneBy(User, { const exist = await transactionalEntityManager.findOneBy(User, {
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),
host: IsNull(), host: IsNull(),
}); });
if (exist) throw new Error('the user is already exists'); if (exist) throw new Error('the user is already exists');
account = await transactionalEntityManager.insert(User, { account = await transactionalEntityManager.insert(User, {
id: this.idService.genId(), id: this.idService.genId(),
createdAt: new Date(), createdAt: new Date(),
@@ -58,25 +58,25 @@ export class CreateSystemUserService {
isExplorable: false, isExplorable: false,
isBot: true, isBot: true,
}).then(x => transactionalEntityManager.findOneByOrFail(User, x.identifiers[0])); }).then(x => transactionalEntityManager.findOneByOrFail(User, x.identifiers[0]));
await transactionalEntityManager.insert(UserKeypair, { await transactionalEntityManager.insert(UserKeypair, {
publicKey: keyPair.publicKey, publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey, privateKey: keyPair.privateKey,
userId: account.id, userId: account.id,
}); });
await transactionalEntityManager.insert(UserProfile, { await transactionalEntityManager.insert(UserProfile, {
userId: account.id, userId: account.id,
autoAcceptFollowed: false, autoAcceptFollowed: false,
password: hash, password: hash,
}); });
await transactionalEntityManager.insert(UsedUsername, { await transactionalEntityManager.insert(UsedUsername, {
createdAt: new Date(), createdAt: new Date(),
username: username.toLowerCase(), username: username.toLowerCase(),
}); });
}); });
return account; return account;
} }
} }

View File

@@ -1,4 +1,4 @@
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { DataSource, In, IsNull } from 'typeorm'; import { DataSource, In, IsNull } from 'typeorm';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@@ -18,7 +18,7 @@ import type { Serialized } from '@/server/api/stream/types.js';
const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/; const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
@Injectable() @Injectable()
export class CustomEmojiService implements OnApplicationShutdown { export class CustomEmojiService {
private cache: MemoryKVCache<Emoji | null>; private cache: MemoryKVCache<Emoji | null>;
public localEmojisCache: RedisSingleCache<Map<string, Emoji>>; public localEmojisCache: RedisSingleCache<Map<string, Emoji>>;
@@ -140,7 +140,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
this.globalEventService.publishBroadcastStream('emojiAdded', { this.globalEventService.publishBroadcastStream('emojiAdded', {
emoji: updated, emoji: updated,
}); });
} }
} }
@@ -194,7 +194,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
} }
this.localEmojisCache.refresh(); this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiUpdated', { this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ids), emojis: await this.emojiEntityService.packDetailedMany(ids),
}); });
@@ -215,7 +215,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
emojis: await this.emojiEntityService.packDetailedMany(ids), emojis: await this.emojiEntityService.packDetailedMany(ids),
}); });
} }
@bindThis @bindThis
public async setLicenseBulk(ids: Emoji['id'][], license: string | null) { public async setLicenseBulk(ids: Emoji['id'][], license: string | null) {
await this.emojisRepository.update({ await this.emojisRepository.update({
@@ -349,14 +349,4 @@ export class CustomEmojiService implements OnApplicationShutdown {
this.cache.set(`${emoji.name} ${emoji.host}`, emoji); this.cache.set(`${emoji.name} ${emoji.host}`, emoji);
} }
} }
@bindThis
public dispose(): void {
this.cache.dispose();
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
} }

View File

@@ -28,11 +28,11 @@ export class DeleteAccountService {
// 物理削除する前にDelete activityを送信する // 物理削除する前にDelete activityを送信する
await this.userSuspendService.doPostSuspend(user).catch(e => {}); await this.userSuspendService.doPostSuspend(user).catch(e => {});
this.queueService.createDeleteAccountJob(user, { this.queueService.createDeleteAccountJob(user, {
soft: false, soft: false,
}); });
await this.usersRepository.update(user.id, { await this.usersRepository.update(user.id, {
isDeleted: true, isDeleted: true,
}); });

View File

@@ -2,7 +2,8 @@ import * as fs from 'node:fs';
import * as stream from 'node:stream'; import * as stream from 'node:stream';
import * as util from 'node:util'; import * as util from 'node:util';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import ipaddr from 'ipaddr.js'; import IPCIDR from 'ip-cidr';
import PrivateIp from 'private-ip';
import chalk from 'chalk'; import chalk from 'chalk';
import got, * as Got from 'got'; import got, * as Got from 'got';
import { parse } from 'content-disposition'; import { parse } from 'content-disposition';
@@ -122,15 +123,15 @@ export class DownloadService {
public async downloadTextFile(url: string): Promise<string> { public async downloadTextFile(url: string): Promise<string> {
// Create temp file // Create temp file
const [path, cleanup] = await createTemp(); const [path, cleanup] = await createTemp();
this.logger.info(`text file: Temp file is ${path}`); this.logger.info(`text file: Temp file is ${path}`);
try { try {
// write content at URL to temp file // write content at URL to temp file
await this.downloadUrl(url, path); await this.downloadUrl(url, path);
const text = await util.promisify(fs.readFile)(path, 'utf8'); const text = await util.promisify(fs.readFile)(path, 'utf8');
return text; return text;
} finally { } finally {
cleanup(); cleanup();
@@ -139,14 +140,13 @@ export class DownloadService {
@bindThis @bindThis
private isPrivateIp(ip: string): boolean { private isPrivateIp(ip: string): boolean {
const parsedIp = ipaddr.parse(ip);
for (const net of this.config.allowedPrivateNetworks ?? []) { for (const net of this.config.allowedPrivateNetworks ?? []) {
if (parsedIp.match(ipaddr.parseCIDR(net))) { const cidr = new IPCIDR(net);
if (cidr.contains(ip)) {
return false; return false;
} }
} }
return parsedIp.range() !== 'unicast'; return PrivateIp(ip) ?? false;
} }
} }

View File

@@ -29,12 +29,12 @@ export class EmailService {
@bindThis @bindThis
public async sendEmail(to: string, subject: string, html: string, text: string) { public async sendEmail(to: string, subject: string, html: string, text: string) {
const meta = await this.metaService.fetch(true); const meta = await this.metaService.fetch(true);
const iconUrl = `${this.config.url}/static-assets/mi-white.png`; const iconUrl = `${this.config.url}/static-assets/mi-white.png`;
const emailSettingUrl = `${this.config.url}/settings/email`; const emailSettingUrl = `${this.config.url}/settings/email`;
const enableAuth = meta.smtpUser != null && meta.smtpUser !== ''; const enableAuth = meta.smtpUser != null && meta.smtpUser !== '';
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
host: meta.smtpHost, host: meta.smtpHost,
port: meta.smtpPort, port: meta.smtpPort,
@@ -46,7 +46,7 @@ export class EmailService {
pass: meta.smtpPass, pass: meta.smtpPass,
} : undefined, } : undefined,
} as any); } as any);
try { try {
// TODO: htmlサニタイズ // TODO: htmlサニタイズ
const info = await transporter.sendMail({ const info = await transporter.sendMail({
@@ -135,7 +135,7 @@ export class EmailService {
</body> </body>
</html>`, </html>`,
}); });
this.logger.info(`Message sent: ${info.messageId}`); this.logger.info(`Message sent: ${info.messageId}`);
} catch (err) { } catch (err) {
this.logger.error(err as Error); this.logger.error(err as Error);
@@ -149,12 +149,12 @@ export class EmailService {
reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp'; reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp';
}> { }> {
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
const exist = await this.userProfilesRepository.countBy({ const exist = await this.userProfilesRepository.countBy({
emailVerified: true, emailVerified: true,
email: emailAddress, email: emailAddress,
}); });
const validated = meta.enableActiveEmailValidation ? await validateEmail({ const validated = meta.enableActiveEmailValidation ? await validateEmail({
email: emailAddress, email: emailAddress,
validateRegex: true, validateRegex: true,
@@ -163,9 +163,9 @@ export class EmailService {
validateDisposable: true, // 捨てアドかどうかチェック validateDisposable: true, // 捨てアドかどうかチェック
validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので
}) : { valid: true, reason: null }; }) : { valid: true, reason: null };
const available = exist === 0 && validated.valid; const available = exist === 0 && validated.valid;
return { return {
available, available,
reason: available ? null : reason: available ? null :

View File

@@ -1,4 +1,4 @@
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import type { InstancesRepository } from '@/models/index.js'; import type { InstancesRepository } from '@/models/index.js';
import type { Instance } from '@/models/entities/Instance.js'; import type { Instance } from '@/models/entities/Instance.js';
@@ -9,7 +9,7 @@ import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class FederatedInstanceService implements OnApplicationShutdown { export class FederatedInstanceService {
public federatedInstanceCache: RedisKVCache<Instance | null>; public federatedInstanceCache: RedisKVCache<Instance | null>;
constructor( constructor(
@@ -43,19 +43,19 @@ export class FederatedInstanceService implements OnApplicationShutdown {
@bindThis @bindThis
public async fetch(host: string): Promise<Instance> { public async fetch(host: string): Promise<Instance> {
host = this.utilityService.toPuny(host); host = this.utilityService.toPuny(host);
const cached = await this.federatedInstanceCache.get(host); const cached = await this.federatedInstanceCache.get(host);
if (cached) return cached; if (cached) return cached;
const index = await this.instancesRepository.findOneBy({ host }); const index = await this.instancesRepository.findOneBy({ host });
if (index == null) { if (index == null) {
const i = await this.instancesRepository.insert({ const i = await this.instancesRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
host, host,
firstRetrievedAt: new Date(), firstRetrievedAt: new Date(),
}).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0])); }).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0]));
this.federatedInstanceCache.set(host, i); this.federatedInstanceCache.set(host, i);
return i; return i;
} else { } else {
@@ -74,17 +74,7 @@ export class FederatedInstanceService implements OnApplicationShutdown {
.then((response) => { .then((response) => {
return response.raw[0]; return response.raw[0];
}); });
this.federatedInstanceCache.set(result.host, result); this.federatedInstanceCache.set(result.host, result);
} }
@bindThis
public dispose(): void {
this.federatedInstanceCache.dispose();
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
} }

View File

@@ -3,6 +3,8 @@ import { Inject, Injectable } from '@nestjs/common';
import { JSDOM } from 'jsdom'; import { JSDOM } from 'jsdom';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import type { Instance } from '@/models/entities/Instance.js'; import type { Instance } from '@/models/entities/Instance.js';
import type { InstancesRepository } from '@/models/index.js';
import { AppLockService } from '@/core/AppLockService.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
@@ -10,7 +12,6 @@ import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import type { DOMWindow } from 'jsdom'; import type { DOMWindow } from 'jsdom';
import * as Redis from 'ioredis';
type NodeInfo = { type NodeInfo = {
openRegistrations?: unknown; openRegistrations?: unknown;
@@ -36,49 +37,39 @@ export class FetchInstanceMetadataService {
private logger: Logger; private logger: Logger;
constructor( constructor(
@Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository,
private appLockService: AppLockService,
private httpRequestService: HttpRequestService, private httpRequestService: HttpRequestService,
private loggerService: LoggerService, private loggerService: LoggerService,
private federatedInstanceService: FederatedInstanceService, private federatedInstanceService: FederatedInstanceService,
@Inject(DI.redis)
private redisClient: Redis.Redis,
) { ) {
this.logger = this.loggerService.getLogger('metadata', 'cyan'); this.logger = this.loggerService.getLogger('metadata', 'cyan');
} }
@bindThis
public async tryLock(host: string): Promise<boolean> {
const mutex = await this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '1', 'GET');
return mutex !== '1';
}
@bindThis
public unlock(host: string): Promise<'OK'> {
return this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '0');
}
@bindThis @bindThis
public async fetchInstanceMetadata(instance: Instance, force = false): Promise<void> { public async fetchInstanceMetadata(instance: Instance, force = false): Promise<void> {
const host = instance.host; const unlock = await this.appLockService.getFetchInstanceMetadataLock(instance.host);
// Acquire mutex to ensure no parallel runs
if (!await this.tryLock(host)) return;
try {
if (!force) {
const _instance = await this.federatedInstanceService.fetch(host);
const now = Date.now();
if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) {
// unlock at the finally caluse
return;
}
}
this.logger.info(`Fetching metadata of ${instance.host} ...`); if (!force) {
const _instance = await this.instancesRepository.findOneBy({ host: instance.host });
const now = Date.now();
if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) {
unlock();
return;
}
}
this.logger.info(`Fetching metadata of ${instance.host} ...`);
try {
const [info, dom, manifest] = await Promise.all([ const [info, dom, manifest] = await Promise.all([
this.fetchNodeinfo(instance).catch(() => null), this.fetchNodeinfo(instance).catch(() => null),
this.fetchDom(instance).catch(() => null), this.fetchDom(instance).catch(() => null),
this.fetchManifest(instance).catch(() => null), this.fetchManifest(instance).catch(() => null),
]); ]);
const [favicon, icon, themeColor, name, description] = await Promise.all([ const [favicon, icon, themeColor, name, description] = await Promise.all([
this.fetchFaviconUrl(instance, dom).catch(() => null), this.fetchFaviconUrl(instance, dom).catch(() => null),
this.fetchIconUrl(instance, dom, manifest).catch(() => null), this.fetchIconUrl(instance, dom, manifest).catch(() => null),
@@ -86,13 +77,13 @@ export class FetchInstanceMetadataService {
this.getSiteName(info, dom, manifest).catch(() => null), this.getSiteName(info, dom, manifest).catch(() => null),
this.getDescription(info, dom, manifest).catch(() => null), this.getDescription(info, dom, manifest).catch(() => null),
]); ]);
this.logger.succ(`Successfuly fetched metadata of ${instance.host}`); this.logger.succ(`Successfuly fetched metadata of ${instance.host}`);
const updates = { const updates = {
infoUpdatedAt: new Date(), infoUpdatedAt: new Date(),
} as Record<string, any>; } as Record<string, any>;
if (info) { if (info) {
updates.softwareName = typeof info.software?.name === 'string' ? info.software.name.toLowerCase() : '?'; updates.softwareName = typeof info.software?.name === 'string' ? info.software.name.toLowerCase() : '?';
updates.softwareVersion = info.software?.version; updates.softwareVersion = info.software?.version;
@@ -100,27 +91,27 @@ export class FetchInstanceMetadataService {
updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name ?? null) : null : null; updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name ?? null) : null : null;
updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email ?? null) : null : null; updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email ?? null) : null : null;
} }
if (name) updates.name = name; if (name) updates.name = name;
if (description) updates.description = description; if (description) updates.description = description;
if (icon || favicon) updates.iconUrl = icon ?? favicon; if (icon || favicon) updates.iconUrl = icon ?? favicon;
if (favicon) updates.faviconUrl = favicon; if (favicon) updates.faviconUrl = favicon;
if (themeColor) updates.themeColor = themeColor; if (themeColor) updates.themeColor = themeColor;
await this.federatedInstanceService.update(instance.id, updates); await this.federatedInstanceService.update(instance.id, updates);
this.logger.succ(`Successfuly updated metadata of ${instance.host}`); this.logger.succ(`Successfuly updated metadata of ${instance.host}`);
} catch (e) { } catch (e) {
this.logger.error(`Failed to update metadata of ${instance.host}: ${e}`); this.logger.error(`Failed to update metadata of ${instance.host}: ${e}`);
} finally { } finally {
await this.unlock(host); unlock();
} }
} }
@bindThis @bindThis
private async fetchNodeinfo(instance: Instance): Promise<NodeInfo> { private async fetchNodeinfo(instance: Instance): Promise<NodeInfo> {
this.logger.info(`Fetching nodeinfo of ${instance.host} ...`); this.logger.info(`Fetching nodeinfo of ${instance.host} ...`);
try { try {
const wellknown = await this.httpRequestService.getJson('https://' + instance.host + '/.well-known/nodeinfo') const wellknown = await this.httpRequestService.getJson('https://' + instance.host + '/.well-known/nodeinfo')
.catch(err => { .catch(err => {
@@ -130,33 +121,33 @@ export class FetchInstanceMetadataService {
throw err.statusCode ?? err.message; throw err.statusCode ?? err.message;
} }
}) as Record<string, unknown>; }) as Record<string, unknown>;
if (wellknown.links == null || !Array.isArray(wellknown.links)) { if (wellknown.links == null || !Array.isArray(wellknown.links)) {
throw new Error('No wellknown links'); throw new Error('No wellknown links');
} }
const links = wellknown.links as any[]; const links = wellknown.links as any[];
const lnik1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0'); const lnik1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0');
const lnik2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0'); const lnik2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0');
const lnik2_1 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.1'); const lnik2_1 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.1');
const link = lnik2_1 ?? lnik2_0 ?? lnik1_0; const link = lnik2_1 ?? lnik2_0 ?? lnik1_0;
if (link == null) { if (link == null) {
throw new Error('No nodeinfo link provided'); throw new Error('No nodeinfo link provided');
} }
const info = await this.httpRequestService.getJson(link.href) const info = await this.httpRequestService.getJson(link.href)
.catch(err => { .catch(err => {
throw err.statusCode ?? err.message; throw err.statusCode ?? err.message;
}); });
this.logger.succ(`Successfuly fetched nodeinfo of ${instance.host}`); this.logger.succ(`Successfuly fetched nodeinfo of ${instance.host}`);
return info as NodeInfo; return info as NodeInfo;
} catch (err) { } catch (err) {
this.logger.error(`Failed to fetch nodeinfo of ${instance.host}: ${err}`); this.logger.error(`Failed to fetch nodeinfo of ${instance.host}: ${err}`);
throw err; throw err;
} }
} }
@@ -164,51 +155,51 @@ export class FetchInstanceMetadataService {
@bindThis @bindThis
private async fetchDom(instance: Instance): Promise<DOMWindow['document']> { private async fetchDom(instance: Instance): Promise<DOMWindow['document']> {
this.logger.info(`Fetching HTML of ${instance.host} ...`); this.logger.info(`Fetching HTML of ${instance.host} ...`);
const url = 'https://' + instance.host; const url = 'https://' + instance.host;
const html = await this.httpRequestService.getHtml(url); const html = await this.httpRequestService.getHtml(url);
const { window } = new JSDOM(html); const { window } = new JSDOM(html);
const doc = window.document; const doc = window.document;
return doc; return doc;
} }
@bindThis @bindThis
private async fetchManifest(instance: Instance): Promise<Record<string, unknown> | null> { private async fetchManifest(instance: Instance): Promise<Record<string, unknown> | null> {
const url = 'https://' + instance.host; const url = 'https://' + instance.host;
const manifestUrl = url + '/manifest.json'; const manifestUrl = url + '/manifest.json';
const manifest = await this.httpRequestService.getJson(manifestUrl) as Record<string, unknown>; const manifest = await this.httpRequestService.getJson(manifestUrl) as Record<string, unknown>;
return manifest; return manifest;
} }
@bindThis @bindThis
private async fetchFaviconUrl(instance: Instance, doc: DOMWindow['document'] | null): Promise<string | null> { private async fetchFaviconUrl(instance: Instance, doc: DOMWindow['document'] | null): Promise<string | null> {
const url = 'https://' + instance.host; const url = 'https://' + instance.host;
if (doc) { if (doc) {
// https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043 // https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043
const href = Array.from(doc.getElementsByTagName('link')).reverse().find(link => link.relList.contains('icon'))?.href; const href = Array.from(doc.getElementsByTagName('link')).reverse().find(link => link.relList.contains('icon'))?.href;
if (href) { if (href) {
return (new URL(href, url)).href; return (new URL(href, url)).href;
} }
} }
const faviconUrl = url + '/favicon.ico'; const faviconUrl = url + '/favicon.ico';
const favicon = await this.httpRequestService.send(faviconUrl, { const favicon = await this.httpRequestService.send(faviconUrl, {
method: 'HEAD', method: 'HEAD',
}, { throwErrorWhenResponseNotOk: false }); }, { throwErrorWhenResponseNotOk: false });
if (favicon.ok) { if (favicon.ok) {
return faviconUrl; return faviconUrl;
} }
return null; return null;
} }
@@ -218,38 +209,38 @@ export class FetchInstanceMetadataService {
const url = 'https://' + instance.host; const url = 'https://' + instance.host;
return (new URL(manifest.icons[0].src, url)).href; return (new URL(manifest.icons[0].src, url)).href;
} }
if (doc) { if (doc) {
const url = 'https://' + instance.host; const url = 'https://' + instance.host;
// https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043 // https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043
const links = Array.from(doc.getElementsByTagName('link')).reverse(); const links = Array.from(doc.getElementsByTagName('link')).reverse();
// https://github.com/misskey-dev/misskey/pull/8220/files/0ec4eba22a914e31b86874f12448f88b3e58dd5a#r796487559 // https://github.com/misskey-dev/misskey/pull/8220/files/0ec4eba22a914e31b86874f12448f88b3e58dd5a#r796487559
const href = const href =
[ [
links.find(link => link.relList.contains('apple-touch-icon-precomposed'))?.href, links.find(link => link.relList.contains('apple-touch-icon-precomposed'))?.href,
links.find(link => link.relList.contains('apple-touch-icon'))?.href, links.find(link => link.relList.contains('apple-touch-icon'))?.href,
links.find(link => link.relList.contains('icon'))?.href, links.find(link => link.relList.contains('icon'))?.href,
] ]
.find(href => href); .find(href => href);
if (href) { if (href) {
return (new URL(href, url)).href; return (new URL(href, url)).href;
} }
} }
return null; return null;
} }
@bindThis @bindThis
private async getThemeColor(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> { private async getThemeColor(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
const themeColor = info?.metadata?.themeColor ?? doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') ?? manifest?.theme_color; const themeColor = info?.metadata?.themeColor ?? doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') ?? manifest?.theme_color;
if (themeColor) { if (themeColor) {
const color = new tinycolor(themeColor); const color = new tinycolor(themeColor);
if (color.isValid()) return color.toHexString(); if (color.isValid()) return color.toHexString();
} }
return null; return null;
} }
@@ -262,19 +253,19 @@ export class FetchInstanceMetadataService {
return info.metadata.name; return info.metadata.name;
} }
} }
if (doc) { if (doc) {
const og = doc.querySelector('meta[property="og:title"]')?.getAttribute('content'); const og = doc.querySelector('meta[property="og:title"]')?.getAttribute('content');
if (og) { if (og) {
return og; return og;
} }
} }
if (manifest) { if (manifest) {
return manifest.name ?? manifest.short_name; return manifest.name ?? manifest.short_name;
} }
return null; return null;
} }
@@ -287,23 +278,23 @@ export class FetchInstanceMetadataService {
return info.metadata.description; return info.metadata.description;
} }
} }
if (doc) { if (doc) {
const meta = doc.querySelector('meta[name="description"]')?.getAttribute('content'); const meta = doc.querySelector('meta[name="description"]')?.getAttribute('content');
if (meta) { if (meta) {
return meta; return meta;
} }
const og = doc.querySelector('meta[property="og:description"]')?.getAttribute('content'); const og = doc.querySelector('meta[property="og:description"]')?.getAttribute('content');
if (og) { if (og) {
return og; return og;
} }
} }
if (manifest) { if (manifest) {
return manifest.name ?? manifest.short_name; return manifest.name ?? manifest.short_name;
} }
return null; return null;
} }
} }

View File

@@ -161,20 +161,20 @@ export class FileInfoService {
private async detectSensitivity(source: string, mime: string, sensitiveThreshold: number, sensitiveThresholdForPorn: number, analyzeVideo: boolean): Promise<[sensitive: boolean, porn: boolean]> { private async detectSensitivity(source: string, mime: string, sensitiveThreshold: number, sensitiveThresholdForPorn: number, analyzeVideo: boolean): Promise<[sensitive: boolean, porn: boolean]> {
let sensitive = false; let sensitive = false;
let porn = false; let porn = false;
function judgePrediction(result: readonly predictionType[]): [sensitive: boolean, porn: boolean] { function judgePrediction(result: readonly predictionType[]): [sensitive: boolean, porn: boolean] {
let sensitive = false; let sensitive = false;
let porn = false; let porn = false;
if ((result.find(x => x.className === 'Sexy')?.probability ?? 0) > sensitiveThreshold) sensitive = true; if ((result.find(x => x.className === 'Sexy')?.probability ?? 0) > sensitiveThreshold) sensitive = true;
if ((result.find(x => x.className === 'Hentai')?.probability ?? 0) > sensitiveThreshold) sensitive = true; if ((result.find(x => x.className === 'Hentai')?.probability ?? 0) > sensitiveThreshold) sensitive = true;
if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThreshold) sensitive = true; if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThreshold) sensitive = true;
if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThresholdForPorn) porn = true; if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThresholdForPorn) porn = true;
return [sensitive, porn]; return [sensitive, porn];
} }
if ([ if ([
'image/jpeg', 'image/jpeg',
'image/png', 'image/png',
@@ -253,10 +253,10 @@ export class FileInfoService {
disposeOutDir(); disposeOutDir();
} }
} }
return [sensitive, porn]; return [sensitive, porn];
} }
private async *asyncIterateFrames(cwd: string, command: FFmpeg.FfmpegCommand): AsyncGenerator<string, void> { private async *asyncIterateFrames(cwd: string, command: FFmpeg.FfmpegCommand): AsyncGenerator<string, void> {
const watcher = new FSWatcher({ const watcher = new FSWatcher({
cwd, cwd,
@@ -295,7 +295,7 @@ export class FileInfoService {
} }
} }
} }
@bindThis @bindThis
private exists(path: string): Promise<boolean> { private exists(path: string): Promise<boolean> {
return fs.promises.access(path).then(() => true, () => false); return fs.promises.access(path).then(() => true, () => false);
@@ -304,11 +304,11 @@ export class FileInfoService {
@bindThis @bindThis
public fixMime(mime: string | fileType.MimeType): string { public fixMime(mime: string | fileType.MimeType): string {
// see https://github.com/misskey-dev/misskey/pull/10686 // see https://github.com/misskey-dev/misskey/pull/10686
if (mime === 'audio/x-flac') { if (mime === "audio/x-flac") {
return 'audio/flac'; return "audio/flac";
} }
if (mime === 'audio/vnd.wave') { if (mime === "audio/vnd.wave") {
return 'audio/wav'; return "audio/wav";
} }
return mime; return mime;
@@ -355,12 +355,11 @@ export class FileInfoService {
* Check the file is SVG or not * Check the file is SVG or not
*/ */
@bindThis @bindThis
public async checkSvg(path: string): Promise<boolean> { public async checkSvg(path: string) {
try { try {
const size = await this.getFileSize(path); const size = await this.getFileSize(path);
if (size > 1 * 1024 * 1024) return false; if (size > 1 * 1024 * 1024) return false;
const buffer = await fs.promises.readFile(path); return isSvg(fs.readFileSync(path));
return isSvg(buffer.toString());
} catch { } catch {
return false; return false;
} }

View File

@@ -20,7 +20,7 @@ import type { Packed } from '@/misc/json-schema.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { Role } from '@/models/index.js'; import { Role } from '@/models';
@Injectable() @Injectable()
export class GlobalEventService { export class GlobalEventService {

View File

@@ -42,21 +42,21 @@ export class HttpRequestService {
errorTtl: 30, // 30secs errorTtl: 30, // 30secs
lookup: false, // nativeのdns.lookupにfallbackしない lookup: false, // nativeのdns.lookupにfallbackしない
}); });
this.http = new http.Agent({ this.http = new http.Agent({
keepAlive: true, keepAlive: true,
keepAliveMsecs: 30 * 1000, keepAliveMsecs: 30 * 1000,
lookup: cache.lookup, lookup: cache.lookup,
} as http.AgentOptions); } as http.AgentOptions);
this.https = new https.Agent({ this.https = new https.Agent({
keepAlive: true, keepAlive: true,
keepAliveMsecs: 30 * 1000, keepAliveMsecs: 30 * 1000,
lookup: cache.lookup, lookup: cache.lookup,
} as https.AgentOptions); } as https.AgentOptions);
const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128); const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128);
this.httpAgent = config.proxy this.httpAgent = config.proxy
? new HttpProxyAgent({ ? new HttpProxyAgent({
keepAlive: true, keepAlive: true,

View File

@@ -5,7 +5,7 @@ import type { Config } from '@/config.js';
import { genAid, parseAid } from '@/misc/id/aid.js'; import { genAid, parseAid } from '@/misc/id/aid.js';
import { genMeid, parseMeid } from '@/misc/id/meid.js'; import { genMeid, parseMeid } from '@/misc/id/meid.js';
import { genMeidg, parseMeidg } from '@/misc/id/meidg.js'; import { genMeidg, parseMeidg } from '@/misc/id/meidg.js';
import { genObjectId, parseObjectId } from '@/misc/id/object-id.js'; import { genObjectId } from '@/misc/id/object-id.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { parseUlid } from '@/misc/id/ulid.js'; import { parseUlid } from '@/misc/id/ulid.js';
@@ -23,7 +23,7 @@ export class IdService {
@bindThis @bindThis
public genId(date?: Date): string { public genId(date?: Date): string {
if (!date || (date > new Date())) date = new Date(); if (!date || (date > new Date())) date = new Date();
switch (this.method) { switch (this.method) {
case 'aid': return genAid(date); case 'aid': return genAid(date);
case 'meid': return genMeid(date); case 'meid': return genMeid(date);
@@ -38,7 +38,7 @@ export class IdService {
public parse(id: string): { date: Date; } { public parse(id: string): { date: Date; } {
switch (this.method) { switch (this.method) {
case 'aid': return parseAid(id); case 'aid': return parseAid(id);
case 'objectid': return parseObjectId(id); case 'objectid':
case 'meid': return parseMeid(id); case 'meid': return parseMeid(id);
case 'meidg': return parseMeidg(id); case 'meidg': return parseMeidg(id);
case 'ulid': return parseUlid(id); case 'ulid': return parseUlid(id);

View File

@@ -26,12 +26,12 @@ export class InstanceActorService {
public async getInstanceActor(): Promise<LocalUser> { public async getInstanceActor(): Promise<LocalUser> {
const cached = this.cache.get(); const cached = this.cache.get();
if (cached) return cached; if (cached) return cached;
const user = await this.usersRepository.findOneBy({ const user = await this.usersRepository.findOneBy({
host: IsNull(), host: IsNull(),
username: ACTOR_USERNAME, username: ACTOR_USERNAME,
}) as LocalUser | undefined; }) as LocalUser | undefined;
if (user) { if (user) {
this.cache.set(user); this.cache.set(user);
return user; return user;

View File

@@ -3,7 +3,7 @@ import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import Logger from '@/logger.js'; import Logger from '@/logger.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { KEYWORD } from 'color-convert/conversions.js'; import type { KEYWORD } from 'color-convert/conversions';
@Injectable() @Injectable()
export class LoggerService { export class LoggerService {

View File

@@ -56,7 +56,7 @@ export class MetaService implements OnApplicationShutdown {
@bindThis @bindThis
public async fetch(noCache = false): Promise<Meta> { public async fetch(noCache = false): Promise<Meta> {
if (!noCache && this.cache) return this.cache; if (!noCache && this.cache) return this.cache;
return await this.db.transaction(async transactionalEntityManager => { return await this.db.transaction(async transactionalEntityManager => {
// 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する // 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する
const metas = await transactionalEntityManager.find(Meta, { const metas = await transactionalEntityManager.find(Meta, {
@@ -64,9 +64,9 @@ export class MetaService implements OnApplicationShutdown {
id: 'DESC', id: 'DESC',
}, },
}); });
const meta = metas[0]; const meta = metas[0];
if (meta) { if (meta) {
this.cache = meta; this.cache = meta;
return meta; return meta;
@@ -81,7 +81,7 @@ export class MetaService implements OnApplicationShutdown {
['id'], ['id'],
) )
.then((x) => transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0])); .then((x) => transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0]));
this.cache = saved; this.cache = saved;
return saved; return saved;
} }

View File

@@ -27,29 +27,29 @@ export class MfmService {
public fromHtml(html: string, hashtagNames?: string[]): string { public fromHtml(html: string, hashtagNames?: string[]): string {
// some AP servers like Pixelfed use br tags as well as newlines // some AP servers like Pixelfed use br tags as well as newlines
html = html.replace(/<br\s?\/?>\r?\n/gi, '\n'); html = html.replace(/<br\s?\/?>\r?\n/gi, '\n');
const dom = parse5.parseFragment(html); const dom = parse5.parseFragment(html);
let text = ''; let text = '';
for (const n of dom.childNodes) { for (const n of dom.childNodes) {
analyze(n); analyze(n);
} }
return text.trim(); return text.trim();
function getText(node: TreeAdapter.Node): string { function getText(node: TreeAdapter.Node): string {
if (treeAdapter.isTextNode(node)) return node.value; if (treeAdapter.isTextNode(node)) return node.value;
if (!treeAdapter.isElementNode(node)) return ''; if (!treeAdapter.isElementNode(node)) return '';
if (node.nodeName === 'br') return '\n'; if (node.nodeName === 'br') return '\n';
if (node.childNodes) { if (node.childNodes) {
return node.childNodes.map(n => getText(n)).join(''); return node.childNodes.map(n => getText(n)).join('');
} }
return ''; return '';
} }
function appendChildren(childNodes: TreeAdapter.ChildNode[]): void { function appendChildren(childNodes: TreeAdapter.ChildNode[]): void {
if (childNodes) { if (childNodes) {
for (const n of childNodes) { for (const n of childNodes) {
@@ -57,35 +57,35 @@ export class MfmService {
} }
} }
} }
function analyze(node: TreeAdapter.Node) { function analyze(node: TreeAdapter.Node) {
if (treeAdapter.isTextNode(node)) { if (treeAdapter.isTextNode(node)) {
text += node.value; text += node.value;
return; return;
} }
// Skip comment or document type node // Skip comment or document type node
if (!treeAdapter.isElementNode(node)) return; if (!treeAdapter.isElementNode(node)) return;
switch (node.nodeName) { switch (node.nodeName) {
case 'br': { case 'br': {
text += '\n'; text += '\n';
break; break;
} }
case 'a': case 'a':
{ {
const txt = getText(node); const txt = getText(node);
const rel = node.attrs.find(x => x.name === 'rel'); const rel = node.attrs.find(x => x.name === 'rel');
const href = node.attrs.find(x => x.name === 'href'); const href = node.attrs.find(x => x.name === 'href');
// ハッシュタグ // ハッシュタグ
if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) { if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) {
text += txt; text += txt;
// メンション // メンション
} else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) { } else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) {
const part = txt.split('@'); const part = txt.split('@');
if (part.length === 2 && href) { if (part.length === 2 && href) {
//#region ホスト名部分が省略されているので復元する //#region ホスト名部分が省略されているので復元する
const acct = `${txt}@${(new URL(href.value)).hostname}`; const acct = `${txt}@${(new URL(href.value)).hostname}`;
@@ -116,12 +116,12 @@ export class MfmService {
return `[${txt}](${href.value})`; return `[${txt}](${href.value})`;
} }
}; };
text += generateLink(); text += generateLink();
} }
break; break;
} }
case 'h1': case 'h1':
{ {
text += '【'; text += '【';
@@ -129,7 +129,7 @@ export class MfmService {
text += '】\n'; text += '】\n';
break; break;
} }
case 'b': case 'b':
case 'strong': case 'strong':
{ {
@@ -138,7 +138,7 @@ export class MfmService {
text += '**'; text += '**';
break; break;
} }
case 'small': case 'small':
{ {
text += '<small>'; text += '<small>';
@@ -146,7 +146,7 @@ export class MfmService {
text += '</small>'; text += '</small>';
break; break;
} }
case 's': case 's':
case 'del': case 'del':
{ {
@@ -155,7 +155,7 @@ export class MfmService {
text += '~~'; text += '~~';
break; break;
} }
case 'i': case 'i':
case 'em': case 'em':
{ {
@@ -164,7 +164,7 @@ export class MfmService {
text += '</i>'; text += '</i>';
break; break;
} }
// block code (<pre><code>) // block code (<pre><code>)
case 'pre': { case 'pre': {
if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') { if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') {
@@ -176,7 +176,7 @@ export class MfmService {
} }
break; break;
} }
// inline code (<code>) // inline code (<code>)
case 'code': { case 'code': {
text += '`'; text += '`';
@@ -184,7 +184,7 @@ export class MfmService {
text += '`'; text += '`';
break; break;
} }
case 'blockquote': { case 'blockquote': {
const t = getText(node); const t = getText(node);
if (t) { if (t) {
@@ -193,7 +193,7 @@ export class MfmService {
} }
break; break;
} }
case 'p': case 'p':
case 'h2': case 'h2':
case 'h3': case 'h3':
@@ -205,7 +205,7 @@ export class MfmService {
appendChildren(node.childNodes); appendChildren(node.childNodes);
break; break;
} }
// other block elements // other block elements
case 'div': case 'div':
case 'header': case 'header':
@@ -219,7 +219,7 @@ export class MfmService {
appendChildren(node.childNodes); appendChildren(node.childNodes);
break; break;
} }
default: // includes inline elements default: // includes inline elements
{ {
appendChildren(node.childNodes); appendChildren(node.childNodes);
@@ -234,48 +234,48 @@ export class MfmService {
if (nodes == null) { if (nodes == null) {
return null; return null;
} }
const { window } = new Window(); const { window } = new Window();
const doc = window.document; const doc = window.document;
function appendChildren(children: mfm.MfmNode[], targetElement: any): void { function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
if (children) { if (children) {
for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child); for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child);
} }
} }
const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = { const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = {
bold: (node) => { bold: (node) => {
const el = doc.createElement('b'); const el = doc.createElement('b');
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
small: (node) => { small: (node) => {
const el = doc.createElement('small'); const el = doc.createElement('small');
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
strike: (node) => { strike: (node) => {
const el = doc.createElement('del'); const el = doc.createElement('del');
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
italic: (node) => { italic: (node) => {
const el = doc.createElement('i'); const el = doc.createElement('i');
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
fn: (node) => { fn: (node) => {
const el = doc.createElement('i'); const el = doc.createElement('i');
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
blockCode: (node) => { blockCode: (node) => {
const pre = doc.createElement('pre'); const pre = doc.createElement('pre');
const inner = doc.createElement('code'); const inner = doc.createElement('code');
@@ -283,21 +283,21 @@ export class MfmService {
pre.appendChild(inner); pre.appendChild(inner);
return pre; return pre;
}, },
center: (node) => { center: (node) => {
const el = doc.createElement('div'); const el = doc.createElement('div');
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
emojiCode: (node) => { emojiCode: (node) => {
return doc.createTextNode(`\u200B:${node.props.name}:\u200B`); return doc.createTextNode(`\u200B:${node.props.name}:\u200B`);
}, },
unicodeEmoji: (node) => { unicodeEmoji: (node) => {
return doc.createTextNode(node.props.emoji); return doc.createTextNode(node.props.emoji);
}, },
hashtag: (node) => { hashtag: (node) => {
const a = doc.createElement('a'); const a = doc.createElement('a');
a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`); a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`);
@@ -305,32 +305,32 @@ export class MfmService {
a.setAttribute('rel', 'tag'); a.setAttribute('rel', 'tag');
return a; return a;
}, },
inlineCode: (node) => { inlineCode: (node) => {
const el = doc.createElement('code'); const el = doc.createElement('code');
el.textContent = node.props.code; el.textContent = node.props.code;
return el; return el;
}, },
mathInline: (node) => { mathInline: (node) => {
const el = doc.createElement('code'); const el = doc.createElement('code');
el.textContent = node.props.formula; el.textContent = node.props.formula;
return el; return el;
}, },
mathBlock: (node) => { mathBlock: (node) => {
const el = doc.createElement('code'); const el = doc.createElement('code');
el.textContent = node.props.formula; el.textContent = node.props.formula;
return el; return el;
}, },
link: (node) => { link: (node) => {
const a = doc.createElement('a'); const a = doc.createElement('a');
a.setAttribute('href', node.props.url); a.setAttribute('href', node.props.url);
appendChildren(node.children, a); appendChildren(node.children, a);
return a; return a;
}, },
mention: (node) => { mention: (node) => {
const a = doc.createElement('a'); const a = doc.createElement('a');
const { username, host, acct } = node.props; const { username, host, acct } = node.props;
@@ -340,47 +340,47 @@ export class MfmService {
a.textContent = acct; a.textContent = acct;
return a; return a;
}, },
quote: (node) => { quote: (node) => {
const el = doc.createElement('blockquote'); const el = doc.createElement('blockquote');
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
text: (node) => { text: (node) => {
const el = doc.createElement('span'); const el = doc.createElement('span');
const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x)); const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x));
for (const x of intersperse<FIXME | 'br'>('br', nodes)) { for (const x of intersperse<FIXME | 'br'>('br', nodes)) {
el.appendChild(x === 'br' ? doc.createElement('br') : x); el.appendChild(x === 'br' ? doc.createElement('br') : x);
} }
return el; return el;
}, },
url: (node) => { url: (node) => {
const a = doc.createElement('a'); const a = doc.createElement('a');
a.setAttribute('href', node.props.url); a.setAttribute('href', node.props.url);
a.textContent = node.props.url; a.textContent = node.props.url;
return a; return a;
}, },
search: (node) => { search: (node) => {
const a = doc.createElement('a'); const a = doc.createElement('a');
a.setAttribute('href', `https://www.google.com/search?q=${node.props.query}`); a.setAttribute('href', `https://www.google.com/search?q=${node.props.query}`);
a.textContent = node.props.content; a.textContent = node.props.content;
return a; return a;
}, },
plain: (node) => { plain: (node) => {
const el = doc.createElement('span'); const el = doc.createElement('span');
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
}; };
appendChildren(nodes, doc.body); appendChildren(nodes, doc.body);
return `<p>${doc.body.innerHTML}</p>`; return `<p>${doc.body.innerHTML}</p>`;
} }
} }

View File

@@ -672,7 +672,7 @@ export class NoteCreateService implements OnApplicationShutdown {
// Register to search database // Register to search database
this.index(note); this.index(note);
} }
@bindThis @bindThis
private isSensitive(note: Option, sensitiveWord: string[]): boolean { private isSensitive(note: Option, sensitiveWord: string[]): boolean {
if (sensitiveWord.length > 0) { if (sensitiveWord.length > 0) {
@@ -758,7 +758,7 @@ export class NoteCreateService implements OnApplicationShutdown {
@bindThis @bindThis
private index(note: Note) { private index(note: Note) {
if (note.text == null && note.cw == null) return; if (note.text == null && note.cw == null) return;
this.searchService.indexNote(note); this.searchService.indexNote(note);
} }

View File

@@ -45,7 +45,7 @@ export class NoteDeleteService {
private perUserNotesChart: PerUserNotesChart, private perUserNotesChart: PerUserNotesChart,
private instanceChart: InstanceChart, private instanceChart: InstanceChart,
) {} ) {}
/** /**
* 投稿を削除します。 * 投稿を削除します。
* @param user 投稿者 * @param user 投稿者
@@ -121,8 +121,10 @@ export class NoteDeleteService {
} }
@bindThis @bindThis
private async findCascadingNotes(note: Note): Promise<Note[]> { private async findCascadingNotes(note: Note) {
const recursive = async (noteId: string): Promise<Note[]> => { const cascadingNotes: Note[] = [];
const recursive = async (noteId: string) => {
const query = this.notesRepository.createQueryBuilder('note') const query = this.notesRepository.createQueryBuilder('note')
.where('note.replyId = :noteId', { noteId }) .where('note.replyId = :noteId', { noteId })
.orWhere(new Brackets(q => { .orWhere(new Brackets(q => {
@@ -131,14 +133,12 @@ export class NoteDeleteService {
})) }))
.leftJoinAndSelect('note.user', 'user'); .leftJoinAndSelect('note.user', 'user');
const replies = await query.getMany(); const replies = await query.getMany();
for (const reply of replies) {
return [ cascadingNotes.push(reply);
replies, await recursive(reply.id);
...await Promise.all(replies.map(reply => recursive(reply.id))), }
].flat();
}; };
await recursive(note.id);
const cascadingNotes: Note[] = await recursive(note.id);
return cascadingNotes.filter(note => note.userHost === null); // filter out non-local users return cascadingNotes.filter(note => note.userHost === null); // filter out non-local users
} }

View File

@@ -99,7 +99,7 @@ export class NoteReadService implements OnApplicationShutdown {
}); });
// TODO: ↓まとめてクエリしたい // TODO: ↓まとめてクエリしたい
this.noteUnreadsRepository.countBy({ this.noteUnreadsRepository.countBy({
userId: userId, userId: userId,
isMentioned: true, isMentioned: true,
@@ -109,7 +109,7 @@ export class NoteReadService implements OnApplicationShutdown {
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions'); this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
} }
}); });
this.noteUnreadsRepository.countBy({ this.noteUnreadsRepository.countBy({
userId: userId, userId: userId,
isSpecified: true, isSpecified: true,

View File

@@ -46,7 +46,7 @@ export class NotificationService implements OnApplicationShutdown {
force = false, force = false,
) { ) {
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`); const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
const latestNotificationIdsRes = await this.redisClient.xrevrange( const latestNotificationIdsRes = await this.redisClient.xrevrange(
`notificationTimeline:${userId}`, `notificationTimeline:${userId}`,
'+', '+',

View File

@@ -39,12 +39,12 @@ export class PollService {
@bindThis @bindThis
public async vote(user: User, note: Note, choice: number) { public async vote(user: User, note: Note, choice: number) {
const poll = await this.pollsRepository.findOneBy({ noteId: note.id }); const poll = await this.pollsRepository.findOneBy({ noteId: note.id });
if (poll == null) throw new Error('poll not found'); if (poll == null) throw new Error('poll not found');
// Check whether is valid choice // Check whether is valid choice
if (poll.choices[choice] == null) throw new Error('invalid choice param'); if (poll.choices[choice] == null) throw new Error('invalid choice param');
// Check blocking // Check blocking
if (note.userId !== user.id) { if (note.userId !== user.id) {
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id); const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
@@ -52,13 +52,13 @@ export class PollService {
throw new Error('blocked'); throw new Error('blocked');
} }
} }
// if already voted // if already voted
const exist = await this.pollVotesRepository.findBy({ const exist = await this.pollVotesRepository.findBy({
noteId: note.id, noteId: note.id,
userId: user.id, userId: user.id,
}); });
if (poll.multiple) { if (poll.multiple) {
if (exist.some(x => x.choice === choice)) { if (exist.some(x => x.choice === choice)) {
throw new Error('already voted'); throw new Error('already voted');
@@ -66,7 +66,7 @@ export class PollService {
} else if (exist.length !== 0) { } else if (exist.length !== 0) {
throw new Error('already voted'); throw new Error('already voted');
} }
// Create vote // Create vote
await this.pollVotesRepository.insert({ await this.pollVotesRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
@@ -75,11 +75,11 @@ export class PollService {
userId: user.id, userId: user.id,
choice: choice, choice: choice,
}); });
// Increment votes count // Increment votes count
const index = choice + 1; // In SQL, array index is 1 based const index = choice + 1; // In SQL, array index is 1 based
await this.pollsRepository.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`); await this.pollsRepository.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`);
this.globalEventService.publishNoteStream(note.id, 'pollVoted', { this.globalEventService.publishNoteStream(note.id, 'pollVoted', {
choice: choice, choice: choice,
userId: user.id, userId: user.id,
@@ -90,10 +90,10 @@ export class PollService {
public async deliverQuestionUpdate(noteId: Note['id']) { public async deliverQuestionUpdate(noteId: Note['id']) {
const note = await this.notesRepository.findOneBy({ id: noteId }); const note = await this.notesRepository.findOneBy({ id: noteId });
if (note == null) throw new Error('note not found'); if (note == null) throw new Error('note not found');
const user = await this.usersRepository.findOneBy({ id: note.userId }); const user = await this.usersRepository.findOneBy({ id: note.userId });
if (user == null) throw new Error('note not found'); if (user == null) throw new Error('note not found');
if (this.userEntityService.isLocalUser(user)) { if (this.userEntityService.isLocalUser(user)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user)); const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user));
this.apDeliverManagerService.deliverToFollowers(user, content); this.apDeliverManagerService.deliverToFollowers(user, content);

View File

@@ -1,9 +1,9 @@
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import push from 'web-push'; import push from 'web-push';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema';
import { getNoteSummary } from '@/misc/get-note-summary.js'; import { getNoteSummary } from '@/misc/get-note-summary.js';
import type { SwSubscription, SwSubscriptionsRepository } from '@/models/index.js'; import type { SwSubscription, SwSubscriptionsRepository } from '@/models/index.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
@@ -31,7 +31,7 @@ function truncateBody<T extends keyof PushNotificationsTypes>(type: T, body: Pus
...body.note, ...body.note,
// textをgetNoteSummaryしたものに置き換える // textをgetNoteSummaryしたものに置き換える
text: getNoteSummary(('type' in body && body.type === 'renote') ? body.note.renote as Packed<'Note'> : body.note), text: getNoteSummary(('type' in body && body.type === 'renote') ? body.note.renote as Packed<'Note'> : body.note),
cw: undefined, cw: undefined,
reply: undefined, reply: undefined,
renote: undefined, renote: undefined,
@@ -42,7 +42,7 @@ function truncateBody<T extends keyof PushNotificationsTypes>(type: T, body: Pus
} }
@Injectable() @Injectable()
export class PushNotificationService implements OnApplicationShutdown { export class PushNotificationService {
private subscriptionsCache: RedisKVCache<SwSubscription[]>; private subscriptionsCache: RedisKVCache<SwSubscription[]>;
constructor( constructor(
@@ -69,16 +69,16 @@ export class PushNotificationService implements OnApplicationShutdown {
@bindThis @bindThis
public async pushNotification<T extends keyof PushNotificationsTypes>(userId: string, type: T, body: PushNotificationsTypes[T]) { public async pushNotification<T extends keyof PushNotificationsTypes>(userId: string, type: T, body: PushNotificationsTypes[T]) {
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return; if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return;
// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録 // アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録
push.setVapidDetails(this.config.url, push.setVapidDetails(this.config.url,
meta.swPublicKey, meta.swPublicKey,
meta.swPrivateKey); meta.swPrivateKey);
const subscriptions = await this.subscriptionsCache.fetch(userId); const subscriptions = await this.subscriptionsCache.fetch(userId);
for (const subscription of subscriptions) { for (const subscription of subscriptions) {
if ([ if ([
'readAllNotifications', 'readAllNotifications',
@@ -103,7 +103,7 @@ export class PushNotificationService implements OnApplicationShutdown {
//swLogger.info(err.statusCode); //swLogger.info(err.statusCode);
//swLogger.info(err.headers); //swLogger.info(err.headers);
//swLogger.info(err.body); //swLogger.info(err.body);
if (err.statusCode === 410) { if (err.statusCode === 410) {
this.swSubscriptionsRepository.delete({ this.swSubscriptionsRepository.delete({
userId: userId, userId: userId,
@@ -115,14 +115,4 @@ export class PushNotificationService implements OnApplicationShutdown {
}); });
} }
} }
@bindThis
public dispose(): void {
this.subscriptionsCache.dispose();
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
} }

View File

@@ -60,8 +60,8 @@ export class QueryService {
q.orderBy(`${q.alias}.id`, 'DESC'); q.orderBy(`${q.alias}.id`, 'DESC');
} }
return q; return q;
} }
// ここでいうBlockedは被Blockedの意 // ここでいうBlockedは被Blockedの意
@bindThis @bindThis
public generateBlockedUserQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }): void { public generateBlockedUserQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }): void {
@@ -109,18 +109,18 @@ export class QueryService {
q.andWhere('note.channelId IS NULL'); q.andWhere('note.channelId IS NULL');
} else { } else {
q.leftJoinAndSelect('note.channel', 'channel'); q.leftJoinAndSelect('note.channel', 'channel');
const channelFollowingQuery = this.channelFollowingsRepository.createQueryBuilder('channelFollowing') const channelFollowingQuery = this.channelFollowingsRepository.createQueryBuilder('channelFollowing')
.select('channelFollowing.followeeId') .select('channelFollowing.followeeId')
.where('channelFollowing.followerId = :followerId', { followerId: me.id }); .where('channelFollowing.followerId = :followerId', { followerId: me.id });
q.andWhere(new Brackets(qb => { qb q.andWhere(new Brackets(qb => { qb
// チャンネルのノートではない // チャンネルのノートではない
.where('note.channelId IS NULL') .where('note.channelId IS NULL')
// または自分がフォローしているチャンネルのノート // または自分がフォローしているチャンネルのノート
.orWhere(`note.channelId IN (${ channelFollowingQuery.getQuery() })`); .orWhere(`note.channelId IN (${ channelFollowingQuery.getQuery() })`);
})); }));
q.setParameters(channelFollowingQuery.getParameters()); q.setParameters(channelFollowingQuery.getParameters());
} }
} }
@@ -130,9 +130,9 @@ export class QueryService {
const mutedQuery = this.mutedNotesRepository.createQueryBuilder('muted') const mutedQuery = this.mutedNotesRepository.createQueryBuilder('muted')
.select('muted.noteId') .select('muted.noteId')
.where('muted.userId = :userId', { userId: me.id }); .where('muted.userId = :userId', { userId: me.id });
q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`); q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
q.setParameters(mutedQuery.getParameters()); q.setParameters(mutedQuery.getParameters());
} }
@@ -141,13 +141,13 @@ export class QueryService {
const mutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted') const mutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted')
.select('threadMuted.threadId') .select('threadMuted.threadId')
.where('threadMuted.userId = :userId', { userId: me.id }); .where('threadMuted.userId = :userId', { userId: me.id });
q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`); q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
q.andWhere(new Brackets(qb => { qb q.andWhere(new Brackets(qb => { qb
.where('note.threadId IS NULL') .where('note.threadId IS NULL')
.orWhere(`note.threadId NOT IN (${ mutedQuery.getQuery() })`); .orWhere(`note.threadId NOT IN (${ mutedQuery.getQuery() })`);
})); }));
q.setParameters(mutedQuery.getParameters()); q.setParameters(mutedQuery.getParameters());
} }
@@ -156,15 +156,15 @@ export class QueryService {
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting') const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
.select('muting.muteeId') .select('muting.muteeId')
.where('muting.muterId = :muterId', { muterId: me.id }); .where('muting.muterId = :muterId', { muterId: me.id });
if (exclude) { if (exclude) {
mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id }); mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id });
} }
const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile') const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile')
.select('user_profile.mutedInstances') .select('user_profile.mutedInstances')
.where('user_profile.userId = :muterId', { muterId: me.id }); .where('user_profile.userId = :muterId', { muterId: me.id });
// 投稿の作者をミュートしていない かつ // 投稿の作者をミュートしていない かつ
// 投稿の返信先の作者をミュートしていない かつ // 投稿の返信先の作者をミュートしていない かつ
// 投稿の引用元の作者をミュートしていない // 投稿の引用元の作者をミュートしていない
@@ -191,7 +191,7 @@ export class QueryService {
.where('note.renoteUserHost IS NULL') .where('note.renoteUserHost IS NULL')
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`); .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`);
})); }));
q.setParameters(mutingQuery.getParameters()); q.setParameters(mutingQuery.getParameters());
q.setParameters(mutingInstanceQuery.getParameters()); q.setParameters(mutingInstanceQuery.getParameters());
} }
@@ -201,9 +201,9 @@ export class QueryService {
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting') const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
.select('muting.muteeId') .select('muting.muteeId')
.where('muting.muterId = :muterId', { muterId: me.id }); .where('muting.muterId = :muterId', { muterId: me.id });
q.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`); q.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`);
q.setParameters(mutingQuery.getParameters()); q.setParameters(mutingQuery.getParameters());
} }
@@ -245,7 +245,7 @@ export class QueryService {
const followingQuery = this.followingsRepository.createQueryBuilder('following') const followingQuery = this.followingsRepository.createQueryBuilder('following')
.select('following.followeeId') .select('following.followeeId')
.where('following.followerId = :meId'); .where('following.followerId = :meId');
q.andWhere(new Brackets(qb => { qb q.andWhere(new Brackets(qb => { qb
// 公開投稿である // 公開投稿である
.where(new Brackets(qb => { qb .where(new Brackets(qb => { qb
@@ -268,7 +268,7 @@ export class QueryService {
})); }));
})); }));
})); }));
q.setParameters({ meId: me.id }); q.setParameters({ meId: me.id });
} }
} }
@@ -278,10 +278,10 @@ export class QueryService {
const mutingQuery = this.renoteMutingsRepository.createQueryBuilder('renote_muting') const mutingQuery = this.renoteMutingsRepository.createQueryBuilder('renote_muting')
.select('renote_muting.muteeId') .select('renote_muting.muteeId')
.where('renote_muting.muterId = :muterId', { muterId: me.id }); .where('renote_muting.muterId = :muterId', { muterId: me.id });
q.andWhere(new Brackets(qb => { q.andWhere(new Brackets(qb => {
qb qb
.where(new Brackets(qb => { .where(new Brackets(qb => {
qb.where('note.renoteId IS NOT NULL'); qb.where('note.renoteId IS NOT NULL');
qb.andWhere('note.text IS NULL'); qb.andWhere('note.text IS NULL');
qb.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`); qb.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`);
@@ -289,7 +289,7 @@ export class QueryService {
.orWhere('note.renoteId IS NULL') .orWhere('note.renoteId IS NULL')
.orWhere('note.text IS NOT NULL'); .orWhere('note.text IS NOT NULL');
})); }));
q.setParameters(mutingQuery.getParameters()); q.setParameters(mutingQuery.getParameters());
} }
} }

View File

@@ -8,7 +8,7 @@ import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js'; import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js'; import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js'; import type { DbJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
import type httpSignature from '@peertube/http-signature'; import type httpSignature from '@peertube/http-signature';
import type * as Bull from 'bullmq'; import type * as Bull from 'bullmq';
@@ -69,7 +69,7 @@ export class QueueService {
if (content == null) return null; if (content == null) return null;
if (to == null) return null; if (to == null) return null;
const data: DeliverJobData = { const data = {
user: { user: {
id: user.id, id: user.id,
}, },
@@ -88,40 +88,6 @@ export class QueueService {
}); });
} }
/**
* ApDeliverManager-DeliverManager.execute()からinboxesを突っ込んでaddBulkしたい
* @param user `{ id: string; }` この関数ではThinUserに変換しないので前もって変換してください
* @param content IActivity | null
* @param inboxes `Map<string, boolean>` / key: to (inbox url), value: isSharedInbox (whether it is sharedInbox)
* @returns void
*/
@bindThis
public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map<string, boolean>) {
if (content == null) return null;
const opts = {
attempts: this.config.deliverJobMaxAttempts ?? 12,
backoff: {
type: 'custom',
},
removeOnComplete: true,
removeOnFail: true,
};
await this.deliverQueue.addBulk(Array.from(inboxes.entries()).map(d => ({
name: d[0],
data: {
user,
content,
to: d[0],
isSharedInbox: d[1],
} as DeliverJobData,
opts,
})));
return;
}
@bindThis @bindThis
public inbox(activity: IActivity, signature: httpSignature.IParsedSignature) { public inbox(activity: IActivity, signature: httpSignature.IParsedSignature) {
const data = { const data = {
@@ -434,11 +400,11 @@ export class QueueService {
this.deliverQueue.once('cleaned', (jobs, status) => { this.deliverQueue.once('cleaned', (jobs, status) => {
//deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`); //deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
}); });
this.deliverQueue.clean(0, 0, 'delayed'); this.deliverQueue.clean(0, Infinity, 'delayed');
this.inboxQueue.once('cleaned', (jobs, status) => { this.inboxQueue.once('cleaned', (jobs, status) => {
//inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`); //inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
}); });
this.inboxQueue.clean(0, 0, 'delayed'); this.inboxQueue.clean(0, Infinity, 'delayed');
} }
} }

View File

@@ -39,9 +39,9 @@ export class RelayService {
host: IsNull(), host: IsNull(),
username: ACTOR_USERNAME, username: ACTOR_USERNAME,
}); });
if (user) return user as LocalUser; if (user) return user as LocalUser;
const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME); const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME);
return created as LocalUser; return created as LocalUser;
} }
@@ -53,12 +53,12 @@ export class RelayService {
inbox, inbox,
status: 'requesting', status: 'requesting',
}).then(x => this.relaysRepository.findOneByOrFail(x.identifiers[0])); }).then(x => this.relaysRepository.findOneByOrFail(x.identifiers[0]));
const relayActor = await this.getRelayActor(); const relayActor = await this.getRelayActor();
const follow = await this.apRendererService.renderFollowRelay(relay, relayActor); const follow = await this.apRendererService.renderFollowRelay(relay, relayActor);
const activity = this.apRendererService.addContext(follow); const activity = this.apRendererService.addContext(follow);
this.queueService.deliver(relayActor, activity, relay.inbox, false); this.queueService.deliver(relayActor, activity, relay.inbox, false);
return relay; return relay;
} }
@@ -67,17 +67,17 @@ export class RelayService {
const relay = await this.relaysRepository.findOneBy({ const relay = await this.relaysRepository.findOneBy({
inbox, inbox,
}); });
if (relay == null) { if (relay == null) {
throw new Error('relay not found'); throw new Error('relay not found');
} }
const relayActor = await this.getRelayActor(); const relayActor = await this.getRelayActor();
const follow = this.apRendererService.renderFollowRelay(relay, relayActor); const follow = this.apRendererService.renderFollowRelay(relay, relayActor);
const undo = this.apRendererService.renderUndo(follow, relayActor); const undo = this.apRendererService.renderUndo(follow, relayActor);
const activity = this.apRendererService.addContext(undo); const activity = this.apRendererService.addContext(undo);
this.queueService.deliver(relayActor, activity, relay.inbox, false); this.queueService.deliver(relayActor, activity, relay.inbox, false);
await this.relaysRepository.delete(relay.id); await this.relaysRepository.delete(relay.id);
} }
@@ -86,13 +86,13 @@ export class RelayService {
const relays = await this.relaysRepository.find(); const relays = await this.relaysRepository.find();
return relays; return relays;
} }
@bindThis @bindThis
public async relayAccepted(id: string): Promise<string> { public async relayAccepted(id: string): Promise<string> {
const result = await this.relaysRepository.update(id, { const result = await this.relaysRepository.update(id, {
status: 'accepted', status: 'accepted',
}); });
return JSON.stringify(result); return JSON.stringify(result);
} }
@@ -101,24 +101,24 @@ export class RelayService {
const result = await this.relaysRepository.update(id, { const result = await this.relaysRepository.update(id, {
status: 'rejected', status: 'rejected',
}); });
return JSON.stringify(result); return JSON.stringify(result);
} }
@bindThis @bindThis
public async deliverToRelays(user: { id: User['id']; host: null; }, activity: any): Promise<void> { public async deliverToRelays(user: { id: User['id']; host: null; }, activity: any): Promise<void> {
if (activity == null) return; if (activity == null) return;
const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({ const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({
status: 'accepted', status: 'accepted',
})); }));
if (relays.length === 0) return; if (relays.length === 0) return;
const copy = deepClone(activity); const copy = deepClone(activity);
if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public']; if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public'];
const signed = await this.apRendererService.attachLdSignature(copy, user); const signed = await this.apRendererService.attachLdSignature(copy, user);
for (const relay of relays) { for (const relay of relays) {
this.queueService.deliver(user, signed, relay.inbox, false); this.queueService.deliver(user, signed, relay.inbox, false);
} }

View File

@@ -35,7 +35,7 @@ export class RemoteUserResolveService {
@bindThis @bindThis
public async resolveUser(username: string, host: string | null): Promise<LocalUser | RemoteUser> { public async resolveUser(username: string, host: string | null): Promise<LocalUser | RemoteUser> {
const usernameLower = username.toLowerCase(); const usernameLower = username.toLowerCase();
if (host == null) { if (host == null) {
this.logger.info(`return local user: ${usernameLower}`); this.logger.info(`return local user: ${usernameLower}`);
return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => { return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => {
@@ -46,9 +46,9 @@ export class RemoteUserResolveService {
} }
}) as LocalUser; }) as LocalUser;
} }
host = this.utilityService.toPuny(host); host = this.utilityService.toPuny(host);
if (this.config.host === host) { if (this.config.host === host) {
this.logger.info(`return local user: ${usernameLower}`); this.logger.info(`return local user: ${usernameLower}`);
return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => { return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => {
@@ -59,39 +59,39 @@ export class RemoteUserResolveService {
} }
}) as LocalUser; }) as LocalUser;
} }
const user = await this.usersRepository.findOneBy({ usernameLower, host }) as RemoteUser | null; const user = await this.usersRepository.findOneBy({ usernameLower, host }) as RemoteUser | null;
const acctLower = `${usernameLower}@${host}`; const acctLower = `${usernameLower}@${host}`;
if (user == null) { if (user == null) {
const self = await this.resolveSelf(acctLower); const self = await this.resolveSelf(acctLower);
this.logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`); this.logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`);
return await this.apPersonService.createPerson(self.href); return await this.apPersonService.createPerson(self.href);
} }
// ユーザー情報が古い場合は、WebFilgerからやりなおして返す // ユーザー情報が古い場合は、WebFilgerからやりなおして返す
if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
// 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する // 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する
await this.usersRepository.update(user.id, { await this.usersRepository.update(user.id, {
lastFetchedAt: new Date(), lastFetchedAt: new Date(),
}); });
this.logger.info(`try resync: ${acctLower}`); this.logger.info(`try resync: ${acctLower}`);
const self = await this.resolveSelf(acctLower); const self = await this.resolveSelf(acctLower);
if (user.uri !== self.href) { if (user.uri !== self.href) {
// if uri mismatch, Fix (user@host <=> AP's Person id(RemoteUser.uri)) mapping. // if uri mismatch, Fix (user@host <=> AP's Person id(RemoteUser.uri)) mapping.
this.logger.info(`uri missmatch: ${acctLower}`); this.logger.info(`uri missmatch: ${acctLower}`);
this.logger.info(`recovery missmatch uri for (username=${username}, host=${host}) from ${user.uri} to ${self.href}`); this.logger.info(`recovery missmatch uri for (username=${username}, host=${host}) from ${user.uri} to ${self.href}`);
// validate uri // validate uri
const uri = new URL(self.href); const uri = new URL(self.href);
if (uri.hostname !== host) { if (uri.hostname !== host) {
throw new Error('Invalid uri'); throw new Error('Invalid uri');
} }
await this.usersRepository.update({ await this.usersRepository.update({
usernameLower, usernameLower,
host: host, host: host,
@@ -101,9 +101,9 @@ export class RemoteUserResolveService {
} else { } else {
this.logger.info(`uri is fine: ${acctLower}`); this.logger.info(`uri is fine: ${acctLower}`);
} }
await this.apPersonService.updatePerson(self.href); await this.apPersonService.updatePerson(self.href);
this.logger.info(`return resynced remote user: ${acctLower}`); this.logger.info(`return resynced remote user: ${acctLower}`);
return await this.usersRepository.findOneBy({ uri: self.href }).then(u => { return await this.usersRepository.findOneBy({ uri: self.href }).then(u => {
if (u == null) { if (u == null) {
@@ -113,7 +113,7 @@ export class RemoteUserResolveService {
} }
}); });
} }
this.logger.info(`return existing remote user: ${acctLower}`); this.logger.info(`return existing remote user: ${acctLower}`);
return user; return user;
} }

View File

@@ -13,7 +13,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { StreamMessages } from '@/server/api/stream/types.js'; import { StreamMessages } from '@/server/api/stream/types.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema';
import type { OnApplicationShutdown } from '@nestjs/common'; import type { OnApplicationShutdown } from '@nestjs/common';
export type RolePolicies = { export type RolePolicies = {
@@ -392,7 +392,7 @@ export class RoleService implements OnApplicationShutdown {
@bindThis @bindThis
public async unassign(userId: User['id'], roleId: Role['id']): Promise<void> { public async unassign(userId: User['id'], roleId: Role['id']): Promise<void> {
const now = new Date(); const now = new Date();
const existing = await this.roleAssignmentsRepository.findOneBy({ roleId, userId }); const existing = await this.roleAssignmentsRepository.findOneBy({ roleId, userId });
if (existing == null) { if (existing == null) {
throw new RoleService.NotAssignedError(); throw new RoleService.NotAssignedError();
@@ -435,7 +435,6 @@ export class RoleService implements OnApplicationShutdown {
@bindThis @bindThis
public dispose(): void { public dispose(): void {
this.redisForSub.off('message', this.onMessage); this.redisForSub.off('message', this.onMessage);
this.roleAssignmentByUserIdCache.dispose();
} }
@bindThis @bindThis

View File

@@ -174,7 +174,7 @@ export class SearchService {
if (me) this.queryService.generateMutedUserQuery(query, me); if (me) this.queryService.generateMutedUserQuery(query, me);
if (me) this.queryService.generateBlockedUserQuery(query, me); if (me) this.queryService.generateBlockedUserQuery(query, me);
return await query.limit(pagination.limit).getMany(); return await query.take(pagination.limit).getMany();
} }
} }
} }

View File

@@ -50,31 +50,31 @@ export class SignupService {
}) { }) {
const { username, password, passwordHash, host } = opts; const { username, password, passwordHash, host } = opts;
let hash = passwordHash; let hash = passwordHash;
// Validate username // Validate username
if (!this.userEntityService.validateLocalUsername(username)) { if (!this.userEntityService.validateLocalUsername(username)) {
throw new Error('INVALID_USERNAME'); throw new Error('INVALID_USERNAME');
} }
if (password != null && passwordHash == null) { if (password != null && passwordHash == null) {
// Validate password // Validate password
if (!this.userEntityService.validatePassword(password)) { if (!this.userEntityService.validatePassword(password)) {
throw new Error('INVALID_PASSWORD'); throw new Error('INVALID_PASSWORD');
} }
// Generate hash of password // Generate hash of password
const salt = await bcrypt.genSalt(8); const salt = await bcrypt.genSalt(8);
hash = await bcrypt.hash(password, salt); hash = await bcrypt.hash(password, salt);
} }
// Generate secret // Generate secret
const secret = generateUserToken(); const secret = generateUserToken();
// Check username duplication // Check username duplication
if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) { if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) {
throw new Error('DUPLICATED_USERNAME'); throw new Error('DUPLICATED_USERNAME');
} }
// Check deleted username duplication // Check deleted username duplication
if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) { if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) {
throw new Error('USED_USERNAME'); throw new Error('USED_USERNAME');
@@ -106,18 +106,18 @@ export class SignupService {
}, (err, publicKey, privateKey) => }, (err, publicKey, privateKey) =>
err ? rej(err) : res([publicKey, privateKey]), err ? rej(err) : res([publicKey, privateKey]),
)); ));
let account!: User; let account!: User;
// Start transaction // Start transaction
await this.db.transaction(async transactionalEntityManager => { await this.db.transaction(async transactionalEntityManager => {
const exist = await transactionalEntityManager.findOneBy(User, { const exist = await transactionalEntityManager.findOneBy(User, {
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),
host: IsNull(), host: IsNull(),
}); });
if (exist) throw new Error(' the username is already used'); if (exist) throw new Error(' the username is already used');
account = await transactionalEntityManager.save(new User({ account = await transactionalEntityManager.save(new User({
id: this.idService.genId(), id: this.idService.genId(),
createdAt: new Date(), createdAt: new Date(),
@@ -127,27 +127,27 @@ export class SignupService {
token: secret, token: secret,
isRoot: isTheFirstUser, isRoot: isTheFirstUser,
})); }));
await transactionalEntityManager.save(new UserKeypair({ await transactionalEntityManager.save(new UserKeypair({
publicKey: keyPair[0], publicKey: keyPair[0],
privateKey: keyPair[1], privateKey: keyPair[1],
userId: account.id, userId: account.id,
})); }));
await transactionalEntityManager.save(new UserProfile({ await transactionalEntityManager.save(new UserProfile({
userId: account.id, userId: account.id,
autoAcceptFollowed: true, autoAcceptFollowed: true,
password: hash, password: hash,
})); }));
await transactionalEntityManager.save(new UsedUsername({ await transactionalEntityManager.save(new UsedUsername({
createdAt: new Date(), createdAt: new Date(),
username: username.toLowerCase(), username: username.toLowerCase(),
})); }));
}); });
this.usersChart.update(account, true); this.usersChart.update(account, true);
return { account, secret }; return { account, secret };
} }
} }

View File

@@ -69,7 +69,7 @@ function verifyCertificateChain(certificates: string[]) {
const certStruct = jsrsasign.ASN1HEX.getTLVbyList(certificate.hex!, 0, [0]); const certStruct = jsrsasign.ASN1HEX.getTLVbyList(certificate.hex!, 0, [0]);
if (certStruct == null) throw new Error('certStruct is null'); if (certStruct == null) throw new Error('certStruct is null');
const algorithm = certificate.getSignatureAlgorithmField(); const algorithm = certificate.getSignatureAlgorithmField();
const signatureHex = certificate.getSignatureValueHex(); const signatureHex = certificate.getSignatureValueHex();
@@ -143,19 +143,19 @@ export class TwoFactorAuthenticationService {
if (clientData.type !== 'webauthn.get') { if (clientData.type !== 'webauthn.get') {
throw new Error('type is not webauthn.get'); throw new Error('type is not webauthn.get');
} }
if (this.hash(clientData.challenge).toString('hex') !== challenge) { if (this.hash(clientData.challenge).toString('hex') !== challenge) {
throw new Error('challenge mismatch'); throw new Error('challenge mismatch');
} }
if (clientData.origin !== this.config.scheme + '://' + this.config.host) { if (clientData.origin !== this.config.scheme + '://' + this.config.host) {
throw new Error('origin mismatch'); throw new Error('origin mismatch');
} }
const verificationData = Buffer.concat( const verificationData = Buffer.concat(
[authenticatorData, this.hash(clientDataJSON)], [authenticatorData, this.hash(clientDataJSON)],
32 + authenticatorData.length, 32 + authenticatorData.length,
); );
return crypto return crypto
.createVerify('SHA256') .createVerify('SHA256')
.update(verificationData) .update(verificationData)
@@ -168,7 +168,7 @@ export class TwoFactorAuthenticationService {
none: { none: {
verify({ publicKey }: { publicKey: Map<number, Buffer> }) { verify({ publicKey }: { publicKey: Map<number, Buffer> }) {
const negTwo = publicKey.get(-2); const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length !== 32) { if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given'); throw new Error('invalid or no -2 key given');
} }
@@ -176,12 +176,12 @@ export class TwoFactorAuthenticationService {
if (!negThree || negThree.length !== 32) { if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given'); throw new Error('invalid or no -3 key given');
} }
const publicKeyU2F = Buffer.concat( const publicKeyU2F = Buffer.concat(
[ECC_PRELUDE, negTwo, negThree], [ECC_PRELUDE, negTwo, negThree],
1 + 32 + 32, 1 + 32 + 32,
); );
return { return {
publicKey: publicKeyU2F, publicKey: publicKeyU2F,
valid: true, valid: true,
@@ -207,16 +207,16 @@ export class TwoFactorAuthenticationService {
if (attStmt.alg !== -7) { if (attStmt.alg !== -7) {
throw new Error('alg mismatch'); throw new Error('alg mismatch');
} }
const verificationData = Buffer.concat([ const verificationData = Buffer.concat([
authenticatorData, authenticatorData,
clientDataHash, clientDataHash,
]); ]);
const attCert: Buffer = attStmt.x5c[0]; const attCert: Buffer = attStmt.x5c[0];
const negTwo = publicKey.get(-2); const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length !== 32) { if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given'); throw new Error('invalid or no -2 key given');
} }
@@ -224,23 +224,23 @@ export class TwoFactorAuthenticationService {
if (!negThree || negThree.length !== 32) { if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given'); throw new Error('invalid or no -3 key given');
} }
const publicKeyData = Buffer.concat( const publicKeyData = Buffer.concat(
[ECC_PRELUDE, negTwo, negThree], [ECC_PRELUDE, negTwo, negThree],
1 + 32 + 32, 1 + 32 + 32,
); );
if (!attCert.equals(publicKeyData)) { if (!attCert.equals(publicKeyData)) {
throw new Error('public key mismatch'); throw new Error('public key mismatch');
} }
const isValid = crypto const isValid = crypto
.createVerify('SHA256') .createVerify('SHA256')
.update(verificationData) .update(verificationData)
.verify(PEMString(attCert), attStmt.sig); .verify(PEMString(attCert), attStmt.sig);
// TODO: Check 'attestationChallenge' field in extension of cert matches hash(clientDataJSON) // TODO: Check 'attestationChallenge' field in extension of cert matches hash(clientDataJSON)
return { return {
valid: isValid, valid: isValid,
publicKey: publicKeyData, publicKey: publicKeyData,
@@ -267,43 +267,43 @@ export class TwoFactorAuthenticationService {
const verificationData = this.hash( const verificationData = this.hash(
Buffer.concat([authenticatorData, clientDataHash]), Buffer.concat([authenticatorData, clientDataHash]),
); );
const jwsParts = attStmt.response.toString('utf-8').split('.'); const jwsParts = attStmt.response.toString('utf-8').split('.');
const header = JSON.parse(base64URLDecode(jwsParts[0]).toString('utf-8')); const header = JSON.parse(base64URLDecode(jwsParts[0]).toString('utf-8'));
const response = JSON.parse( const response = JSON.parse(
base64URLDecode(jwsParts[1]).toString('utf-8'), base64URLDecode(jwsParts[1]).toString('utf-8'),
); );
const signature = jwsParts[2]; const signature = jwsParts[2];
if (!verificationData.equals(Buffer.from(response.nonce, 'base64'))) { if (!verificationData.equals(Buffer.from(response.nonce, 'base64'))) {
throw new Error('invalid nonce'); throw new Error('invalid nonce');
} }
const certificateChain = header.x5c const certificateChain = header.x5c
.map((key: any) => PEMString(key)) .map((key: any) => PEMString(key))
.concat([GSR2]); .concat([GSR2]);
if (getCertSubject(certificateChain[0]).CN !== 'attest.android.com') { if (getCertSubject(certificateChain[0]).CN !== 'attest.android.com') {
throw new Error('invalid common name'); throw new Error('invalid common name');
} }
if (!verifyCertificateChain(certificateChain)) { if (!verifyCertificateChain(certificateChain)) {
throw new Error('Invalid certificate chain!'); throw new Error('Invalid certificate chain!');
} }
const signatureBase = Buffer.from( const signatureBase = Buffer.from(
jwsParts[0] + '.' + jwsParts[1], jwsParts[0] + '.' + jwsParts[1],
'utf-8', 'utf-8',
); );
const valid = crypto const valid = crypto
.createVerify('sha256') .createVerify('sha256')
.update(signatureBase) .update(signatureBase)
.verify(certificateChain[0], base64URLDecode(signature)); .verify(certificateChain[0], base64URLDecode(signature));
const negTwo = publicKey.get(-2); const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length !== 32) { if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given'); throw new Error('invalid or no -2 key given');
} }
@@ -311,7 +311,7 @@ export class TwoFactorAuthenticationService {
if (!negThree || negThree.length !== 32) { if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given'); throw new Error('invalid or no -3 key given');
} }
const publicKeyData = Buffer.concat( const publicKeyData = Buffer.concat(
[ECC_PRELUDE, negTwo, negThree], [ECC_PRELUDE, negTwo, negThree],
1 + 32 + 32, 1 + 32 + 32,
@@ -342,17 +342,17 @@ export class TwoFactorAuthenticationService {
authenticatorData, authenticatorData,
clientDataHash, clientDataHash,
]); ]);
if (attStmt.x5c) { if (attStmt.x5c) {
const attCert = attStmt.x5c[0]; const attCert = attStmt.x5c[0];
const validSignature = crypto const validSignature = crypto
.createVerify('SHA256') .createVerify('SHA256')
.update(verificationData) .update(verificationData)
.verify(PEMString(attCert), attStmt.sig); .verify(PEMString(attCert), attStmt.sig);
const negTwo = publicKey.get(-2); const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length !== 32) { if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given'); throw new Error('invalid or no -2 key given');
} }
@@ -360,12 +360,12 @@ export class TwoFactorAuthenticationService {
if (!negThree || negThree.length !== 32) { if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given'); throw new Error('invalid or no -3 key given');
} }
const publicKeyData = Buffer.concat( const publicKeyData = Buffer.concat(
[ECC_PRELUDE, negTwo, negThree], [ECC_PRELUDE, negTwo, negThree],
1 + 32 + 32, 1 + 32 + 32,
); );
return { return {
valid: validSignature, valid: validSignature,
publicKey: publicKeyData, publicKey: publicKeyData,
@@ -375,12 +375,12 @@ export class TwoFactorAuthenticationService {
throw new Error('ECDAA-Verify is not supported'); throw new Error('ECDAA-Verify is not supported');
} else { } else {
if (attStmt.alg !== -7) throw new Error('alg mismatch'); if (attStmt.alg !== -7) throw new Error('alg mismatch');
throw new Error('self attestation is not supported'); throw new Error('self attestation is not supported');
} }
}, },
}, },
'fido-u2f': { 'fido-u2f': {
verify({ verify({
attStmt, attStmt,
@@ -401,13 +401,13 @@ export class TwoFactorAuthenticationService {
if (x5c.length !== 1) { if (x5c.length !== 1) {
throw new Error('x5c length does not match expectation'); throw new Error('x5c length does not match expectation');
} }
const attCert = x5c[0]; const attCert = x5c[0];
// TODO: make sure attCert is an Elliptic Curve (EC) public key over the P-256 curve // TODO: make sure attCert is an Elliptic Curve (EC) public key over the P-256 curve
const negTwo: Buffer = publicKey.get(-2); const negTwo: Buffer = publicKey.get(-2);
if (!negTwo || negTwo.length !== 32) { if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given'); throw new Error('invalid or no -2 key given');
} }
@@ -415,12 +415,12 @@ export class TwoFactorAuthenticationService {
if (!negThree || negThree.length !== 32) { if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given'); throw new Error('invalid or no -3 key given');
} }
const publicKeyU2F = Buffer.concat( const publicKeyU2F = Buffer.concat(
[ECC_PRELUDE, negTwo, negThree], [ECC_PRELUDE, negTwo, negThree],
1 + 32 + 32, 1 + 32 + 32,
); );
const verificationData = Buffer.concat([ const verificationData = Buffer.concat([
NULL_BYTE, NULL_BYTE,
rpIdHash, rpIdHash,
@@ -428,12 +428,12 @@ export class TwoFactorAuthenticationService {
credentialId, credentialId,
publicKeyU2F, publicKeyU2F,
]); ]);
const validSignature = crypto const validSignature = crypto
.createVerify('SHA256') .createVerify('SHA256')
.update(verificationData) .update(verificationData)
.verify(PEMString(attCert), attStmt.sig); .verify(PEMString(attCert), attStmt.sig);
return { return {
valid: validSignature, valid: validSignature,
publicKey: publicKeyU2F, publicKey: publicKeyU2F,

View File

@@ -1,4 +1,4 @@
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import type { UserKeypairsRepository } from '@/models/index.js'; import type { UserKeypairsRepository } from '@/models/index.js';
@@ -8,7 +8,7 @@ import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class UserKeypairService implements OnApplicationShutdown { export class UserKeypairService {
private cache: RedisKVCache<UserKeypair>; private cache: RedisKVCache<UserKeypair>;
constructor( constructor(
@@ -31,14 +31,4 @@ export class UserKeypairService implements OnApplicationShutdown {
public async getUserKeypair(userId: User['id']): Promise<UserKeypair> { public async getUserKeypair(userId: User['id']): Promise<UserKeypair> {
return await this.cache.fetch(userId); return await this.cache.fetch(userId);
} }
@bindThis
public dispose(): void {
this.cache.dispose();
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
} }

View File

@@ -32,13 +32,13 @@ export class UserSuspendService {
@bindThis @bindThis
public async doPostSuspend(user: { id: User['id']; host: User['host'] }): Promise<void> { public async doPostSuspend(user: { id: User['id']; host: User['host'] }): Promise<void> {
this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true }); this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true });
if (this.userEntityService.isLocalUser(user)) { if (this.userEntityService.isLocalUser(user)) {
// 知り得る全SharedInboxにDelete配信 // 知り得る全SharedInboxにDelete配信
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user)); const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user));
const queue: string[] = []; const queue: string[] = [];
const followings = await this.followingsRepository.find({ const followings = await this.followingsRepository.find({
where: [ where: [
{ followerSharedInbox: Not(IsNull()) }, { followerSharedInbox: Not(IsNull()) },
@@ -46,13 +46,13 @@ export class UserSuspendService {
], ],
select: ['followerSharedInbox', 'followeeSharedInbox'], select: ['followerSharedInbox', 'followeeSharedInbox'],
}); });
const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox); const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox);
for (const inbox of inboxes) { for (const inbox of inboxes) {
if (inbox != null && !queue.includes(inbox)) queue.push(inbox); if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
} }
for (const inbox of queue) { for (const inbox of queue) {
this.queueService.deliver(user, content, inbox, true); this.queueService.deliver(user, content, inbox, true);
} }
@@ -62,13 +62,13 @@ export class UserSuspendService {
@bindThis @bindThis
public async doPostUnsuspend(user: User): Promise<void> { public async doPostUnsuspend(user: User): Promise<void> {
this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false }); this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false });
if (this.userEntityService.isLocalUser(user)) { if (this.userEntityService.isLocalUser(user)) {
// 知り得る全SharedInboxにUndo Delete配信 // 知り得る全SharedInboxにUndo Delete配信
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user), user)); const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user), user));
const queue: string[] = []; const queue: string[] = [];
const followings = await this.followingsRepository.find({ const followings = await this.followingsRepository.find({
where: [ where: [
{ followerSharedInbox: Not(IsNull()) }, { followerSharedInbox: Not(IsNull()) },
@@ -76,13 +76,13 @@ export class UserSuspendService {
], ],
select: ['followerSharedInbox', 'followeeSharedInbox'], select: ['followerSharedInbox', 'followeeSharedInbox'],
}); });
const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox); const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox);
for (const inbox of inboxes) { for (const inbox of inboxes) {
if (inbox != null && !queue.includes(inbox)) queue.push(inbox); if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
} }
for (const inbox of queue) { for (const inbox of queue) {
this.queueService.deliver(user as any, content, inbox, true); this.queueService.deliver(user as any, content, inbox, true);
} }

View File

@@ -21,7 +21,7 @@ export class VideoProcessingService {
@bindThis @bindThis
public async generateVideoThumbnail(source: string): Promise<IImage> { public async generateVideoThumbnail(source: string): Promise<IImage> {
const [dir, cleanup] = await createTempDir(); const [dir, cleanup] = await createTempDir();
try { try {
await new Promise((res, rej) => { await new Promise((res, rej) => {
FFmpeg({ FFmpeg({

View File

@@ -31,7 +31,7 @@ export class WebhookService implements OnApplicationShutdown {
}); });
this.webhooksFetched = true; this.webhooksFetched = true;
} }
return this.webhooks; return this.webhooks;
} }

View File

@@ -27,14 +27,14 @@ export class ApAudienceService {
public async parseAudience(actor: RemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise<AudienceInfo> { public async parseAudience(actor: RemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise<AudienceInfo> {
const toGroups = this.groupingAudience(getApIds(to), actor); const toGroups = this.groupingAudience(getApIds(to), actor);
const ccGroups = this.groupingAudience(getApIds(cc), actor); const ccGroups = this.groupingAudience(getApIds(cc), actor);
const others = unique(concat([toGroups.other, ccGroups.other])); const others = unique(concat([toGroups.other, ccGroups.other]));
const limit = promiseLimit<User | null>(2); const limit = promiseLimit<User | null>(2);
const mentionedUsers = (await Promise.all( const mentionedUsers = (await Promise.all(
others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))), others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))),
)).filter((x): x is User => x != null); )).filter((x): x is User => x != null);
if (toGroups.public.length > 0) { if (toGroups.public.length > 0) {
return { return {
visibility: 'public', visibility: 'public',
@@ -42,7 +42,7 @@ export class ApAudienceService {
visibleUsers: [], visibleUsers: [],
}; };
} }
if (ccGroups.public.length > 0) { if (ccGroups.public.length > 0) {
return { return {
visibility: 'home', visibility: 'home',
@@ -50,7 +50,7 @@ export class ApAudienceService {
visibleUsers: [], visibleUsers: [],
}; };
} }
if (toGroups.followers.length > 0) { if (toGroups.followers.length > 0) {
return { return {
visibility: 'followers', visibility: 'followers',
@@ -58,14 +58,14 @@ export class ApAudienceService {
visibleUsers: [], visibleUsers: [],
}; };
} }
return { return {
visibility: 'specified', visibility: 'specified',
mentionedUsers, mentionedUsers,
visibleUsers: mentionedUsers, visibleUsers: mentionedUsers,
}; };
} }
@bindThis @bindThis
private groupingAudience(ids: string[], actor: RemoteUser) { private groupingAudience(ids: string[], actor: RemoteUser) {
const groups = { const groups = {
@@ -73,7 +73,7 @@ export class ApAudienceService {
followers: [] as string[], followers: [] as string[],
other: [] as string[], other: [] as string[],
}; };
for (const id of ids) { for (const id of ids) {
if (this.isPublic(id)) { if (this.isPublic(id)) {
groups.public.push(id); groups.public.push(id);
@@ -83,12 +83,12 @@ export class ApAudienceService {
groups.other.push(id); groups.other.push(id);
} }
} }
groups.other = unique(groups.other); groups.other = unique(groups.other);
return groups; return groups;
} }
@bindThis @bindThis
private isPublic(id: string) { private isPublic(id: string) {
return [ return [
@@ -97,7 +97,7 @@ export class ApAudienceService {
'Public', 'Public',
].includes(id); ].includes(id);
} }
@bindThis @bindThis
private isFollowers(id: string, actor: RemoteUser) { private isFollowers(id: string, actor: RemoteUser) {
return ( return (

View File

@@ -1,4 +1,5 @@
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import escapeRegexp from 'escape-regexp';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
@@ -29,7 +30,7 @@ export type UriParseResult = {
}; };
@Injectable() @Injectable()
export class ApDbResolverService implements OnApplicationShutdown { export class ApDbResolverService {
private publicKeyCache: MemoryKVCache<UserPublickey | null>; private publicKeyCache: MemoryKVCache<UserPublickey | null>;
private publicKeyByUserIdCache: MemoryKVCache<UserPublickey | null>; private publicKeyByUserIdCache: MemoryKVCache<UserPublickey | null>;
@@ -55,18 +56,25 @@ export class ApDbResolverService implements OnApplicationShutdown {
@bindThis @bindThis
public parseUri(value: string | IObject): UriParseResult { public parseUri(value: string | IObject): UriParseResult {
const separator = '/'; const uri = getApId(value);
const uri = new URL(getApId(value)); // the host part of a URL is case insensitive, so use the 'i' flag.
if (uri.origin !== this.config.url) return { local: false, uri: uri.href }; const localRegex = new RegExp('^' + escapeRegexp(this.config.url) + '/(\\w+)/(\\w+)(?:\/(.+))?', 'i');
const matchLocal = uri.match(localRegex);
const [, type, id, ...rest] = uri.pathname.split(separator);
return { if (matchLocal) {
local: true, return {
type, local: true,
id, type: matchLocal[1],
rest: rest.length === 0 ? undefined : rest.join(separator), id: matchLocal[2],
}; rest: matchLocal[3],
};
} else {
return {
local: false,
uri,
};
}
} }
/** /**
@@ -121,7 +129,7 @@ export class ApDbResolverService implements OnApplicationShutdown {
const key = await this.userPublickeysRepository.findOneBy({ const key = await this.userPublickeysRepository.findOneBy({
keyId, keyId,
}); });
if (key == null) return null; if (key == null) return null;
return key; return key;
@@ -147,22 +155,11 @@ export class ApDbResolverService implements OnApplicationShutdown {
if (user == null) return null; if (user == null) return null;
const key = await this.publicKeyByUserIdCache.fetch(user.id, () => this.userPublickeysRepository.findOneBy({ userId: user.id }), v => v != null); const key = await this.publicKeyByUserIdCache.fetch(user.id, () => this.userPublickeysRepository.findOneBy({ userId: user.id }), v => v != null);
return { return {
user, user,
key, key,
}; };
} }
@bindThis
public dispose(): void {
this.publicKeyCache.dispose();
this.publicKeyByUserIdCache.dispose();
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
} }

View File

@@ -7,8 +7,6 @@ import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { IActivity } from '@/core/activitypub/type.js';
import { ThinUser } from '@/queue/types.js';
interface IRecipe { interface IRecipe {
type: string; type: string;
@@ -23,10 +21,10 @@ interface IDirectRecipe extends IRecipe {
to: RemoteUser; to: RemoteUser;
} }
const isFollowers = (recipe: IRecipe): recipe is IFollowersRecipe => const isFollowers = (recipe: any): recipe is IFollowersRecipe =>
recipe.type === 'Followers'; recipe.type === 'Followers';
const isDirect = (recipe: IRecipe): recipe is IDirectRecipe => const isDirect = (recipe: any): recipe is IDirectRecipe =>
recipe.type === 'Direct'; recipe.type === 'Direct';
@Injectable() @Injectable()
@@ -48,11 +46,11 @@ export class ApDeliverManagerService {
/** /**
* Deliver activity to followers * Deliver activity to followers
* @param actor
* @param activity Activity * @param activity Activity
* @param from Followee
*/ */
@bindThis @bindThis
public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: IActivity) { public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: any) {
const manager = new DeliverManager( const manager = new DeliverManager(
this.userEntityService, this.userEntityService,
this.followingsRepository, this.followingsRepository,
@@ -66,12 +64,11 @@ export class ApDeliverManagerService {
/** /**
* Deliver activity to user * Deliver activity to user
* @param actor
* @param activity Activity * @param activity Activity
* @param to Target user * @param to Target user
*/ */
@bindThis @bindThis
public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: IActivity, to: RemoteUser) { public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: any, to: RemoteUser) {
const manager = new DeliverManager( const manager = new DeliverManager(
this.userEntityService, this.userEntityService,
this.followingsRepository, this.followingsRepository,
@@ -84,28 +81,25 @@ export class ApDeliverManagerService {
} }
@bindThis @bindThis
public createDeliverManager(actor: { id: User['id']; host: null; }, activity: IActivity | null) { public createDeliverManager(actor: { id: User['id']; host: null; }, activity: any) {
return new DeliverManager( return new DeliverManager(
this.userEntityService, this.userEntityService,
this.followingsRepository, this.followingsRepository,
this.queueService, this.queueService,
actor, actor,
activity, activity,
); );
} }
} }
class DeliverManager { class DeliverManager {
private actor: ThinUser; private actor: { id: User['id']; host: null; };
private activity: IActivity | null; private activity: any;
private recipes: IRecipe[] = []; private recipes: IRecipe[] = [];
/** /**
* Constructor * Constructor
* @param userEntityService
* @param followingsRepository
* @param queueService
* @param actor Actor * @param actor Actor
* @param activity Activity to deliver * @param activity Activity to deliver
*/ */
@@ -115,15 +109,9 @@ class DeliverManager {
private queueService: QueueService, private queueService: QueueService,
actor: { id: User['id']; host: null; }, actor: { id: User['id']; host: null; },
activity: IActivity | null, activity: any,
) { ) {
// 型で弾いてはいるが一応ローカルユーザーかチェック this.actor = actor;
if (actor.host != null) throw new Error('actor.host must be null');
// パフォーマンス向上のためキューに突っ込むのはidのみに絞る
this.actor = {
id: actor.id,
};
this.activity = activity; this.activity = activity;
} }
@@ -167,8 +155,9 @@ class DeliverManager {
*/ */
@bindThis @bindThis
public async execute() { public async execute() {
if (!this.userEntityService.isLocalUser(this.actor)) return;
// The value flags whether it is shared or not. // The value flags whether it is shared or not.
// key: inbox URL, value: whether it is sharedInbox
const inboxes = new Map<string, boolean>(); const inboxes = new Map<string, boolean>();
/* /*
@@ -212,6 +201,9 @@ class DeliverManager {
.forEach(recipe => inboxes.set(recipe.to.inbox!, false)); .forEach(recipe => inboxes.set(recipe.to.inbox!, false));
// deliver // deliver
this.queueService.deliverMany(this.actor, this.activity, inboxes); for (const inbox of inboxes) {
// inbox[0]: inbox, inbox[1]: whether it is sharedInbox
this.queueService.deliver(this.actor, this.activity, inbox[0], inbox[1]);
}
} }
} }

View File

@@ -21,7 +21,7 @@ export class ApMfmService {
@bindThis @bindThis
public htmlToMfm(html: string, tag?: IObject | IObject[]) { public htmlToMfm(html: string, tag?: IObject | IObject[]) {
const hashtagNames = extractApHashtagObjects(tag).map(x => x.name).filter((x): x is string => x != null); const hashtagNames = extractApHashtagObjects(tag).map(x => x.name).filter((x): x is string => x != null);
return this.mfmService.fromHtml(html, hashtagNames); return this.mfmService.fromHtml(html, hashtagNames);
} }
@@ -29,5 +29,5 @@ export class ApMfmService {
public getNoteHtml(note: Note) { public getNoteHtml(note: Note) {
if (!note.text) return ''; if (!note.text) return '';
return this.mfmService.toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)); return this.mfmService.toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers));
} }
} }

View File

@@ -10,10 +10,9 @@ import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
import { DriveService } from '@/core/DriveService.js'; import { DriveService } from '@/core/DriveService.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { checkHttps } from '@/misc/check-https.js';
import { ApResolverService } from '../ApResolverService.js'; import { ApResolverService } from '../ApResolverService.js';
import { ApLoggerService } from '../ApLoggerService.js'; import { ApLoggerService } from '../ApLoggerService.js';
import type { IObject } from '../type.js'; import { checkHttps } from '@/misc/check-https.js';
@Injectable() @Injectable()
export class ApImageService { export class ApImageService {
@@ -33,27 +32,23 @@ export class ApImageService {
) { ) {
this.logger = this.apLoggerService.logger; this.logger = this.apLoggerService.logger;
} }
/** /**
* Imageを作成します。 * Imageを作成します。
*/ */
@bindThis @bindThis
public async createImage(actor: RemoteUser, value: string | IObject): Promise<DriveFile> { public async createImage(actor: RemoteUser, value: any): Promise<DriveFile> {
// 投稿者が凍結されていたらスキップ // 投稿者が凍結されていたらスキップ
if (actor.isSuspended) { if (actor.isSuspended) {
throw new Error('actor has been suspended'); throw new Error('actor has been suspended');
} }
const image = await this.apResolverService.createResolver().resolve(value); const image = await this.apResolverService.createResolver().resolve(value) as any;
if (image.url == null) { if (image.url == null) {
throw new Error('invalid image: url not privided'); throw new Error('invalid image: url not privided');
} }
if (typeof image.url !== 'string') {
throw new Error('invalid image: unexpected type of url: ' + JSON.stringify(image.url, null, 2));
}
if (!checkHttps(image.url)) { if (!checkHttps(image.url)) {
throw new Error('invalid image: unexpected schema of url: ' + image.url); throw new Error('invalid image: unexpected schema of url: ' + image.url);
} }
@@ -62,19 +57,29 @@ export class ApImageService {
const instance = await this.metaService.fetch(); const instance = await this.metaService.fetch();
const file = await this.driveService.uploadFromUrl({ let file = await this.driveService.uploadFromUrl({
url: image.url, url: image.url,
user: actor, user: actor,
uri: image.url, uri: image.url,
sensitive: image.sensitive, sensitive: image.sensitive,
isLink: !instance.cacheRemoteFiles, isLink: !instance.cacheRemoteFiles,
comment: truncate(image.name ?? undefined, DB_MAX_IMAGE_COMMENT_LENGTH), comment: truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH),
}); });
if (!file.isLink || file.url === image.url) return file;
// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、URLを更新する if (file.isLink) {
await this.driveFilesRepository.update({ id: file.id }, { url: image.url, uri: image.url }); // URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、
return await this.driveFilesRepository.findOneByOrFail({ id: file.id }); // URLを更新する
if (file.url !== image.url) {
await this.driveFilesRepository.update({ id: file.id }, {
url: image.url,
uri: image.url,
});
file = await this.driveFilesRepository.findOneByOrFail({ id: file.id });
}
}
return file;
} }
/** /**
@@ -84,7 +89,7 @@ export class ApImageService {
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
*/ */
@bindThis @bindThis
public async resolveImage(actor: RemoteUser, value: string | IObject): Promise<DriveFile> { public async resolveImage(actor: RemoteUser, value: any): Promise<DriveFile> {
// TODO // TODO
// リモートサーバーからフェッチしてきて登録 // リモートサーバーからフェッチしてきて登録

View File

@@ -22,17 +22,17 @@ export class ApMentionService {
} }
@bindThis @bindThis
public async extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver): Promise<User[]> { public async extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver) {
const hrefs = unique(this.extractApMentionObjects(tags).map(x => x.href)); const hrefs = unique(this.extractApMentionObjects(tags).map(x => x.href as string));
const limit = promiseLimit<User | null>(2); const limit = promiseLimit<User | null>(2);
const mentionedUsers = (await Promise.all( const mentionedUsers = (await Promise.all(
hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))), hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))),
)).filter((x): x is User => x != null); )).filter((x): x is User => x != null);
return mentionedUsers; return mentionedUsers;
} }
@bindThis @bindThis
public extractApMentionObjects(tags: IObject | IObject[] | null | undefined): IApMention[] { public extractApMentionObjects(tags: IObject | IObject[] | null | undefined): IApMention[] {
if (tags == null) return []; if (tags == null) return [];

View File

@@ -20,6 +20,7 @@ import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { checkHttps } from '@/misc/check-https.js'; import { checkHttps } from '@/misc/check-https.js';
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js'; import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
import { ApLoggerService } from '../ApLoggerService.js'; import { ApLoggerService } from '../ApLoggerService.js';
import { ApMfmService } from '../ApMfmService.js'; import { ApMfmService } from '../ApMfmService.js';
import { ApDbResolverService } from '../ApDbResolverService.js'; import { ApDbResolverService } from '../ApDbResolverService.js';
@@ -54,7 +55,7 @@ export class ApNoteService {
// 循環参照のため / for circular dependency // 循環参照のため / for circular dependency
@Inject(forwardRef(() => ApPersonService)) @Inject(forwardRef(() => ApPersonService))
private apPersonService: ApPersonService, private apPersonService: ApPersonService,
private utilityService: UtilityService, private utilityService: UtilityService,
private apAudienceService: ApAudienceService, private apAudienceService: ApAudienceService,
private apMentionService: ApMentionService, private apMentionService: ApMentionService,
@@ -71,13 +72,17 @@ export class ApNoteService {
} }
@bindThis @bindThis
public validateNote(object: IObject, uri: string): Error | null { public validateNote(object: IObject, uri: string) {
const expectHost = this.utilityService.extractDbHost(uri); const expectHost = this.utilityService.extractDbHost(uri);
if (object == null) {
return new Error('invalid Note: object is null');
}
if (!validPost.includes(getApType(object))) { if (!validPost.includes(getApType(object))) {
return new Error(`invalid Note: invalid object type ${getApType(object)}`); return new Error(`invalid Note: invalid object type ${getApType(object)}`);
} }
if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) { if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) {
return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`); return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`);
} }
@@ -86,10 +91,10 @@ export class ApNoteService {
if (object.attributedTo && actualHost !== expectHost) { if (object.attributedTo && actualHost !== expectHost) {
return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`); return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
} }
return null; return null;
} }
/** /**
* Noteをフェッチします。 * Noteをフェッチします。
* *
@@ -99,30 +104,31 @@ export class ApNoteService {
public async fetchNote(object: string | IObject): Promise<Note | null> { public async fetchNote(object: string | IObject): Promise<Note | null> {
return await this.apDbResolverService.getNoteFromApId(object); return await this.apDbResolverService.getNoteFromApId(object);
} }
/** /**
* Noteを作成します。 * Noteを作成します。
*/ */
@bindThis @bindThis
public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<Note | null> { public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<Note | null> {
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver(); if (resolver == null) resolver = this.apResolverService.createResolver();
const object = await resolver.resolve(value); const object = await resolver.resolve(value);
const entryUri = getApId(value); const entryUri = getApId(value);
const err = this.validateNote(object, entryUri); const err = this.validateNote(object, entryUri);
if (err) { if (err) {
this.logger.error(err.message, { this.logger.error(`${err.message}`, {
resolver: { history: resolver.getHistory() }, resolver: {
value, history: resolver.getHistory(),
object, },
value: value,
object: object,
}); });
throw new Error('invalid note'); throw new Error('invalid note');
} }
const note = object as IPost; const note = object as IPost;
this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
if (note.id && !checkHttps(note.id)) { if (note.id && !checkHttps(note.id)) {
@@ -134,25 +140,21 @@ export class ApNoteService {
if (url && !checkHttps(url)) { if (url && !checkHttps(url)) {
throw new Error('unexpected shcema of note url: ' + url); throw new Error('unexpected shcema of note url: ' + url);
} }
this.logger.info(`Creating the Note: ${note.id}`); this.logger.info(`Creating the Note: ${note.id}`);
// 投稿者をフェッチ // 投稿者をフェッチ
if (note.attributedTo == null) { const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo!), resolver) as RemoteUser;
throw new Error('invalid note.attributedTo: ' + note.attributedTo);
}
const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as RemoteUser;
// 投稿者が凍結されていたらスキップ // 投稿者が凍結されていたらスキップ
if (actor.isSuspended) { if (actor.isSuspended) {
throw new Error('actor has been suspended'); throw new Error('actor has been suspended');
} }
const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver); const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver);
let visibility = noteAudience.visibility; let visibility = noteAudience.visibility;
const visibleUsers = noteAudience.visibleUsers; const visibleUsers = noteAudience.visibleUsers;
// Audience (to, cc) が指定されてなかった場合 // Audience (to, cc) が指定されてなかった場合
if (visibility === 'specified' && visibleUsers.length === 0) { if (visibility === 'specified' && visibleUsers.length === 0) {
if (typeof value === 'string') { // 入力がstringならばresolverでGETが発生している if (typeof value === 'string') { // 入力がstringならばresolverでGETが発生している
@@ -160,71 +162,81 @@ export class ApNoteService {
visibility = 'public'; visibility = 'public';
} }
} }
const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
const apHashtags = extractApHashtags(note.tag); const apHashtags = await extractApHashtags(note.tag);
// 添付ファイル // 添付ファイル
// TODO: attachmentは必ずしもImageではない // TODO: attachmentは必ずしもImageではない
// TODO: attachmentは必ずしも配列ではない // TODO: attachmentは必ずしも配列ではない
const limit = promiseLimit<DriveFile>(2); // Noteがsensitiveなら添付もsensitiveにする
const files = (await Promise.all(toArray(note.attachment).map(attach => ( const limit = promiseLimit(2);
limit(() => this.apImageService.resolveImage(actor, {
...attach, note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : [];
sensitive: note.sensitive, // Noteがsensitiveなら添付もsensitiveにする const files = note.attachment
})) .map(attach => attach.sensitive = note.sensitive)
)))); ? (await Promise.all(note.attachment.map(x => limit(() => this.apImageService.resolveImage(actor, x)) as Promise<DriveFile>)))
.filter(image => image != null)
: [];
// リプライ // リプライ
const reply: Note | null = note.inReplyTo const reply: Note | null = note.inReplyTo
? await this.resolveNote(note.inReplyTo, resolver) ? await this.resolveNote(note.inReplyTo, resolver).then(x => {
.then(x => { if (x == null) {
if (x == null) { this.logger.warn('Specified inReplyTo, but not found');
this.logger.warn('Specified inReplyTo, but not found'); throw new Error('inReplyTo not found');
throw new Error('inReplyTo not found'); } else {
}
return x; return x;
}) }
.catch(async err => { }).catch(async err => {
this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`); this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`);
throw err; throw err;
}) })
: null; : null;
// 引用 // 引用
let quote: Note | undefined | null = null; let quote: Note | undefined | null;
if (note._misskey_quote || note.quoteUrl) { if (note._misskey_quote || note.quoteUrl) {
const tryResolveNote = async (uri: string): Promise< const tryResolveNote = async (uri: string): Promise<{
| { status: 'ok'; res: Note } status: 'ok';
| { status: 'permerror' | 'temperror' } res: Note | null;
> => { } | {
if (!uri.match(/^https?:/)) return { status: 'permerror' }; status: 'permerror' | 'temperror';
}> => {
if (typeof uri !== 'string' || !uri.match(/^https?:/)) return { status: 'permerror' };
try { try {
const res = await this.resolveNote(uri); const res = await this.resolveNote(uri);
if (res == null) return { status: 'permerror' }; if (res) {
return { status: 'ok', res }; return {
status: 'ok',
res,
};
} else {
return {
status: 'permerror',
};
}
} catch (e) { } catch (e) {
return { return {
status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror', status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror',
}; };
} }
}; };
const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string')); const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string'));
const results = await Promise.all(uris.map(tryResolveNote)); const results = await Promise.all(uris.map(uri => tryResolveNote(uri)));
quote = results.filter((x): x is { status: 'ok', res: Note } => x.status === 'ok').map(x => x.res).at(0); quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x);
if (!quote) { if (!quote) {
if (results.some(x => x.status === 'temperror')) { if (results.some(x => x.status === 'temperror')) {
throw new Error('quote resolve failed'); throw new Error('quote resolve failed');
} }
} }
} }
const cw = note.summary === '' ? null : note.summary; const cw = note.summary === '' ? null : note.summary;
// テキストのパース // テキストのパース
let text: string | null = null; let text: string | null = null;
if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') { if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') {
@@ -234,38 +246,38 @@ export class ApNoteService {
} else if (typeof note.content === 'string') { } else if (typeof note.content === 'string') {
text = this.apMfmService.htmlToMfm(note.content, note.tag); text = this.apMfmService.htmlToMfm(note.content, note.tag);
} }
// vote // vote
if (reply && reply.hasPoll) { if (reply && reply.hasPoll) {
const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id }); const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id });
const tryCreateVote = async (name: string, index: number): Promise<null> => { const tryCreateVote = async (name: string, index: number): Promise<null> => {
if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) { if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) {
this.logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); this.logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
} else if (index >= 0) { } else if (index >= 0) {
this.logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); this.logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
await this.pollService.vote(actor, reply, index); await this.pollService.vote(actor, reply, index);
// リモートフォロワーにUpdate配信 // リモートフォロワーにUpdate配信
this.pollService.deliverQuestionUpdate(reply.id); this.pollService.deliverQuestionUpdate(reply.id);
} }
return null; return null;
}; };
if (note.name) { if (note.name) {
return await tryCreateVote(note.name, poll.choices.findIndex(x => x === note.name)); return await tryCreateVote(note.name, poll.choices.findIndex(x => x === note.name));
} }
} }
const emojis = await this.extractEmojis(note.tag ?? [], actor.host).catch(e => { const emojis = await this.extractEmojis(note.tag ?? [], actor.host).catch(e => {
this.logger.info(`extractEmojis: ${e}`); this.logger.info(`extractEmojis: ${e}`);
return []; return [] as Emoji[];
}); });
const apEmojis = emojis.map(emoji => emoji.name); const apEmojis = emojis.map(emoji => emoji.name);
const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined); const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined);
return await this.noteCreateService.create(actor, { return await this.noteCreateService.create(actor, {
createdAt: note.published ? new Date(note.published) : null, createdAt: note.published ? new Date(note.published) : null,
files, files,
@@ -285,7 +297,7 @@ export class ApNoteService {
url: url, url: url,
}, silent); }, silent);
} }
/** /**
* Noteを解決します。 * Noteを解決します。
* *
@@ -296,25 +308,26 @@ export class ApNoteService {
public async resolveNote(value: string | IObject, resolver?: Resolver): Promise<Note | null> { public async resolveNote(value: string | IObject, resolver?: Resolver): Promise<Note | null> {
const uri = typeof value === 'string' ? value : value.id; const uri = typeof value === 'string' ? value : value.id;
if (uri == null) throw new Error('missing uri'); if (uri == null) throw new Error('missing uri');
// ブロックしてたら中断 // ブロックしてたら中断
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) { if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) throw new StatusError('blocked host', 451);
throw new StatusError('blocked host', 451);
}
const unlock = await this.appLockService.getApLock(uri); const unlock = await this.appLockService.getApLock(uri);
try { try {
//#region このサーバーに既に登録されていたらそれを返す //#region このサーバーに既に登録されていたらそれを返す
const exist = await this.fetchNote(uri); const exist = await this.fetchNote(uri);
if (exist) return exist;
if (exist) {
return exist;
}
//#endregion //#endregion
if (uri.startsWith(this.config.url)) { if (uri.startsWith(this.config.url)) {
throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note'); throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note');
} }
// リモートサーバーからフェッチしてきて登録 // リモートサーバーからフェッチしてきて登録
// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにートが生成されるが // ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにートが生成されるが
// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。 // 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
@@ -323,61 +336,63 @@ export class ApNoteService {
unlock(); unlock();
} }
} }
@bindThis @bindThis
public async extractEmojis(tags: IObject | IObject[], host: string): Promise<Emoji[]> { public async extractEmojis(tags: IObject | IObject[], host: string): Promise<Emoji[]> {
// eslint-disable-next-line no-param-reassign
host = this.utilityService.toPuny(host); host = this.utilityService.toPuny(host);
if (!tags) return [];
const eomjiTags = toArray(tags).filter(isEmoji); const eomjiTags = toArray(tags).filter(isEmoji);
const existingEmojis = await this.emojisRepository.findBy({ const existingEmojis = await this.emojisRepository.findBy({
host, host,
name: In(eomjiTags.map(tag => tag.name.replaceAll(':', ''))), name: In(eomjiTags.map(tag => tag.name!.replaceAll(':', ''))),
}); });
return await Promise.all(eomjiTags.map(async tag => { return await Promise.all(eomjiTags.map(async tag => {
const name = tag.name.replaceAll(':', ''); const name = tag.name!.replaceAll(':', '');
tag.icon = toSingle(tag.icon); tag.icon = toSingle(tag.icon);
const exists = existingEmojis.find(x => x.name === name); const exists = existingEmojis.find(x => x.name === name);
if (exists) { if (exists) {
if ((exists.updatedAt == null) if ((tag.updated != null && exists.updatedAt == null)
|| (tag.id != null && exists.uri == null) || (tag.id != null && exists.uri == null)
|| (new Date(tag.updated) > exists.updatedAt) || (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt)
|| (tag.icon.url !== exists.originalUrl) || (tag.icon!.url !== exists.originalUrl)
) { ) {
await this.emojisRepository.update({ await this.emojisRepository.update({
host, host,
name, name,
}, { }, {
uri: tag.id, uri: tag.id,
originalUrl: tag.icon.url, originalUrl: tag.icon!.url,
publicUrl: tag.icon.url, publicUrl: tag.icon!.url,
updatedAt: new Date(), updatedAt: new Date(),
}); });
const emoji = await this.emojisRepository.findOneBy({ host, name }); return await this.emojisRepository.findOneBy({
if (emoji == null) throw new Error('emoji update failed'); host,
return emoji; name,
}) as Emoji;
} }
return exists; return exists;
} }
this.logger.info(`register emoji host=${host}, name=${name}`); this.logger.info(`register emoji host=${host}, name=${name}`);
return await this.emojisRepository.insert({ return await this.emojisRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
host, host,
name, name,
uri: tag.id, uri: tag.id,
originalUrl: tag.icon.url, originalUrl: tag.icon!.url,
publicUrl: tag.icon.url, publicUrl: tag.icon!.url,
updatedAt: new Date(), updatedAt: new Date(),
aliases: [], aliases: [],
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); } as Partial<Emoji>).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
})); }));
} }
} }

View File

@@ -3,7 +3,7 @@ import promiseLimit from 'promise-limit';
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
import { ModuleRef } from '@nestjs/core'; import { ModuleRef } from '@nestjs/core';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; import type { BlockingsRepository, MutingsRepository, FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { LocalUser, RemoteUser } from '@/models/entities/User.js'; import type { LocalUser, RemoteUser } from '@/models/entities/User.js';
import { User } from '@/models/entities/User.js'; import { User } from '@/models/entities/User.js';
@@ -15,6 +15,7 @@ import type Logger from '@/logger.js';
import type { Note } from '@/models/entities/Note.js'; import type { Note } from '@/models/entities/Note.js';
import type { IdService } from '@/core/IdService.js'; import type { IdService } from '@/core/IdService.js';
import type { MfmService } from '@/core/MfmService.js'; import type { MfmService } from '@/core/MfmService.js';
import type { Emoji } from '@/models/entities/Emoji.js';
import { toArray } from '@/misc/prelude/array.js'; import { toArray } from '@/misc/prelude/array.js';
import type { GlobalEventService } from '@/core/GlobalEventService.js'; import type { GlobalEventService } from '@/core/GlobalEventService.js';
import type { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import type { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
@@ -47,8 +48,6 @@ import type { IActor, IObject } from '../type.js';
const nameLength = 128; const nameLength = 128;
const summaryLength = 2048; const summaryLength = 2048;
type Field = Record<'name' | 'value', string>;
@Injectable() @Injectable()
export class ApPersonService implements OnModuleInit { export class ApPersonService implements OnModuleInit {
private utilityService: UtilityService; private utilityService: UtilityService;
@@ -95,10 +94,28 @@ export class ApPersonService implements OnModuleInit {
@Inject(DI.followingsRepository) @Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository, private followingsRepository: FollowingsRepository,
//private utilityService: UtilityService,
//private userEntityService: UserEntityService,
//private idService: IdService,
//private globalEventService: GlobalEventService,
//private metaService: MetaService,
//private federatedInstanceService: FederatedInstanceService,
//private fetchInstanceMetadataService: FetchInstanceMetadataService,
//private cacheService: CacheService,
//private apResolverService: ApResolverService,
//private apNoteService: ApNoteService,
//private apImageService: ApImageService,
//private apMfmService: ApMfmService,
//private mfmService: MfmService,
//private hashtagService: HashtagService,
//private usersChart: UsersChart,
//private instanceChart: InstanceChart,
//private apLoggerService: ApLoggerService,
) { ) {
} }
onModuleInit(): void { onModuleInit() {
this.utilityService = this.moduleRef.get('UtilityService'); this.utilityService = this.moduleRef.get('UtilityService');
this.userEntityService = this.moduleRef.get('UserEntityService'); this.userEntityService = this.moduleRef.get('UserEntityService');
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService'); this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
@@ -136,6 +153,10 @@ export class ApPersonService implements OnModuleInit {
private validateActor(x: IObject, uri: string): IActor { private validateActor(x: IObject, uri: string): IActor {
const expectHost = this.punyHost(uri); const expectHost = this.punyHost(uri);
if (x == null) {
throw new Error('invalid Actor: object is null');
}
if (!isActor(x)) { if (!isActor(x)) {
throw new Error(`invalid Actor type '${x.type}'`); throw new Error(`invalid Actor type '${x.type}'`);
} }
@@ -197,19 +218,21 @@ export class ApPersonService implements OnModuleInit {
*/ */
@bindThis @bindThis
public async fetchPerson(uri: string): Promise<LocalUser | RemoteUser | null> { public async fetchPerson(uri: string): Promise<LocalUser | RemoteUser | null> {
const cached = this.cacheService.uriPersonCache.get(uri) as LocalUser | RemoteUser | null | undefined; if (typeof uri !== 'string') throw new Error('uri is not string');
const cached = this.cacheService.uriPersonCache.get(uri) as LocalUser | RemoteUser | null;
if (cached) return cached; if (cached) return cached;
// URIがこのサーバーを指しているならデータベースからフェッチ // URIがこのサーバーを指しているならデータベースからフェッチ
if (uri.startsWith(`${this.config.url}/`)) { if (uri.startsWith(`${this.config.url}/`)) {
const id = uri.split('/').pop(); const id = uri.split('/').pop();
const u = await this.usersRepository.findOneBy({ id }) as LocalUser | null; const u = await this.usersRepository.findOneBy({ id }) as LocalUser;
if (u) this.cacheService.uriPersonCache.set(uri, u); if (u) this.cacheService.uriPersonCache.set(uri, u);
return u; return u;
} }
//#region このサーバーに既に登録されていたらそれを返す //#region このサーバーに既に登録されていたらそれを返す
const exist = await this.usersRepository.findOneBy({ uri }) as LocalUser | RemoteUser | null; const exist = await this.usersRepository.findOneBy({ uri }) as LocalUser | RemoteUser;
if (exist) { if (exist) {
this.cacheService.uriPersonCache.set(uri, exist); this.cacheService.uriPersonCache.set(uri, exist);
@@ -231,11 +254,9 @@ export class ApPersonService implements OnModuleInit {
throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user'); throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
} }
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver(); if (resolver == null) resolver = this.apResolverService.createResolver();
const object = await resolver.resolve(uri); const object = await resolver.resolve(uri) as any;
if (object.id == null) throw new Error('invalid object.id: ' + object.id);
const person = this.validateActor(object, uri); const person = this.validateActor(object, uri);
@@ -243,9 +264,9 @@ export class ApPersonService implements OnModuleInit {
const host = this.punyHost(object.id); const host = this.punyHost(object.id);
const fields = this.analyzeAttachments(person.attachment ?? []); const { fields } = this.analyzeAttachments(person.attachment ?? []);
const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32); const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32);
const isBot = getApType(object) === 'Service'; const isBot = getApType(object) === 'Service';
@@ -258,7 +279,7 @@ export class ApPersonService implements OnModuleInit {
} }
// Create user // Create user
let user: RemoteUser | null = null; let user: RemoteUser;
try { try {
// Start transaction // Start transaction
await this.db.transaction(async transactionalEntityManager => { await this.db.transaction(async transactionalEntityManager => {
@@ -269,16 +290,16 @@ export class ApPersonService implements OnModuleInit {
createdAt: new Date(), createdAt: new Date(),
lastFetchedAt: new Date(), lastFetchedAt: new Date(),
name: truncate(person.name, nameLength), name: truncate(person.name, nameLength),
isLocked: person.manuallyApprovesFollowers, isLocked: !!person.manuallyApprovesFollowers,
movedToUri: person.movedTo, movedToUri: person.movedTo,
movedAt: person.movedTo ? new Date() : null, movedAt: person.movedTo ? new Date() : null,
alsoKnownAs: person.alsoKnownAs, alsoKnownAs: person.alsoKnownAs,
isExplorable: person.discoverable, isExplorable: !!person.discoverable,
username: person.preferredUsername, username: person.preferredUsername,
usernameLower: person.preferredUsername?.toLowerCase(), usernameLower: person.preferredUsername!.toLowerCase(),
host, host,
inbox: person.inbox, inbox: person.inbox,
sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox, sharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined),
followersUri: person.followers ? getApId(person.followers) : undefined, followersUri: person.followers ? getApId(person.followers) : undefined,
featured: person.featured ? getApId(person.featured) : undefined, featured: person.featured ? getApId(person.featured) : undefined,
uri: person.id, uri: person.id,
@@ -290,9 +311,9 @@ export class ApPersonService implements OnModuleInit {
await transactionalEntityManager.save(new UserProfile({ await transactionalEntityManager.save(new UserProfile({
userId: user.id, userId: user.id,
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
url, url: url,
fields, fields,
birthday: bday?.[0] ?? null, birthday: bday ? bday[0] : null,
location: person['vcard:Address'] ?? null, location: person['vcard:Address'] ?? null,
userHost: host, userHost: host,
})); }));
@@ -309,18 +330,21 @@ export class ApPersonService implements OnModuleInit {
// duplicate key error // duplicate key error
if (isDuplicateKeyValueError(e)) { if (isDuplicateKeyValueError(e)) {
// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応 // /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
const u = await this.usersRepository.findOneBy({ uri: person.id }); const u = await this.usersRepository.findOneBy({
if (u == null) throw new Error('already registered'); uri: person.id,
});
user = u as RemoteUser; if (u) {
user = u as RemoteUser;
} else {
throw new Error('already registered');
}
} else { } else {
this.logger.error(e instanceof Error ? e : new Error(e as string)); this.logger.error(e instanceof Error ? e : new Error(e as string));
throw e; throw e;
} }
} }
if (user == null) throw new Error('failed to create user: user is null');
// Register host // Register host
this.federatedInstanceService.fetch(host).then(async i => { this.federatedInstanceService.fetch(host).then(async i => {
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1); this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
@@ -330,26 +354,29 @@ export class ApPersonService implements OnModuleInit {
} }
}); });
this.usersChart.update(user, true); this.usersChart.update(user!, true);
// ハッシュタグ更新 // ハッシュタグ更新
this.hashtagService.updateUsertags(user, tags); this.hashtagService.updateUsertags(user!, tags);
//#region アバターとヘッダー画像をフェッチ //#region アバターとヘッダー画像をフェッチ
const [avatar, banner] = await Promise.all([person.icon, person.image].map(img => { const [avatar, banner] = await Promise.all([
if (img == null) return null; person.icon,
if (user == null) throw new Error('failed to create user: user is null'); person.image,
return this.apImageService.resolveImage(user, img).catch(() => null); ].map(img =>
})); img == null
? Promise.resolve(null)
: this.apImageService.resolveImage(user!, img).catch(() => null),
));
const avatarId = avatar?.id ?? null; const avatarId = avatar ? avatar.id : null;
const bannerId = banner?.id ?? null; const bannerId = banner ? banner.id : null;
const avatarUrl = avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null; const avatarUrl = avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null;
const bannerUrl = banner ? this.driveFileEntityService.getPublicUrl(banner) : null; const bannerUrl = banner ? this.driveFileEntityService.getPublicUrl(banner) : null;
const avatarBlurhash = avatar?.blurhash ?? null; const avatarBlurhash = avatar ? avatar.blurhash : null;
const bannerBlurhash = banner?.blurhash ?? null; const bannerBlurhash = banner ? banner.blurhash : null;
await this.usersRepository.update(user.id, { await this.usersRepository.update(user!.id, {
avatarId, avatarId,
bannerId, bannerId,
avatarUrl, avatarUrl,
@@ -358,35 +385,37 @@ export class ApPersonService implements OnModuleInit {
bannerBlurhash, bannerBlurhash,
}); });
user.avatarId = avatarId; user!.avatarId = avatarId;
user.bannerId = bannerId; user!.bannerId = bannerId;
user.avatarUrl = avatarUrl; user!.avatarUrl = avatarUrl;
user.bannerUrl = bannerUrl; user!.bannerUrl = bannerUrl;
user.avatarBlurhash = avatarBlurhash; user!.avatarBlurhash = avatarBlurhash;
user.bannerBlurhash = bannerBlurhash; user!.bannerBlurhash = bannerBlurhash;
//#endregion //#endregion
//#region カスタム絵文字取得 //#region カスタム絵文字取得
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => { const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => {
this.logger.info(`extractEmojis: ${err}`); this.logger.info(`extractEmojis: ${err}`);
return []; return [] as Emoji[];
}); });
const emojiNames = emojis.map(emoji => emoji.name); const emojiNames = emojis.map(emoji => emoji.name);
await this.usersRepository.update(user.id, { emojis: emojiNames }); await this.usersRepository.update(user!.id, {
emojis: emojiNames,
});
//#endregion //#endregion
await this.updateFeatured(user.id, resolver).catch(err => this.logger.error(err)); await this.updateFeatured(user!.id, resolver).catch(err => this.logger.error(err));
return user; return user!;
} }
/** /**
* Personの情報を更新します。 * Personの情報を更新します。
* Misskeyに対象のPersonが登録されていなければ無視します。 * Misskeyに対象のPersonが登録されていなければ無視します。
* もしアカウントの移行が確認された場合、アカウント移行処理を行います。 * もしアカウントの移行が確認された場合、アカウント移行処理を行います。
* *
* @param uri URI of Person * @param uri URI of Person
* @param resolver Resolver * @param resolver Resolver
* @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します) * @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します)
@@ -397,14 +426,18 @@ export class ApPersonService implements OnModuleInit {
if (typeof uri !== 'string') throw new Error('uri is not string'); if (typeof uri !== 'string') throw new Error('uri is not string');
// URIがこのサーバーを指しているならスキップ // URIがこのサーバーを指しているならスキップ
if (uri.startsWith(`${this.config.url}/`)) return; if (uri.startsWith(`${this.config.url}/`)) {
return;
}
//#region このサーバーに既に登録されているか //#region このサーバーに既に登録されているか
const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser | null; const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser | null;
if (exist === null) return;
if (exist === null) {
return;
}
//#endregion //#endregion
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver(); if (resolver == null) resolver = this.apResolverService.createResolver();
const object = hint ?? await resolver.resolve(uri); const object = hint ?? await resolver.resolve(uri);
@@ -414,22 +447,26 @@ export class ApPersonService implements OnModuleInit {
this.logger.info(`Updating the Person: ${person.id}`); this.logger.info(`Updating the Person: ${person.id}`);
// アバターとヘッダー画像をフェッチ // アバターとヘッダー画像をフェッチ
const [avatar, banner] = await Promise.all([person.icon, person.image].map(img => { const [avatar, banner] = await Promise.all([
if (img == null) return null; person.icon,
return this.apImageService.resolveImage(exist, img).catch(() => null); person.image,
})); ].map(img =>
img == null
? Promise.resolve(null)
: this.apImageService.resolveImage(exist, img).catch(() => null),
));
// カスタム絵文字取得 // カスタム絵文字取得
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], exist.host).catch(e => { const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], exist.host).catch(e => {
this.logger.info(`extractEmojis: ${e}`); this.logger.info(`extractEmojis: ${e}`);
return []; return [] as Emoji[];
}); });
const emojiNames = emojis.map(emoji => emoji.name); const emojiNames = emojis.map(emoji => emoji.name);
const fields = this.analyzeAttachments(person.attachment ?? []); const { fields } = this.analyzeAttachments(person.attachment ?? []);
const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32); const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32);
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
@@ -442,7 +479,7 @@ export class ApPersonService implements OnModuleInit {
const updates = { const updates = {
lastFetchedAt: new Date(), lastFetchedAt: new Date(),
inbox: person.inbox, inbox: person.inbox,
sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox, sharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined),
followersUri: person.followers ? getApId(person.followers) : undefined, followersUri: person.followers ? getApId(person.followers) : undefined,
featured: person.featured, featured: person.featured,
emojis: emojiNames, emojis: emojiNames,
@@ -450,29 +487,18 @@ export class ApPersonService implements OnModuleInit {
tags, tags,
isBot: getApType(object) === 'Service', isBot: getApType(object) === 'Service',
isCat: (person as any).isCat === true, isCat: (person as any).isCat === true,
isLocked: person.manuallyApprovesFollowers, isLocked: !!person.manuallyApprovesFollowers,
movedToUri: person.movedTo ?? null, movedToUri: person.movedTo ?? null,
alsoKnownAs: person.alsoKnownAs ?? null, alsoKnownAs: person.alsoKnownAs ?? null,
isExplorable: person.discoverable, isExplorable: !!person.discoverable,
} as Partial<RemoteUser> & Pick<RemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>; } as Partial<RemoteUser> & Pick<RemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;
const moving = ((): boolean => { const moving =
// 移行先がない→ある // 移行先がない→ある
if ( (!exist.movedToUri && updates.movedToUri) ||
exist.movedToUri === null &&
updates.movedToUri
) return true;
// 移行先がある→別のもの // 移行先がある→別のもの
if ( (exist.movedToUri !== updates.movedToUri && exist.movedToUri && updates.movedToUri);
exist.movedToUri !== null &&
updates.movedToUri !== null &&
exist.movedToUri !== updates.movedToUri
) return true;
// 移行先がある→ない、ない→ないは無視 // 移行先がある→ない、ない→ないは無視
return false;
})();
if (moving) updates.movedAt = new Date(); if (moving) updates.movedAt = new Date();
@@ -499,10 +525,10 @@ export class ApPersonService implements OnModuleInit {
} }
await this.userProfilesRepository.update({ userId: exist.id }, { await this.userProfilesRepository.update({ userId: exist.id }, {
url, url: url,
fields, fields,
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
birthday: bday?.[0] ?? null, birthday: bday ? bday[0] : null,
location: person['vcard:Address'] ?? null, location: person['vcard:Address'] ?? null,
}); });
@@ -512,10 +538,11 @@ export class ApPersonService implements OnModuleInit {
this.hashtagService.updateUsertags(exist, tags); this.hashtagService.updateUsertags(exist, tags);
// 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする // 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする
await this.followingsRepository.update( await this.followingsRepository.update({
{ followerId: exist.id }, followerId: exist.id,
{ followerSharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox }, }, {
); followerSharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined),
});
await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err)); await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err));
@@ -553,22 +580,27 @@ export class ApPersonService implements OnModuleInit {
*/ */
@bindThis @bindThis
public async resolvePerson(uri: string, resolver?: Resolver): Promise<LocalUser | RemoteUser> { public async resolvePerson(uri: string, resolver?: Resolver): Promise<LocalUser | RemoteUser> {
if (typeof uri !== 'string') throw new Error('uri is not string');
//#region このサーバーに既に登録されていたらそれを返す //#region このサーバーに既に登録されていたらそれを返す
const exist = await this.fetchPerson(uri); const exist = await this.fetchPerson(uri);
if (exist) return exist;
if (exist) {
return exist;
}
//#endregion //#endregion
// リモートサーバーからフェッチしてきて登録 // リモートサーバーからフェッチしてきて登録
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver(); if (resolver == null) resolver = this.apResolverService.createResolver();
return await this.createPerson(uri, resolver); return await this.createPerson(uri, resolver);
} }
@bindThis @bindThis
// TODO: `attachments`が`IObject`だった場合、返り値が`[]`になるようだが構わないのか? public analyzeAttachments(attachments: IObject | IObject[] | undefined) {
public analyzeAttachments(attachments: IObject | IObject[] | undefined): Field[] { const fields: {
const fields: Field[] = []; name: string,
value: string
}[] = [];
if (Array.isArray(attachments)) { if (Array.isArray(attachments)) {
for (const attachment of attachments.filter(isPropertyValue)) { for (const attachment of attachments.filter(isPropertyValue)) {
fields.push({ fields.push({
@@ -578,11 +610,11 @@ export class ApPersonService implements OnModuleInit {
} }
} }
return fields; return { fields };
} }
@bindThis @bindThis
public async updateFeatured(userId: User['id'], resolver?: Resolver): Promise<void> { public async updateFeatured(userId: User['id'], resolver?: Resolver) {
const user = await this.usersRepository.findOneByOrFail({ id: userId }); const user = await this.usersRepository.findOneByOrFail({ id: userId });
if (!this.userEntityService.isRemoteUser(user)) return; if (!this.userEntityService.isRemoteUser(user)) return;
if (!user.featured) return; if (!user.featured) return;
@@ -611,13 +643,13 @@ export class ApPersonService implements OnModuleInit {
// とりあえずidを別の時間で生成して順番を維持 // とりあえずidを別の時間で生成して順番を維持
let td = 0; let td = 0;
for (const note of featuredNotes.filter((note): note is Note => note != null)) { for (const note of featuredNotes.filter(note => note != null)) {
td -= 1000; td -= 1000;
transactionalEntityManager.insert(UserNotePining, { transactionalEntityManager.insert(UserNotePining, {
id: this.idService.genId(new Date(Date.now() + td)), id: this.idService.genId(new Date(Date.now() + td)),
createdAt: new Date(), createdAt: new Date(),
userId: user.id, userId: user.id,
noteId: note.id, noteId: note!.id,
}); });
} }
}); });
@@ -656,7 +688,7 @@ export class ApPersonService implements OnModuleInit {
// (uriが存在しなかったり応答がなかったりする場合resolvePersonはthrow Errorする) // (uriが存在しなかったり応答がなかったりする場合resolvePersonはthrow Errorする)
dst = await this.resolvePerson(src.movedToUri); dst = await this.resolvePerson(src.movedToUri);
} }
if (dst.movedToUri === dst.uri) return 'skip: movedTo itself (dst)'; // if (dst.movedToUri === dst.uri) return 'skip: movedTo itself (dst)'; //
if (src.movedToUri !== dst.uri) return 'skip: missmatch uri'; // if (src.movedToUri !== dst.uri) return 'skip: missmatch uri'; //
if (dst.movedToUri === src.uri) return 'skip: dst.movedToUri === src.uri'; if (dst.movedToUri === src.uri) return 'skip: dst.movedToUri === src.uri';

View File

@@ -4,12 +4,12 @@ import type { NotesRepository, PollsRepository } from '@/models/index.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { IPoll } from '@/models/entities/Poll.js'; import type { IPoll } from '@/models/entities/Poll.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { isQuestion } from '../type.js'; import { isQuestion } from '../type.js';
import { ApLoggerService } from '../ApLoggerService.js'; import { ApLoggerService } from '../ApLoggerService.js';
import { ApResolverService } from '../ApResolverService.js'; import { ApResolverService } from '../ApResolverService.js';
import type { Resolver } from '../ApResolverService.js'; import type { Resolver } from '../ApResolverService.js';
import type { IObject, IQuestion } from '../type.js'; import type { IObject, IQuestion } from '../type.js';
import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class ApQuestionService { export class ApQuestionService {
@@ -33,25 +33,33 @@ export class ApQuestionService {
@bindThis @bindThis
public async extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise<IPoll> { public async extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise<IPoll> {
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver(); if (resolver == null) resolver = this.apResolverService.createResolver();
const question = await resolver.resolve(source); const question = await resolver.resolve(source);
if (!isQuestion(question)) throw new Error('invalid type');
const multiple = question.oneOf === undefined; if (!isQuestion(question)) {
if (multiple && question.anyOf === undefined) throw new Error('invalid question'); throw new Error('invalid type');
}
const multiple = !question.oneOf;
const expiresAt = question.endTime ? new Date(question.endTime) : question.closed ? new Date(question.closed) : null; const expiresAt = question.endTime ? new Date(question.endTime) : question.closed ? new Date(question.closed) : null;
const choices = question[multiple ? 'anyOf' : 'oneOf'] if (multiple && !question.anyOf) {
?.map((x) => x.name) throw new Error('invalid question');
.filter((x): x is string => typeof x === 'string') }
?? [];
const votes = question[multiple ? 'anyOf' : 'oneOf']?.map((x) => x.replies?.totalItems ?? x._misskey_votes ?? 0); const choices = question[multiple ? 'anyOf' : 'oneOf']!
.map((x, i) => x.name!);
return { choices, votes, multiple, expiresAt }; const votes = question[multiple ? 'anyOf' : 'oneOf']!
.map((x, i) => x.replies && x.replies.totalItems || x._misskey_votes || 0);
return {
choices,
votes,
multiple,
expiresAt,
};
} }
/** /**
@@ -60,9 +68,8 @@ export class ApQuestionService {
* @returns true if updated * @returns true if updated
*/ */
@bindThis @bindThis
public async updateQuestion(value: string | IObject, resolver?: Resolver): Promise<boolean> { public async updateQuestion(value: any, resolver?: Resolver) {
const uri = typeof value === 'string' ? value : value.id; const uri = typeof value === 'string' ? value : value.id;
if (uri == null) throw new Error('uri is null');
// URIがこのサーバーを指しているならスキップ // URIがこのサーバーを指しているならスキップ
if (uri.startsWith(this.config.url + '/')) throw new Error('uri points local'); if (uri.startsWith(this.config.url + '/')) throw new Error('uri points local');
@@ -76,7 +83,6 @@ export class ApQuestionService {
//#endregion //#endregion
// resolve new Question object // resolve new Question object
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver(); if (resolver == null) resolver = this.apResolverService.createResolver();
const question = await resolver.resolve(value) as IQuestion; const question = await resolver.resolve(value) as IQuestion;
this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`); this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
@@ -84,14 +90,12 @@ export class ApQuestionService {
if (question.type !== 'Question') throw new Error('object is not a Question'); if (question.type !== 'Question') throw new Error('object is not a Question');
const apChoices = question.oneOf ?? question.anyOf; const apChoices = question.oneOf ?? question.anyOf;
if (apChoices == null) throw new Error('invalid apChoices: ' + apChoices);
let changed = false; let changed = false;
for (const choice of poll.choices) { for (const choice of poll.choices) {
const oldCount = poll.votes[poll.choices.indexOf(choice)]; const oldCount = poll.votes[poll.choices.indexOf(choice)];
const newCount = apChoices.filter(ap => ap.name === choice).at(0)?.replies?.totalItems; const newCount = apChoices!.filter(ap => ap.name === choice)[0].replies!.totalItems;
if (newCount == null) throw new Error('invalid newCount: ' + newCount);
if (oldCount !== newCount) { if (oldCount !== newCount) {
changed = true; changed = true;
@@ -99,7 +103,9 @@ export class ApQuestionService {
} }
} }
await this.pollsRepository.update({ noteId: note.id }, { votes: poll.votes }); await this.pollsRepository.update({ noteId: note.id }, {
votes: poll.votes,
});
return changed; return changed;
} }

View File

@@ -2,7 +2,7 @@ import { toArray } from '@/misc/prelude/array.js';
import { isHashtag } from '../type.js'; import { isHashtag } from '../type.js';
import type { IObject, IApHashtag } from '../type.js'; import type { IObject, IApHashtag } from '../type.js';
export function extractApHashtags(tags: IObject | IObject[] | null | undefined): string[] { export function extractApHashtags(tags: IObject | IObject[] | null | undefined) {
if (tags == null) return []; if (tags == null) return [];
const hashtags = extractApHashtagObjects(tags); const hashtags = extractApHashtagObjects(tags);

View File

@@ -47,7 +47,7 @@ export class DriveFileEntityService {
private videoProcessingService: VideoProcessingService, private videoProcessingService: VideoProcessingService,
) { ) {
} }
@bindThis @bindThis
public validateFileName(name: string): boolean { public validateFileName(name: string): boolean {
return ( return (

View File

@@ -24,7 +24,7 @@ export class NoteEntityService implements OnModuleInit {
private driveFileEntityService: DriveFileEntityService; private driveFileEntityService: DriveFileEntityService;
private customEmojiService: CustomEmojiService; private customEmojiService: CustomEmojiService;
private reactionService: ReactionService; private reactionService: ReactionService;
constructor( constructor(
private moduleRef: ModuleRef, private moduleRef: ModuleRef,
@@ -68,7 +68,7 @@ export class NoteEntityService implements OnModuleInit {
this.customEmojiService = this.moduleRef.get('CustomEmojiService'); this.customEmojiService = this.moduleRef.get('CustomEmojiService');
this.reactionService = this.moduleRef.get('ReactionService'); this.reactionService = this.moduleRef.get('ReactionService');
} }
@bindThis @bindThis
private async hideNote(packedNote: Packed<'Note'>, meId: User['id'] | null) { private async hideNote(packedNote: Packed<'Note'>, meId: User['id'] | null) {
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど) // TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
@@ -457,12 +457,12 @@ export class NoteEntityService implements OnModuleInit {
const query = this.notesRepository.createQueryBuilder('note') const query = this.notesRepository.createQueryBuilder('note')
.where('note.userId = :userId', { userId }) .where('note.userId = :userId', { userId })
.andWhere('note.renoteId = :renoteId', { renoteId }); .andWhere('note.renoteId = :renoteId', { renoteId });
// 指定した投稿を除く // 指定した投稿を除く
if (excludeNoteId) { if (excludeNoteId) {
query.andWhere('note.id != :excludeNoteId', { excludeNoteId }); query.andWhere('note.id != :excludeNoteId', { excludeNoteId });
} }
return await query.getCount(); return await query.getCount();
} }
} }

View File

@@ -17,7 +17,7 @@ export class NoteReactionEntityService implements OnModuleInit {
private userEntityService: UserEntityService; private userEntityService: UserEntityService;
private noteEntityService: NoteEntityService; private noteEntityService: NoteEntityService;
private reactionService: ReactionService; private reactionService: ReactionService;
constructor( constructor(
private moduleRef: ModuleRef, private moduleRef: ModuleRef,

View File

@@ -59,7 +59,7 @@ export class NotificationEntityService implements OnModuleInit {
meId: User['id'], meId: User['id'],
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
options: { options: {
}, },
hint?: { hint?: {
packedNotes: Map<Note['id'], Packed<'Note'>>; packedNotes: Map<Note['id'], Packed<'Note'>>;

View File

@@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { In, Not } from 'typeorm'; import { In, Not } from 'typeorm';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import _Ajv from 'ajv'; import Ajv from 'ajv';
import { ModuleRef } from '@nestjs/core'; import { ModuleRef } from '@nestjs/core';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
@@ -31,7 +31,6 @@ type IsMeAndIsUserDetailed<ExpectsMe extends boolean | null, Detailed extends bo
Packed<'UserDetailed'> : Packed<'UserDetailed'> :
Packed<'UserLite'>; Packed<'UserLite'>;
const Ajv = _Ajv.default;
const ajv = new Ajv(); const ajv = new Ajv();
function isLocalUser(user: User): user is LocalUser; function isLocalUser(user: User): user is LocalUser;
@@ -113,7 +112,7 @@ export class UserEntityService implements OnModuleInit {
@Inject(DI.pagesRepository) @Inject(DI.pagesRepository)
private pagesRepository: PagesRepository, private pagesRepository: PagesRepository,
@Inject(DI.userMemosRepository) @Inject(DI.userMemosRepository)
private userMemosRepository: UserMemoRepository, private userMemosRepository: UserMemoRepository,

View File

@@ -81,7 +81,7 @@ export class QueueStatsService implements OnApplicationShutdown {
this.intervalId = setInterval(tick, interval); this.intervalId = setInterval(tick, interval);
} }
@bindThis @bindThis
public dispose(): void { public dispose(): void {
clearInterval(this.intervalId); clearInterval(this.intervalId);

View File

@@ -3,7 +3,6 @@ import si from 'systeminformation';
import Xev from 'xev'; import Xev from 'xev';
import * as osUtils from 'os-utils'; import * as osUtils from 'os-utils';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import type { OnApplicationShutdown } from '@nestjs/common'; import type { OnApplicationShutdown } from '@nestjs/common';
const ev = new Xev(); const ev = new Xev();
@@ -15,10 +14,9 @@ const round = (num: number) => Math.round(num * 10) / 10;
@Injectable() @Injectable()
export class ServerStatsService implements OnApplicationShutdown { export class ServerStatsService implements OnApplicationShutdown {
private intervalId: NodeJS.Timer | null = null; private intervalId: NodeJS.Timer;
constructor( constructor(
private metaService: MetaService,
) { ) {
} }
@@ -26,9 +24,7 @@ export class ServerStatsService implements OnApplicationShutdown {
* Report server stats regularly * Report server stats regularly
*/ */
@bindThis @bindThis
public async start(): Promise<void> { public start(): void {
if (!(await this.metaService.fetch(true)).enableServerMachineStats) return;
const log = [] as any[]; const log = [] as any[];
ev.on('requestServerStatsLog', x => { ev.on('requestServerStatsLog', x => {
@@ -68,9 +64,7 @@ export class ServerStatsService implements OnApplicationShutdown {
@bindThis @bindThis
public dispose(): void { public dispose(): void {
if (this.intervalId) { clearInterval(this.intervalId);
clearInterval(this.intervalId);
}
} }
@bindThis @bindThis

View File

@@ -4,7 +4,7 @@ import { default as convertColor } from 'color-convert';
import { format as dateFormat } from 'date-fns'; import { format as dateFormat } from 'date-fns';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { envOption } from './env.js'; import { envOption } from './env.js';
import type { KEYWORD } from 'color-convert/conversions.js'; import type { KEYWORD } from 'color-convert/conversions';
type Context = { type Context = {
name: string; name: string;

View File

@@ -83,16 +83,6 @@ export class RedisKVCache<T> {
// TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする // TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする
} }
@bindThis
public gc() {
this.memoryCache.gc();
}
@bindThis
public dispose() {
this.memoryCache.dispose();
}
} }
export class RedisSingleCache<T> { export class RedisSingleCache<T> {
@@ -184,15 +174,10 @@ export class RedisSingleCache<T> {
export class MemoryKVCache<T> { export class MemoryKVCache<T> {
public cache: Map<string, { date: number; value: T; }>; public cache: Map<string, { date: number; value: T; }>;
private lifetime: number; private lifetime: number;
private gcIntervalHandle: NodeJS.Timer;
constructor(lifetime: MemoryKVCache<never>['lifetime']) { constructor(lifetime: MemoryKVCache<never>['lifetime']) {
this.cache = new Map(); this.cache = new Map();
this.lifetime = lifetime; this.lifetime = lifetime;
this.gcIntervalHandle = setInterval(() => {
this.gc();
}, 1000 * 60 * 3);
} }
@bindThis @bindThis
@@ -215,7 +200,7 @@ export class MemoryKVCache<T> {
} }
@bindThis @bindThis
public delete(key: string): void { public delete(key: string) {
this.cache.delete(key); this.cache.delete(key);
} }
@@ -270,21 +255,6 @@ export class MemoryKVCache<T> {
} }
return value; return value;
} }
@bindThis
public gc(): void {
const now = Date.now();
for (const [key, { date }] of this.cache.entries()) {
if ((now - date) > this.lifetime) {
this.cache.delete(key);
}
}
}
@bindThis
public dispose(): void {
clearInterval(this.gcIntervalHandle);
}
} }
export class MemorySingleCache<T> { export class MemorySingleCache<T> {

View File

@@ -1,4 +1,4 @@
export function checkHttps(url: string): boolean { export function checkHttps(url: string) {
return url.startsWith('https://') || return url.startsWith('https://') ||
(url.startsWith('http://') && process.env.NODE_ENV !== 'production'); (url.startsWith('http://') && process.env.NODE_ENV !== 'production');
} }

View File

@@ -1,3 +1,3 @@
import { secureRndstr } from '@/misc/secure-rndstr.js'; import { secureRndstr } from '@/misc/secure-rndstr.js';
export default () => secureRndstr(16); export default () => secureRndstr(16, true);

View File

@@ -1,6 +1,6 @@
import IPCIDR from 'ip-cidr'; import IPCIDR from 'ip-cidr';
export function getIpHash(ip: string): string { export function getIpHash(ip: string) {
try { try {
// because a single person may control many IPv6 addresses, // because a single person may control many IPv6 addresses,
// only a /64 subnet prefix of any IP will be taken into account. // only a /64 subnet prefix of any IP will be taken into account.

View File

@@ -131,7 +131,7 @@ type NullOrUndefined<p extends Schema, T> =
| T; | T;
// https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection // https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
// Get intersection from union // Get intersection from union
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
type PartialIntersection<T> = Partial<UnionToIntersection<T>>; type PartialIntersection<T> = Partial<UnionToIntersection<T>>;

View File

@@ -2,7 +2,7 @@
* 1. 配列に何も入っていない時はクエリを付けない * 1. 配列に何も入っていない時はクエリを付けない
* 2. プロパティがundefinedの時はクエリを付けない * 2. プロパティがundefinedの時はクエリを付けない
* new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない) * new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない)
*/ */
export function query(obj: Record<string, unknown>): string { export function query(obj: Record<string, unknown>): string {
const params = Object.entries(obj) const params = Object.entries(obj)
.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined)

View File

@@ -1,9 +1,10 @@
import * as crypto from 'node:crypto'; import * as crypto from 'node:crypto';
export const L_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz'; const L_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz';
const LU_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; const LU_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
export function secureRndstr(length = 32, { chars = LU_CHARS } = {}): string { export function secureRndstr(length = 32, useLU = true): string {
const chars = useLU ? LU_CHARS : L_CHARS;
const chars_len = chars.length; const chars_len = chars.length;
let str = ''; let str = '';

View File

@@ -55,10 +55,7 @@ export class Ad {
length: 8192, nullable: false, length: 8192, nullable: false,
}) })
public memo: string; public memo: string;
@Column('integer', {
default: 0, nullable: false,
})
public dayOfWeek: number;
constructor(data: Partial<Ad>) { constructor(data: Partial<Ad>) {
if (data == null) return; if (data == null) return;

View File

@@ -97,30 +97,18 @@ export class Meta {
}) })
public logoImageUrl: string | null; public logoImageUrl: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public errorImageUrl: string | null;
@Column('varchar', { @Column('varchar', {
length: 1024, length: 1024,
nullable: true, nullable: true,
}) })
public iconUrl: string | null; public iconUrl: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public serverErrorImageUrl: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public notFoundImageUrl: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public infoImageUrl: string | null;
@Column('boolean', { @Column('boolean', {
default: true, default: true,
}) })
@@ -413,16 +401,6 @@ export class Meta {
}) })
public enableChartsForFederatedInstances: boolean; public enableChartsForFederatedInstances: boolean;
@Column('boolean', {
default: false,
})
public enableServerMachineStats: boolean;
@Column('boolean', {
default: true,
})
public enableIdenticonGeneration: boolean;
@Column('jsonb', { @Column('jsonb', {
default: { }, default: { },
}) })

View File

@@ -207,7 +207,7 @@ export class UserProfile {
public mutedInstances: string[]; public mutedInstances: string[];
@Column('enum', { @Column('enum', {
enum: [ enum: [
...notificationTypes, ...notificationTypes,
// マイグレーションで削除が困難なので古いenumは残しておく // マイグレーションで削除が困難なので古いenumは残しておく
...obsoleteNotificationTypes, ...obsoleteNotificationTypes,

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