mirror of
https://activitypub.software/TransFem-org/Sharkey.git
synced 2024-12-12 16:30:45 +01:00
Merge remote-tracking branch 'misskey/master' into feature/2024.9.0
This commit is contained in:
commit
f00576bce6
564 changed files with 19993 additions and 8169 deletions
211
.config/cypress-devcontainer.yml
Normal file
211
.config/cypress-devcontainer.yml
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
# Misskey configuration
|
||||||
|
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
# ┌─────┐
|
||||||
|
#───┘ URL └─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Final accessible URL seen by a user.
|
||||||
|
url: 'http://misskey.local'
|
||||||
|
|
||||||
|
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||||
|
# URL SETTINGS AFTER THAT!
|
||||||
|
|
||||||
|
# ┌───────────────────────┐
|
||||||
|
#───┘ Port and TLS settings └───────────────────────────────────
|
||||||
|
|
||||||
|
#
|
||||||
|
# Misskey requires a reverse proxy to support HTTPS connections.
|
||||||
|
#
|
||||||
|
# +----- https://example.tld/ ------------+
|
||||||
|
# +------+ |+-------------+ +----------------+|
|
||||||
|
# | User | ---> || Proxy (443) | ---> | Misskey (3000) ||
|
||||||
|
# +------+ |+-------------+ +----------------+|
|
||||||
|
# +---------------------------------------+
|
||||||
|
#
|
||||||
|
# You need to set up a reverse proxy. (e.g. nginx)
|
||||||
|
# An encrypted connection with HTTPS is highly recommended
|
||||||
|
# because tokens may be transferred in GET requests.
|
||||||
|
|
||||||
|
# The port that your Misskey server should listen on.
|
||||||
|
port: 61812
|
||||||
|
|
||||||
|
# ┌──────────────────────────┐
|
||||||
|
#───┘ PostgreSQL configuration └────────────────────────────────
|
||||||
|
|
||||||
|
db:
|
||||||
|
host: db
|
||||||
|
port: 5432
|
||||||
|
|
||||||
|
# Database name
|
||||||
|
db: misskey
|
||||||
|
|
||||||
|
# Auth
|
||||||
|
user: postgres
|
||||||
|
pass: postgres
|
||||||
|
|
||||||
|
# Whether disable Caching queries
|
||||||
|
#disableCache: true
|
||||||
|
|
||||||
|
# Extra Connection options
|
||||||
|
#extra:
|
||||||
|
# ssl: true
|
||||||
|
|
||||||
|
dbReplications: false
|
||||||
|
|
||||||
|
# You can configure any number of replicas here
|
||||||
|
#dbSlaves:
|
||||||
|
# -
|
||||||
|
# host:
|
||||||
|
# port:
|
||||||
|
# db:
|
||||||
|
# user:
|
||||||
|
# pass:
|
||||||
|
# -
|
||||||
|
# host:
|
||||||
|
# port:
|
||||||
|
# db:
|
||||||
|
# user:
|
||||||
|
# pass:
|
||||||
|
|
||||||
|
# ┌─────────────────────┐
|
||||||
|
#───┘ Redis configuration └─────────────────────────────────────
|
||||||
|
|
||||||
|
redis:
|
||||||
|
host: redis
|
||||||
|
port: 6379
|
||||||
|
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
#pass: example-pass
|
||||||
|
#prefix: example-prefix
|
||||||
|
#db: 1
|
||||||
|
|
||||||
|
#redisForPubsub:
|
||||||
|
# host: redis
|
||||||
|
# port: 6379
|
||||||
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
# #pass: example-pass
|
||||||
|
# #prefix: example-prefix
|
||||||
|
# #db: 1
|
||||||
|
|
||||||
|
#redisForJobQueue:
|
||||||
|
# host: redis
|
||||||
|
# port: 6379
|
||||||
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
# #pass: example-pass
|
||||||
|
# #prefix: example-prefix
|
||||||
|
# #db: 1
|
||||||
|
|
||||||
|
#redisForTimelines:
|
||||||
|
# host: redis
|
||||||
|
# port: 6379
|
||||||
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
# #pass: example-pass
|
||||||
|
# #prefix: example-prefix
|
||||||
|
# #db: 1
|
||||||
|
|
||||||
|
#redisForReactions:
|
||||||
|
# host: redis
|
||||||
|
# port: 6379
|
||||||
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
# #pass: example-pass
|
||||||
|
# #prefix: example-prefix
|
||||||
|
# #db: 1
|
||||||
|
|
||||||
|
# ┌───────────────────────────┐
|
||||||
|
#───┘ MeiliSearch configuration └─────────────────────────────
|
||||||
|
|
||||||
|
#meilisearch:
|
||||||
|
# host: meilisearch
|
||||||
|
# port: 7700
|
||||||
|
# apiKey: ''
|
||||||
|
# ssl: true
|
||||||
|
# index: ''
|
||||||
|
|
||||||
|
# ┌───────────────┐
|
||||||
|
#───┘ ID generation └───────────────────────────────────────────
|
||||||
|
|
||||||
|
# You can select the ID generation method.
|
||||||
|
# You don't usually need to change this setting, but you can
|
||||||
|
# change it according to your preferences.
|
||||||
|
|
||||||
|
# Available methods:
|
||||||
|
# aid ... Short, Millisecond accuracy
|
||||||
|
# aidx ... Millisecond accuracy
|
||||||
|
# meid ... Similar to ObjectID, Millisecond accuracy
|
||||||
|
# ulid ... Millisecond accuracy
|
||||||
|
# objectid ... This is left for backward compatibility
|
||||||
|
|
||||||
|
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||||
|
# ID SETTINGS AFTER THAT!
|
||||||
|
|
||||||
|
id: 'aidx'
|
||||||
|
|
||||||
|
# ┌────────────────┐
|
||||||
|
#───┘ Error tracking └──────────────────────────────────────────
|
||||||
|
|
||||||
|
# Sentry is available for error tracking.
|
||||||
|
# See the Sentry documentation for more details on options.
|
||||||
|
|
||||||
|
#sentryForBackend:
|
||||||
|
# enableNodeProfiling: true
|
||||||
|
# options:
|
||||||
|
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
|
||||||
|
|
||||||
|
#sentryForFrontend:
|
||||||
|
# options:
|
||||||
|
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
|
||||||
|
|
||||||
|
# ┌─────────────────────┐
|
||||||
|
#───┘ Other configuration └─────────────────────────────────────
|
||||||
|
|
||||||
|
# Whether disable HSTS
|
||||||
|
#disableHsts: true
|
||||||
|
|
||||||
|
# Number of worker processes
|
||||||
|
#clusterLimit: 1
|
||||||
|
|
||||||
|
# Job concurrency per worker
|
||||||
|
# deliverJobConcurrency: 128
|
||||||
|
# inboxJobConcurrency: 16
|
||||||
|
|
||||||
|
# Job rate limiter
|
||||||
|
# deliverJobPerSec: 128
|
||||||
|
# inboxJobPerSec: 32
|
||||||
|
|
||||||
|
# Job attempts
|
||||||
|
# deliverJobMaxAttempts: 12
|
||||||
|
# inboxJobMaxAttempts: 8
|
||||||
|
|
||||||
|
# IP address family used for outgoing request (ipv4, ipv6 or dual)
|
||||||
|
#outgoingAddressFamily: ipv4
|
||||||
|
|
||||||
|
# Proxy for HTTP/HTTPS
|
||||||
|
#proxy: http://127.0.0.1:3128
|
||||||
|
|
||||||
|
proxyBypassHosts:
|
||||||
|
- api.deepl.com
|
||||||
|
- api-free.deepl.com
|
||||||
|
- www.recaptcha.net
|
||||||
|
- hcaptcha.com
|
||||||
|
- challenges.cloudflare.com
|
||||||
|
|
||||||
|
# Proxy for SMTP/SMTPS
|
||||||
|
#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT
|
||||||
|
#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4
|
||||||
|
#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5
|
||||||
|
|
||||||
|
# Media Proxy
|
||||||
|
#mediaProxy: https://example.com/proxy
|
||||||
|
|
||||||
|
# Proxy remote files (default: true)
|
||||||
|
proxyRemoteFiles: true
|
||||||
|
|
||||||
|
# Sign to ActivityPub GET request (default: true)
|
||||||
|
signToActivityPubGet: true
|
||||||
|
|
||||||
|
allowedPrivateNetworks: [
|
||||||
|
'127.0.0.1/32'
|
||||||
|
]
|
||||||
|
|
||||||
|
# Upload or download file size limits (bytes)
|
||||||
|
#maxFileSize: 262144000
|
|
@ -163,6 +163,14 @@ redis:
|
||||||
# #prefix: example-prefix
|
# #prefix: example-prefix
|
||||||
# #db: 1
|
# #db: 1
|
||||||
|
|
||||||
|
#redisForReactions:
|
||||||
|
# host: redis
|
||||||
|
# port: 6379
|
||||||
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
# #pass: example-pass
|
||||||
|
# #prefix: example-prefix
|
||||||
|
# #db: 1
|
||||||
|
|
||||||
# ┌───────────────────────────┐
|
# ┌───────────────────────────┐
|
||||||
#───┘ MeiliSearch configuration └─────────────────────────────
|
#───┘ MeiliSearch configuration └─────────────────────────────
|
||||||
|
|
||||||
|
|
|
@ -172,6 +172,16 @@ redis:
|
||||||
# # You can specify more ioredis options...
|
# # You can specify more ioredis options...
|
||||||
# #username: example-username
|
# #username: example-username
|
||||||
|
|
||||||
|
#redisForReactions:
|
||||||
|
# host: localhost
|
||||||
|
# port: 6379
|
||||||
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
# #pass: example-pass
|
||||||
|
# #prefix: example-prefix
|
||||||
|
# #db: 1
|
||||||
|
# # You can specify more ioredis options...
|
||||||
|
# #username: example-username
|
||||||
|
|
||||||
# ┌───────────────────────────┐
|
# ┌───────────────────────────┐
|
||||||
#───┘ MeiliSearch configuration └─────────────────────────────
|
#───┘ MeiliSearch configuration └─────────────────────────────
|
||||||
|
|
||||||
|
|
|
@ -103,6 +103,14 @@ redis:
|
||||||
# #prefix: example-prefix
|
# #prefix: example-prefix
|
||||||
# #db: 1
|
# #db: 1
|
||||||
|
|
||||||
|
#redisForReactions:
|
||||||
|
# host: redis
|
||||||
|
# port: 6379
|
||||||
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
# #pass: example-pass
|
||||||
|
# #prefix: example-prefix
|
||||||
|
# #db: 1
|
||||||
|
|
||||||
# ┌───────────────────────────┐
|
# ┌───────────────────────────┐
|
||||||
#───┘ MeiliSearch configuration └─────────────────────────────
|
#───┘ MeiliSearch configuration └─────────────────────────────
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
set -xe
|
set -xe
|
||||||
|
|
||||||
sudo chown node node_modules
|
sudo chown node node_modules
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2 libxtst6 xauth xvfb
|
||||||
git config --global --add safe.directory /workspace
|
git config --global --add safe.directory /workspace
|
||||||
git submodule update --init
|
git submodule update --init
|
||||||
corepack install
|
corepack install
|
||||||
|
@ -12,3 +14,4 @@ pnpm install --frozen-lockfile
|
||||||
cp .devcontainer/devcontainer.yml .config/default.yml
|
cp .devcontainer/devcontainer.yml .config/default.yml
|
||||||
pnpm build
|
pnpm build
|
||||||
pnpm migrate
|
pnpm migrate
|
||||||
|
pnpm exec cypress install
|
||||||
|
|
10
.gitignore
vendored
10
.gitignore
vendored
|
@ -38,6 +38,7 @@ coverage
|
||||||
!/.config/example.yml
|
!/.config/example.yml
|
||||||
!/.config/docker_example.yml
|
!/.config/docker_example.yml
|
||||||
!/.config/docker_example.env
|
!/.config/docker_example.env
|
||||||
|
!/.config/cypress-devcontainer.yml
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
compose.yml
|
compose.yml
|
||||||
.devcontainer/compose.yml
|
.devcontainer/compose.yml
|
||||||
|
@ -47,6 +48,7 @@ compose.yml
|
||||||
/build
|
/build
|
||||||
built
|
built
|
||||||
built-test
|
built-test
|
||||||
|
js-built
|
||||||
/data
|
/data
|
||||||
/.cache-loader
|
/.cache-loader
|
||||||
/db
|
/db
|
||||||
|
@ -66,8 +68,9 @@ temp
|
||||||
tsdoc-metadata.json
|
tsdoc-metadata.json
|
||||||
misskey-assets
|
misskey-assets
|
||||||
|
|
||||||
# Sharkey
|
# Vite temporary files
|
||||||
/packages/megalodon/lib
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
|
|
||||||
# blender backups
|
# blender backups
|
||||||
*.blend1
|
*.blend1
|
||||||
|
@ -78,3 +81,6 @@ misskey-assets
|
||||||
|
|
||||||
# VSCode addon
|
# VSCode addon
|
||||||
.favorites.json
|
.favorites.json
|
||||||
|
|
||||||
|
# Sharkey
|
||||||
|
/packages/megalodon/lib
|
||||||
|
|
53
CHANGELOG.md
53
CHANGELOG.md
|
@ -1,3 +1,56 @@
|
||||||
|
## 2024.9.0
|
||||||
|
|
||||||
|
### General
|
||||||
|
- Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能
|
||||||
|
- 埋め込みコードやウェブサイトへの実装方法の詳細は https://misskey-hub.net/docs/for-users/features/embed/ をご覧ください
|
||||||
|
- Feat: パスキーでログインボタンを実装 (#14574)
|
||||||
|
- Feat: フォローされた際のメッセージを設定できるように
|
||||||
|
- Feat: 連合をホワイトリスト制にできるように
|
||||||
|
- Feat: UserWebhookとSystemWebhookのテスト送信機能を追加 (#14445)
|
||||||
|
- Feat: モデレーターはユーザーにかかわらずファイルが添付されているノートを検索できるように
|
||||||
|
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/680)
|
||||||
|
- Feat: データエクスポートが完了した際に通知を発行するように
|
||||||
|
- Enhance: ユーザーによるコンテンツインポートの可否をロールポリシーで制御できるように
|
||||||
|
- Enhance: 依存関係の更新
|
||||||
|
- Enhance: l10nの更新
|
||||||
|
|
||||||
|
### Client
|
||||||
|
- Enhance: サイズ制限を超過するファイルをアップロードしようとした際にエラーを出すように
|
||||||
|
- Enhance: アイコンデコレーション管理画面にプレビューを追加
|
||||||
|
- Enhance: コントロールパネル内のファイル一覧でセンシティブなファイルを区別しやすく
|
||||||
|
- Enhance: ScratchpadにUIインスペクターを追加
|
||||||
|
- Enhance: Play編集画面の項目の並びを少しリデザイン
|
||||||
|
- Enhance: 各種メニューをドロワー表示するかどうか設定可能に
|
||||||
|
- Enhance: AiScriptのMk:C:containerのオプションに`borderStyle`と`borderRadius`を追加
|
||||||
|
- Enhance: CWでも絵文字をクリックしてメニューを表示できるように
|
||||||
|
- Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正
|
||||||
|
- Fix: コントロールパネル内のAp requests内のチャートの表示がおかしかった問題を修正
|
||||||
|
- Fix: 月の違う同じ日はセパレータが表示されないのを修正
|
||||||
|
- Fix: タッチ画面でレンジスライダーを操作するとツールチップが複数表示される問題を修正
|
||||||
|
(Cherry-picked from https://github.com/taiyme/misskey/pull/265)
|
||||||
|
- Fix: 縦横比が極端なカスタム絵文字を表示する際にレイアウトが崩れる箇所があるのを修正
|
||||||
|
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/725)
|
||||||
|
- Fix: 設定変更時のリロード確認ダイアログが複数個表示されることがある問題を修正
|
||||||
|
- Fix: ファイルの詳細ページのファイルの説明で改行が正しく表示されない問題を修正
|
||||||
|
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/bde6bb0bd2e8b0d027e724d2acdb8ae0585a8110)
|
||||||
|
- Fix: 一部画面のページネーションが動作しにくくなっていたのを修正 ( #12766 , #11449 )
|
||||||
|
|
||||||
|
### Server
|
||||||
|
- Feat: Misskey® Reactions Boost Technology™ (RBT)により、リアクションの作成負荷を低減することが可能に
|
||||||
|
- Fix: アンテナの書き込み時にキーワードが与えられなかった場合のエラーをApiErrorとして投げるように
|
||||||
|
- この変更により、公式フロントエンドでは入力の不備が内部エラーとして報告される代わりに一般的なエラーダイアログで報告されます
|
||||||
|
- Fix: ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正
|
||||||
|
- Fix: 外部ページを解析する際に、ページに紐づけられた関連リソースも読み込まれてしまう問題を修正
|
||||||
|
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/26e0412fbb91447c37e8fb06ffb0487346063bb8)
|
||||||
|
- Fix: Continue importing from file if single emoji import fails
|
||||||
|
- Fix: `Retry-After`ヘッダーが送信されなかった問題を修正
|
||||||
|
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/8a982c61c01909e7540ff1be9f019df07c3f0624)
|
||||||
|
- Fix: サーバーサイドのDOM解析完了時にリソースを開放するように
|
||||||
|
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/634)
|
||||||
|
- Fix: `<link rel="alternate">`を追って照会するのはOKレスポンスが返却された場合のみに
|
||||||
|
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/633)
|
||||||
|
- Fix: メールにスタイルが適用されていなかった問題を修正
|
||||||
|
|
||||||
## 2024.8.0
|
## 2024.8.0
|
||||||
|
|
||||||
### General
|
### General
|
||||||
|
|
|
@ -573,6 +573,26 @@ marginはそのコンポーネントを使う側が設定する
|
||||||
### indexというファイル名を使うな
|
### indexというファイル名を使うな
|
||||||
ESMではディレクトリインポートは廃止されているのと、ディレクトリインポートせずともファイル名が index だと何故か一部のライブラリ?でディレクトリインポートだと見做されてエラーになる
|
ESMではディレクトリインポートは廃止されているのと、ディレクトリインポートせずともファイル名が index だと何故か一部のライブラリ?でディレクトリインポートだと見做されてエラーになる
|
||||||
|
|
||||||
|
## CSS Recipe
|
||||||
|
|
||||||
|
### Lighten CSS vars
|
||||||
|
|
||||||
|
``` css
|
||||||
|
color: hsl(from var(--accent) h s calc(l + 10));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Darken CSS vars
|
||||||
|
|
||||||
|
``` css
|
||||||
|
color: hsl(from var(--accent) h s calc(l - 10));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add alpha to CSS vars
|
||||||
|
|
||||||
|
``` css
|
||||||
|
color: color(from var(--accent) srgb r g b / 0.5);
|
||||||
|
```
|
||||||
|
|
||||||
## Merging from Misskey into Sharkey
|
## Merging from Misskey into Sharkey
|
||||||
|
|
||||||
Make sure you have both remotes in the same clone (`git remote add misskey
|
Make sure you have both remotes in the same clone (`git remote add misskey
|
||||||
|
|
|
@ -23,9 +23,10 @@ RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
|
||||||
RUN pnpm build
|
RUN pnpm build
|
||||||
RUN node scripts/trim-deps.mjs
|
RUN node scripts/trim-deps.mjs
|
||||||
RUN mv packages/frontend/assets sharkey-assets
|
RUN mv packages/frontend/assets sharkey-assets
|
||||||
|
RUN mv packages/frontend-embed/assets sharkey-embed-assets
|
||||||
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
|
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
|
||||||
pnpm prune
|
pnpm prune
|
||||||
RUN rm -r node_modules packages/frontend packages/sw
|
RUN rm -r node_modules packages/frontend packages/frontend-shared packages/frontend-embed packages/sw
|
||||||
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
|
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
|
||||||
pnpm i --prod --frozen-lockfile --aggregate-output
|
pnpm i --prod --frozen-lockfile --aggregate-output
|
||||||
RUN rm -rf .git
|
RUN rm -rf .git
|
||||||
|
@ -64,6 +65,7 @@ COPY --chown=sharkey:sharkey --from=build /sharkey/packages/megalodon/lib ./pack
|
||||||
COPY --chown=sharkey:sharkey --from=build /sharkey/fluent-emojis ./fluent-emojis
|
COPY --chown=sharkey:sharkey --from=build /sharkey/fluent-emojis ./fluent-emojis
|
||||||
COPY --chown=sharkey:sharkey --from=build /sharkey/tossface-emojis/dist ./tossface-emojis/dist
|
COPY --chown=sharkey:sharkey --from=build /sharkey/tossface-emojis/dist ./tossface-emojis/dist
|
||||||
COPY --chown=sharkey:sharkey --from=build /sharkey/sharkey-assets ./packages/frontend/assets
|
COPY --chown=sharkey:sharkey --from=build /sharkey/sharkey-assets ./packages/frontend/assets
|
||||||
|
COPY --chown=sharkey:sharkey --from=build /sharkey/sharkey-embed-assets ./packages/frontend-embed/assets
|
||||||
|
|
||||||
COPY --chown=sharkey:sharkey pnpm-workspace.yaml ./pnpm-workspace.yaml
|
COPY --chown=sharkey:sharkey pnpm-workspace.yaml ./pnpm-workspace.yaml
|
||||||
COPY --chown=sharkey:sharkey packages/backend/package.json ./packages/backend/package.json
|
COPY --chown=sharkey:sharkey packages/backend/package.json ./packages/backend/package.json
|
||||||
|
|
|
@ -124,6 +124,14 @@ redis:
|
||||||
# #prefix: example-prefix
|
# #prefix: example-prefix
|
||||||
# #db: 1
|
# #db: 1
|
||||||
|
|
||||||
|
#redisForReactions:
|
||||||
|
# host: redis
|
||||||
|
# port: 6379
|
||||||
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
# #pass: example-pass
|
||||||
|
# #prefix: example-prefix
|
||||||
|
# #db: 1
|
||||||
|
|
||||||
# ┌───────────────────────────┐
|
# ┌───────────────────────────┐
|
||||||
#───┘ MeiliSearch configuration └─────────────────────────────
|
#───┘ MeiliSearch configuration └─────────────────────────────
|
||||||
|
|
||||||
|
|
41
idea/MkDisableSection.vue
Normal file
41
idea/MkDisableSection.vue
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="[$style.root]">
|
||||||
|
<div :inert="disabled" :class="[{ [$style.disabled]: disabled }]">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
<div v-if="disabled" :class="[$style.cover]"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineProps<{
|
||||||
|
disabled?: boolean;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
cursor: not-allowed;
|
||||||
|
--color: color(from var(--error) srgb r g b / 0.25);
|
||||||
|
background-size: auto auto;
|
||||||
|
background-image: repeating-linear-gradient(135deg, transparent, transparent 10px, var(--color) 4px, var(--color) 14px);
|
||||||
|
}
|
||||||
|
</style>
|
1
idea/README.md
Normal file
1
idea/README.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
使われなくなったけど消すのは勿体ない(将来使えるかもしれない)コードを入れておくとこ
|
|
@ -601,6 +601,8 @@ reportAbuseOf: "{name}님얼 신고하기"
|
||||||
reporter: "신고한 사람"
|
reporter: "신고한 사람"
|
||||||
reporteeOrigin: "신고덴 사람"
|
reporteeOrigin: "신고덴 사람"
|
||||||
reporterOrigin: "신고한 곳"
|
reporterOrigin: "신고한 곳"
|
||||||
|
forwardReport: "웬겍 서버에 신고 보내기"
|
||||||
|
forwardReportIsAnonymous: "웬겍 서버서는 나으 정보럴 몬 보고 익멩으 시스템 게정어로 보입니다."
|
||||||
waitingFor: "{x}(얼)럴 지달리고 잇십니다"
|
waitingFor: "{x}(얼)럴 지달리고 잇십니다"
|
||||||
random: "무작이"
|
random: "무작이"
|
||||||
system: "시스템"
|
system: "시스템"
|
||||||
|
|
|
@ -1122,7 +1122,7 @@ preservedUsernames: "예약한 사용자 이름"
|
||||||
preservedUsernamesDescription: "예약할 사용자명을 한 줄에 하나씩 입력합니다. 여기에서 지정한 사용자명으로는 계정을 생성할 수 없게 됩니다. 단, 관리자 권한으로 계정을 생성할 때에는 해당되지 않으며, 이미 존재하는 계정도 영향을 받지 않습니다."
|
preservedUsernamesDescription: "예약할 사용자명을 한 줄에 하나씩 입력합니다. 여기에서 지정한 사용자명으로는 계정을 생성할 수 없게 됩니다. 단, 관리자 권한으로 계정을 생성할 때에는 해당되지 않으며, 이미 존재하는 계정도 영향을 받지 않습니다."
|
||||||
createNoteFromTheFile: "이 파일로 노트를 작성"
|
createNoteFromTheFile: "이 파일로 노트를 작성"
|
||||||
archive: "아카이브"
|
archive: "아카이브"
|
||||||
archived: "아카이브 됨"
|
archived: "보관됨"
|
||||||
unarchive: "보관 취소"
|
unarchive: "보관 취소"
|
||||||
channelArchiveConfirmTitle: "{name} 채널을 보존하시겠습니까?"
|
channelArchiveConfirmTitle: "{name} 채널을 보존하시겠습니까?"
|
||||||
channelArchiveConfirmDescription: "보존한 채널은 채널 목록과 검색 결과에 표시되지 않으며 새로운 노트도 작성할 수 없습니다."
|
channelArchiveConfirmDescription: "보존한 채널은 채널 목록과 검색 결과에 표시되지 않으며 새로운 노트도 작성할 수 없습니다."
|
||||||
|
|
|
@ -25,7 +25,7 @@ basicSettings: "Configurações básicas"
|
||||||
otherSettings: "Outras configurações"
|
otherSettings: "Outras configurações"
|
||||||
openInWindow: "Abrir em um janela"
|
openInWindow: "Abrir em um janela"
|
||||||
profile: "Perfil"
|
profile: "Perfil"
|
||||||
timeline: "Linha do tempo"
|
timeline: "Cronologia"
|
||||||
noAccountDescription: "Este usuário não tem uma descrição."
|
noAccountDescription: "Este usuário não tem uma descrição."
|
||||||
login: "Iniciar sessão"
|
login: "Iniciar sessão"
|
||||||
loggingIn: "Iniciando sessão…"
|
loggingIn: "Iniciando sessão…"
|
||||||
|
|
19
package.json
19
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "sharkey",
|
"name": "sharkey",
|
||||||
"version": "2024.9.0-dev",
|
"version": "2024.9.1-rc",
|
||||||
"codename": "shonk",
|
"codename": "shonk",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -8,7 +8,9 @@
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.6.0",
|
"packageManager": "pnpm@9.6.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
|
"packages/frontend-shared",
|
||||||
"packages/frontend",
|
"packages/frontend",
|
||||||
|
"packages/frontend-embed",
|
||||||
"packages/backend",
|
"packages/backend",
|
||||||
"packages/sw",
|
"packages/sw",
|
||||||
"packages/misskey-js",
|
"packages/misskey-js",
|
||||||
|
@ -35,6 +37,7 @@
|
||||||
"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",
|
||||||
"e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run",
|
"e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run",
|
||||||
|
"e2e-dev-container": "cp ./.config/cypress-devcontainer.yml ./.config/test.yml && pnpm start-server-and-test start:test http://localhost:61812 cy:run",
|
||||||
"jest": "cd packages/backend && pnpm jest",
|
"jest": "cd packages/backend && pnpm jest",
|
||||||
"jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage",
|
"jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage",
|
||||||
"test": "pnpm -r test",
|
"test": "pnpm -r test",
|
||||||
|
@ -53,11 +56,11 @@
|
||||||
"fast-glob": "3.3.2",
|
"fast-glob": "3.3.2",
|
||||||
"ignore-walk": "6.0.5",
|
"ignore-walk": "6.0.5",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"postcss": "8.4.40",
|
"postcss": "8.4.47",
|
||||||
"tar": "6.2.1",
|
"tar": "6.2.1",
|
||||||
"terser": "5.31.3",
|
"terser": "5.33.0",
|
||||||
"typescript": "5.5.4",
|
"typescript": "5.6.2",
|
||||||
"esbuild": "0.23.0",
|
"esbuild": "0.23.1",
|
||||||
"glob": "11.0.0"
|
"glob": "11.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -66,10 +69,10 @@
|
||||||
"@typescript-eslint/eslint-plugin": "7.17.0",
|
"@typescript-eslint/eslint-plugin": "7.17.0",
|
||||||
"@typescript-eslint/parser": "7.17.0",
|
"@typescript-eslint/parser": "7.17.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "13.13.1",
|
"cypress": "13.14.2",
|
||||||
"eslint": "9.8.0",
|
"eslint": "9.8.0",
|
||||||
"globals": "15.8.0",
|
"globals": "15.9.0",
|
||||||
"ncp": "2.0.0",
|
"ncp": "2.0.0",
|
||||||
"start-server-and-test": "2.0.4"
|
"start-server-and-test": "2.0.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
31
packages/backend/assets/embed.js
Normal file
31
packages/backend/assets/embed.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: MIT
|
||||||
|
*/
|
||||||
|
//@ts-check
|
||||||
|
(() => {
|
||||||
|
/** @type {NodeListOf<HTMLIFrameElement>} */
|
||||||
|
const els = document.querySelectorAll('iframe[data-misskey-embed-id]');
|
||||||
|
|
||||||
|
window.addEventListener('message', function (event) {
|
||||||
|
els.forEach((el) => {
|
||||||
|
if (event.source !== el.contentWindow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = el.dataset.misskeyEmbedId;
|
||||||
|
|
||||||
|
if (event.data.type === 'misskey:embed:ready') {
|
||||||
|
el.contentWindow?.postMessage({
|
||||||
|
type: 'misskey:embedParent:registerIframeId',
|
||||||
|
payload: {
|
||||||
|
iframeId: id,
|
||||||
|
}
|
||||||
|
}, '*');
|
||||||
|
}
|
||||||
|
if (event.data.type === 'misskey:embed:changeHeight' && event.data.iframeId === id) {
|
||||||
|
el.style.height = event.data.payload.height + 'px';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
16
packages/backend/migration/1723944246767-followedMessage.js
Normal file
16
packages/backend/migration/1723944246767-followedMessage.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class FollowedMessage1723944246767 {
|
||||||
|
name = 'FollowedMessage1723944246767';
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query('ALTER TABLE "user_profile" ADD "followedMessage" character varying(256)');
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query('ALTER TABLE "user_profile" DROP COLUMN "followedMessage"');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class ReactionsBuffering1726804538569 {
|
||||||
|
name = 'ReactionsBuffering1726804538569'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "enableReactionsBuffering" boolean NOT NULL DEFAULT false`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableReactionsBuffering"`);
|
||||||
|
}
|
||||||
|
}
|
16
packages/backend/migration/1727491883993-user-score.js
Normal file
16
packages/backend/migration/1727491883993-user-score.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class UserScore1727491883993 {
|
||||||
|
name = 'UserScore1727491883993'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "score" integer NOT NULL DEFAULT '0'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "score"`);
|
||||||
|
}
|
||||||
|
}
|
18
packages/backend/migration/1727512908322-meta-federation.js
Normal file
18
packages/backend/migration/1727512908322-meta-federation.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class MetaFederation1727512908322 {
|
||||||
|
name = 'MetaFederation1727512908322'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "federation" character varying(128) NOT NULL DEFAULT 'all'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "federationHosts" character varying(1024) array NOT NULL DEFAULT '{}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "federationHosts"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "federation"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -65,24 +65,24 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "3.620.0",
|
"@aws-sdk/client-s3": "3.620.0",
|
||||||
"@aws-sdk/lib-storage": "3.620.0",
|
"@aws-sdk/lib-storage": "3.620.0",
|
||||||
"@bull-board/api": "5.21.1",
|
"@bull-board/api": "6.0.0",
|
||||||
"@bull-board/fastify": "5.21.1",
|
"@bull-board/fastify": "6.0.0",
|
||||||
"@bull-board/ui": "5.21.1",
|
"@bull-board/ui": "6.0.0",
|
||||||
"@discordapp/twemoji": "15.0.3",
|
"@discordapp/twemoji": "15.1.0",
|
||||||
"@fastify/accepts": "4.3.0",
|
"@fastify/accepts": "5.0.0",
|
||||||
"@fastify/cookie": "9.3.1",
|
"@fastify/cookie": "10.0.0",
|
||||||
"@fastify/cors": "9.0.1",
|
"@fastify/cors": "10.0.0",
|
||||||
"@fastify/express": "3.0.0",
|
"@fastify/express": "4.0.0",
|
||||||
"@fastify/http-proxy": "9.5.0",
|
"@fastify/http-proxy": "10.0.0",
|
||||||
"@fastify/multipart": "8.3.0",
|
"@fastify/multipart": "9.0.0",
|
||||||
"@fastify/static": "7.0.4",
|
"@fastify/static": "8.0.0",
|
||||||
"@fastify/view": "9.1.0",
|
"@fastify/view": "10.0.0",
|
||||||
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
||||||
"@misskey-dev/summaly": "5.1.0",
|
"@misskey-dev/summaly": "5.1.0",
|
||||||
"@napi-rs/canvas": "^0.1.53",
|
"@napi-rs/canvas": "0.1.56",
|
||||||
"@nestjs/common": "10.3.10",
|
"@nestjs/common": "10.4.3",
|
||||||
"@nestjs/core": "10.3.10",
|
"@nestjs/core": "10.4.3",
|
||||||
"@nestjs/testing": "10.3.10",
|
"@nestjs/testing": "10.4.3",
|
||||||
"@peertube/http-signature": "1.7.0",
|
"@peertube/http-signature": "1.7.0",
|
||||||
"@sentry/node": "8.20.0",
|
"@sentry/node": "8.20.0",
|
||||||
"@sentry/profiling-node": "8.20.0",
|
"@sentry/profiling-node": "8.20.0",
|
||||||
|
@ -100,8 +100,8 @@
|
||||||
"async-mutex": "0.5.0",
|
"async-mutex": "0.5.0",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
"body-parser": "1.20.2",
|
"body-parser": "1.20.3",
|
||||||
"bullmq": "5.10.4",
|
"bullmq": "5.13.2",
|
||||||
"cacheable-lookup": "7.0.0",
|
"cacheable-lookup": "7.0.0",
|
||||||
"cbor": "9.0.2",
|
"cbor": "9.0.2",
|
||||||
"chalk": "5.3.0",
|
"chalk": "5.3.0",
|
||||||
|
@ -113,11 +113,11 @@
|
||||||
"date-fns": "2.30.0",
|
"date-fns": "2.30.0",
|
||||||
"deep-email-validator": "0.1.21",
|
"deep-email-validator": "0.1.21",
|
||||||
"fast-xml-parser": "^4.4.0",
|
"fast-xml-parser": "^4.4.0",
|
||||||
"fastify": "4.28.1",
|
"fastify": "5.0.0",
|
||||||
"fastify-multer": "^2.0.3",
|
"fastify-multer": "^2.0.3",
|
||||||
"fastify-raw-body": "4.3.0",
|
"fastify-raw-body": "5.0.0",
|
||||||
"feed": "4.2.2",
|
"feed": "4.2.2",
|
||||||
"file-type": "19.3.0",
|
"file-type": "19.5.0",
|
||||||
"fluent-ffmpeg": "2.1.3",
|
"fluent-ffmpeg": "2.1.3",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"glob": "11.0.0",
|
"glob": "11.0.0",
|
||||||
|
@ -127,16 +127,17 @@
|
||||||
"htmlescape": "1.1.1",
|
"htmlescape": "1.1.1",
|
||||||
"http-link-header": "1.1.3",
|
"http-link-header": "1.1.3",
|
||||||
"ioredis": "5.4.1",
|
"ioredis": "5.4.1",
|
||||||
"ip-cidr": "4.0.1",
|
"ip-cidr": "4.0.2",
|
||||||
"ipaddr.js": "2.2.0",
|
"ipaddr.js": "2.2.0",
|
||||||
"is-svg": "5.0.1",
|
"is-svg": "5.1.0",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"jsdom": "24.1.1",
|
"jsdom": "24.1.1",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"jsonld": "8.3.2",
|
"jsonld": "8.3.2",
|
||||||
"jsrsasign": "11.1.0",
|
"jsrsasign": "11.1.0",
|
||||||
"megalodon": "workspace:*",
|
"megalodon": "workspace:*",
|
||||||
"meilisearch": "0.41.0",
|
"meilisearch": "0.42.0",
|
||||||
|
"juice": "11.0.0",
|
||||||
"microformats-parser": "2.0.2",
|
"microformats-parser": "2.0.2",
|
||||||
"mime-types": "2.1.35",
|
"mime-types": "2.1.35",
|
||||||
"misskey-js": "workspace:*",
|
"misskey-js": "workspace:*",
|
||||||
|
@ -145,24 +146,24 @@
|
||||||
"nanoid": "5.0.7",
|
"nanoid": "5.0.7",
|
||||||
"nested-property": "4.0.0",
|
"nested-property": "4.0.0",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
"nodemailer": "6.9.14",
|
"nodemailer": "6.9.15",
|
||||||
"oauth": "0.10.0",
|
"oauth": "0.10.0",
|
||||||
"oauth2orize": "1.12.0",
|
"oauth2orize": "1.12.0",
|
||||||
"oauth2orize-pkce": "0.1.2",
|
"oauth2orize-pkce": "0.1.2",
|
||||||
"os-utils": "0.0.14",
|
"os-utils": "0.0.14",
|
||||||
"otpauth": "9.3.1",
|
"otpauth": "9.3.2",
|
||||||
"parse5": "7.1.2",
|
"parse5": "7.1.2",
|
||||||
"pg": "8.12.0",
|
"pg": "8.13.0",
|
||||||
"pkce-challenge": "4.1.0",
|
"pkce-challenge": "4.1.0",
|
||||||
"probe-image-size": "7.2.3",
|
"probe-image-size": "7.2.3",
|
||||||
"promise-limit": "2.7.0",
|
"promise-limit": "2.7.0",
|
||||||
"proxy-addr": "^2.0.7",
|
"proxy-addr": "^2.0.7",
|
||||||
"pug": "3.0.3",
|
"pug": "3.0.3",
|
||||||
"punycode": "2.3.1",
|
"punycode": "2.3.1",
|
||||||
"qrcode": "1.5.3",
|
"qrcode": "1.5.4",
|
||||||
"random-seed": "0.3.0",
|
"random-seed": "0.3.0",
|
||||||
"ratelimiter": "3.4.1",
|
"ratelimiter": "3.4.1",
|
||||||
"re2": "1.21.3",
|
"re2": "1.21.4",
|
||||||
"redis-lock": "0.1.4",
|
"redis-lock": "0.1.4",
|
||||||
"reflect-metadata": "0.2.2",
|
"reflect-metadata": "0.2.2",
|
||||||
"rename": "1.0.4",
|
"rename": "1.0.4",
|
||||||
|
@ -170,17 +171,17 @@
|
||||||
"rxjs": "7.8.1",
|
"rxjs": "7.8.1",
|
||||||
"sanitize-html": "2.13.0",
|
"sanitize-html": "2.13.0",
|
||||||
"secure-json-parse": "2.7.0",
|
"secure-json-parse": "2.7.0",
|
||||||
"sharp": "0.33.4",
|
"sharp": "0.33.5",
|
||||||
"slacc": "0.0.10",
|
"slacc": "0.0.10",
|
||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"stringz": "2.1.0",
|
"stringz": "2.1.0",
|
||||||
"systeminformation": "5.22.11",
|
"systeminformation": "5.23.5",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"tmp": "0.2.3",
|
"tmp": "0.2.3",
|
||||||
"tsc-alias": "1.8.10",
|
"tsc-alias": "1.8.10",
|
||||||
"tsconfig-paths": "4.2.0",
|
"tsconfig-paths": "4.2.0",
|
||||||
"typeorm": "0.3.20",
|
"typeorm": "0.3.20",
|
||||||
"typescript": "5.5.4",
|
"typescript": "5.6.2",
|
||||||
"ulid": "2.3.0",
|
"ulid": "2.3.0",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"vary": "1.1.2",
|
"vary": "1.1.2",
|
||||||
|
@ -190,7 +191,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/globals": "29.7.0",
|
"@jest/globals": "29.7.0",
|
||||||
"@nestjs/platform-express": "10.3.10",
|
"@nestjs/platform-express": "10.4.3",
|
||||||
"@simplewebauthn/types": "10.0.0",
|
"@simplewebauthn/types": "10.0.0",
|
||||||
"@swc/jest": "0.2.36",
|
"@swc/jest": "0.2.36",
|
||||||
"@types/accepts": "1.3.7",
|
"@types/accepts": "1.3.7",
|
||||||
|
@ -199,10 +200,10 @@
|
||||||
"@types/body-parser": "1.19.5",
|
"@types/body-parser": "1.19.5",
|
||||||
"@types/color-convert": "2.0.3",
|
"@types/color-convert": "2.0.3",
|
||||||
"@types/content-disposition": "0.5.8",
|
"@types/content-disposition": "0.5.8",
|
||||||
"@types/fluent-ffmpeg": "2.1.24",
|
"@types/fluent-ffmpeg": "2.1.26",
|
||||||
"@types/htmlescape": "1.1.3",
|
"@types/htmlescape": "1.1.3",
|
||||||
"@types/http-link-header": "1.0.7",
|
"@types/http-link-header": "1.0.7",
|
||||||
"@types/jest": "29.5.12",
|
"@types/jest": "29.5.13",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/jsdom": "21.1.7",
|
"@types/jsdom": "21.1.7",
|
||||||
"@types/jsonld": "1.5.15",
|
"@types/jsonld": "1.5.15",
|
||||||
|
@ -210,11 +211,11 @@
|
||||||
"@types/mime-types": "2.1.4",
|
"@types/mime-types": "2.1.4",
|
||||||
"@types/ms": "0.7.34",
|
"@types/ms": "0.7.34",
|
||||||
"@types/node": "20.14.12",
|
"@types/node": "20.14.12",
|
||||||
"@types/nodemailer": "6.4.15",
|
"@types/nodemailer": "6.4.16",
|
||||||
"@types/oauth": "0.9.5",
|
"@types/oauth": "0.9.5",
|
||||||
"@types/oauth2orize": "1.11.5",
|
"@types/oauth2orize": "1.11.5",
|
||||||
"@types/oauth2orize-pkce": "0.1.2",
|
"@types/oauth2orize-pkce": "0.1.2",
|
||||||
"@types/pg": "8.11.6",
|
"@types/pg": "8.11.10",
|
||||||
"@types/proxy-addr": "^2.0.3",
|
"@types/proxy-addr": "^2.0.3",
|
||||||
"@types/pug": "2.0.10",
|
"@types/pug": "2.0.10",
|
||||||
"@types/punycode": "2.1.4",
|
"@types/punycode": "2.1.4",
|
||||||
|
@ -222,7 +223,7 @@
|
||||||
"@types/random-seed": "0.3.5",
|
"@types/random-seed": "0.3.5",
|
||||||
"@types/ratelimiter": "3.4.6",
|
"@types/ratelimiter": "3.4.6",
|
||||||
"@types/rename": "1.0.7",
|
"@types/rename": "1.0.7",
|
||||||
"@types/sanitize-html": "2.11.0",
|
"@types/sanitize-html": "2.13.0",
|
||||||
"@types/semver": "7.5.8",
|
"@types/semver": "7.5.8",
|
||||||
"@types/simple-oauth2": "5.0.7",
|
"@types/simple-oauth2": "5.0.7",
|
||||||
"@types/sinonjs__fake-timers": "8.1.5",
|
"@types/sinonjs__fake-timers": "8.1.5",
|
||||||
|
@ -231,17 +232,17 @@
|
||||||
"@types/uuid": "^9.0.4",
|
"@types/uuid": "^9.0.4",
|
||||||
"@types/vary": "1.1.3",
|
"@types/vary": "1.1.3",
|
||||||
"@types/web-push": "3.6.3",
|
"@types/web-push": "3.6.3",
|
||||||
"@types/ws": "8.5.11",
|
"@types/ws": "8.5.12",
|
||||||
"@typescript-eslint/eslint-plugin": "7.17.0",
|
"@typescript-eslint/eslint-plugin": "7.17.0",
|
||||||
"@typescript-eslint/parser": "7.17.0",
|
"@typescript-eslint/parser": "7.17.0",
|
||||||
"aws-sdk-client-mock": "4.0.1",
|
"aws-sdk-client-mock": "4.0.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint-plugin-import": "2.29.1",
|
"eslint-plugin-import": "2.30.0",
|
||||||
"execa": "9.3.0",
|
"execa": "9.4.0",
|
||||||
"fkill": "9.0.0",
|
"fkill": "9.0.0",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-mock": "29.7.0",
|
"jest-mock": "29.7.0",
|
||||||
"nodemon": "3.1.4",
|
"nodemon": "3.1.7",
|
||||||
"pid-port": "1.0.0",
|
"pid-port": "1.0.0",
|
||||||
"simple-oauth2": "5.1.0"
|
"simple-oauth2": "5.1.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,8 @@ import { createPostgresDataSource } from './postgres.js';
|
||||||
import { RepositoryModule } from './models/RepositoryModule.js';
|
import { RepositoryModule } from './models/RepositoryModule.js';
|
||||||
import { allSettled } from './misc/promise-tracker.js';
|
import { allSettled } from './misc/promise-tracker.js';
|
||||||
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
|
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
import { MiMeta } from '@/models/Meta.js';
|
||||||
|
import { GlobalEvents } from './core/GlobalEventService.js';
|
||||||
|
|
||||||
const $config: Provider = {
|
const $config: Provider = {
|
||||||
provide: DI.config,
|
provide: DI.config,
|
||||||
|
@ -78,11 +80,76 @@ const $redisForTimelines: Provider = {
|
||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const $redisForReactions: Provider = {
|
||||||
|
provide: DI.redisForReactions,
|
||||||
|
useFactory: (config: Config) => {
|
||||||
|
return new Redis.Redis(config.redisForReactions);
|
||||||
|
},
|
||||||
|
inject: [DI.config],
|
||||||
|
};
|
||||||
|
|
||||||
|
const $meta: Provider = {
|
||||||
|
provide: DI.meta,
|
||||||
|
useFactory: async (db: DataSource, redisForSub: Redis.Redis) => {
|
||||||
|
const meta = await db.transaction(async transactionalEntityManager => {
|
||||||
|
// 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する
|
||||||
|
const metas = await transactionalEntityManager.find(MiMeta, {
|
||||||
|
order: {
|
||||||
|
id: 'DESC',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const meta = metas[0];
|
||||||
|
|
||||||
|
if (meta) {
|
||||||
|
return meta;
|
||||||
|
} else {
|
||||||
|
// metaが空のときfetchMetaが同時に呼ばれるとここが同時に呼ばれてしまうことがあるのでフェイルセーフなupsertを使う
|
||||||
|
const saved = await transactionalEntityManager
|
||||||
|
.upsert(
|
||||||
|
MiMeta,
|
||||||
|
{
|
||||||
|
id: 'x',
|
||||||
|
},
|
||||||
|
['id'],
|
||||||
|
)
|
||||||
|
.then((x) => transactionalEntityManager.findOneByOrFail(MiMeta, x.identifiers[0]));
|
||||||
|
|
||||||
|
return saved;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function onMessage(_: string, data: string): Promise<void> {
|
||||||
|
const obj = JSON.parse(data);
|
||||||
|
|
||||||
|
if (obj.channel === 'internal') {
|
||||||
|
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||||
|
switch (type) {
|
||||||
|
case 'metaUpdated': {
|
||||||
|
for (const key in body.after) {
|
||||||
|
(meta as any)[key] = (body.after as any)[key];
|
||||||
|
}
|
||||||
|
meta.proxyAccount = null; // joinなカラムは通常取ってこないので
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redisForSub.on('message', onMessage);
|
||||||
|
|
||||||
|
return meta;
|
||||||
|
},
|
||||||
|
inject: [DI.db, DI.redisForSub],
|
||||||
|
};
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
imports: [RepositoryModule],
|
imports: [RepositoryModule],
|
||||||
providers: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines],
|
providers: [$config, $db, $meta, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions],
|
||||||
exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, RepositoryModule],
|
exports: [$config, $db, $meta, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions, RepositoryModule],
|
||||||
})
|
})
|
||||||
export class GlobalModule implements OnApplicationShutdown {
|
export class GlobalModule implements OnApplicationShutdown {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -91,6 +158,7 @@ export class GlobalModule implements OnApplicationShutdown {
|
||||||
@Inject(DI.redisForPub) private redisForPub: Redis.Redis,
|
@Inject(DI.redisForPub) private redisForPub: Redis.Redis,
|
||||||
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
|
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
|
||||||
@Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis,
|
@Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis,
|
||||||
|
@Inject(DI.redisForReactions) private redisForReactions: Redis.Redis,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
public async dispose(): Promise<void> {
|
public async dispose(): Promise<void> {
|
||||||
|
@ -103,6 +171,7 @@ export class GlobalModule implements OnApplicationShutdown {
|
||||||
this.redisForPub.disconnect(),
|
this.redisForPub.disconnect(),
|
||||||
this.redisForSub.disconnect(),
|
this.redisForSub.disconnect(),
|
||||||
this.redisForTimelines.disconnect(),
|
this.redisForTimelines.disconnect(),
|
||||||
|
this.redisForReactions.disconnect(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,7 @@ type Source = {
|
||||||
redisForPubsub?: RedisOptionsSource;
|
redisForPubsub?: RedisOptionsSource;
|
||||||
redisForJobQueue?: RedisOptionsSource;
|
redisForJobQueue?: RedisOptionsSource;
|
||||||
redisForTimelines?: RedisOptionsSource;
|
redisForTimelines?: RedisOptionsSource;
|
||||||
|
redisForReactions?: RedisOptionsSource;
|
||||||
meilisearch?: {
|
meilisearch?: {
|
||||||
host: string;
|
host: string;
|
||||||
port: string;
|
port: string;
|
||||||
|
@ -146,7 +147,7 @@ export type Config = {
|
||||||
proxySmtp: string | undefined;
|
proxySmtp: string | undefined;
|
||||||
proxyBypassHosts: string[] | undefined;
|
proxyBypassHosts: string[] | undefined;
|
||||||
allowedPrivateNetworks: string[] | undefined;
|
allowedPrivateNetworks: string[] | undefined;
|
||||||
maxFileSize: number | undefined;
|
maxFileSize: number;
|
||||||
maxNoteLength: number;
|
maxNoteLength: number;
|
||||||
clusterLimit: number | undefined;
|
clusterLimit: number | undefined;
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -177,8 +178,10 @@ export type Config = {
|
||||||
authUrl: string;
|
authUrl: string;
|
||||||
driveUrl: string;
|
driveUrl: string;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
clientEntry: string;
|
frontendEntry: string;
|
||||||
clientManifestExists: boolean;
|
frontendManifestExists: boolean;
|
||||||
|
frontendEmbedEntry: string;
|
||||||
|
frontendEmbedManifestExists: boolean;
|
||||||
mediaProxy: string;
|
mediaProxy: string;
|
||||||
externalMediaProxyEnabled: boolean;
|
externalMediaProxyEnabled: boolean;
|
||||||
videoThumbnailGenerator: string | null;
|
videoThumbnailGenerator: string | null;
|
||||||
|
@ -186,6 +189,7 @@ export type Config = {
|
||||||
redisForPubsub: RedisOptions & RedisOptionsSource;
|
redisForPubsub: RedisOptions & RedisOptionsSource;
|
||||||
redisForJobQueue: RedisOptions & RedisOptionsSource;
|
redisForJobQueue: RedisOptions & RedisOptionsSource;
|
||||||
redisForTimelines: RedisOptions & RedisOptionsSource;
|
redisForTimelines: RedisOptions & RedisOptionsSource;
|
||||||
|
redisForReactions: RedisOptions & RedisOptionsSource;
|
||||||
sentryForBackend: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; } | undefined;
|
sentryForBackend: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; } | undefined;
|
||||||
sentryForFrontend: { options: Partial<Sentry.NodeOptions> } | undefined;
|
sentryForFrontend: { options: Partial<Sentry.NodeOptions> } | undefined;
|
||||||
perChannelMaxNoteCacheCount: number;
|
perChannelMaxNoteCacheCount: number;
|
||||||
|
@ -219,10 +223,15 @@ const path = process.env.MISSKEY_CONFIG_YML
|
||||||
|
|
||||||
export function loadConfig(): Config {
|
export function loadConfig(): Config {
|
||||||
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8'));
|
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8'));
|
||||||
const clientManifestExists = fs.existsSync(`${_dirname}/../../../built/_vite_/manifest.json`);
|
|
||||||
const clientManifest = clientManifestExists ?
|
const frontendManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_vite_/manifest.json');
|
||||||
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8'))
|
const frontendEmbedManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_embed_vite_/manifest.json');
|
||||||
|
const frontendManifest = frontendManifestExists ?
|
||||||
|
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_vite_/manifest.json`, 'utf-8'))
|
||||||
: { 'src/_boot_.ts': { file: 'src/_boot_.ts' } };
|
: { 'src/_boot_.ts': { file: 'src/_boot_.ts' } };
|
||||||
|
const frontendEmbedManifest = frontendEmbedManifestExists ?
|
||||||
|
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_embed_vite_/manifest.json`, 'utf-8'))
|
||||||
|
: { 'src/boot.ts': { file: 'src/boot.ts' } };
|
||||||
|
|
||||||
const configFiles = globSync(path).sort();
|
const configFiles = globSync(path).sort();
|
||||||
|
|
||||||
|
@ -282,6 +291,7 @@ export function loadConfig(): Config {
|
||||||
redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis,
|
redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis,
|
||||||
redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis,
|
redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis,
|
||||||
redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,
|
redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,
|
||||||
|
redisForReactions: config.redisForReactions ? convertRedisOptions(config.redisForReactions, host) : redis,
|
||||||
sentryForBackend: config.sentryForBackend,
|
sentryForBackend: config.sentryForBackend,
|
||||||
sentryForFrontend: config.sentryForFrontend,
|
sentryForFrontend: config.sentryForFrontend,
|
||||||
id: config.id,
|
id: config.id,
|
||||||
|
@ -289,7 +299,7 @@ export function loadConfig(): Config {
|
||||||
proxySmtp: config.proxySmtp,
|
proxySmtp: config.proxySmtp,
|
||||||
proxyBypassHosts: config.proxyBypassHosts,
|
proxyBypassHosts: config.proxyBypassHosts,
|
||||||
allowedPrivateNetworks: config.allowedPrivateNetworks,
|
allowedPrivateNetworks: config.allowedPrivateNetworks,
|
||||||
maxFileSize: config.maxFileSize,
|
maxFileSize: config.maxFileSize ?? 262144000,
|
||||||
maxNoteLength: config.maxNoteLength ?? 3000,
|
maxNoteLength: config.maxNoteLength ?? 3000,
|
||||||
clusterLimit: config.clusterLimit,
|
clusterLimit: config.clusterLimit,
|
||||||
outgoingAddress: config.outgoingAddress,
|
outgoingAddress: config.outgoingAddress,
|
||||||
|
@ -313,8 +323,10 @@ export function loadConfig(): Config {
|
||||||
config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator
|
config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator
|
||||||
: null,
|
: null,
|
||||||
userAgent: `Misskey/${version} (${config.url})`,
|
userAgent: `Misskey/${version} (${config.url})`,
|
||||||
clientEntry: clientManifest['src/_boot_.ts'],
|
frontendEntry: frontendManifest['src/_boot_.ts'],
|
||||||
clientManifestExists: clientManifestExists,
|
frontendManifestExists: frontendManifestExists,
|
||||||
|
frontendEmbedEntry: frontendEmbedManifest['src/boot.ts'],
|
||||||
|
frontendEmbedManifestExists: frontendEmbedManifestExists,
|
||||||
perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000,
|
perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000,
|
||||||
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500,
|
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500,
|
||||||
deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
|
deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
|
||||||
|
@ -455,7 +467,7 @@ function applyEnvOverrides(config: Source) {
|
||||||
_apply_top(['db', ['host', 'port', 'db', 'user', 'pass', 'disableCache']]);
|
_apply_top(['db', ['host', 'port', 'db', 'user', 'pass', 'disableCache']]);
|
||||||
_apply_top(['dbSlaves', Array.from((config.dbSlaves ?? []).keys()), ['host', 'port', 'db', 'user', 'pass']]);
|
_apply_top(['dbSlaves', Array.from((config.dbSlaves ?? []).keys()), ['host', 'port', 'db', 'user', 'pass']]);
|
||||||
_apply_top([
|
_apply_top([
|
||||||
['redis', 'redisForPubsub', 'redisForJobQueue', 'redisForTimelines'],
|
['redis', 'redisForPubsub', 'redisForJobQueue', 'redisForTimelines', 'redisForReactions'],
|
||||||
['host', 'port', 'username', 'pass', 'db', 'prefix'],
|
['host', 'port', 'username', 'pass', 'db', 'prefix'],
|
||||||
]);
|
]);
|
||||||
_apply_top(['meilisearch', ['host', 'port', 'apikey', 'ssl', 'index', 'scope']]);
|
_apply_top(['meilisearch', ['host', 'port', 'apikey', 'ssl', 'index', 'scope']]);
|
||||||
|
|
|
@ -8,6 +8,8 @@ export const MAX_NOTE_TEXT_LENGTH = 3000;
|
||||||
export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min
|
export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min
|
||||||
export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days
|
export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days
|
||||||
|
|
||||||
|
export const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
|
||||||
|
|
||||||
//#region hard limits
|
//#region hard limits
|
||||||
// If you change DB_* values, you must also change the DB schema.
|
// If you change DB_* values, you must also change the DB schema.
|
||||||
|
|
||||||
|
|
|
@ -14,10 +14,10 @@ import type {
|
||||||
AbuseReportNotificationRecipientRepository,
|
AbuseReportNotificationRecipientRepository,
|
||||||
MiAbuseReportNotificationRecipient,
|
MiAbuseReportNotificationRecipient,
|
||||||
MiAbuseUserReport,
|
MiAbuseUserReport,
|
||||||
|
MiMeta,
|
||||||
MiUser,
|
MiUser,
|
||||||
} from '@/models/_.js';
|
} from '@/models/_.js';
|
||||||
import { EmailService } from '@/core/EmailService.js';
|
import { EmailService } from '@/core/EmailService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
|
import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
@ -27,15 +27,19 @@ import { IdService } from './IdService.js';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AbuseReportNotificationService implements OnApplicationShutdown {
|
export class AbuseReportNotificationService implements OnApplicationShutdown {
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
|
|
||||||
@Inject(DI.abuseReportNotificationRecipientRepository)
|
@Inject(DI.abuseReportNotificationRecipientRepository)
|
||||||
private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository,
|
private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository,
|
||||||
|
|
||||||
@Inject(DI.redisForSub)
|
@Inject(DI.redisForSub)
|
||||||
private redisForSub: Redis.Redis,
|
private redisForSub: Redis.Redis,
|
||||||
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
private systemWebhookService: SystemWebhookService,
|
private systemWebhookService: SystemWebhookService,
|
||||||
private emailService: EmailService,
|
private emailService: EmailService,
|
||||||
private metaService: MetaService,
|
|
||||||
private moderationLogService: ModerationLogService,
|
private moderationLogService: ModerationLogService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
|
@ -93,10 +97,8 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
|
||||||
.filter(x => x != null),
|
.filter(x => x != null),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 送信先の鮮度を保つため、毎回取得する
|
|
||||||
const meta = await this.metaService.fetch(true);
|
|
||||||
recipientEMailAddresses.push(
|
recipientEMailAddresses.push(
|
||||||
...(meta.email ? [meta.email] : []),
|
...(this.meta.email ? [this.meta.email] : []),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (recipientEMailAddresses.length <= 0) {
|
if (recipientEMailAddresses.length <= 0) {
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { IsNull, In, MoreThan, Not } from 'typeorm';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
||||||
import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js';
|
import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MiMeta, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js';
|
||||||
import type { RelationshipJobData, ThinUser } from '@/queue/types.js';
|
import type { RelationshipJobData, ThinUser } from '@/queue/types.js';
|
||||||
|
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
@ -22,13 +22,15 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { ProxyAccountService } from '@/core/ProxyAccountService.js';
|
import { ProxyAccountService } from '@/core/ProxyAccountService.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import InstanceChart from '@/core/chart/charts/instance.js';
|
import InstanceChart from '@/core/chart/charts/instance.js';
|
||||||
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
|
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AccountMoveService {
|
export class AccountMoveService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@ -57,7 +59,6 @@ export class AccountMoveService {
|
||||||
private perUserFollowingChart: PerUserFollowingChart,
|
private perUserFollowingChart: PerUserFollowingChart,
|
||||||
private federatedInstanceService: FederatedInstanceService,
|
private federatedInstanceService: FederatedInstanceService,
|
||||||
private instanceChart: InstanceChart,
|
private instanceChart: InstanceChart,
|
||||||
private metaService: MetaService,
|
|
||||||
private relayService: RelayService,
|
private relayService: RelayService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
) {
|
) {
|
||||||
|
@ -276,7 +277,7 @@ export class AccountMoveService {
|
||||||
if (this.userEntityService.isRemoteUser(oldAccount)) {
|
if (this.userEntityService.isRemoteUser(oldAccount)) {
|
||||||
this.federatedInstanceService.fetch(oldAccount.host).then(async i => {
|
this.federatedInstanceService.fetch(oldAccount.host).then(async i => {
|
||||||
this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length);
|
this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length);
|
||||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
if (this.meta.enableChartsForFederatedInstances) {
|
||||||
this.instanceChart.updateFollowers(i.host, false);
|
this.instanceChart.updateFollowers(i.host, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -123,11 +123,14 @@ export class AntennaService implements OnApplicationShutdown {
|
||||||
if (antenna.src === 'home') {
|
if (antenna.src === 'home') {
|
||||||
// TODO
|
// TODO
|
||||||
} else if (antenna.src === 'list') {
|
} else if (antenna.src === 'list') {
|
||||||
const listUsers = (await this.userListMembershipsRepository.findBy({
|
if (antenna.userListId == null) return false;
|
||||||
userListId: antenna.userListId!,
|
const exists = await this.userListMembershipsRepository.exists({
|
||||||
})).map(x => x.userId);
|
where: {
|
||||||
|
userListId: antenna.userListId,
|
||||||
if (!listUsers.includes(note.userId)) return false;
|
userId: note.userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!exists) return false;
|
||||||
} else if (antenna.src === 'users') {
|
} else if (antenna.src === 'users') {
|
||||||
const accts = antenna.users.map(x => {
|
const accts = antenna.users.map(x => {
|
||||||
const { username, host } = Acct.parse(x);
|
const { username, host } = Acct.parse(x);
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
|
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
|
||||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||||
import { UserSearchService } from '@/core/UserSearchService.js';
|
import { UserSearchService } from '@/core/UserSearchService.js';
|
||||||
|
import { WebhookTestService } from '@/core/WebhookTestService.js';
|
||||||
import { AccountMoveService } from './AccountMoveService.js';
|
import { AccountMoveService } from './AccountMoveService.js';
|
||||||
import { AccountUpdateService } from './AccountUpdateService.js';
|
import { AccountUpdateService } from './AccountUpdateService.js';
|
||||||
import { AnnouncementService } from './AnnouncementService.js';
|
import { AnnouncementService } from './AnnouncementService.js';
|
||||||
|
@ -49,6 +50,7 @@ import { PollService } from './PollService.js';
|
||||||
import { PushNotificationService } from './PushNotificationService.js';
|
import { PushNotificationService } from './PushNotificationService.js';
|
||||||
import { QueryService } from './QueryService.js';
|
import { QueryService } from './QueryService.js';
|
||||||
import { ReactionService } from './ReactionService.js';
|
import { ReactionService } from './ReactionService.js';
|
||||||
|
import { ReactionsBufferingService } from './ReactionsBufferingService.js';
|
||||||
import { RelayService } from './RelayService.js';
|
import { RelayService } from './RelayService.js';
|
||||||
import { RoleService } from './RoleService.js';
|
import { RoleService } from './RoleService.js';
|
||||||
import { S3Service } from './S3Service.js';
|
import { S3Service } from './S3Service.js';
|
||||||
|
@ -193,6 +195,7 @@ const $ProxyAccountService: Provider = { provide: 'ProxyAccountService', useExis
|
||||||
const $PushNotificationService: Provider = { provide: 'PushNotificationService', useExisting: PushNotificationService };
|
const $PushNotificationService: Provider = { provide: 'PushNotificationService', useExisting: PushNotificationService };
|
||||||
const $QueryService: Provider = { provide: 'QueryService', useExisting: QueryService };
|
const $QueryService: Provider = { provide: 'QueryService', useExisting: QueryService };
|
||||||
const $ReactionService: Provider = { provide: 'ReactionService', useExisting: ReactionService };
|
const $ReactionService: Provider = { provide: 'ReactionService', useExisting: ReactionService };
|
||||||
|
const $ReactionsBufferingService: Provider = { provide: 'ReactionsBufferingService', useExisting: ReactionsBufferingService };
|
||||||
const $RelayService: Provider = { provide: 'RelayService', useExisting: RelayService };
|
const $RelayService: Provider = { provide: 'RelayService', useExisting: RelayService };
|
||||||
const $RoleService: Provider = { provide: 'RoleService', useExisting: RoleService };
|
const $RoleService: Provider = { provide: 'RoleService', useExisting: RoleService };
|
||||||
const $S3Service: Provider = { provide: 'S3Service', useExisting: S3Service };
|
const $S3Service: Provider = { provide: 'S3Service', useExisting: S3Service };
|
||||||
|
@ -212,6 +215,7 @@ const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: Us
|
||||||
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
|
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
|
||||||
const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
|
const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
|
||||||
const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
|
const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
|
||||||
|
const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService };
|
||||||
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
|
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
|
||||||
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
|
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
|
||||||
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
|
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
|
||||||
|
@ -343,6 +347,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
|
||||||
PushNotificationService,
|
PushNotificationService,
|
||||||
QueryService,
|
QueryService,
|
||||||
ReactionService,
|
ReactionService,
|
||||||
|
ReactionsBufferingService,
|
||||||
RelayService,
|
RelayService,
|
||||||
RoleService,
|
RoleService,
|
||||||
S3Service,
|
S3Service,
|
||||||
|
@ -362,6 +367,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
|
||||||
VideoProcessingService,
|
VideoProcessingService,
|
||||||
UserWebhookService,
|
UserWebhookService,
|
||||||
SystemWebhookService,
|
SystemWebhookService,
|
||||||
|
WebhookTestService,
|
||||||
UtilityService,
|
UtilityService,
|
||||||
FileInfoService,
|
FileInfoService,
|
||||||
SearchService,
|
SearchService,
|
||||||
|
@ -489,6 +495,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
|
||||||
$PushNotificationService,
|
$PushNotificationService,
|
||||||
$QueryService,
|
$QueryService,
|
||||||
$ReactionService,
|
$ReactionService,
|
||||||
|
$ReactionsBufferingService,
|
||||||
$RelayService,
|
$RelayService,
|
||||||
$RoleService,
|
$RoleService,
|
||||||
$S3Service,
|
$S3Service,
|
||||||
|
@ -508,6 +515,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
|
||||||
$VideoProcessingService,
|
$VideoProcessingService,
|
||||||
$UserWebhookService,
|
$UserWebhookService,
|
||||||
$SystemWebhookService,
|
$SystemWebhookService,
|
||||||
|
$WebhookTestService,
|
||||||
$UtilityService,
|
$UtilityService,
|
||||||
$FileInfoService,
|
$FileInfoService,
|
||||||
$SearchService,
|
$SearchService,
|
||||||
|
@ -636,6 +644,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
|
||||||
PushNotificationService,
|
PushNotificationService,
|
||||||
QueryService,
|
QueryService,
|
||||||
ReactionService,
|
ReactionService,
|
||||||
|
ReactionsBufferingService,
|
||||||
RelayService,
|
RelayService,
|
||||||
RoleService,
|
RoleService,
|
||||||
S3Service,
|
S3Service,
|
||||||
|
@ -655,6 +664,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
|
||||||
VideoProcessingService,
|
VideoProcessingService,
|
||||||
UserWebhookService,
|
UserWebhookService,
|
||||||
SystemWebhookService,
|
SystemWebhookService,
|
||||||
|
WebhookTestService,
|
||||||
UtilityService,
|
UtilityService,
|
||||||
FileInfoService,
|
FileInfoService,
|
||||||
SearchService,
|
SearchService,
|
||||||
|
@ -781,6 +791,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
|
||||||
$PushNotificationService,
|
$PushNotificationService,
|
||||||
$QueryService,
|
$QueryService,
|
||||||
$ReactionService,
|
$ReactionService,
|
||||||
|
$ReactionsBufferingService,
|
||||||
$RelayService,
|
$RelayService,
|
||||||
$RoleService,
|
$RoleService,
|
||||||
$S3Service,
|
$S3Service,
|
||||||
|
@ -800,6 +811,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
|
||||||
$VideoProcessingService,
|
$VideoProcessingService,
|
||||||
$UserWebhookService,
|
$UserWebhookService,
|
||||||
$SystemWebhookService,
|
$SystemWebhookService,
|
||||||
|
$WebhookTestService,
|
||||||
$UtilityService,
|
$UtilityService,
|
||||||
$FileInfoService,
|
$FileInfoService,
|
||||||
$SearchService,
|
$SearchService,
|
||||||
|
|
|
@ -42,7 +42,7 @@ export class DownloadService {
|
||||||
|
|
||||||
const timeout = options.timeout ?? 30 * 1000;
|
const timeout = options.timeout ?? 30 * 1000;
|
||||||
const operationTimeout = options.operationTimeout ?? 60 * 1000;
|
const operationTimeout = options.operationTimeout ?? 60 * 1000;
|
||||||
const maxSize = options.maxSize ?? this.config.maxFileSize ?? 262144000;
|
const maxSize = options.maxSize ?? this.config.maxFileSize;
|
||||||
|
|
||||||
const urlObj = new URL(url);
|
const urlObj = new URL(url);
|
||||||
let filename = urlObj.pathname.split('/').pop() ?? 'untitled';
|
let filename = urlObj.pathname.split('/').pop() ?? 'untitled';
|
||||||
|
|
|
@ -11,11 +11,10 @@ import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3';
|
import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/_.js';
|
import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository, MiMeta } from '@/models/_.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import Logger from '@/logger.js';
|
import Logger from '@/logger.js';
|
||||||
import type { MiRemoteUser, MiUser } from '@/models/User.js';
|
import type { MiRemoteUser, MiUser } from '@/models/User.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { MiDriveFile } from '@/models/DriveFile.js';
|
import { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||||
|
@ -99,6 +98,9 @@ export class DriveService {
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@ -115,7 +117,6 @@ export class DriveService {
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private driveFileEntityService: DriveFileEntityService,
|
private driveFileEntityService: DriveFileEntityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private metaService: MetaService,
|
|
||||||
private downloadService: DownloadService,
|
private downloadService: DownloadService,
|
||||||
private internalStorageService: InternalStorageService,
|
private internalStorageService: InternalStorageService,
|
||||||
private s3Service: S3Service,
|
private s3Service: S3Service,
|
||||||
|
@ -149,9 +150,7 @@ export class DriveService {
|
||||||
// thunbnail, webpublic を必要なら生成
|
// thunbnail, webpublic を必要なら生成
|
||||||
const alts = await this.generateAlts(path, type, !file.uri);
|
const alts = await this.generateAlts(path, type, !file.uri);
|
||||||
|
|
||||||
const meta = await this.metaService.fetch();
|
if (this.meta.useObjectStorage) {
|
||||||
|
|
||||||
if (meta.useObjectStorage) {
|
|
||||||
//#region ObjectStorage params
|
//#region ObjectStorage params
|
||||||
let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) ?? ['']);
|
let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) ?? ['']);
|
||||||
|
|
||||||
|
@ -170,11 +169,11 @@ export class DriveService {
|
||||||
ext = '';
|
ext = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseUrl = meta.objectStorageBaseUrl
|
const baseUrl = this.meta.objectStorageBaseUrl
|
||||||
?? `${ meta.objectStorageUseSSL ? 'https' : 'http' }://${ meta.objectStorageEndpoint }${ meta.objectStoragePort ? `:${meta.objectStoragePort}` : '' }/${ meta.objectStorageBucket }`;
|
?? `${ this.meta.objectStorageUseSSL ? 'https' : 'http' }://${ this.meta.objectStorageEndpoint }${ this.meta.objectStoragePort ? `:${this.meta.objectStoragePort}` : '' }/${ this.meta.objectStorageBucket }`;
|
||||||
|
|
||||||
// for original
|
// for original
|
||||||
const key = `${meta.objectStoragePrefix}/${randomUUID()}${ext}`;
|
const key = `${this.meta.objectStoragePrefix}/${randomUUID()}${ext}`;
|
||||||
const url = `${ baseUrl }/${ key }`;
|
const url = `${ baseUrl }/${ key }`;
|
||||||
|
|
||||||
// for alts
|
// for alts
|
||||||
|
@ -191,7 +190,7 @@ export class DriveService {
|
||||||
];
|
];
|
||||||
|
|
||||||
if (alts.webpublic) {
|
if (alts.webpublic) {
|
||||||
webpublicKey = `${meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`;
|
webpublicKey = `${this.meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`;
|
||||||
webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
|
webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
|
||||||
|
|
||||||
this.registerLogger.info(`uploading webpublic: ${webpublicKey}`);
|
this.registerLogger.info(`uploading webpublic: ${webpublicKey}`);
|
||||||
|
@ -199,7 +198,7 @@ export class DriveService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (alts.thumbnail) {
|
if (alts.thumbnail) {
|
||||||
thumbnailKey = `${meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`;
|
thumbnailKey = `${this.meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`;
|
||||||
thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
|
thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
|
||||||
|
|
||||||
this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`);
|
this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`);
|
||||||
|
@ -376,10 +375,8 @@ export class DriveService {
|
||||||
if (type === 'image/apng') type = 'image/png';
|
if (type === 'image/apng') type = 'image/png';
|
||||||
if (!FILE_TYPE_BROWSERSAFE.includes(type)) type = 'application/octet-stream';
|
if (!FILE_TYPE_BROWSERSAFE.includes(type)) type = 'application/octet-stream';
|
||||||
|
|
||||||
const meta = await this.metaService.fetch();
|
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: meta.objectStorageBucket,
|
Bucket: this.meta.objectStorageBucket,
|
||||||
Key: key,
|
Key: key,
|
||||||
Body: stream,
|
Body: stream,
|
||||||
ContentType: type,
|
ContentType: type,
|
||||||
|
@ -392,9 +389,9 @@ export class DriveService {
|
||||||
// 許可されているファイル形式でしか拡張子をつけない
|
// 許可されているファイル形式でしか拡張子をつけない
|
||||||
ext ? correctFilename(filename, ext) : filename,
|
ext ? correctFilename(filename, ext) : filename,
|
||||||
);
|
);
|
||||||
if (meta.objectStorageSetPublicRead) params.ACL = 'public-read';
|
if (this.meta.objectStorageSetPublicRead) params.ACL = 'public-read';
|
||||||
|
|
||||||
await this.s3Service.upload(meta, params)
|
await this.s3Service.upload(this.meta, params)
|
||||||
.then(
|
.then(
|
||||||
result => {
|
result => {
|
||||||
if ('Bucket' in result) { // CompleteMultipartUploadCommandOutput
|
if ('Bucket' in result) { // CompleteMultipartUploadCommandOutput
|
||||||
|
@ -463,9 +460,7 @@ export class DriveService {
|
||||||
requestHeaders = null,
|
requestHeaders = null,
|
||||||
ext = null,
|
ext = null,
|
||||||
}: AddFileArgs): Promise<MiDriveFile> {
|
}: AddFileArgs): Promise<MiDriveFile> {
|
||||||
const instance = await this.metaService.fetch();
|
|
||||||
const userRoleNSFW = user && (await this.roleService.getUserPolicies(user.id)).alwaysMarkNsfw;
|
const userRoleNSFW = user && (await this.roleService.getUserPolicies(user.id)).alwaysMarkNsfw;
|
||||||
|
|
||||||
const info = await this.fileInfoService.getFileInfo(path);
|
const info = await this.fileInfoService.getFileInfo(path);
|
||||||
this.registerLogger.info(`${JSON.stringify(info)}`);
|
this.registerLogger.info(`${JSON.stringify(info)}`);
|
||||||
|
|
||||||
|
@ -569,7 +564,7 @@ export class DriveService {
|
||||||
sensitive ?? false
|
sensitive ?? false
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
if (user && this.utilityService.isMediaSilencedHost(instance.mediaSilencedHosts, user.host)) file.isSensitive = true;
|
if (user && this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) file.isSensitive = true;
|
||||||
if (info.sensitive && profile!.autoSensitive) file.isSensitive = true;
|
if (info.sensitive && profile!.autoSensitive) file.isSensitive = true;
|
||||||
if (userRoleNSFW) file.isSensitive = true;
|
if (userRoleNSFW) file.isSensitive = true;
|
||||||
|
|
||||||
|
@ -631,7 +626,7 @@ export class DriveService {
|
||||||
// ローカルユーザーのみ
|
// ローカルユーザーのみ
|
||||||
this.perUserDriveChart.update(file, true);
|
this.perUserDriveChart.update(file, true);
|
||||||
} else {
|
} else {
|
||||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
if (this.meta.enableChartsForFederatedInstances) {
|
||||||
this.instanceChart.updateDrive(file, true);
|
this.instanceChart.updateDrive(file, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -778,7 +773,7 @@ export class DriveService {
|
||||||
// ローカルユーザーのみ
|
// ローカルユーザーのみ
|
||||||
this.perUserDriveChart.update(file, false);
|
this.perUserDriveChart.update(file, false);
|
||||||
} else {
|
} else {
|
||||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
if (this.meta.enableChartsForFederatedInstances) {
|
||||||
this.instanceChart.updateDrive(file, false);
|
this.instanceChart.updateDrive(file, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -800,14 +795,13 @@ export class DriveService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async deleteObjectStorageFile(key: string) {
|
public async deleteObjectStorageFile(key: string) {
|
||||||
const meta = await this.metaService.fetch();
|
|
||||||
try {
|
try {
|
||||||
const param = {
|
const param = {
|
||||||
Bucket: meta.objectStorageBucket,
|
Bucket: this.meta.objectStorageBucket,
|
||||||
Key: key,
|
Key: key,
|
||||||
} as DeleteObjectCommandInput;
|
} as DeleteObjectCommandInput;
|
||||||
|
|
||||||
await this.s3Service.delete(meta, param);
|
await this.s3Service.delete(this.meta, param);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.name === 'NoSuchKey') {
|
if (err.name === 'NoSuchKey') {
|
||||||
this.deleteLogger.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err as Error);
|
this.deleteLogger.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err as Error);
|
||||||
|
|
|
@ -5,18 +5,17 @@
|
||||||
|
|
||||||
import { URLSearchParams } from 'node:url';
|
import { URLSearchParams } from 'node:url';
|
||||||
import * as nodemailer from 'nodemailer';
|
import * as nodemailer from 'nodemailer';
|
||||||
|
import juice from 'juice';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { validate as validateEmail } from 'deep-email-validator';
|
import { validate as validateEmail } from 'deep-email-validator';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.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 type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import type { UserProfilesRepository } from '@/models/_.js';
|
import type { MiMeta, UserProfilesRepository } from '@/models/_.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EmailService {
|
export class EmailService {
|
||||||
|
@ -26,49 +25,41 @@ export class EmailService {
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
|
|
||||||
@Inject(DI.userProfilesRepository)
|
@Inject(DI.userProfilesRepository)
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
|
||||||
private metaService: MetaService,
|
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private httpRequestService: HttpRequestService,
|
private httpRequestService: HttpRequestService,
|
||||||
private queueService: QueueService,
|
|
||||||
) {
|
) {
|
||||||
this.logger = this.loggerService.getLogger('email');
|
this.logger = this.loggerService.getLogger('email');
|
||||||
}
|
}
|
||||||
|
|
||||||
@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);
|
if (!this.meta.enableEmail) return;
|
||||||
|
|
||||||
if (!meta.enableEmail) return;
|
|
||||||
|
|
||||||
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 = this.meta.smtpUser != null && this.meta.smtpUser !== '';
|
||||||
|
|
||||||
const transporter = nodemailer.createTransport({
|
const transporter = nodemailer.createTransport({
|
||||||
host: meta.smtpHost,
|
host: this.meta.smtpHost,
|
||||||
port: meta.smtpPort,
|
port: this.meta.smtpPort,
|
||||||
secure: meta.smtpSecure,
|
secure: this.meta.smtpSecure,
|
||||||
ignoreTLS: !enableAuth,
|
ignoreTLS: !enableAuth,
|
||||||
proxy: this.config.proxySmtp,
|
proxy: this.config.proxySmtp,
|
||||||
auth: enableAuth ? {
|
auth: enableAuth ? {
|
||||||
user: meta.smtpUser,
|
user: this.meta.smtpUser,
|
||||||
pass: meta.smtpPass,
|
pass: this.meta.smtpPass,
|
||||||
} : undefined,
|
} : undefined,
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
try {
|
const htmlContent = `<!doctype html>
|
||||||
// TODO: htmlサニタイズ
|
|
||||||
const info = await transporter.sendMail({
|
|
||||||
from: meta.email!,
|
|
||||||
to: to,
|
|
||||||
subject: subject,
|
|
||||||
text: text,
|
|
||||||
html: `<!doctype html>
|
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
@ -133,7 +124,7 @@ export class EmailService {
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
<header>
|
<header>
|
||||||
<img src="${ meta.logoImageUrl ?? meta.iconUrl ?? iconUrl }"/>
|
<img src="${ this.meta.logoImageUrl ?? this.meta.iconUrl ?? iconUrl }"/>
|
||||||
</header>
|
</header>
|
||||||
<article>
|
<article>
|
||||||
<h1>${ subject }</h1>
|
<h1>${ subject }</h1>
|
||||||
|
@ -147,7 +138,18 @@ export class EmailService {
|
||||||
<a href="${ this.config.url }">${ this.config.host }</a>
|
<a href="${ this.config.url }">${ this.config.host }</a>
|
||||||
</nav>
|
</nav>
|
||||||
</body>
|
</body>
|
||||||
</html>`,
|
</html>`;
|
||||||
|
|
||||||
|
const inlinedHtml = juice(htmlContent);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// TODO: htmlサニタイズ
|
||||||
|
const info = await transporter.sendMail({
|
||||||
|
from: this.meta.email!,
|
||||||
|
to: to,
|
||||||
|
subject: subject,
|
||||||
|
text: text,
|
||||||
|
html: inlinedHtml,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.info(`Message sent: ${info.messageId}`);
|
this.logger.info(`Message sent: ${info.messageId}`);
|
||||||
|
@ -162,8 +164,6 @@ export class EmailService {
|
||||||
available: boolean;
|
available: boolean;
|
||||||
reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned' | 'network' | 'blacklist';
|
reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned' | 'network' | 'blacklist';
|
||||||
}> {
|
}> {
|
||||||
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,
|
||||||
|
@ -181,11 +181,11 @@ export class EmailService {
|
||||||
reason?: string | null,
|
reason?: string | null,
|
||||||
} = { valid: true, reason: null };
|
} = { valid: true, reason: null };
|
||||||
|
|
||||||
if (meta.enableActiveEmailValidation) {
|
if (this.meta.enableActiveEmailValidation) {
|
||||||
if (meta.enableVerifymailApi && meta.verifymailAuthKey != null) {
|
if (this.meta.enableVerifymailApi && this.meta.verifymailAuthKey != null) {
|
||||||
validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey);
|
validated = await this.verifyMail(emailAddress, this.meta.verifymailAuthKey);
|
||||||
} else if (meta.enableTruemailApi && meta.truemailInstance && meta.truemailAuthKey != null) {
|
} else if (this.meta.enableTruemailApi && this.meta.truemailInstance && this.meta.truemailAuthKey != null) {
|
||||||
validated = await this.trueMail(meta.truemailInstance, emailAddress, meta.truemailAuthKey);
|
validated = await this.trueMail(this.meta.truemailInstance, emailAddress, this.meta.truemailAuthKey);
|
||||||
} else {
|
} else {
|
||||||
validated = await validateEmail({
|
validated = await validateEmail({
|
||||||
email: emailAddress,
|
email: emailAddress,
|
||||||
|
@ -215,7 +215,7 @@ export class EmailService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const emailDomain: string = emailAddress.split('@')[1];
|
const emailDomain: string = emailAddress.split('@')[1];
|
||||||
const isBanned = this.utilityService.isBlockedHost(meta.bannedEmailDomains, emailDomain);
|
const isBanned = this.utilityService.isBlockedHost(this.meta.bannedEmailDomains, emailDomain);
|
||||||
|
|
||||||
if (isBanned) {
|
if (isBanned) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -246,7 +246,7 @@ export interface InternalEventTypes {
|
||||||
avatarDecorationCreated: MiAvatarDecoration;
|
avatarDecorationCreated: MiAvatarDecoration;
|
||||||
avatarDecorationDeleted: MiAvatarDecoration;
|
avatarDecorationDeleted: MiAvatarDecoration;
|
||||||
avatarDecorationUpdated: MiAvatarDecoration;
|
avatarDecorationUpdated: MiAvatarDecoration;
|
||||||
metaUpdated: MiMeta;
|
metaUpdated: { before?: MiMeta; after: MiMeta; };
|
||||||
followChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
|
followChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
|
||||||
unfollowChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
|
unfollowChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
|
||||||
updateUserProfile: MiUserProfile;
|
updateUserProfile: MiUserProfile;
|
||||||
|
|
|
@ -10,16 +10,18 @@ import type { MiUser } from '@/models/User.js';
|
||||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import type { MiHashtag } from '@/models/Hashtag.js';
|
import type { MiHashtag } from '@/models/Hashtag.js';
|
||||||
import type { HashtagsRepository } from '@/models/_.js';
|
import type { HashtagsRepository, MiMeta } from '@/models/_.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 { FeaturedService } from '@/core/FeaturedService.js';
|
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class HashtagService {
|
export class HashtagService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
|
|
||||||
@Inject(DI.redis)
|
@Inject(DI.redis)
|
||||||
private redisClient: Redis.Redis, // TODO: 専用のRedisサーバーを設定できるようにする
|
private redisClient: Redis.Redis, // TODO: 専用のRedisサーバーを設定できるようにする
|
||||||
|
|
||||||
|
@ -29,7 +31,6 @@ export class HashtagService {
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private featuredService: FeaturedService,
|
private featuredService: FeaturedService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private metaService: MetaService,
|
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
@ -160,10 +161,9 @@ export class HashtagService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async updateHashtagsRanking(hashtag: string, userId: MiUser['id']): Promise<void> {
|
public async updateHashtagsRanking(hashtag: string, userId: MiUser['id']): Promise<void> {
|
||||||
const instance = await this.metaService.fetch();
|
const hiddenTags = this.meta.hiddenTags.map(t => normalizeForSearch(t));
|
||||||
const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t));
|
|
||||||
if (hiddenTags.includes(hashtag)) return;
|
if (hiddenTags.includes(hashtag)) return;
|
||||||
if (this.utilityService.isKeyWordIncluded(hashtag, instance.sensitiveWords)) return;
|
if (this.utilityService.isKeyWordIncluded(hashtag, this.meta.sensitiveWords)) return;
|
||||||
|
|
||||||
// YYYYMMDDHHmm (10分間隔)
|
// YYYYMMDDHHmm (10分間隔)
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
|
@ -52,7 +52,7 @@ export class MetaService implements OnApplicationShutdown {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'metaUpdated': {
|
case 'metaUpdated': {
|
||||||
this.cache = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
this.cache = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||||
...body,
|
...(body.after),
|
||||||
proxyAccount: null, // joinなカラムは通常取ってこないので
|
proxyAccount: null, // joinなカラムは通常取ってこないので
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
@ -141,7 +141,7 @@ export class MetaService implements OnApplicationShutdown {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.globalEventService.publishInternalEvent('metaUpdated', updated);
|
this.globalEventService.publishInternalEvent('metaUpdated', { before, after: updated });
|
||||||
|
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import { URL } from 'node:url';
|
import { URL } from 'node:url';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as parse5 from 'parse5';
|
import * as parse5 from 'parse5';
|
||||||
import { Window, DocumentFragment, XMLSerializer } from 'happy-dom';
|
import { Window, XMLSerializer } from 'happy-dom';
|
||||||
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 { intersperse } from '@/misc/prelude/array.js';
|
import { intersperse } from '@/misc/prelude/array.js';
|
||||||
|
@ -465,7 +465,7 @@ export class MfmService {
|
||||||
|
|
||||||
const serialized = new XMLSerializer().serializeToString(body);
|
const serialized = new XMLSerializer().serializeToString(body);
|
||||||
|
|
||||||
happyDOM.close().catch(e => {});
|
happyDOM.close().catch(err => {});
|
||||||
|
|
||||||
return serialized;
|
return serialized;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,14 +8,13 @@ import * as mfm from '@transfem-org/sfm-js';
|
||||||
import { In, DataSource, IsNull, LessThan } from 'typeorm';
|
import { In, DataSource, IsNull, LessThan } from 'typeorm';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import RE2 from 're2';
|
|
||||||
import { extractMentions } from '@/misc/extract-mentions.js';
|
import { extractMentions } from '@/misc/extract-mentions.js';
|
||||||
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
||||||
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
||||||
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
||||||
import { MiNote } from '@/models/Note.js';
|
import { MiNote } from '@/models/Note.js';
|
||||||
import { LatestNote } from '@/models/LatestNote.js';
|
import { LatestNote } from '@/models/LatestNote.js';
|
||||||
import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, LatestNotesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, LatestNotesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
import type { MiApp } from '@/models/App.js';
|
import type { MiApp } from '@/models/App.js';
|
||||||
import { concat } from '@/misc/prelude/array.js';
|
import { concat } from '@/misc/prelude/array.js';
|
||||||
|
@ -24,11 +23,8 @@ import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
||||||
import type { IPoll } from '@/models/Poll.js';
|
import type { IPoll } from '@/models/Poll.js';
|
||||||
import { MiPoll } from '@/models/Poll.js';
|
import { MiPoll } from '@/models/Poll.js';
|
||||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||||
import { checkWordMute } from '@/misc/check-word-mute.js';
|
|
||||||
import type { MiChannel } from '@/models/Channel.js';
|
import type { MiChannel } from '@/models/Channel.js';
|
||||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||||
import { MemorySingleCache } from '@/misc/cache.js';
|
|
||||||
import type { MiUserProfile } from '@/models/UserProfile.js';
|
|
||||||
import { RelayService } from '@/core/RelayService.js';
|
import { RelayService } from '@/core/RelayService.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
@ -52,7 +48,6 @@ import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { SearchService } from '@/core/SearchService.js';
|
import { SearchService } from '@/core/SearchService.js';
|
||||||
import { FeaturedService } from '@/core/FeaturedService.js';
|
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||||
|
@ -64,6 +59,7 @@ import { trackPromise } from '@/misc/promise-tracker.js';
|
||||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||||
|
import { CollapsedQueue } from '@/misc/collapsed-queue.js';
|
||||||
|
|
||||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||||
|
|
||||||
|
@ -155,11 +151,15 @@ type Option = {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NoteCreateService implements OnApplicationShutdown {
|
export class NoteCreateService implements OnApplicationShutdown {
|
||||||
#shutdownController = new AbortController();
|
#shutdownController = new AbortController();
|
||||||
|
private updateNotesCountQueue: CollapsedQueue<MiNote['id'], number>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
|
|
||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
|
@ -217,7 +217,6 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
private apDeliverManagerService: ApDeliverManagerService,
|
private apDeliverManagerService: ApDeliverManagerService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
private metaService: MetaService,
|
|
||||||
private searchService: SearchService,
|
private searchService: SearchService,
|
||||||
private notesChart: NotesChart,
|
private notesChart: NotesChart,
|
||||||
private perUserNotesChart: PerUserNotesChart,
|
private perUserNotesChart: PerUserNotesChart,
|
||||||
|
@ -226,7 +225,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private userBlockingService: UserBlockingService,
|
private userBlockingService: UserBlockingService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
) { }
|
) {
|
||||||
|
this.updateNotesCountQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseNotesCount, this.performUpdateNotesCount);
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async create(user: {
|
public async create(user: {
|
||||||
|
@ -259,10 +260,8 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
if (data.channel != null) data.visibleUsers = [];
|
if (data.channel != null) data.visibleUsers = [];
|
||||||
if (data.channel != null) data.localOnly = true;
|
if (data.channel != null) data.localOnly = true;
|
||||||
|
|
||||||
const meta = await this.metaService.fetch();
|
|
||||||
|
|
||||||
if (data.visibility === 'public' && data.channel == null) {
|
if (data.visibility === 'public' && data.channel == null) {
|
||||||
const sensitiveWords = meta.sensitiveWords;
|
const sensitiveWords = this.meta.sensitiveWords;
|
||||||
if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) {
|
if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) {
|
||||||
data.visibility = 'home';
|
data.visibility = 'home';
|
||||||
} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
|
} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
|
||||||
|
@ -270,17 +269,17 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasProhibitedWords = await this.checkProhibitedWordsContain({
|
const hasProhibitedWords = this.checkProhibitedWordsContain({
|
||||||
cw: data.cw,
|
cw: data.cw,
|
||||||
text: data.text,
|
text: data.text,
|
||||||
pollChoices: data.poll?.choices,
|
pollChoices: data.poll?.choices,
|
||||||
}, meta.prohibitedWords);
|
}, this.meta.prohibitedWords);
|
||||||
|
|
||||||
if (hasProhibitedWords) {
|
if (hasProhibitedWords) {
|
||||||
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
|
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
|
||||||
}
|
}
|
||||||
|
|
||||||
const inSilencedInstance = this.utilityService.isSilencedHost(meta.silencedHosts, user.host);
|
const inSilencedInstance = this.utilityService.isSilencedHost(this.meta.silencedHosts, user.host);
|
||||||
|
|
||||||
if (data.visibility === 'public' && inSilencedInstance && user.host !== null) {
|
if (data.visibility === 'public' && inSilencedInstance && user.host !== null) {
|
||||||
data.visibility = 'home';
|
data.visibility = 'home';
|
||||||
|
@ -385,7 +384,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the host is media-silenced, custom emojis are not allowed
|
// if the host is media-silenced, custom emojis are not allowed
|
||||||
if (this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, user.host)) emojis = [];
|
if (this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) emojis = [];
|
||||||
|
|
||||||
tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32);
|
tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32);
|
||||||
|
|
||||||
|
@ -556,10 +555,8 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
isBot: MiUser['isBot'];
|
isBot: MiUser['isBot'];
|
||||||
noindex: MiUser['noindex'];
|
noindex: MiUser['noindex'];
|
||||||
}, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) {
|
}, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) {
|
||||||
const meta = await this.metaService.fetch();
|
|
||||||
|
|
||||||
this.notesChart.update(note, true);
|
this.notesChart.update(note, true);
|
||||||
if (note.visibility !== 'specified' && (meta.enableChartsForRemoteUser || (user.host == null))) {
|
if (note.visibility !== 'specified' && (this.meta.enableChartsForRemoteUser || (user.host == null))) {
|
||||||
this.perUserNotesChart.update(user, note, true);
|
this.perUserNotesChart.update(user, note, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -567,11 +564,11 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
if (this.userEntityService.isRemoteUser(user)) {
|
if (this.userEntityService.isRemoteUser(user)) {
|
||||||
this.federatedInstanceService.fetch(user.host).then(async i => {
|
this.federatedInstanceService.fetch(user.host).then(async i => {
|
||||||
if (note.renote && note.text) {
|
if (note.renote && note.text) {
|
||||||
this.instancesRepository.increment({ id: i.id }, 'notesCount', 1);
|
this.updateNotesCountQueue.enqueue(i.id, 1);
|
||||||
} else if (!note.renote) {
|
} else if (!note.renote) {
|
||||||
this.instancesRepository.increment({ id: i.id }, 'notesCount', 1);
|
this.updateNotesCountQueue.enqueue(i.id, 1);
|
||||||
}
|
}
|
||||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
if (this.meta.enableChartsForFederatedInstances) {
|
||||||
this.instanceChart.updateNote(i.host, note, true);
|
this.instanceChart.updateNote(i.host, note, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -953,15 +950,14 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) {
|
private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) {
|
||||||
const meta = await this.metaService.fetch();
|
if (!this.meta.enableFanoutTimeline) return;
|
||||||
if (!meta.enableFanoutTimeline) return;
|
|
||||||
|
|
||||||
const r = this.redisForTimelines.pipeline();
|
const r = this.redisForTimelines.pipeline();
|
||||||
|
|
||||||
if (note.channelId) {
|
if (note.channelId) {
|
||||||
this.fanoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r);
|
this.fanoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r);
|
||||||
|
|
||||||
this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
|
this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r);
|
||||||
|
|
||||||
const channelFollowings = await this.channelFollowingsRepository.find({
|
const channelFollowings = await this.channelFollowingsRepository.find({
|
||||||
where: {
|
where: {
|
||||||
|
@ -971,9 +967,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const channelFollowing of channelFollowings) {
|
for (const channelFollowing of channelFollowings) {
|
||||||
this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax, r);
|
||||||
if (note.fileIds.length > 0) {
|
if (note.fileIds.length > 0) {
|
||||||
this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -1011,9 +1007,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
if (!following.withReplies) continue;
|
if (!following.withReplies) continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax, r);
|
||||||
if (note.fileIds.length > 0) {
|
if (note.fileIds.length > 0) {
|
||||||
this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1030,25 +1026,25 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
if (!userListMembership.withReplies) continue;
|
if (!userListMembership.withReplies) continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r);
|
this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, this.meta.perUserListTimelineCacheMax, r);
|
||||||
if (note.fileIds.length > 0) {
|
if (note.fileIds.length > 0) {
|
||||||
this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r);
|
this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, this.meta.perUserListTimelineCacheMax / 2, r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自分自身のHTL
|
// 自分自身のHTL
|
||||||
if (note.userHost == null) {
|
if (note.userHost == null) {
|
||||||
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) {
|
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) {
|
||||||
this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, this.meta.perUserHomeTimelineCacheMax, r);
|
||||||
if (note.fileIds.length > 0) {
|
if (note.fileIds.length > 0) {
|
||||||
this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自分自身以外への返信
|
// 自分自身以外への返信
|
||||||
if (isReply(note)) {
|
if (isReply(note)) {
|
||||||
this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
|
this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r);
|
||||||
|
|
||||||
if (note.visibility === 'public' && note.userHost == null) {
|
if (note.visibility === 'public' && note.userHost == null) {
|
||||||
this.fanoutTimelineService.push('localTimelineWithReplies', note.id, 300, r);
|
this.fanoutTimelineService.push('localTimelineWithReplies', note.id, 300, r);
|
||||||
|
@ -1057,9 +1053,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
|
this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r);
|
||||||
if (note.fileIds.length > 0) {
|
if (note.fileIds.length > 0) {
|
||||||
this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r);
|
this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax / 2 : this.meta.perRemoteUserUserTimelineCacheMax / 2, r);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.visibility === 'public' && note.userHost == null) {
|
if (note.visibility === 'public' && note.userHost == null) {
|
||||||
|
@ -1118,9 +1114,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async checkProhibitedWordsContain(content: Parameters<UtilityService['concatNoteContentsForKeyWordCheck']>[0], prohibitedWords?: string[]) {
|
public checkProhibitedWordsContain(content: Parameters<UtilityService['concatNoteContentsForKeyWordCheck']>[0], prohibitedWords?: string[]) {
|
||||||
if (prohibitedWords == null) {
|
if (prohibitedWords == null) {
|
||||||
prohibitedWords = (await this.metaService.fetch()).prohibitedWords;
|
prohibitedWords = this.meta.prohibitedWords;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -1136,13 +1132,24 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public dispose(): void {
|
private collapseNotesCount(oldValue: number, newValue: number) {
|
||||||
this.#shutdownController.abort();
|
return oldValue + newValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public onApplicationShutdown(signal?: string | undefined): void {
|
private async performUpdateNotesCount(id: MiNote['id'], incrBy: number) {
|
||||||
this.dispose();
|
await this.instancesRepository.increment({ id: id }, 'notesCount', incrBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async dispose(): Promise<void> {
|
||||||
|
this.#shutdownController.abort();
|
||||||
|
await this.updateNotesCountQueue.performAllNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async onApplicationShutdown(signal?: string | undefined): Promise<void> {
|
||||||
|
await this.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateLatestNote(note: MiNote) {
|
private async updateLatestNote(note: MiNote) {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { Injectable, Inject } from '@nestjs/common';
|
||||||
import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
||||||
import type { MiNote, IMentionedRemoteUsers } from '@/models/Note.js';
|
import type { MiNote, IMentionedRemoteUsers } from '@/models/Note.js';
|
||||||
import { LatestNote } from '@/models/LatestNote.js';
|
import { LatestNote } from '@/models/LatestNote.js';
|
||||||
import type { InstancesRepository, LatestNotesRepository, NotesRepository, UsersRepository } from '@/models/_.js';
|
import type { InstancesRepository, MiMeta, LatestNotesRepository, NotesRepository, UsersRepository } from '@/models/_.js';
|
||||||
import { RelayService } from '@/core/RelayService.js';
|
import { RelayService } from '@/core/RelayService.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
@ -20,9 +20,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
|
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { SearchService } from '@/core/SearchService.js';
|
import { SearchService } from '@/core/SearchService.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||||
|
@ -33,6 +31,9 @@ export class NoteDeleteService {
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@ -46,13 +47,11 @@ export class NoteDeleteService {
|
||||||
private instancesRepository: InstancesRepository,
|
private instancesRepository: InstancesRepository,
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private noteEntityService: NoteEntityService,
|
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private relayService: RelayService,
|
private relayService: RelayService,
|
||||||
private federatedInstanceService: FederatedInstanceService,
|
private federatedInstanceService: FederatedInstanceService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private apDeliverManagerService: ApDeliverManagerService,
|
private apDeliverManagerService: ApDeliverManagerService,
|
||||||
private metaService: MetaService,
|
|
||||||
private searchService: SearchService,
|
private searchService: SearchService,
|
||||||
private moderationLogService: ModerationLogService,
|
private moderationLogService: ModerationLogService,
|
||||||
private notesChart: NotesChart,
|
private notesChart: NotesChart,
|
||||||
|
@ -113,10 +112,8 @@ export class NoteDeleteService {
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
const meta = await this.metaService.fetch();
|
|
||||||
|
|
||||||
this.notesChart.update(note, false);
|
this.notesChart.update(note, false);
|
||||||
if (meta.enableChartsForRemoteUser || (user.host == null)) {
|
if (this.meta.enableChartsForRemoteUser || (user.host == null)) {
|
||||||
this.perUserNotesChart.update(user, note, false);
|
this.perUserNotesChart.update(user, note, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,7 +132,7 @@ export class NoteDeleteService {
|
||||||
} else if (!note.renoteId) {
|
} else if (!note.renoteId) {
|
||||||
this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1);
|
this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1);
|
||||||
}
|
}
|
||||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
if (this.meta.enableChartsForFederatedInstances) {
|
||||||
this.instanceChart.updateNote(i.host, note, false);
|
this.instanceChart.updateNote(i.host, note, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,26 +4,25 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { UsersRepository } from '@/models/_.js';
|
import type { MiMeta, UsersRepository } from '@/models/_.js';
|
||||||
import type { MiLocalUser } from '@/models/User.js';
|
import type { MiLocalUser } from '@/models/User.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ProxyAccountService {
|
export class ProxyAccountService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
private metaService: MetaService,
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async fetch(): Promise<MiLocalUser | null> {
|
public async fetch(): Promise<MiLocalUser | null> {
|
||||||
const meta = await this.metaService.fetch();
|
if (this.meta.proxyAccountId == null) return null;
|
||||||
if (meta.proxyAccountId == null) return null;
|
return await this.usersRepository.findOneByOrFail({ id: this.meta.proxyAccountId }) as MiLocalUser;
|
||||||
return await this.usersRepository.findOneByOrFail({ id: meta.proxyAccountId }) as MiLocalUser;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,7 @@ 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.js';
|
||||||
import { getNoteSummary } from '@/misc/get-note-summary.js';
|
import { getNoteSummary } from '@/misc/get-note-summary.js';
|
||||||
import type { MiSwSubscription, SwSubscriptionsRepository } from '@/models/_.js';
|
import type { MiMeta, MiSwSubscription, SwSubscriptionsRepository } from '@/models/_.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { RedisKVCache } from '@/misc/cache.js';
|
import { RedisKVCache } from '@/misc/cache.js';
|
||||||
|
|
||||||
|
@ -54,13 +53,14 @@ export class PushNotificationService implements OnApplicationShutdown {
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
|
|
||||||
@Inject(DI.redis)
|
@Inject(DI.redis)
|
||||||
private redisClient: Redis.Redis,
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.swSubscriptionsRepository)
|
@Inject(DI.swSubscriptionsRepository)
|
||||||
private swSubscriptionsRepository: SwSubscriptionsRepository,
|
private swSubscriptionsRepository: SwSubscriptionsRepository,
|
||||||
|
|
||||||
private metaService: MetaService,
|
|
||||||
) {
|
) {
|
||||||
this.subscriptionsCache = new RedisKVCache<MiSwSubscription[]>(this.redisClient, 'userSwSubscriptions', {
|
this.subscriptionsCache = new RedisKVCache<MiSwSubscription[]>(this.redisClient, 'userSwSubscriptions', {
|
||||||
lifetime: 1000 * 60 * 60 * 1, // 1h
|
lifetime: 1000 * 60 * 60 * 1, // 1h
|
||||||
|
@ -73,14 +73,12 @@ 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();
|
if (!this.meta.enableServiceWorker || this.meta.swPublicKey == null || this.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,
|
this.meta.swPublicKey,
|
||||||
meta.swPrivateKey);
|
this.meta.swPrivateKey);
|
||||||
|
|
||||||
const subscriptions = await this.subscriptionsCache.fetch(userId);
|
const subscriptions = await this.subscriptionsCache.fetch(userId);
|
||||||
|
|
||||||
|
|
|
@ -88,6 +88,12 @@ export class QueueService {
|
||||||
repeat: { pattern: '*/5 * * * *' },
|
repeat: { pattern: '*/5 * * * *' },
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.systemQueue.add('bakeBufferedReactions', {
|
||||||
|
}, {
|
||||||
|
repeat: { pattern: '0 0 * * *' },
|
||||||
|
removeOnComplete: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -511,10 +517,15 @@ export class QueueService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see UserWebhookDeliverJobData
|
* @see UserWebhookDeliverJobData
|
||||||
* @see WebhookDeliverProcessorService
|
* @see UserWebhookDeliverProcessorService
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public userWebhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
|
public userWebhookDeliver(
|
||||||
|
webhook: MiWebhook,
|
||||||
|
type: typeof webhookEventTypes[number],
|
||||||
|
content: unknown,
|
||||||
|
opts?: { attempts?: number },
|
||||||
|
) {
|
||||||
const data: UserWebhookDeliverJobData = {
|
const data: UserWebhookDeliverJobData = {
|
||||||
type,
|
type,
|
||||||
content,
|
content,
|
||||||
|
@ -527,7 +538,7 @@ export class QueueService {
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.userWebhookDeliverQueue.add(webhook.id, data, {
|
return this.userWebhookDeliverQueue.add(webhook.id, data, {
|
||||||
attempts: 4,
|
attempts: opts?.attempts ?? 4,
|
||||||
backoff: {
|
backoff: {
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
},
|
},
|
||||||
|
@ -538,10 +549,15 @@ export class QueueService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see SystemWebhookDeliverJobData
|
* @see SystemWebhookDeliverJobData
|
||||||
* @see WebhookDeliverProcessorService
|
* @see SystemWebhookDeliverProcessorService
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public systemWebhookDeliver(webhook: MiSystemWebhook, type: SystemWebhookEventType, content: unknown) {
|
public systemWebhookDeliver(
|
||||||
|
webhook: MiSystemWebhook,
|
||||||
|
type: SystemWebhookEventType,
|
||||||
|
content: unknown,
|
||||||
|
opts?: { attempts?: number },
|
||||||
|
) {
|
||||||
const data: SystemWebhookDeliverJobData = {
|
const data: SystemWebhookDeliverJobData = {
|
||||||
type,
|
type,
|
||||||
content,
|
content,
|
||||||
|
@ -553,7 +569,7 @@ export class QueueService {
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.systemWebhookDeliverQueue.add(webhook.id, data, {
|
return this.systemWebhookDeliverQueue.add(webhook.id, data, {
|
||||||
attempts: 4,
|
attempts: opts?.attempts ?? 4,
|
||||||
backoff: {
|
backoff: {
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,9 +4,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository, NoteThreadMutingsRepository } from '@/models/_.js';
|
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository, NoteThreadMutingsRepository, MiMeta } from '@/models/_.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import type { MiRemoteUser, MiUser } from '@/models/User.js';
|
import type { MiRemoteUser, MiUser } from '@/models/User.js';
|
||||||
import type { MiNote } from '@/models/Note.js';
|
import type { MiNote } from '@/models/Note.js';
|
||||||
|
@ -21,7 +20,6 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||||
|
@ -30,9 +28,10 @@ import { RoleService } from '@/core/RoleService.js';
|
||||||
import { FeaturedService } from '@/core/FeaturedService.js';
|
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||||
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||||
|
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
|
||||||
|
import { PER_NOTE_REACTION_USER_PAIR_CACHE_MAX } from '@/const.js';
|
||||||
|
|
||||||
const FALLBACK = '\u2764';
|
const FALLBACK = '\u2764';
|
||||||
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
|
|
||||||
|
|
||||||
const legacies: Record<string, string> = {
|
const legacies: Record<string, string> = {
|
||||||
'like': '👍',
|
'like': '👍',
|
||||||
|
@ -71,8 +70,8 @@ const decodeCustomEmojiRegexp = /^:([\p{Letter}\p{Number}\p{Mark}_+-]+)(?:@([\w.
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ReactionService {
|
export class ReactionService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redis)
|
@Inject(DI.meta)
|
||||||
private redisClient: Redis.Redis,
|
private meta: MiMeta,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
@ -90,12 +89,12 @@ export class ReactionService {
|
||||||
private emojisRepository: EmojisRepository,
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private metaService: MetaService,
|
|
||||||
private customEmojiService: CustomEmojiService,
|
private customEmojiService: CustomEmojiService,
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private userBlockingService: UserBlockingService,
|
private userBlockingService: UserBlockingService,
|
||||||
|
private reactionsBufferingService: ReactionsBufferingService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private featuredService: FeaturedService,
|
private featuredService: FeaturedService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
|
@ -108,8 +107,6 @@ export class ReactionService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async create(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot'] }, note: MiNote, _reaction?: string | null) {
|
public async create(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot'] }, note: MiNote, _reaction?: string | null) {
|
||||||
const meta = await this.metaService.fetch();
|
|
||||||
|
|
||||||
// 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);
|
||||||
|
@ -155,7 +152,7 @@ export class ReactionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// for media silenced host, custom emoji reactions are not allowed
|
// for media silenced host, custom emoji reactions are not allowed
|
||||||
if (reacterHost != null && this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, reacterHost)) {
|
if (reacterHost != null && this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, reacterHost)) {
|
||||||
reaction = FALLBACK;
|
reaction = FALLBACK;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -177,7 +174,6 @@ export class ReactionService {
|
||||||
reaction,
|
reaction,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create reaction
|
|
||||||
try {
|
try {
|
||||||
await this.noteReactionsRepository.insert(record);
|
await this.noteReactionsRepository.insert(record);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -201,6 +197,9 @@ export class ReactionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment reactions count
|
// Increment reactions count
|
||||||
|
if (this.meta.enableReactionsBuffering) {
|
||||||
|
await this.reactionsBufferingService.create(note.id, user.id, reaction, note.reactionAndUserPairCache);
|
||||||
|
} else {
|
||||||
const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;
|
const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;
|
||||||
await this.notesRepository.createQueryBuilder().update()
|
await this.notesRepository.createQueryBuilder().update()
|
||||||
.set({
|
.set({
|
||||||
|
@ -211,6 +210,7 @@ export class ReactionService {
|
||||||
})
|
})
|
||||||
.where('id = :id', { id: note.id })
|
.where('id = :id', { id: note.id })
|
||||||
.execute();
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
// 30%の確率、セルフではない、3日以内に投稿されたノートの場合ハイライト用ランキング更新
|
// 30%の確率、セルフではない、3日以内に投稿されたノートの場合ハイライト用ランキング更新
|
||||||
if (
|
if (
|
||||||
|
@ -230,7 +230,7 @@ export class ReactionService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (meta.enableChartsForRemoteUser || (user.host == null)) {
|
if (this.meta.enableChartsForRemoteUser || (user.host == null)) {
|
||||||
this.perUserReactionsChart.update(user, note);
|
this.perUserReactionsChart.update(user, note);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -317,6 +317,9 @@ export class ReactionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrement reactions count
|
// Decrement reactions count
|
||||||
|
if (this.meta.enableReactionsBuffering) {
|
||||||
|
await this.reactionsBufferingService.delete(note.id, user.id, exist.reaction);
|
||||||
|
} else {
|
||||||
const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`;
|
const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`;
|
||||||
await this.notesRepository.createQueryBuilder().update()
|
await this.notesRepository.createQueryBuilder().update()
|
||||||
.set({
|
.set({
|
||||||
|
@ -325,6 +328,7 @@ export class ReactionService {
|
||||||
})
|
})
|
||||||
.where('id = :id', { id: note.id })
|
.where('id = :id', { id: note.id })
|
||||||
.execute();
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
this.globalEventService.publishNoteStream(note.id, 'unreacted', {
|
this.globalEventService.publishNoteStream(note.id, 'unreacted', {
|
||||||
reaction: this.decodeReaction(exist.reaction).reaction,
|
reaction: this.decodeReaction(exist.reaction).reaction,
|
||||||
|
@ -346,8 +350,21 @@ export class ReactionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文字列タイプのレガシーな形式のリアクションを現在の形式に変換しつつ、
|
* - 文字列タイプのレガシーな形式のリアクションを現在の形式に変換する
|
||||||
* データベース上には存在する「0個のリアクションがついている」という情報を削除する。
|
* - ローカルのリアクションのホストを `@.` にする(`decodeReaction()`の効果)
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public convertLegacyReaction(reaction: string): string {
|
||||||
|
reaction = this.decodeReaction(reaction).reaction;
|
||||||
|
if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
|
||||||
|
return reaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 廃止
|
||||||
|
/**
|
||||||
|
* - 文字列タイプのレガシーな形式のリアクションを現在の形式に変換する
|
||||||
|
* - ローカルのリアクションのホストを `@.` にする(`decodeReaction()`の効果)
|
||||||
|
* - データベース上には存在する「0個のリアクションがついている」という情報を削除する
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public convertLegacyReactions(reactions: MiNote['reactions']): MiNote['reactions'] {
|
public convertLegacyReactions(reactions: MiNote['reactions']): MiNote['reactions'] {
|
||||||
|
@ -360,10 +377,7 @@ export class ReactionService {
|
||||||
return count > 0;
|
return count > 0;
|
||||||
})
|
})
|
||||||
.map(([reaction, count]) => {
|
.map(([reaction, count]) => {
|
||||||
// unchecked indexed access
|
const key = this.convertLegacyReaction(reaction);
|
||||||
const convertedReaction = legacies[reaction] as string | undefined;
|
|
||||||
|
|
||||||
const key = this.decodeReaction(convertedReaction ?? reaction).reaction;
|
|
||||||
|
|
||||||
return [key, count] as const;
|
return [key, count] as const;
|
||||||
})
|
})
|
||||||
|
@ -418,11 +432,4 @@ export class ReactionService {
|
||||||
host: undefined,
|
host: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public convertLegacyReaction(reaction: string): string {
|
|
||||||
reaction = this.decodeReaction(reaction).reaction;
|
|
||||||
if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
|
|
||||||
return reaction;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
211
packages/backend/src/core/ReactionsBufferingService.ts
Normal file
211
packages/backend/src/core/ReactionsBufferingService.ts
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import type { MiNote } from '@/models/Note.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import type { MiUser, NotesRepository } from '@/models/_.js';
|
||||||
|
import type { Config } from '@/config.js';
|
||||||
|
import { PER_NOTE_REACTION_USER_PAIR_CACHE_MAX } from '@/const.js';
|
||||||
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
|
const REDIS_DELTA_PREFIX = 'reactionsBufferDeltas';
|
||||||
|
const REDIS_PAIR_PREFIX = 'reactionsBufferPairs';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ReactionsBufferingService implements OnApplicationShutdown {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.config)
|
||||||
|
private config: Config,
|
||||||
|
|
||||||
|
@Inject(DI.redisForSub)
|
||||||
|
private redisForSub: Redis.Redis,
|
||||||
|
|
||||||
|
@Inject(DI.redisForReactions)
|
||||||
|
private redisForReactions: Redis.Redis, // TODO: 専用のRedisインスタンスにする
|
||||||
|
|
||||||
|
@Inject(DI.notesRepository)
|
||||||
|
private notesRepository: NotesRepository,
|
||||||
|
) {
|
||||||
|
this.redisForSub.on('message', this.onMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async onMessage(_: string, data: string) {
|
||||||
|
const obj = JSON.parse(data);
|
||||||
|
|
||||||
|
if (obj.channel === 'internal') {
|
||||||
|
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||||
|
switch (type) {
|
||||||
|
case 'metaUpdated': {
|
||||||
|
// リアクションバッファリングが有効→無効になったら即bake
|
||||||
|
if (body.before != null && body.before.enableReactionsBuffering && !body.after.enableReactionsBuffering) {
|
||||||
|
this.bake();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async create(noteId: MiNote['id'], userId: MiUser['id'], reaction: string, currentPairs: string[]): Promise<void> {
|
||||||
|
const pipeline = this.redisForReactions.pipeline();
|
||||||
|
pipeline.hincrby(`${REDIS_DELTA_PREFIX}:${noteId}`, reaction, 1);
|
||||||
|
for (let i = 0; i < currentPairs.length; i++) {
|
||||||
|
pipeline.zadd(`${REDIS_PAIR_PREFIX}:${noteId}`, i, currentPairs[i]);
|
||||||
|
}
|
||||||
|
pipeline.zadd(`${REDIS_PAIR_PREFIX}:${noteId}`, Date.now(), `${userId}/${reaction}`);
|
||||||
|
pipeline.zremrangebyrank(`${REDIS_PAIR_PREFIX}:${noteId}`, 0, -(PER_NOTE_REACTION_USER_PAIR_CACHE_MAX + 1));
|
||||||
|
await pipeline.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async delete(noteId: MiNote['id'], userId: MiUser['id'], reaction: string): Promise<void> {
|
||||||
|
const pipeline = this.redisForReactions.pipeline();
|
||||||
|
pipeline.hincrby(`${REDIS_DELTA_PREFIX}:${noteId}`, reaction, -1);
|
||||||
|
pipeline.zrem(`${REDIS_PAIR_PREFIX}:${noteId}`, `${userId}/${reaction}`);
|
||||||
|
// TODO: 「消した要素一覧」も持っておかないとcreateされた時に上書きされて復活する
|
||||||
|
await pipeline.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async get(noteId: MiNote['id']): Promise<{
|
||||||
|
deltas: Record<string, number>;
|
||||||
|
pairs: ([MiUser['id'], string])[];
|
||||||
|
}> {
|
||||||
|
const pipeline = this.redisForReactions.pipeline();
|
||||||
|
pipeline.hgetall(`${REDIS_DELTA_PREFIX}:${noteId}`);
|
||||||
|
pipeline.zrange(`${REDIS_PAIR_PREFIX}:${noteId}`, 0, -1);
|
||||||
|
const results = await pipeline.exec();
|
||||||
|
|
||||||
|
const resultDeltas = results![0][1] as Record<string, string>;
|
||||||
|
const resultPairs = results![1][1] as string[];
|
||||||
|
|
||||||
|
const deltas = {} as Record<string, number>;
|
||||||
|
for (const [name, count] of Object.entries(resultDeltas)) {
|
||||||
|
deltas[name] = parseInt(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pairs = resultPairs.map(x => x.split('/') as [MiUser['id'], string]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
deltas,
|
||||||
|
pairs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async getMany(noteIds: MiNote['id'][]): Promise<Map<MiNote['id'], {
|
||||||
|
deltas: Record<string, number>;
|
||||||
|
pairs: ([MiUser['id'], string])[];
|
||||||
|
}>> {
|
||||||
|
const map = new Map<MiNote['id'], {
|
||||||
|
deltas: Record<string, number>;
|
||||||
|
pairs: ([MiUser['id'], string])[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const pipeline = this.redisForReactions.pipeline();
|
||||||
|
for (const noteId of noteIds) {
|
||||||
|
pipeline.hgetall(`${REDIS_DELTA_PREFIX}:${noteId}`);
|
||||||
|
pipeline.zrange(`${REDIS_PAIR_PREFIX}:${noteId}`, 0, -1);
|
||||||
|
}
|
||||||
|
const results = await pipeline.exec();
|
||||||
|
|
||||||
|
const opsForEachNotes = 2;
|
||||||
|
for (let i = 0; i < noteIds.length; i++) {
|
||||||
|
const noteId = noteIds[i];
|
||||||
|
const resultDeltas = results![i * opsForEachNotes][1] as Record<string, string>;
|
||||||
|
const resultPairs = results![i * opsForEachNotes + 1][1] as string[];
|
||||||
|
|
||||||
|
const deltas = {} as Record<string, number>;
|
||||||
|
for (const [name, count] of Object.entries(resultDeltas)) {
|
||||||
|
deltas[name] = parseInt(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pairs = resultPairs.map(x => x.split('/') as [MiUser['id'], string]);
|
||||||
|
|
||||||
|
map.set(noteId, {
|
||||||
|
deltas,
|
||||||
|
pairs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: scanは重い可能性があるので、別途 bufferedNoteIds を直接Redis上に持っておいてもいいかもしれない
|
||||||
|
@bindThis
|
||||||
|
public async bake(): Promise<void> {
|
||||||
|
const bufferedNoteIds = [];
|
||||||
|
let cursor = '0';
|
||||||
|
do {
|
||||||
|
// https://github.com/redis/ioredis#transparent-key-prefixing
|
||||||
|
const result = await this.redisForReactions.scan(
|
||||||
|
cursor,
|
||||||
|
'MATCH',
|
||||||
|
`${this.config.redis.prefix}:${REDIS_DELTA_PREFIX}:*`,
|
||||||
|
'COUNT',
|
||||||
|
'1000');
|
||||||
|
|
||||||
|
cursor = result[0];
|
||||||
|
bufferedNoteIds.push(...result[1].map(x => x.replace(`${this.config.redis.prefix}:${REDIS_DELTA_PREFIX}:`, '')));
|
||||||
|
} while (cursor !== '0');
|
||||||
|
|
||||||
|
const bufferedMap = await this.getMany(bufferedNoteIds);
|
||||||
|
|
||||||
|
// clear
|
||||||
|
const pipeline = this.redisForReactions.pipeline();
|
||||||
|
for (const noteId of bufferedNoteIds) {
|
||||||
|
pipeline.del(`${REDIS_DELTA_PREFIX}:${noteId}`);
|
||||||
|
pipeline.del(`${REDIS_PAIR_PREFIX}:${noteId}`);
|
||||||
|
}
|
||||||
|
await pipeline.exec();
|
||||||
|
|
||||||
|
// TODO: SQL一個にまとめたい
|
||||||
|
for (const [noteId, buffered] of bufferedMap) {
|
||||||
|
const sql = Object.entries(buffered.deltas)
|
||||||
|
.map(([reaction, count]) =>
|
||||||
|
`jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + ${count})::text::jsonb)`)
|
||||||
|
.join(' || ');
|
||||||
|
|
||||||
|
this.notesRepository.createQueryBuilder().update()
|
||||||
|
.set({
|
||||||
|
reactions: () => sql,
|
||||||
|
reactionAndUserPairCache: buffered.pairs.map(x => x.join('/')),
|
||||||
|
})
|
||||||
|
.where('id = :id', { id: noteId })
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public mergeReactions(src: MiNote['reactions'], delta: Record<string, number>): MiNote['reactions'] {
|
||||||
|
const reactions = { ...src };
|
||||||
|
for (const [name, count] of Object.entries(delta)) {
|
||||||
|
if (reactions[name] != null) {
|
||||||
|
reactions[name] += count;
|
||||||
|
} else {
|
||||||
|
reactions[name] = count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public dispose(): void {
|
||||||
|
this.redisForSub.off('message', this.onMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public onApplicationShutdown(signal?: string | undefined): void {
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import * as Redis from 'ioredis';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { ModuleRef } from '@nestjs/core';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
import type {
|
import type {
|
||||||
|
MiMeta,
|
||||||
MiRole,
|
MiRole,
|
||||||
MiRoleAssignment,
|
MiRoleAssignment,
|
||||||
RoleAssignmentsRepository,
|
RoleAssignmentsRepository,
|
||||||
|
@ -18,7 +19,6 @@ import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import type { RoleCondFormulaValue } from '@/models/Role.js';
|
import type { RoleCondFormulaValue } from '@/models/Role.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
|
@ -60,6 +60,11 @@ export type RolePolicies = {
|
||||||
rateLimitFactor: number;
|
rateLimitFactor: number;
|
||||||
canImportNotes: boolean;
|
canImportNotes: boolean;
|
||||||
avatarDecorationLimit: number;
|
avatarDecorationLimit: number;
|
||||||
|
canImportAntennas: boolean;
|
||||||
|
canImportBlocking: boolean;
|
||||||
|
canImportFollowing: boolean;
|
||||||
|
canImportMuting: boolean;
|
||||||
|
canImportUserLists: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_POLICIES: RolePolicies = {
|
export const DEFAULT_POLICIES: RolePolicies = {
|
||||||
|
@ -91,6 +96,11 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||||
rateLimitFactor: 1,
|
rateLimitFactor: 1,
|
||||||
canImportNotes: true,
|
canImportNotes: true,
|
||||||
avatarDecorationLimit: 1,
|
avatarDecorationLimit: 1,
|
||||||
|
canImportAntennas: true,
|
||||||
|
canImportBlocking: true,
|
||||||
|
canImportFollowing: true,
|
||||||
|
canImportMuting: true,
|
||||||
|
canImportUserLists: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -105,8 +115,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
constructor(
|
constructor(
|
||||||
private moduleRef: ModuleRef,
|
private moduleRef: ModuleRef,
|
||||||
|
|
||||||
@Inject(DI.redis)
|
@Inject(DI.meta)
|
||||||
private redisClient: Redis.Redis,
|
private meta: MiMeta,
|
||||||
|
|
||||||
@Inject(DI.redisForTimelines)
|
@Inject(DI.redisForTimelines)
|
||||||
private redisForTimelines: Redis.Redis,
|
private redisForTimelines: Redis.Redis,
|
||||||
|
@ -123,7 +133,6 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
@Inject(DI.roleAssignmentsRepository)
|
@Inject(DI.roleAssignmentsRepository)
|
||||||
private roleAssignmentsRepository: RoleAssignmentsRepository,
|
private roleAssignmentsRepository: RoleAssignmentsRepository,
|
||||||
|
|
||||||
private metaService: MetaService,
|
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
|
@ -343,8 +352,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getUserPolicies(userId: MiUser['id'] | null): Promise<RolePolicies> {
|
public async getUserPolicies(userId: MiUser['id'] | null): Promise<RolePolicies> {
|
||||||
const meta = await this.metaService.fetch();
|
const basePolicies = { ...DEFAULT_POLICIES, ...this.meta.policies };
|
||||||
const basePolicies = { ...DEFAULT_POLICIES, ...meta.policies };
|
|
||||||
|
|
||||||
if (userId == null) return basePolicies;
|
if (userId == null) return basePolicies;
|
||||||
|
|
||||||
|
@ -393,6 +401,11 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)),
|
rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)),
|
||||||
canImportNotes: calc('canImportNotes', vs => vs.some(v => v === true)),
|
canImportNotes: calc('canImportNotes', vs => vs.some(v => v === true)),
|
||||||
avatarDecorationLimit: calc('avatarDecorationLimit', vs => Math.max(...vs)),
|
avatarDecorationLimit: calc('avatarDecorationLimit', vs => Math.max(...vs)),
|
||||||
|
canImportAntennas: calc('canImportAntennas', vs => vs.some(v => v === true)),
|
||||||
|
canImportBlocking: calc('canImportBlocking', vs => vs.some(v => v === true)),
|
||||||
|
canImportFollowing: calc('canImportFollowing', vs => vs.some(v => v === true)),
|
||||||
|
canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)),
|
||||||
|
canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as argon2 from 'argon2';
|
import * as argon2 from 'argon2';
|
||||||
import { DataSource, IsNull } from 'typeorm';
|
import { DataSource, IsNull } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { UsedUsernamesRepository, UsersRepository } from '@/models/_.js';
|
import type { MiMeta, UsedUsernamesRepository, UsersRepository } from '@/models/_.js';
|
||||||
import { MiUser } from '@/models/User.js';
|
import { MiUser } from '@/models/User.js';
|
||||||
import { MiUserProfile } from '@/models/UserProfile.js';
|
import { MiUserProfile } from '@/models/UserProfile.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
@ -21,7 +21,6 @@ import { InstanceActorService } from '@/core/InstanceActorService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import UsersChart from '@/core/chart/charts/users.js';
|
import UsersChart from '@/core/chart/charts/users.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { UserService } from '@/core/UserService.js';
|
import { UserService } from '@/core/UserService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -30,6 +29,9 @@ export class SignupService {
|
||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@ -40,7 +42,6 @@ export class SignupService {
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private metaService: MetaService,
|
|
||||||
private instanceActorService: InstanceActorService,
|
private instanceActorService: InstanceActorService,
|
||||||
private usersChart: UsersChart,
|
private usersChart: UsersChart,
|
||||||
) {
|
) {
|
||||||
|
@ -91,7 +92,7 @@ export class SignupService {
|
||||||
const isTheFirstUser = !await this.instanceActorService.realLocalUsersPresent();
|
const isTheFirstUser = !await this.instanceActorService.realLocalUsersPresent();
|
||||||
|
|
||||||
if (!opts.ignorePreservedUsernames && !isTheFirstUser) {
|
if (!opts.ignorePreservedUsernames && !isTheFirstUser) {
|
||||||
const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
|
const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
|
||||||
if (isPreserved) {
|
if (isPreserved) {
|
||||||
throw new Error('USED_USERNAME');
|
throw new Error('USED_USERNAME');
|
||||||
}
|
}
|
||||||
|
@ -163,4 +164,3 @@ export class SignupService {
|
||||||
return { account, secret };
|
return { account, secret };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,7 @@ export class SystemWebhookService implements OnApplicationShutdown {
|
||||||
* SystemWebhook の一覧を取得する.
|
* SystemWebhook の一覧を取得する.
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async fetchSystemWebhooks(params?: {
|
public fetchSystemWebhooks(params?: {
|
||||||
ids?: MiSystemWebhook['id'][];
|
ids?: MiSystemWebhook['id'][];
|
||||||
isActive?: MiSystemWebhook['isActive'];
|
isActive?: MiSystemWebhook['isActive'];
|
||||||
on?: MiSystemWebhook['on'];
|
on?: MiSystemWebhook['on'];
|
||||||
|
@ -165,19 +165,24 @@ export class SystemWebhookService implements OnApplicationShutdown {
|
||||||
/**
|
/**
|
||||||
* SystemWebhook をWebhook配送キューに追加する
|
* SystemWebhook をWebhook配送キューに追加する
|
||||||
* @see QueueService.systemWebhookDeliver
|
* @see QueueService.systemWebhookDeliver
|
||||||
|
* // TODO: contentの型を厳格化する
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async enqueueSystemWebhook(webhook: MiSystemWebhook | MiSystemWebhook['id'], type: SystemWebhookEventType, content: unknown) {
|
public async enqueueSystemWebhook<T extends SystemWebhookEventType>(
|
||||||
|
webhook: MiSystemWebhook | MiSystemWebhook['id'],
|
||||||
|
type: T,
|
||||||
|
content: unknown,
|
||||||
|
) {
|
||||||
const webhookEntity = typeof webhook === 'string'
|
const webhookEntity = typeof webhook === 'string'
|
||||||
? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)
|
? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)
|
||||||
: webhook;
|
: webhook;
|
||||||
if (!webhookEntity || !webhookEntity.isActive) {
|
if (!webhookEntity || !webhookEntity.isActive) {
|
||||||
this.logger.info(`Webhook is not active or not found : ${webhook}`);
|
this.logger.info(`SystemWebhook is not active or not found : ${webhook}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!webhookEntity.on.includes(type)) {
|
if (!webhookEntity.on.includes(type)) {
|
||||||
this.logger.info(`Webhook ${webhookEntity.id} is not listening to ${type}`);
|
this.logger.info(`SystemWebhook ${webhookEntity.id} is not listening to ${type}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,23 +13,20 @@ import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
|
||||||
import InstanceChart from '@/core/chart/charts/instance.js';
|
import InstanceChart from '@/core/chart/charts/instance.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
||||||
import { NotificationService } from '@/core/NotificationService.js';
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
|
||||||
import type { ThinUser } from '@/queue/types.js';
|
import type { ThinUser } from '@/queue/types.js';
|
||||||
import Logger from '../logger.js';
|
import Logger from '../logger.js';
|
||||||
|
|
||||||
|
@ -58,6 +55,9 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@ -79,13 +79,11 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private metaService: MetaService,
|
|
||||||
private notificationService: NotificationService,
|
private notificationService: NotificationService,
|
||||||
private federatedInstanceService: FederatedInstanceService,
|
private federatedInstanceService: FederatedInstanceService,
|
||||||
private webhookService: UserWebhookService,
|
private webhookService: UserWebhookService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private accountMoveService: AccountMoveService,
|
private accountMoveService: AccountMoveService,
|
||||||
private fanoutTimelineService: FanoutTimelineService,
|
|
||||||
private perUserFollowingChart: PerUserFollowingChart,
|
private perUserFollowingChart: PerUserFollowingChart,
|
||||||
private instanceChart: InstanceChart,
|
private instanceChart: InstanceChart,
|
||||||
) {
|
) {
|
||||||
|
@ -172,7 +170,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
followee.isLocked ||
|
followee.isLocked ||
|
||||||
(followeeProfile.carefulBot && follower.isBot) ||
|
(followeeProfile.carefulBot && follower.isBot) ||
|
||||||
(this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee) && process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING !== 'true') ||
|
(this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee) && process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING !== 'true') ||
|
||||||
(this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower) && this.utilityService.isSilencedHost((await this.metaService.fetch()).silencedHosts, follower.host))
|
(this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower) && this.utilityService.isSilencedHost(this.meta.silencedHosts, follower.host))
|
||||||
) {
|
) {
|
||||||
let autoAccept = false;
|
let autoAccept = false;
|
||||||
|
|
||||||
|
@ -277,16 +275,19 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 通知を作成
|
|
||||||
if (follower.host === null) {
|
|
||||||
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
|
|
||||||
}, followee.id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (alreadyFollowed) return;
|
if (alreadyFollowed) return;
|
||||||
|
|
||||||
|
// 通知を作成
|
||||||
|
if (follower.host === null) {
|
||||||
|
const profile = await this.cacheService.userProfileCache.fetch(followee.id);
|
||||||
|
|
||||||
|
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
|
||||||
|
message: profile.followedMessage,
|
||||||
|
}, followee.id);
|
||||||
|
}
|
||||||
|
|
||||||
this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id });
|
this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id });
|
||||||
|
|
||||||
const [followeeUser, followerUser] = await Promise.all([
|
const [followeeUser, followerUser] = await Promise.all([
|
||||||
|
@ -307,14 +308,14 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||||
this.federatedInstanceService.fetch(follower.host).then(async i => {
|
this.federatedInstanceService.fetch(follower.host).then(async i => {
|
||||||
this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
|
this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
|
||||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
if (this.meta.enableChartsForFederatedInstances) {
|
||||||
this.instanceChart.updateFollowing(i.host, true);
|
this.instanceChart.updateFollowing(i.host, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||||
this.federatedInstanceService.fetch(followee.host).then(async i => {
|
this.federatedInstanceService.fetch(followee.host).then(async i => {
|
||||||
this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
|
this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
|
||||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
if (this.meta.enableChartsForFederatedInstances) {
|
||||||
this.instanceChart.updateFollowers(i.host, true);
|
this.instanceChart.updateFollowers(i.host, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -439,14 +440,14 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||||
this.federatedInstanceService.fetch(follower.host).then(async i => {
|
this.federatedInstanceService.fetch(follower.host).then(async i => {
|
||||||
this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
|
this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
|
||||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
if (this.meta.enableChartsForFederatedInstances) {
|
||||||
this.instanceChart.updateFollowing(i.host, false);
|
this.instanceChart.updateFollowing(i.host, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||||
this.federatedInstanceService.fetch(followee.host).then(async i => {
|
this.federatedInstanceService.fetch(followee.host).then(async i => {
|
||||||
this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
|
this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
|
||||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
if (this.meta.enableChartsForFederatedInstances) {
|
||||||
this.instanceChart.updateFollowers(i.host, false);
|
this.instanceChart.updateFollowers(i.host, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import type { WebhooksRepository } from '@/models/_.js';
|
import { type WebhooksRepository } from '@/models/_.js';
|
||||||
import type { MiWebhook } from '@/models/Webhook.js';
|
import { MiWebhook } from '@/models/Webhook.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { GlobalEvents } from '@/core/GlobalEventService.js';
|
import { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
|
@ -38,6 +38,31 @@ export class UserWebhookService implements OnApplicationShutdown {
|
||||||
return this.activeWebhooks;
|
return this.activeWebhooks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserWebhook の一覧を取得する.
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public fetchWebhooks(params?: {
|
||||||
|
ids?: MiWebhook['id'][];
|
||||||
|
isActive?: MiWebhook['active'];
|
||||||
|
on?: MiWebhook['on'];
|
||||||
|
}): Promise<MiWebhook[]> {
|
||||||
|
const query = this.webhooksRepository.createQueryBuilder('webhook');
|
||||||
|
if (params) {
|
||||||
|
if (params.ids && params.ids.length > 0) {
|
||||||
|
query.andWhere('webhook.id IN (:...ids)', { ids: params.ids });
|
||||||
|
}
|
||||||
|
if (params.isActive !== undefined) {
|
||||||
|
query.andWhere('webhook.active = :isActive', { isActive: params.isActive });
|
||||||
|
}
|
||||||
|
if (params.on && params.on.length > 0) {
|
||||||
|
query.andWhere(':on <@ webhook.on', { on: params.on });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.getMany();
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async onMessage(_: string, data: string): Promise<void> {
|
private async onMessage(_: string, data: string): Promise<void> {
|
||||||
const obj = JSON.parse(data);
|
const obj = JSON.parse(data);
|
||||||
|
|
|
@ -10,12 +10,16 @@ import RE2 from 're2';
|
||||||
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 { MiMeta } from '@/models/Meta.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UtilityService {
|
export class UtilityService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,4 +116,18 @@ export class UtilityService {
|
||||||
const host = `${this.toPuny(urlObj.hostname)}${urlObj.port.length > 0 ? ':' + urlObj.port : ''}`;
|
const host = `${this.toPuny(urlObj.hostname)}${urlObj.port.length > 0 ? ':' + urlObj.port : ''}`;
|
||||||
return host;
|
return host;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isFederationAllowedHost(host: string): boolean {
|
||||||
|
if (this.meta.federation === 'none') return false;
|
||||||
|
if (this.meta.federation === 'specified' && !this.meta.federationHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`))) return false;
|
||||||
|
if (this.isBlockedHost(this.meta.blockedHosts, host)) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public isFederationAllowedUri(uri: string): boolean {
|
||||||
|
const host = this.extractDbHost(uri);
|
||||||
|
return this.isFederationAllowedHost(host);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,10 +12,9 @@ import {
|
||||||
} from '@simplewebauthn/server';
|
} from '@simplewebauthn/server';
|
||||||
import { AttestationFormat, isoCBOR, isoUint8Array } from '@simplewebauthn/server/helpers';
|
import { AttestationFormat, isoCBOR, isoUint8Array } from '@simplewebauthn/server/helpers';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { UserSecurityKeysRepository } from '@/models/_.js';
|
import type { MiMeta, UserSecurityKeysRepository } from '@/models/_.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { MiUser } from '@/models/_.js';
|
import { MiUser } from '@/models/_.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import type {
|
import type {
|
||||||
|
@ -23,7 +22,6 @@ import type {
|
||||||
AuthenticatorTransportFuture,
|
AuthenticatorTransportFuture,
|
||||||
CredentialDeviceType,
|
CredentialDeviceType,
|
||||||
PublicKeyCredentialCreationOptionsJSON,
|
PublicKeyCredentialCreationOptionsJSON,
|
||||||
PublicKeyCredentialDescriptorFuture,
|
|
||||||
PublicKeyCredentialRequestOptionsJSON,
|
PublicKeyCredentialRequestOptionsJSON,
|
||||||
RegistrationResponseJSON,
|
RegistrationResponseJSON,
|
||||||
} from '@simplewebauthn/types';
|
} from '@simplewebauthn/types';
|
||||||
|
@ -31,33 +29,33 @@ import type {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WebAuthnService {
|
export class WebAuthnService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redis)
|
|
||||||
private redisClient: Redis.Redis,
|
|
||||||
|
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
|
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.userSecurityKeysRepository)
|
@Inject(DI.userSecurityKeysRepository)
|
||||||
private userSecurityKeysRepository: UserSecurityKeysRepository,
|
private userSecurityKeysRepository: UserSecurityKeysRepository,
|
||||||
|
|
||||||
private metaService: MetaService,
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getRelyingParty(): Promise<{ origin: string; rpId: string; rpName: string; rpIcon?: string; }> {
|
public getRelyingParty(): { origin: string; rpId: string; rpName: string; rpIcon?: string; } {
|
||||||
const instance = await this.metaService.fetch();
|
|
||||||
return {
|
return {
|
||||||
origin: this.config.url,
|
origin: this.config.url,
|
||||||
rpId: this.config.hostname,
|
rpId: this.config.hostname,
|
||||||
rpName: instance.name ?? this.config.host,
|
rpName: this.meta.name ?? this.config.host,
|
||||||
rpIcon: instance.iconUrl ?? undefined,
|
rpIcon: this.meta.iconUrl ?? undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async initiateRegistration(userId: MiUser['id'], userName: string, userDisplayName?: string): Promise<PublicKeyCredentialCreationOptionsJSON> {
|
public async initiateRegistration(userId: MiUser['id'], userName: string, userDisplayName?: string): Promise<PublicKeyCredentialCreationOptionsJSON> {
|
||||||
const relyingParty = await this.getRelyingParty();
|
const relyingParty = this.getRelyingParty();
|
||||||
const keys = await this.userSecurityKeysRepository.findBy({
|
const keys = await this.userSecurityKeysRepository.findBy({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
});
|
});
|
||||||
|
@ -104,7 +102,7 @@ export class WebAuthnService {
|
||||||
|
|
||||||
await this.redisClient.del(`webauthn:challenge:${userId}`);
|
await this.redisClient.del(`webauthn:challenge:${userId}`);
|
||||||
|
|
||||||
const relyingParty = await this.getRelyingParty();
|
const relyingParty = this.getRelyingParty();
|
||||||
|
|
||||||
let verification;
|
let verification;
|
||||||
try {
|
try {
|
||||||
|
@ -143,7 +141,7 @@ export class WebAuthnService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async initiateAuthentication(userId: MiUser['id']): Promise<PublicKeyCredentialRequestOptionsJSON> {
|
public async initiateAuthentication(userId: MiUser['id']): Promise<PublicKeyCredentialRequestOptionsJSON> {
|
||||||
const relyingParty = await this.getRelyingParty();
|
const relyingParty = this.getRelyingParty();
|
||||||
const keys = await this.userSecurityKeysRepository.findBy({
|
const keys = await this.userSecurityKeysRepository.findBy({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
});
|
});
|
||||||
|
@ -166,6 +164,86 @@ export class WebAuthnService {
|
||||||
return authenticationOptions;
|
return authenticationOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiate Passkey Auth (Without specifying user)
|
||||||
|
* @returns authenticationOptions
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async initiateSignInWithPasskeyAuthentication(context: string): Promise<PublicKeyCredentialRequestOptionsJSON> {
|
||||||
|
const relyingParty = await this.getRelyingParty();
|
||||||
|
|
||||||
|
const authenticationOptions = await generateAuthenticationOptions({
|
||||||
|
rpID: relyingParty.rpId,
|
||||||
|
userVerification: 'preferred',
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.redisClient.setex(`webauthn:challenge:${context}`, 90, authenticationOptions.challenge);
|
||||||
|
|
||||||
|
return authenticationOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify Webauthn AuthenticationCredential
|
||||||
|
* @throws IdentifiableError
|
||||||
|
* @returns If the challenge is successful, return the user ID. Otherwise, return null.
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async verifySignInWithPasskeyAuthentication(context: string, response: AuthenticationResponseJSON): Promise<MiUser['id'] | null> {
|
||||||
|
const challenge = await this.redisClient.get(`webauthn:challenge:${context}`);
|
||||||
|
|
||||||
|
if (!challenge) {
|
||||||
|
throw new IdentifiableError('2d16e51c-007b-4edd-afd2-f7dd02c947f6', `challenge '${context}' not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.redisClient.del(`webauthn:challenge:${context}`);
|
||||||
|
|
||||||
|
const key = await this.userSecurityKeysRepository.findOneBy({
|
||||||
|
id: response.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
throw new IdentifiableError('36b96a7d-b547-412d-aeed-2d611cdc8cdc', 'Unknown Webauthn key');
|
||||||
|
}
|
||||||
|
|
||||||
|
const relyingParty = await this.getRelyingParty();
|
||||||
|
|
||||||
|
let verification;
|
||||||
|
try {
|
||||||
|
verification = await verifyAuthenticationResponse({
|
||||||
|
response: response,
|
||||||
|
expectedChallenge: challenge,
|
||||||
|
expectedOrigin: relyingParty.origin,
|
||||||
|
expectedRPID: relyingParty.rpId,
|
||||||
|
authenticator: {
|
||||||
|
credentialID: key.id,
|
||||||
|
credentialPublicKey: Buffer.from(key.publicKey, 'base64url'),
|
||||||
|
counter: key.counter,
|
||||||
|
transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined,
|
||||||
|
},
|
||||||
|
requireUserVerification: true,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new IdentifiableError('b18c89a7-5b5e-4cec-bb5b-0419f332d430', `verification failed: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { verified, authenticationInfo } = verification;
|
||||||
|
|
||||||
|
if (!verified) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.userSecurityKeysRepository.update({
|
||||||
|
id: response.id,
|
||||||
|
}, {
|
||||||
|
lastUsed: new Date(),
|
||||||
|
counter: authenticationInfo.newCounter,
|
||||||
|
credentialDeviceType: authenticationInfo.credentialDeviceType,
|
||||||
|
credentialBackedUp: authenticationInfo.credentialBackedUp,
|
||||||
|
});
|
||||||
|
|
||||||
|
return key.userId;
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async verifyAuthentication(userId: MiUser['id'], response: AuthenticationResponseJSON): Promise<boolean> {
|
public async verifyAuthentication(userId: MiUser['id'], response: AuthenticationResponseJSON): Promise<boolean> {
|
||||||
const challenge = await this.redisClient.get(`webauthn:challenge:${userId}`);
|
const challenge = await this.redisClient.get(`webauthn:challenge:${userId}`);
|
||||||
|
@ -209,7 +287,7 @@ export class WebAuthnService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const relyingParty = await this.getRelyingParty();
|
const relyingParty = this.getRelyingParty();
|
||||||
|
|
||||||
let verification;
|
let verification;
|
||||||
try {
|
try {
|
||||||
|
|
435
packages/backend/src/core/WebhookTestService.ts
Normal file
435
packages/backend/src/core/WebhookTestService.ts
Normal file
|
@ -0,0 +1,435 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { MiAbuseUserReport, MiNote, MiUser, MiWebhook } from '@/models/_.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
|
||||||
|
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||||
|
import { Packed } from '@/misc/json-schema.js';
|
||||||
|
import { type WebhookEventTypes } from '@/models/Webhook.js';
|
||||||
|
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
||||||
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
|
||||||
|
const oneDayMillis = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
function generateAbuseReport(override?: Partial<MiAbuseUserReport>): MiAbuseUserReport {
|
||||||
|
return {
|
||||||
|
id: 'dummy-abuse-report1',
|
||||||
|
targetUserId: 'dummy-target-user',
|
||||||
|
targetUser: null,
|
||||||
|
reporterId: 'dummy-reporter-user',
|
||||||
|
reporter: null,
|
||||||
|
assigneeId: null,
|
||||||
|
assignee: null,
|
||||||
|
resolved: false,
|
||||||
|
forwarded: false,
|
||||||
|
comment: 'This is a dummy report for testing purposes.',
|
||||||
|
targetUserHost: null,
|
||||||
|
reporterHost: null,
|
||||||
|
...override,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateDummyUser(override?: Partial<MiUser>): MiUser {
|
||||||
|
return {
|
||||||
|
id: 'dummy-user-1',
|
||||||
|
updatedAt: new Date(Date.now() - oneDayMillis * 7),
|
||||||
|
lastFetchedAt: new Date(Date.now() - oneDayMillis * 5),
|
||||||
|
lastActiveDate: new Date(Date.now() - oneDayMillis * 3),
|
||||||
|
hideOnlineStatus: false,
|
||||||
|
username: 'dummy1',
|
||||||
|
usernameLower: 'dummy1',
|
||||||
|
name: 'DummyUser1',
|
||||||
|
followersCount: 10,
|
||||||
|
followingCount: 5,
|
||||||
|
movedToUri: null,
|
||||||
|
movedAt: null,
|
||||||
|
alsoKnownAs: null,
|
||||||
|
notesCount: 30,
|
||||||
|
avatarId: null,
|
||||||
|
avatar: null,
|
||||||
|
bannerId: null,
|
||||||
|
banner: null,
|
||||||
|
avatarUrl: null,
|
||||||
|
bannerUrl: null,
|
||||||
|
avatarBlurhash: null,
|
||||||
|
bannerBlurhash: null,
|
||||||
|
avatarDecorations: [],
|
||||||
|
tags: [],
|
||||||
|
isSuspended: false,
|
||||||
|
isLocked: false,
|
||||||
|
isBot: false,
|
||||||
|
isCat: true,
|
||||||
|
isRoot: false,
|
||||||
|
isExplorable: true,
|
||||||
|
isHibernated: false,
|
||||||
|
isDeleted: false,
|
||||||
|
emojis: [],
|
||||||
|
score: 0,
|
||||||
|
host: null,
|
||||||
|
inbox: null,
|
||||||
|
sharedInbox: null,
|
||||||
|
featured: null,
|
||||||
|
uri: null,
|
||||||
|
followersUri: null,
|
||||||
|
token: null,
|
||||||
|
...override,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateDummyNote(override?: Partial<MiNote>): MiNote {
|
||||||
|
return {
|
||||||
|
id: 'dummy-note-1',
|
||||||
|
replyId: null,
|
||||||
|
reply: null,
|
||||||
|
renoteId: null,
|
||||||
|
renote: null,
|
||||||
|
threadId: null,
|
||||||
|
text: 'This is a dummy note for testing purposes.',
|
||||||
|
name: null,
|
||||||
|
cw: null,
|
||||||
|
userId: 'dummy-user-1',
|
||||||
|
user: null,
|
||||||
|
localOnly: true,
|
||||||
|
reactionAcceptance: 'likeOnly',
|
||||||
|
renoteCount: 10,
|
||||||
|
repliesCount: 5,
|
||||||
|
clippedCount: 0,
|
||||||
|
reactions: {},
|
||||||
|
visibility: 'public',
|
||||||
|
uri: null,
|
||||||
|
url: null,
|
||||||
|
fileIds: [],
|
||||||
|
attachedFileTypes: [],
|
||||||
|
visibleUserIds: [],
|
||||||
|
mentions: [],
|
||||||
|
mentionedRemoteUsers: '[]',
|
||||||
|
reactionAndUserPairCache: [],
|
||||||
|
emojis: [],
|
||||||
|
tags: [],
|
||||||
|
hasPoll: false,
|
||||||
|
channelId: null,
|
||||||
|
channel: null,
|
||||||
|
userHost: null,
|
||||||
|
replyUserId: null,
|
||||||
|
replyUserHost: null,
|
||||||
|
renoteUserId: null,
|
||||||
|
renoteUserHost: null,
|
||||||
|
...override,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPackedNote(note: MiNote, detail = true, override?: Packed<'Note'>): Packed<'Note'> {
|
||||||
|
return {
|
||||||
|
id: note.id,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
deletedAt: null,
|
||||||
|
text: note.text,
|
||||||
|
cw: note.cw,
|
||||||
|
userId: note.userId,
|
||||||
|
user: toPackedUserLite(note.user ?? generateDummyUser()),
|
||||||
|
replyId: note.replyId,
|
||||||
|
renoteId: note.renoteId,
|
||||||
|
isHidden: false,
|
||||||
|
visibility: note.visibility,
|
||||||
|
mentions: note.mentions,
|
||||||
|
visibleUserIds: note.visibleUserIds,
|
||||||
|
fileIds: note.fileIds,
|
||||||
|
files: [],
|
||||||
|
tags: note.tags,
|
||||||
|
poll: null,
|
||||||
|
emojis: note.emojis,
|
||||||
|
channelId: note.channelId,
|
||||||
|
channel: note.channel,
|
||||||
|
localOnly: note.localOnly,
|
||||||
|
reactionAcceptance: note.reactionAcceptance,
|
||||||
|
reactionEmojis: {},
|
||||||
|
reactions: {},
|
||||||
|
reactionCount: 0,
|
||||||
|
renoteCount: note.renoteCount,
|
||||||
|
repliesCount: note.repliesCount,
|
||||||
|
uri: note.uri ?? undefined,
|
||||||
|
url: note.url ?? undefined,
|
||||||
|
reactionAndUserPairCache: note.reactionAndUserPairCache,
|
||||||
|
...(detail ? {
|
||||||
|
clippedCount: note.clippedCount,
|
||||||
|
reply: note.reply ? toPackedNote(note.reply, false) : null,
|
||||||
|
renote: note.renote ? toPackedNote(note.renote, true) : null,
|
||||||
|
myReaction: null,
|
||||||
|
} : {}),
|
||||||
|
...override,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPackedUserLite(user: MiUser, override?: Packed<'UserLite'>): Packed<'UserLite'> {
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
username: user.username,
|
||||||
|
host: user.host,
|
||||||
|
avatarUrl: user.avatarUrl,
|
||||||
|
avatarBlurhash: user.avatarBlurhash,
|
||||||
|
avatarDecorations: user.avatarDecorations.map(it => ({
|
||||||
|
id: it.id,
|
||||||
|
angle: it.angle,
|
||||||
|
flipH: it.flipH,
|
||||||
|
url: 'https://example.com/dummy-image001.png',
|
||||||
|
offsetX: it.offsetX,
|
||||||
|
offsetY: it.offsetY,
|
||||||
|
})),
|
||||||
|
isBot: user.isBot,
|
||||||
|
isCat: user.isCat,
|
||||||
|
emojis: user.emojis,
|
||||||
|
onlineStatus: 'active',
|
||||||
|
badgeRoles: [],
|
||||||
|
...override,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPackedUserDetailedNotMe(user: MiUser, override?: Packed<'UserDetailedNotMe'>): Packed<'UserDetailedNotMe'> {
|
||||||
|
return {
|
||||||
|
...toPackedUserLite(user),
|
||||||
|
url: null,
|
||||||
|
uri: null,
|
||||||
|
movedTo: null,
|
||||||
|
alsoKnownAs: [],
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: user.updatedAt?.toISOString() ?? null,
|
||||||
|
lastFetchedAt: user.lastFetchedAt?.toISOString() ?? null,
|
||||||
|
bannerUrl: user.bannerUrl,
|
||||||
|
bannerBlurhash: user.bannerBlurhash,
|
||||||
|
isLocked: user.isLocked,
|
||||||
|
isSilenced: false,
|
||||||
|
isSuspended: user.isSuspended,
|
||||||
|
description: null,
|
||||||
|
location: null,
|
||||||
|
birthday: null,
|
||||||
|
lang: null,
|
||||||
|
fields: [],
|
||||||
|
verifiedLinks: [],
|
||||||
|
followersCount: user.followersCount,
|
||||||
|
followingCount: user.followingCount,
|
||||||
|
notesCount: user.notesCount,
|
||||||
|
pinnedNoteIds: [],
|
||||||
|
pinnedNotes: [],
|
||||||
|
pinnedPageId: null,
|
||||||
|
pinnedPage: null,
|
||||||
|
publicReactions: true,
|
||||||
|
followersVisibility: 'public',
|
||||||
|
followingVisibility: 'public',
|
||||||
|
twoFactorEnabled: false,
|
||||||
|
usePasswordLessLogin: false,
|
||||||
|
securityKeys: false,
|
||||||
|
roles: [],
|
||||||
|
memo: null,
|
||||||
|
moderationNote: undefined,
|
||||||
|
isFollowing: false,
|
||||||
|
isFollowed: false,
|
||||||
|
hasPendingFollowRequestFromYou: false,
|
||||||
|
hasPendingFollowRequestToYou: false,
|
||||||
|
isBlocking: false,
|
||||||
|
isBlocked: false,
|
||||||
|
isMuted: false,
|
||||||
|
isRenoteMuted: false,
|
||||||
|
notify: 'none',
|
||||||
|
withReplies: true,
|
||||||
|
...override,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const dummyUser1 = generateDummyUser();
|
||||||
|
const dummyUser2 = generateDummyUser({
|
||||||
|
id: 'dummy-user-2',
|
||||||
|
updatedAt: new Date(Date.now() - oneDayMillis * 30),
|
||||||
|
lastFetchedAt: new Date(Date.now() - oneDayMillis),
|
||||||
|
lastActiveDate: new Date(Date.now() - oneDayMillis),
|
||||||
|
username: 'dummy2',
|
||||||
|
usernameLower: 'dummy2',
|
||||||
|
name: 'DummyUser2',
|
||||||
|
followersCount: 40,
|
||||||
|
followingCount: 50,
|
||||||
|
notesCount: 900,
|
||||||
|
});
|
||||||
|
const dummyUser3 = generateDummyUser({
|
||||||
|
id: 'dummy-user-3',
|
||||||
|
updatedAt: new Date(Date.now() - oneDayMillis * 15),
|
||||||
|
lastFetchedAt: new Date(Date.now() - oneDayMillis * 2),
|
||||||
|
lastActiveDate: new Date(Date.now() - oneDayMillis * 2),
|
||||||
|
username: 'dummy3',
|
||||||
|
usernameLower: 'dummy3',
|
||||||
|
name: 'DummyUser3',
|
||||||
|
followersCount: 60,
|
||||||
|
followingCount: 70,
|
||||||
|
notesCount: 15900,
|
||||||
|
});
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WebhookTestService {
|
||||||
|
public static NoSuchWebhookError = class extends Error {};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private userWebhookService: UserWebhookService,
|
||||||
|
private systemWebhookService: SystemWebhookService,
|
||||||
|
private queueService: QueueService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserWebhookのテスト送信を行う.
|
||||||
|
* 送信されるペイロードはいずれもダミーの値で、実際にはデータベース上に存在しない.
|
||||||
|
*
|
||||||
|
* また、この関数経由で送信されるWebhookは以下の設定を無視する.
|
||||||
|
* - Webhookそのものの有効・無効設定(active)
|
||||||
|
* - 送信対象イベント(on)に関する設定
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async testUserWebhook(
|
||||||
|
params: {
|
||||||
|
webhookId: MiWebhook['id'],
|
||||||
|
type: WebhookEventTypes,
|
||||||
|
override?: Partial<Omit<MiWebhook, 'id'>>,
|
||||||
|
},
|
||||||
|
sender: MiUser | null,
|
||||||
|
) {
|
||||||
|
const webhooks = await this.userWebhookService.fetchWebhooks({ ids: [params.webhookId] })
|
||||||
|
.then(it => it.filter(it => it.userId === sender?.id));
|
||||||
|
if (webhooks.length === 0) {
|
||||||
|
throw new WebhookTestService.NoSuchWebhookError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const webhook = webhooks[0];
|
||||||
|
const send = (contents: unknown) => {
|
||||||
|
const merged = {
|
||||||
|
...webhook,
|
||||||
|
...params.override,
|
||||||
|
};
|
||||||
|
|
||||||
|
// テスト目的なのでUserWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図).
|
||||||
|
// また、Jobの試行回数も1回だけ.
|
||||||
|
this.queueService.userWebhookDeliver(merged, params.type, contents, { attempts: 1 });
|
||||||
|
};
|
||||||
|
|
||||||
|
const dummyNote1 = generateDummyNote({
|
||||||
|
userId: dummyUser1.id,
|
||||||
|
user: dummyUser1,
|
||||||
|
});
|
||||||
|
const dummyReply1 = generateDummyNote({
|
||||||
|
id: 'dummy-reply-1',
|
||||||
|
replyId: dummyNote1.id,
|
||||||
|
reply: dummyNote1,
|
||||||
|
userId: dummyUser1.id,
|
||||||
|
user: dummyUser1,
|
||||||
|
});
|
||||||
|
const dummyRenote1 = generateDummyNote({
|
||||||
|
id: 'dummy-renote-1',
|
||||||
|
renoteId: dummyNote1.id,
|
||||||
|
renote: dummyNote1,
|
||||||
|
userId: dummyUser2.id,
|
||||||
|
user: dummyUser2,
|
||||||
|
text: null,
|
||||||
|
});
|
||||||
|
const dummyMention1 = generateDummyNote({
|
||||||
|
id: 'dummy-mention-1',
|
||||||
|
userId: dummyUser1.id,
|
||||||
|
user: dummyUser1,
|
||||||
|
text: `@${dummyUser2.username} This is a mention to you.`,
|
||||||
|
mentions: [dummyUser2.id],
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (params.type) {
|
||||||
|
case 'note': {
|
||||||
|
send(toPackedNote(dummyNote1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'reply': {
|
||||||
|
send(toPackedNote(dummyReply1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'renote': {
|
||||||
|
send(toPackedNote(dummyRenote1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'mention': {
|
||||||
|
send(toPackedNote(dummyMention1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'follow': {
|
||||||
|
send(toPackedUserDetailedNotMe(dummyUser1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'followed': {
|
||||||
|
send(toPackedUserLite(dummyUser2));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'unfollow': {
|
||||||
|
send(toPackedUserDetailedNotMe(dummyUser3));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SystemWebhookのテスト送信を行う.
|
||||||
|
* 送信されるペイロードはいずれもダミーの値で、実際にはデータベース上に存在しない.
|
||||||
|
*
|
||||||
|
* また、この関数経由で送信されるWebhookは以下の設定を無視する.
|
||||||
|
* - Webhookそのものの有効・無効設定(isActive)
|
||||||
|
* - 送信対象イベント(on)に関する設定
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async testSystemWebhook(
|
||||||
|
params: {
|
||||||
|
webhookId: MiSystemWebhook['id'],
|
||||||
|
type: SystemWebhookEventType,
|
||||||
|
override?: Partial<Omit<MiSystemWebhook, 'id'>>,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const webhooks = await this.systemWebhookService.fetchSystemWebhooks({ ids: [params.webhookId] });
|
||||||
|
if (webhooks.length === 0) {
|
||||||
|
throw new WebhookTestService.NoSuchWebhookError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const webhook = webhooks[0];
|
||||||
|
const send = (contents: unknown) => {
|
||||||
|
const merged = {
|
||||||
|
...webhook,
|
||||||
|
...params.override,
|
||||||
|
};
|
||||||
|
|
||||||
|
// テスト目的なのでSystemWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図).
|
||||||
|
// また、Jobの試行回数も1回だけ.
|
||||||
|
this.queueService.systemWebhookDeliver(merged, params.type, contents, { attempts: 1 });
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (params.type) {
|
||||||
|
case 'abuseReport': {
|
||||||
|
send(generateAbuseReport({
|
||||||
|
targetUserId: dummyUser1.id,
|
||||||
|
targetUser: dummyUser1,
|
||||||
|
reporterId: dummyUser2.id,
|
||||||
|
reporter: dummyUser2,
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'abuseReportResolved': {
|
||||||
|
send(generateAbuseReport({
|
||||||
|
targetUserId: dummyUser1.id,
|
||||||
|
targetUser: dummyUser1,
|
||||||
|
reporterId: dummyUser2.id,
|
||||||
|
reporter: dummyUser2,
|
||||||
|
assigneeId: dummyUser3.id,
|
||||||
|
assignee: dummyUser3,
|
||||||
|
resolved: true,
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'userCreated': {
|
||||||
|
send(toPackedUserLite(dummyUser1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,14 +18,13 @@ import { NoteCreateService } from '@/core/NoteCreateService.js';
|
||||||
import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js';
|
import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js';
|
||||||
import { AppLockService } from '@/core/AppLockService.js';
|
import { AppLockService } from '@/core/AppLockService.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { StatusError } from '@/misc/status-error.js';
|
import { StatusError } from '@/misc/status-error.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js';
|
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { MiRemoteUser } from '@/models/User.js';
|
import type { MiRemoteUser } from '@/models/User.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
@ -50,6 +49,9 @@ export class ApInboxService {
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@ -66,7 +68,6 @@ export class ApInboxService {
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private metaService: MetaService,
|
|
||||||
private abuseReportService: AbuseReportService,
|
private abuseReportService: AbuseReportService,
|
||||||
private userFollowingService: UserFollowingService,
|
private userFollowingService: UserFollowingService,
|
||||||
private apAudienceService: ApAudienceService,
|
private apAudienceService: ApAudienceService,
|
||||||
|
@ -292,9 +293,8 @@ export class ApInboxService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// アナウンス先をブロックしてたら中断
|
// アナウンス先が許可されているかチェック
|
||||||
const meta = await this.metaService.fetch();
|
if (!this.utilityService.isFederationAllowedUri(uri)) return;
|
||||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) return;
|
|
||||||
|
|
||||||
const unlock = await this.appLockService.getApLock(uri);
|
const unlock = await this.appLockService.getApLock(uri);
|
||||||
|
|
||||||
|
|
|
@ -517,6 +517,7 @@ export class ApRendererService {
|
||||||
name: user.name,
|
name: user.name,
|
||||||
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
|
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
|
||||||
_misskey_summary: profile.description,
|
_misskey_summary: profile.description,
|
||||||
|
_misskey_followedMessage: profile.followedMessage,
|
||||||
icon: avatar ? this.renderImage(avatar) : null,
|
icon: avatar ? this.renderImage(avatar) : null,
|
||||||
image: banner ? this.renderImage(banner) : null,
|
image: banner ? this.renderImage(banner) : null,
|
||||||
backgroundUrl: background ? this.renderImage(background) : null,
|
backgroundUrl: background ? this.renderImage(background) : null,
|
||||||
|
|
|
@ -208,12 +208,12 @@ export class ApRequestService {
|
||||||
const contentType = res.headers.get('content-type');
|
const contentType = res.headers.get('content-type');
|
||||||
|
|
||||||
if (
|
if (
|
||||||
res.ok
|
res.ok &&
|
||||||
&& (contentType ?? '').split(';')[0].trimEnd().toLowerCase() === 'text/html'
|
(contentType ?? '').split(';')[0].trimEnd().toLowerCase() === 'text/html' &&
|
||||||
&& _followAlternate === true
|
_followAlternate === true
|
||||||
) {
|
) {
|
||||||
const html = await res.text();
|
const html = await res.text();
|
||||||
const window = new Window({
|
const { window, happyDOM } = new Window({
|
||||||
settings: {
|
settings: {
|
||||||
disableJavaScriptEvaluation: true,
|
disableJavaScriptEvaluation: true,
|
||||||
disableJavaScriptFileLoading: true,
|
disableJavaScriptFileLoading: true,
|
||||||
|
@ -247,7 +247,7 @@ export class ApRequestService {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// something went wrong parsing the HTML, ignore the whole thing
|
// something went wrong parsing the HTML, ignore the whole thing
|
||||||
} finally {
|
} finally {
|
||||||
await window.happyDOM.close();
|
happyDOM.close().catch(err => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
|
@ -7,9 +7,8 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { IsNull, Not } from 'typeorm';
|
import { IsNull, Not } from 'typeorm';
|
||||||
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
||||||
import { InstanceActorService } from '@/core/InstanceActorService.js';
|
import { InstanceActorService } from '@/core/InstanceActorService.js';
|
||||||
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js';
|
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
|
@ -29,6 +28,7 @@ export class Resolver {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
private meta: MiMeta,
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
private pollsRepository: PollsRepository,
|
private pollsRepository: PollsRepository,
|
||||||
|
@ -36,7 +36,6 @@ export class Resolver {
|
||||||
private followRequestsRepository: FollowRequestsRepository,
|
private followRequestsRepository: FollowRequestsRepository,
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private instanceActorService: InstanceActorService,
|
private instanceActorService: InstanceActorService,
|
||||||
private metaService: MetaService,
|
|
||||||
private apRequestService: ApRequestService,
|
private apRequestService: ApRequestService,
|
||||||
private httpRequestService: HttpRequestService,
|
private httpRequestService: HttpRequestService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
|
@ -94,8 +93,7 @@ export class Resolver {
|
||||||
return await this.resolveLocal(value);
|
return await this.resolveLocal(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const meta = await this.metaService.fetch();
|
if (!this.utilityService.isFederationAllowedHost(host)) {
|
||||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, host)) {
|
|
||||||
throw new Error('Instance is blocked');
|
throw new Error('Instance is blocked');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,6 +184,9 @@ export class ApResolverService {
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@ -203,7 +204,6 @@ export class ApResolverService {
|
||||||
|
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private instanceActorService: InstanceActorService,
|
private instanceActorService: InstanceActorService,
|
||||||
private metaService: MetaService,
|
|
||||||
private apRequestService: ApRequestService,
|
private apRequestService: ApRequestService,
|
||||||
private httpRequestService: HttpRequestService,
|
private httpRequestService: HttpRequestService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
|
@ -216,6 +216,7 @@ export class ApResolverService {
|
||||||
public createResolver(): Resolver {
|
public createResolver(): Resolver {
|
||||||
return new Resolver(
|
return new Resolver(
|
||||||
this.config,
|
this.config,
|
||||||
|
this.meta,
|
||||||
this.usersRepository,
|
this.usersRepository,
|
||||||
this.notesRepository,
|
this.notesRepository,
|
||||||
this.pollsRepository,
|
this.pollsRepository,
|
||||||
|
@ -223,7 +224,6 @@ export class ApResolverService {
|
||||||
this.followRequestsRepository,
|
this.followRequestsRepository,
|
||||||
this.utilityService,
|
this.utilityService,
|
||||||
this.instanceActorService,
|
this.instanceActorService,
|
||||||
this.metaService,
|
|
||||||
this.apRequestService,
|
this.apRequestService,
|
||||||
this.httpRequestService,
|
this.httpRequestService,
|
||||||
this.apRendererService,
|
this.apRendererService,
|
||||||
|
|
|
@ -557,6 +557,7 @@ const extension_context_definition = {
|
||||||
'_misskey_reaction': 'misskey:_misskey_reaction',
|
'_misskey_reaction': 'misskey:_misskey_reaction',
|
||||||
'_misskey_votes': 'misskey:_misskey_votes',
|
'_misskey_votes': 'misskey:_misskey_votes',
|
||||||
'_misskey_summary': 'misskey:_misskey_summary',
|
'_misskey_summary': 'misskey:_misskey_summary',
|
||||||
|
'_misskey_followedMessage': 'misskey:_misskey_followedMessage',
|
||||||
'isCat': 'misskey:isCat',
|
'isCat': 'misskey:isCat',
|
||||||
// Firefish
|
// Firefish
|
||||||
firefish: 'https://joinfirefish.org/ns#',
|
firefish: 'https://joinfirefish.org/ns#',
|
||||||
|
|
|
@ -5,10 +5,9 @@
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { DriveFilesRepository } from '@/models/_.js';
|
import type { DriveFilesRepository, MiMeta } from '@/models/_.js';
|
||||||
import type { MiRemoteUser } from '@/models/User.js';
|
import type { MiRemoteUser } from '@/models/User.js';
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { truncate } from '@/misc/truncate.js';
|
import { truncate } from '@/misc/truncate.js';
|
||||||
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
|
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
|
||||||
import { DriveService } from '@/core/DriveService.js';
|
import { DriveService } from '@/core/DriveService.js';
|
||||||
|
@ -25,10 +24,12 @@ export class ApImageService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
|
|
||||||
@Inject(DI.driveFilesRepository)
|
@Inject(DI.driveFilesRepository)
|
||||||
private driveFilesRepository: DriveFilesRepository,
|
private driveFilesRepository: DriveFilesRepository,
|
||||||
|
|
||||||
private metaService: MetaService,
|
|
||||||
private apResolverService: ApResolverService,
|
private apResolverService: ApResolverService,
|
||||||
private driveService: DriveService,
|
private driveService: DriveService,
|
||||||
private apLoggerService: ApLoggerService,
|
private apLoggerService: ApLoggerService,
|
||||||
|
@ -65,12 +66,10 @@ export class ApImageService {
|
||||||
|
|
||||||
this.logger.info(`Creating the Image: ${image.url}`);
|
this.logger.info(`Creating the Image: ${image.url}`);
|
||||||
|
|
||||||
const instance = await this.metaService.fetch();
|
|
||||||
|
|
||||||
// Cache if remote file cache is on AND either
|
// Cache if remote file cache is on AND either
|
||||||
// 1. remote sensitive file is also on
|
// 1. remote sensitive file is also on
|
||||||
// 2. or the image is not sensitive
|
// 2. or the image is not sensitive
|
||||||
const shouldBeCached = instance.cacheRemoteFiles && (instance.cacheRemoteSensitiveFiles || !image.sensitive);
|
const shouldBeCached = this.meta.cacheRemoteFiles && (this.meta.cacheRemoteSensitiveFiles || !image.sensitive);
|
||||||
|
|
||||||
await this.federatedInstanceService.fetch(actor.host).then(async i => {
|
await this.federatedInstanceService.fetch(actor.host).then(async i => {
|
||||||
if (i.isNSFW) {
|
if (i.isNSFW) {
|
||||||
|
|
|
@ -6,13 +6,12 @@
|
||||||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { PollsRepository, EmojisRepository, NotesRepository } from '@/models/_.js';
|
import type { PollsRepository, EmojisRepository, NotesRepository, MiMeta } from '@/models/_.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { MiRemoteUser } from '@/models/User.js';
|
import type { MiRemoteUser } from '@/models/User.js';
|
||||||
import type { MiNote } from '@/models/Note.js';
|
import type { MiNote } from '@/models/Note.js';
|
||||||
import { toArray, toSingle, unique } from '@/misc/prelude/array.js';
|
import { toArray, toSingle, unique } from '@/misc/prelude/array.js';
|
||||||
import type { MiEmoji } from '@/models/Emoji.js';
|
import type { MiEmoji } from '@/models/Emoji.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { AppLockService } from '@/core/AppLockService.js';
|
import { AppLockService } from '@/core/AppLockService.js';
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
||||||
|
@ -47,6 +46,9 @@ export class ApNoteService {
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
|
|
||||||
@Inject(DI.pollsRepository)
|
@Inject(DI.pollsRepository)
|
||||||
private pollsRepository: PollsRepository,
|
private pollsRepository: PollsRepository,
|
||||||
|
|
||||||
|
@ -69,7 +71,6 @@ export class ApNoteService {
|
||||||
private apMentionService: ApMentionService,
|
private apMentionService: ApMentionService,
|
||||||
private apImageService: ApImageService,
|
private apImageService: ApImageService,
|
||||||
private apQuestionService: ApQuestionService,
|
private apQuestionService: ApQuestionService,
|
||||||
private metaService: MetaService,
|
|
||||||
private appLockService: AppLockService,
|
private appLockService: AppLockService,
|
||||||
private pollService: PollService,
|
private pollService: PollService,
|
||||||
private noteCreateService: NoteCreateService,
|
private noteCreateService: NoteCreateService,
|
||||||
|
@ -187,7 +188,7 @@ export class ApNoteService {
|
||||||
/**
|
/**
|
||||||
* 禁止ワードチェック
|
* 禁止ワードチェック
|
||||||
*/
|
*/
|
||||||
const hasProhibitedWords = await this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices });
|
const hasProhibitedWords = this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices });
|
||||||
if (hasProhibitedWords) {
|
if (hasProhibitedWords) {
|
||||||
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
|
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
|
||||||
}
|
}
|
||||||
|
@ -565,9 +566,7 @@ export class ApNoteService {
|
||||||
public async resolveNote(value: string | IObject, options: { sentFrom?: URL, resolver?: Resolver } = {}): Promise<MiNote | null> {
|
public async resolveNote(value: string | IObject, options: { sentFrom?: URL, resolver?: Resolver } = {}): Promise<MiNote | null> {
|
||||||
const uri = getApId(value);
|
const uri = getApId(value);
|
||||||
|
|
||||||
// ブロックしていたら中断
|
if (!this.utilityService.isFederationAllowedUri(uri)) {
|
||||||
const meta = await this.metaService.fetch();
|
|
||||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) {
|
|
||||||
throw new StatusError('blocked host', 451);
|
throw new StatusError('blocked host', 451);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,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/_.js';
|
import type { FollowingsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
||||||
import { MiUser } from '@/models/User.js';
|
import { MiUser } from '@/models/User.js';
|
||||||
|
@ -35,7 +35,6 @@ import type { UtilityService } from '@/core/UtilityService.js';
|
||||||
import type { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import type { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||||
import type { AccountMoveService } from '@/core/AccountMoveService.js';
|
import type { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||||
import { checkHttps } from '@/misc/check-https.js';
|
import { checkHttps } from '@/misc/check-https.js';
|
||||||
|
@ -46,7 +45,7 @@ import type { ApNoteService } from './ApNoteService.js';
|
||||||
import type { ApMfmService } from '../ApMfmService.js';
|
import type { ApMfmService } from '../ApMfmService.js';
|
||||||
import type { ApResolverService, Resolver } from '../ApResolverService.js';
|
import type { ApResolverService, Resolver } from '../ApResolverService.js';
|
||||||
import type { ApLoggerService } from '../ApLoggerService.js';
|
import type { ApLoggerService } from '../ApLoggerService.js';
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
||||||
import type { ApImageService } from './ApImageService.js';
|
import type { ApImageService } from './ApImageService.js';
|
||||||
import type { IActor, ICollection, IObject, IOrderedCollection } from '../type.js';
|
import type { IActor, ICollection, IObject, IOrderedCollection } from '../type.js';
|
||||||
|
|
||||||
|
@ -62,7 +61,6 @@ export class ApPersonService implements OnModuleInit {
|
||||||
private driveFileEntityService: DriveFileEntityService;
|
private driveFileEntityService: DriveFileEntityService;
|
||||||
private idService: IdService;
|
private idService: IdService;
|
||||||
private globalEventService: GlobalEventService;
|
private globalEventService: GlobalEventService;
|
||||||
private metaService: MetaService;
|
|
||||||
private federatedInstanceService: FederatedInstanceService;
|
private federatedInstanceService: FederatedInstanceService;
|
||||||
private fetchInstanceMetadataService: FetchInstanceMetadataService;
|
private fetchInstanceMetadataService: FetchInstanceMetadataService;
|
||||||
private cacheService: CacheService;
|
private cacheService: CacheService;
|
||||||
|
@ -84,6 +82,9 @@ export class ApPersonService implements OnModuleInit {
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
|
|
||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
|
@ -112,7 +113,6 @@ export class ApPersonService implements OnModuleInit {
|
||||||
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
|
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
|
||||||
this.idService = this.moduleRef.get('IdService');
|
this.idService = this.moduleRef.get('IdService');
|
||||||
this.globalEventService = this.moduleRef.get('GlobalEventService');
|
this.globalEventService = this.moduleRef.get('GlobalEventService');
|
||||||
this.metaService = this.moduleRef.get('MetaService');
|
|
||||||
this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
|
this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
|
||||||
this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService');
|
this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService');
|
||||||
this.cacheService = this.moduleRef.get('CacheService');
|
this.cacheService = this.moduleRef.get('CacheService');
|
||||||
|
@ -319,8 +319,8 @@ export class ApPersonService implements OnModuleInit {
|
||||||
this.logger.error('error occurred while fetching following/followers collection', { stack: err });
|
this.logger.error('error occurred while fetching following/followers collection', { stack: err });
|
||||||
}
|
}
|
||||||
return 'private';
|
return 'private';
|
||||||
})
|
}),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
||||||
|
@ -395,6 +395,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
await transactionalEntityManager.save(new MiUserProfile({
|
await transactionalEntityManager.save(new MiUserProfile({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
description: _description,
|
description: _description,
|
||||||
|
followedMessage: person._misskey_followedMessage != null ? truncate(person._misskey_followedMessage, 256) : null,
|
||||||
url,
|
url,
|
||||||
fields,
|
fields,
|
||||||
followingVisibility,
|
followingVisibility,
|
||||||
|
@ -433,10 +434,10 @@ export class ApPersonService implements OnModuleInit {
|
||||||
this.cacheService.uriPersonCache.set(user.uri, user);
|
this.cacheService.uriPersonCache.set(user.uri, user);
|
||||||
|
|
||||||
// Register host
|
// Register host
|
||||||
this.federatedInstanceService.fetch(host).then(async i => {
|
this.federatedInstanceService.fetch(host).then(i => {
|
||||||
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
|
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
|
||||||
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
|
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
|
||||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
if (this.meta.enableChartsForFederatedInstances) {
|
||||||
this.instanceChart.newUser(i.host);
|
this.instanceChart.newUser(i.host);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -520,8 +521,8 @@ export class ApPersonService implements OnModuleInit {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return 'private';
|
return 'private';
|
||||||
})
|
}),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
||||||
|
@ -595,6 +596,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
url,
|
url,
|
||||||
fields,
|
fields,
|
||||||
description: _description,
|
description: _description,
|
||||||
|
followedMessage: person._misskey_followedMessage != null ? truncate(person._misskey_followedMessage, 256) : null,
|
||||||
followingVisibility,
|
followingVisibility,
|
||||||
followersVisibility,
|
followersVisibility,
|
||||||
birthday: bday?.[0] ?? null,
|
birthday: bday?.[0] ?? null,
|
||||||
|
|
|
@ -13,6 +13,7 @@ export interface IObject {
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
summary?: string | null;
|
summary?: string | null;
|
||||||
_misskey_summary?: string;
|
_misskey_summary?: string;
|
||||||
|
_misskey_followedMessage?: string | null;
|
||||||
published?: string;
|
published?: string;
|
||||||
cc?: ApObject;
|
cc?: ApObject;
|
||||||
to?: ApObject;
|
to?: ApObject;
|
||||||
|
|
|
@ -5,10 +5,9 @@
|
||||||
|
|
||||||
import { Injectable, Inject } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import type { FollowingsRepository, InstancesRepository } from '@/models/_.js';
|
import type { FollowingsRepository, InstancesRepository, MiMeta } from '@/models/_.js';
|
||||||
import { AppLockService } from '@/core/AppLockService.js';
|
import { AppLockService } from '@/core/AppLockService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import Chart from '../core.js';
|
import Chart from '../core.js';
|
||||||
import { ChartLoggerService } from '../ChartLoggerService.js';
|
import { ChartLoggerService } from '../ChartLoggerService.js';
|
||||||
|
@ -24,13 +23,15 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
|
|
||||||
@Inject(DI.followingsRepository)
|
@Inject(DI.followingsRepository)
|
||||||
private followingsRepository: FollowingsRepository,
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
@Inject(DI.instancesRepository)
|
@Inject(DI.instancesRepository)
|
||||||
private instancesRepository: InstancesRepository,
|
private instancesRepository: InstancesRepository,
|
||||||
|
|
||||||
private metaService: MetaService,
|
|
||||||
private appLockService: AppLockService,
|
private appLockService: AppLockService,
|
||||||
private chartLoggerService: ChartLoggerService,
|
private chartLoggerService: ChartLoggerService,
|
||||||
) {
|
) {
|
||||||
|
@ -43,8 +44,6 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
|
protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
|
||||||
const meta = await this.metaService.fetch();
|
|
||||||
|
|
||||||
const suspendedInstancesQuery = this.instancesRepository.createQueryBuilder('instance')
|
const suspendedInstancesQuery = this.instancesRepository.createQueryBuilder('instance')
|
||||||
.select('instance.host')
|
.select('instance.host')
|
||||||
.where('instance.suspensionState != \'none\'');
|
.where('instance.suspensionState != \'none\'');
|
||||||
|
@ -65,21 +64,21 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
||||||
this.followingsRepository.createQueryBuilder('following')
|
this.followingsRepository.createQueryBuilder('following')
|
||||||
.select('COUNT(DISTINCT following.followeeHost)')
|
.select('COUNT(DISTINCT following.followeeHost)')
|
||||||
.where('following.followeeHost IS NOT NULL')
|
.where('following.followeeHost IS NOT NULL')
|
||||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||||
.andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
|
.andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
|
||||||
.getRawOne()
|
.getRawOne()
|
||||||
.then(x => parseInt(x.count, 10)),
|
.then(x => parseInt(x.count, 10)),
|
||||||
this.followingsRepository.createQueryBuilder('following')
|
this.followingsRepository.createQueryBuilder('following')
|
||||||
.select('COUNT(DISTINCT following.followerHost)')
|
.select('COUNT(DISTINCT following.followerHost)')
|
||||||
.where('following.followerHost IS NOT NULL')
|
.where('following.followerHost IS NOT NULL')
|
||||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||||
.andWhere(`following.followerHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
|
.andWhere(`following.followerHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
|
||||||
.getRawOne()
|
.getRawOne()
|
||||||
.then(x => parseInt(x.count, 10)),
|
.then(x => parseInt(x.count, 10)),
|
||||||
this.followingsRepository.createQueryBuilder('following')
|
this.followingsRepository.createQueryBuilder('following')
|
||||||
.select('COUNT(DISTINCT following.followeeHost)')
|
.select('COUNT(DISTINCT following.followeeHost)')
|
||||||
.where('following.followeeHost IS NOT NULL')
|
.where('following.followeeHost IS NOT NULL')
|
||||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||||
.andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
|
.andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
|
||||||
.andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`)
|
.andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`)
|
||||||
.setParameters(pubsubSubQuery.getParameters())
|
.setParameters(pubsubSubQuery.getParameters())
|
||||||
|
@ -88,7 +87,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
||||||
this.instancesRepository.createQueryBuilder('instance')
|
this.instancesRepository.createQueryBuilder('instance')
|
||||||
.select('COUNT(instance.id)')
|
.select('COUNT(instance.id)')
|
||||||
.where(`instance.host IN (${ subInstancesQuery.getQuery() })`)
|
.where(`instance.host IN (${ subInstancesQuery.getQuery() })`)
|
||||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||||
.andWhere('instance.suspensionState = \'none\'')
|
.andWhere('instance.suspensionState = \'none\'')
|
||||||
.andWhere('instance.isNotResponding = false')
|
.andWhere('instance.isNotResponding = false')
|
||||||
.getRawOne()
|
.getRawOne()
|
||||||
|
@ -96,7 +95,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
||||||
this.instancesRepository.createQueryBuilder('instance')
|
this.instancesRepository.createQueryBuilder('instance')
|
||||||
.select('COUNT(instance.id)')
|
.select('COUNT(instance.id)')
|
||||||
.where(`instance.host IN (${ pubInstancesQuery.getQuery() })`)
|
.where(`instance.host IN (${ pubInstancesQuery.getQuery() })`)
|
||||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||||
.andWhere('instance.suspensionState = \'none\'')
|
.andWhere('instance.suspensionState = \'none\'')
|
||||||
.andWhere('instance.isNotResponding = false')
|
.andWhere('instance.isNotResponding = false')
|
||||||
.getRawOne()
|
.getRawOne()
|
||||||
|
|
|
@ -3,19 +3,22 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import type { MiInstance } from '@/models/Instance.js';
|
import type { MiInstance } from '@/models/Instance.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { MiUser } from '@/models/User.js';
|
import { MiUser } from '@/models/User.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { MiMeta } from '@/models/_.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class InstanceEntityService {
|
export class InstanceEntityService {
|
||||||
constructor(
|
constructor(
|
||||||
private metaService: MetaService,
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
|
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
|
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
|
@ -27,7 +30,6 @@ export class InstanceEntityService {
|
||||||
instance: MiInstance,
|
instance: MiInstance,
|
||||||
me?: { id: MiUser['id']; } | null | undefined,
|
me?: { id: MiUser['id']; } | null | undefined,
|
||||||
): Promise<Packed<'FederationInstance'>> {
|
): Promise<Packed<'FederationInstance'>> {
|
||||||
const meta = await this.metaService.fetch();
|
|
||||||
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
|
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -41,7 +43,7 @@ export class InstanceEntityService {
|
||||||
isNotResponding: instance.isNotResponding,
|
isNotResponding: instance.isNotResponding,
|
||||||
isSuspended: instance.suspensionState !== 'none',
|
isSuspended: instance.suspensionState !== 'none',
|
||||||
suspensionState: instance.suspensionState,
|
suspensionState: instance.suspensionState,
|
||||||
isBlocked: this.utilityService.isBlockedHost(meta.blockedHosts, instance.host),
|
isBlocked: this.utilityService.isBlockedHost(this.meta.blockedHosts, instance.host),
|
||||||
softwareName: instance.softwareName,
|
softwareName: instance.softwareName,
|
||||||
softwareVersion: instance.softwareVersion,
|
softwareVersion: instance.softwareVersion,
|
||||||
openRegistrations: instance.openRegistrations,
|
openRegistrations: instance.openRegistrations,
|
||||||
|
@ -49,8 +51,8 @@ export class InstanceEntityService {
|
||||||
description: instance.description,
|
description: instance.description,
|
||||||
maintainerName: instance.maintainerName,
|
maintainerName: instance.maintainerName,
|
||||||
maintainerEmail: instance.maintainerEmail,
|
maintainerEmail: instance.maintainerEmail,
|
||||||
isSilenced: this.utilityService.isSilencedHost(meta.silencedHosts, instance.host),
|
isSilenced: this.utilityService.isSilencedHost(this.meta.silencedHosts, instance.host),
|
||||||
isMediaSilenced: this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, instance.host),
|
isMediaSilenced: this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, instance.host),
|
||||||
iconUrl: instance.iconUrl,
|
iconUrl: instance.iconUrl,
|
||||||
faviconUrl: instance.faviconUrl,
|
faviconUrl: instance.faviconUrl,
|
||||||
themeColor: instance.themeColor,
|
themeColor: instance.themeColor,
|
||||||
|
|
|
@ -9,7 +9,6 @@ import JSON5 from 'json5';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import type { MiMeta } from '@/models/Meta.js';
|
import type { MiMeta } from '@/models/Meta.js';
|
||||||
import type { AdsRepository } from '@/models/_.js';
|
import type { AdsRepository } from '@/models/_.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { InstanceActorService } from '@/core/InstanceActorService.js';
|
import { InstanceActorService } from '@/core/InstanceActorService.js';
|
||||||
|
@ -23,11 +22,13 @@ export class MetaEntityService {
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
|
|
||||||
@Inject(DI.adsRepository)
|
@Inject(DI.adsRepository)
|
||||||
private adsRepository: AdsRepository,
|
private adsRepository: AdsRepository,
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private metaService: MetaService,
|
|
||||||
private instanceActorService: InstanceActorService,
|
private instanceActorService: InstanceActorService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
@ -36,7 +37,7 @@ export class MetaEntityService {
|
||||||
let instance = meta;
|
let instance = meta;
|
||||||
|
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
instance = await this.metaService.fetch();
|
instance = this.meta;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ads = await this.adsRepository.createQueryBuilder('ads')
|
const ads = await this.adsRepository.createQueryBuilder('ads')
|
||||||
|
@ -134,6 +135,7 @@ export class MetaEntityService {
|
||||||
mediaProxy: this.config.mediaProxy,
|
mediaProxy: this.config.mediaProxy,
|
||||||
enableUrlPreview: instance.urlPreviewEnabled,
|
enableUrlPreview: instance.urlPreviewEnabled,
|
||||||
noteSearchableScope: (this.config.meilisearch == null || this.config.meilisearch.scope !== 'local') ? 'global' : 'local',
|
noteSearchableScope: (this.config.meilisearch == null || this.config.meilisearch.scope !== 'local') ? 'global' : 'local',
|
||||||
|
maxFileSize: this.config.maxFileSize,
|
||||||
};
|
};
|
||||||
|
|
||||||
return packed;
|
return packed;
|
||||||
|
@ -144,7 +146,7 @@ export class MetaEntityService {
|
||||||
let instance = meta;
|
let instance = meta;
|
||||||
|
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
instance = await this.metaService.fetch();
|
instance = this.meta;
|
||||||
}
|
}
|
||||||
|
|
||||||
const packed = await this.pack(instance);
|
const packed = await this.pack(instance);
|
||||||
|
|
|
@ -11,11 +11,11 @@ import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import type { MiNote } from '@/models/Note.js';
|
import type { MiNote } from '@/models/Note.js';
|
||||||
import type { MiNoteReaction } from '@/models/NoteReaction.js';
|
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, MiMeta } from '@/models/_.js';
|
||||||
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository } from '@/models/_.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { DebounceLoader } from '@/misc/loader.js';
|
import { DebounceLoader } from '@/misc/loader.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
|
||||||
import type { OnModuleInit } from '@nestjs/common';
|
import type { OnModuleInit } from '@nestjs/common';
|
||||||
import type { CustomEmojiService } from '../CustomEmojiService.js';
|
import type { CustomEmojiService } from '../CustomEmojiService.js';
|
||||||
import type { ReactionService } from '../ReactionService.js';
|
import type { ReactionService } from '../ReactionService.js';
|
||||||
|
@ -29,12 +29,16 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
private driveFileEntityService: DriveFileEntityService;
|
private driveFileEntityService: DriveFileEntityService;
|
||||||
private customEmojiService: CustomEmojiService;
|
private customEmojiService: CustomEmojiService;
|
||||||
private reactionService: ReactionService;
|
private reactionService: ReactionService;
|
||||||
|
private reactionsBufferingService: ReactionsBufferingService;
|
||||||
private idService: IdService;
|
private idService: IdService;
|
||||||
private noteLoader = new DebounceLoader(this.findNoteOrFail);
|
private noteLoader = new DebounceLoader(this.findNoteOrFail);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private moduleRef: ModuleRef,
|
private moduleRef: ModuleRef,
|
||||||
|
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@ -63,6 +67,8 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
//private driveFileEntityService: DriveFileEntityService,
|
//private driveFileEntityService: DriveFileEntityService,
|
||||||
//private customEmojiService: CustomEmojiService,
|
//private customEmojiService: CustomEmojiService,
|
||||||
//private reactionService: ReactionService,
|
//private reactionService: ReactionService,
|
||||||
|
//private reactionsBufferingService: ReactionsBufferingService,
|
||||||
|
//private idService: IdService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,6 +77,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
|
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
|
||||||
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
|
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
|
||||||
this.reactionService = this.moduleRef.get('ReactionService');
|
this.reactionService = this.moduleRef.get('ReactionService');
|
||||||
|
this.reactionsBufferingService = this.moduleRef.get('ReactionsBufferingService');
|
||||||
this.idService = this.moduleRef.get('IdService');
|
this.idService = this.moduleRef.get('IdService');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -304,6 +311,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
skipHide?: boolean;
|
skipHide?: boolean;
|
||||||
withReactionAndUserPairCache?: boolean;
|
withReactionAndUserPairCache?: boolean;
|
||||||
_hint_?: {
|
_hint_?: {
|
||||||
|
bufferedReactions: Map<MiNote['id'], { deltas: Record<string, number>; pairs: ([MiUser['id'], string])[] }> | null;
|
||||||
myReactions: Map<MiNote['id'], string | null>;
|
myReactions: Map<MiNote['id'], string | null>;
|
||||||
packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
|
packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
|
||||||
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>
|
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>
|
||||||
|
@ -320,6 +328,15 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
const note = typeof src === 'object' ? src : await this.noteLoader.load(src);
|
const note = typeof src === 'object' ? src : await this.noteLoader.load(src);
|
||||||
const host = note.userHost;
|
const host = note.userHost;
|
||||||
|
|
||||||
|
const bufferedReactions = opts._hint_?.bufferedReactions != null
|
||||||
|
? (opts._hint_.bufferedReactions.get(note.id) ?? { deltas: {}, pairs: [] })
|
||||||
|
: this.meta.enableReactionsBuffering
|
||||||
|
? await this.reactionsBufferingService.get(note.id)
|
||||||
|
: { deltas: {}, pairs: [] };
|
||||||
|
const reactions = this.reactionService.convertLegacyReactions(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions.deltas ?? {}));
|
||||||
|
|
||||||
|
const reactionAndUserPairCache = note.reactionAndUserPairCache.concat(bufferedReactions.pairs.map(x => x.join('/')));
|
||||||
|
|
||||||
let text = note.text;
|
let text = note.text;
|
||||||
|
|
||||||
if (note.name && (note.url ?? note.uri)) {
|
if (note.name && (note.url ?? note.uri)) {
|
||||||
|
@ -332,7 +349,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
: await this.channelsRepository.findOneBy({ id: note.channelId })
|
: await this.channelsRepository.findOneBy({ id: note.channelId })
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const reactionEmojiNames = Object.keys(note.reactions)
|
const reactionEmojiNames = Object.keys(reactions)
|
||||||
.filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ
|
.filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ
|
||||||
.map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', ''));
|
.map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', ''));
|
||||||
const packedFiles = options?._hint_?.packedFiles;
|
const packedFiles = options?._hint_?.packedFiles;
|
||||||
|
@ -352,10 +369,10 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined,
|
visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined,
|
||||||
renoteCount: note.renoteCount,
|
renoteCount: note.renoteCount,
|
||||||
repliesCount: note.repliesCount,
|
repliesCount: note.repliesCount,
|
||||||
reactionCount: Object.values(note.reactions).reduce((a, b) => a + b, 0),
|
reactionCount: Object.values(reactions).reduce((a, b) => a + b, 0),
|
||||||
reactions: this.reactionService.convertLegacyReactions(note.reactions),
|
reactions: reactions,
|
||||||
reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host),
|
reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host),
|
||||||
reactionAndUserPairCache: opts.withReactionAndUserPairCache ? note.reactionAndUserPairCache : undefined,
|
reactionAndUserPairCache: opts.withReactionAndUserPairCache ? reactionAndUserPairCache : undefined,
|
||||||
emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined,
|
emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined,
|
||||||
tags: note.tags.length > 0 ? note.tags : undefined,
|
tags: note.tags.length > 0 ? note.tags : undefined,
|
||||||
fileIds: note.fileIds,
|
fileIds: note.fileIds,
|
||||||
|
@ -375,8 +392,12 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
uri: note.uri ?? undefined,
|
uri: note.uri ?? undefined,
|
||||||
url: note.url ?? undefined,
|
url: note.url ?? undefined,
|
||||||
poll: note.hasPoll ? this.populatePoll(note, meId) : undefined,
|
poll: note.hasPoll ? this.populatePoll(note, meId) : undefined,
|
||||||
...(meId && Object.keys(note.reactions).length > 0 ? {
|
...(meId && Object.keys(reactions).length > 0 ? {
|
||||||
myReaction: this.populateMyReaction(note, meId, options?._hint_),
|
myReaction: this.populateMyReaction({
|
||||||
|
id: note.id,
|
||||||
|
reactions: reactions,
|
||||||
|
reactionAndUserPairCache: reactionAndUserPairCache,
|
||||||
|
}, meId, options?._hint_),
|
||||||
} : {}),
|
} : {}),
|
||||||
|
|
||||||
...(opts.detail ? {
|
...(opts.detail ? {
|
||||||
|
@ -416,6 +437,8 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
) {
|
) {
|
||||||
if (notes.length === 0) return [];
|
if (notes.length === 0) return [];
|
||||||
|
|
||||||
|
const bufferedReactions = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany(notes.map(x => x.id)) : null;
|
||||||
|
|
||||||
const meId = me ? me.id : null;
|
const meId = me ? me.id : null;
|
||||||
const myReactionsMap = new Map<MiNote['id'], string | null>();
|
const myReactionsMap = new Map<MiNote['id'], string | null>();
|
||||||
if (meId) {
|
if (meId) {
|
||||||
|
@ -426,23 +449,33 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
|
|
||||||
for (const note of notes) {
|
for (const note of notes) {
|
||||||
if (note.renote && (note.text == null && note.fileIds.length === 0)) { // pure renote
|
if (note.renote && (note.text == null && note.fileIds.length === 0)) { // pure renote
|
||||||
const reactionsCount = Object.values(note.renote.reactions).reduce((a, b) => a + b, 0);
|
const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.renote.reactions, bufferedReactions?.get(note.renote.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
|
||||||
if (reactionsCount === 0) {
|
if (reactionsCount === 0) {
|
||||||
myReactionsMap.set(note.renote.id, null);
|
myReactionsMap.set(note.renote.id, null);
|
||||||
} else if (reactionsCount <= note.renote.reactionAndUserPairCache.length) {
|
} else if (reactionsCount <= note.renote.reactionAndUserPairCache.length + (bufferedReactions?.get(note.renote.id)?.pairs.length ?? 0)) {
|
||||||
|
const pairInBuffer = bufferedReactions?.get(note.renote.id)?.pairs.find(p => p[0] === meId);
|
||||||
|
if (pairInBuffer) {
|
||||||
|
myReactionsMap.set(note.renote.id, pairInBuffer[1]);
|
||||||
|
} else {
|
||||||
const pair = note.renote.reactionAndUserPairCache.find(p => p.startsWith(meId));
|
const pair = note.renote.reactionAndUserPairCache.find(p => p.startsWith(meId));
|
||||||
myReactionsMap.set(note.renote.id, pair ? pair.split('/')[1] : null);
|
myReactionsMap.set(note.renote.id, pair ? pair.split('/')[1] : null);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
idsNeedFetchMyReaction.add(note.renote.id);
|
idsNeedFetchMyReaction.add(note.renote.id);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (note.id < oldId) {
|
if (note.id < oldId) {
|
||||||
const reactionsCount = Object.values(note.reactions).reduce((a, b) => a + b, 0);
|
const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions?.get(note.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
|
||||||
if (reactionsCount === 0) {
|
if (reactionsCount === 0) {
|
||||||
myReactionsMap.set(note.id, null);
|
myReactionsMap.set(note.id, null);
|
||||||
} else if (reactionsCount <= note.reactionAndUserPairCache.length) {
|
} else if (reactionsCount <= note.reactionAndUserPairCache.length + (bufferedReactions?.get(note.id)?.pairs.length ?? 0)) {
|
||||||
|
const pairInBuffer = bufferedReactions?.get(note.id)?.pairs.find(p => p[0] === meId);
|
||||||
|
if (pairInBuffer) {
|
||||||
|
myReactionsMap.set(note.id, pairInBuffer[1]);
|
||||||
|
} else {
|
||||||
const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
|
const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
|
||||||
myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null);
|
myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
idsNeedFetchMyReaction.add(note.id);
|
idsNeedFetchMyReaction.add(note.id);
|
||||||
}
|
}
|
||||||
|
@ -477,6 +510,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
return await Promise.all(notes.map(n => this.pack(n, me, {
|
return await Promise.all(notes.map(n => this.pack(n, me, {
|
||||||
...options,
|
...options,
|
||||||
_hint_: {
|
_hint_: {
|
||||||
|
bufferedReactions,
|
||||||
myReactions: myReactionsMap,
|
myReactions: myReactionsMap,
|
||||||
packedFiles,
|
packedFiles,
|
||||||
packedUsers,
|
packedUsers,
|
||||||
|
|
|
@ -59,7 +59,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
async #packInternal <T extends MiNotification | MiGroupedNotification> (
|
async #packInternal <T extends MiNotification | MiGroupedNotification> (
|
||||||
src: T,
|
src: T,
|
||||||
meId: MiUser['id'],
|
meId: MiUser['id'],
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
||||||
options: {
|
options: {
|
||||||
checkValidNotifier?: boolean;
|
checkValidNotifier?: boolean;
|
||||||
},
|
},
|
||||||
|
@ -159,9 +159,16 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
...(notification.type === 'roleAssigned' ? {
|
...(notification.type === 'roleAssigned' ? {
|
||||||
role: role,
|
role: role,
|
||||||
} : {}),
|
} : {}),
|
||||||
|
...(notification.type === 'followRequestAccepted' ? {
|
||||||
|
message: notification.message,
|
||||||
|
} : {}),
|
||||||
...(notification.type === 'achievementEarned' ? {
|
...(notification.type === 'achievementEarned' ? {
|
||||||
achievement: notification.achievement,
|
achievement: notification.achievement,
|
||||||
} : {}),
|
} : {}),
|
||||||
|
...(notification.type === 'exportCompleted' ? {
|
||||||
|
exportedEntity: notification.exportedEntity,
|
||||||
|
fileId: notification.fileId,
|
||||||
|
} : {}),
|
||||||
...(notification.type === 'app' ? {
|
...(notification.type === 'app' ? {
|
||||||
body: notification.customBody,
|
body: notification.customBody,
|
||||||
header: notification.customHeader,
|
header: notification.customHeader,
|
||||||
|
@ -229,7 +236,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
public async pack(
|
public async pack(
|
||||||
src: MiNotification | MiGroupedNotification,
|
src: MiNotification | MiGroupedNotification,
|
||||||
meId: MiUser['id'],
|
meId: MiUser['id'],
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
||||||
options: {
|
options: {
|
||||||
checkValidNotifier?: boolean;
|
checkValidNotifier?: boolean;
|
||||||
},
|
},
|
||||||
|
|
|
@ -556,7 +556,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
name: r.name,
|
name: r.name,
|
||||||
iconUrl: r.iconUrl,
|
iconUrl: r.iconUrl,
|
||||||
displayOrder: r.displayOrder,
|
displayOrder: r.displayOrder,
|
||||||
}))
|
})),
|
||||||
) : undefined,
|
) : undefined,
|
||||||
|
|
||||||
...(isDetailed ? {
|
...(isDetailed ? {
|
||||||
|
@ -613,6 +613,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
avatarId: user.avatarId,
|
avatarId: user.avatarId,
|
||||||
bannerId: user.bannerId,
|
bannerId: user.bannerId,
|
||||||
backgroundId: user.backgroundId,
|
backgroundId: user.backgroundId,
|
||||||
|
followedMessage: profile!.followedMessage,
|
||||||
isModerator: isModerator,
|
isModerator: isModerator,
|
||||||
isAdmin: isAdmin,
|
isAdmin: isAdmin,
|
||||||
isSystem: isSystemAccount(user),
|
isSystem: isSystemAccount(user),
|
||||||
|
@ -683,6 +684,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
isRenoteMuted: relation.isRenoteMuted,
|
isRenoteMuted: relation.isRenoteMuted,
|
||||||
notify: relation.following?.notify ?? 'none',
|
notify: relation.following?.notify ?? 'none',
|
||||||
withReplies: relation.following?.withReplies ?? false,
|
withReplies: relation.following?.withReplies ?? false,
|
||||||
|
followedMessage: relation.isFollowing ? profile!.followedMessage : undefined,
|
||||||
} : {}),
|
} : {}),
|
||||||
} as Promiseable<Packed<S>>;
|
} as Promiseable<Packed<S>>;
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,14 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import si from 'systeminformation';
|
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';
|
||||||
|
import { MiMeta } from '@/models/_.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
|
||||||
const ev = new Xev();
|
const ev = new Xev();
|
||||||
|
|
||||||
|
@ -23,7 +24,8 @@ export class ServerStatsService implements OnApplicationShutdown {
|
||||||
private intervalId: NodeJS.Timeout | null = null;
|
private intervalId: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private metaService: MetaService,
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,7 +34,7 @@ export class ServerStatsService implements OnApplicationShutdown {
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async start(): Promise<void> {
|
public async start(): Promise<void> {
|
||||||
if (!(await this.metaService.fetch(true)).enableServerMachineStats) return;
|
if (!this.meta.enableServerMachineStats) return;
|
||||||
|
|
||||||
const log = [] as any[];
|
const log = [] as any[];
|
||||||
|
|
||||||
|
|
|
@ -10,8 +10,9 @@
|
||||||
* The getter will return a .bind version of the function
|
* The getter will return a .bind version of the function
|
||||||
* and memoize the result against a symbol on the instance
|
* and memoize the result against a symbol on the instance
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export function bindThis(target: any, key: string, descriptor: any) {
|
export function bindThis(target: any, key: string, descriptor: any) {
|
||||||
let fn = descriptor.value;
|
const fn = descriptor.value;
|
||||||
|
|
||||||
if (typeof fn !== 'function') {
|
if (typeof fn !== 'function') {
|
||||||
throw new TypeError(`@bindThis decorator can only be applied to methods not: ${typeof fn}`);
|
throw new TypeError(`@bindThis decorator can only be applied to methods not: ${typeof fn}`);
|
||||||
|
@ -21,26 +22,18 @@ export function bindThis(target: any, key: string, descriptor: any) {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
get() {
|
get() {
|
||||||
// eslint-disable-next-line no-prototype-builtins
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
if (this === target.prototype || this.hasOwnProperty(key) ||
|
if (this === target.prototype || this.hasOwnProperty(key)) {
|
||||||
typeof fn !== 'function') {
|
|
||||||
return fn;
|
return fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
const boundFn = fn.bind(this);
|
const boundFn = fn.bind(this);
|
||||||
Object.defineProperty(this, key, {
|
Reflect.defineProperty(this, key, {
|
||||||
|
value: boundFn,
|
||||||
configurable: true,
|
configurable: true,
|
||||||
get() {
|
writable: true,
|
||||||
return boundFn;
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
fn = value;
|
|
||||||
delete this[key];
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return boundFn;
|
return boundFn;
|
||||||
},
|
},
|
||||||
set(value: any) {
|
|
||||||
fn = value;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,13 @@
|
||||||
export const DI = {
|
export const DI = {
|
||||||
config: Symbol('config'),
|
config: Symbol('config'),
|
||||||
db: Symbol('db'),
|
db: Symbol('db'),
|
||||||
|
meta: Symbol('meta'),
|
||||||
meilisearch: Symbol('meilisearch'),
|
meilisearch: Symbol('meilisearch'),
|
||||||
redis: Symbol('redis'),
|
redis: Symbol('redis'),
|
||||||
redisForPub: Symbol('redisForPub'),
|
redisForPub: Symbol('redisForPub'),
|
||||||
redisForSub: Symbol('redisForSub'),
|
redisForSub: Symbol('redisForSub'),
|
||||||
redisForTimelines: Symbol('redisForTimelines'),
|
redisForTimelines: Symbol('redisForTimelines'),
|
||||||
|
redisForReactions: Symbol('redisForReactions'),
|
||||||
|
|
||||||
//#region Repositories
|
//#region Repositories
|
||||||
usersRepository: Symbol('usersRepository'),
|
usersRepository: Symbol('usersRepository'),
|
||||||
|
|
44
packages/backend/src/misc/collapsed-queue.ts
Normal file
44
packages/backend/src/misc/collapsed-queue.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
type Job<V> = {
|
||||||
|
value: V;
|
||||||
|
timer: NodeJS.Timeout;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: redis使えるようにする
|
||||||
|
export class CollapsedQueue<K, V> {
|
||||||
|
private jobs: Map<K, Job<V>> = new Map();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private timeout: number,
|
||||||
|
private collapse: (oldValue: V, newValue: V) => V,
|
||||||
|
private perform: (key: K, value: V) => Promise<void>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
enqueue(key: K, value: V) {
|
||||||
|
if (this.jobs.has(key)) {
|
||||||
|
const old = this.jobs.get(key)!;
|
||||||
|
const merged = this.collapse(old.value, value);
|
||||||
|
this.jobs.set(key, { ...old, value: merged });
|
||||||
|
} else {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
const job = this.jobs.get(key)!;
|
||||||
|
this.jobs.delete(key);
|
||||||
|
this.perform(key, job.value);
|
||||||
|
}, this.timeout);
|
||||||
|
this.jobs.set(key, { value, timer });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async performAllNow() {
|
||||||
|
const entries = [...this.jobs.entries()];
|
||||||
|
this.jobs.clear();
|
||||||
|
for (const [_key, job] of entries) {
|
||||||
|
clearTimeout(job.timer);
|
||||||
|
}
|
||||||
|
await Promise.allSettled(entries.map(([key, job]) => this.perform(key, job.value)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ import type { onRequestHookHandler } from 'fastify';
|
||||||
export const handleRequestRedirectToOmitSearch: onRequestHookHandler = (request, reply, done) => {
|
export const handleRequestRedirectToOmitSearch: onRequestHookHandler = (request, reply, done) => {
|
||||||
const index = request.url.indexOf('?');
|
const index = request.url.indexOf('?');
|
||||||
if (~index) {
|
if (~index) {
|
||||||
reply.redirect(301, request.url.slice(0, index));
|
reply.redirect(request.url.slice(0, index), 301);
|
||||||
}
|
}
|
||||||
done();
|
done();
|
||||||
};
|
};
|
||||||
|
|
|
@ -144,7 +144,9 @@ export interface Schema extends OfSchema {
|
||||||
readonly type?: TypeStringef;
|
readonly type?: TypeStringef;
|
||||||
readonly nullable?: boolean;
|
readonly nullable?: boolean;
|
||||||
readonly optional?: boolean;
|
readonly optional?: boolean;
|
||||||
|
readonly prefixItems?: ReadonlyArray<Schema>;
|
||||||
readonly items?: Schema;
|
readonly items?: Schema;
|
||||||
|
readonly unevaluatedItems?: Schema | boolean;
|
||||||
readonly properties?: Obj;
|
readonly properties?: Obj;
|
||||||
readonly required?: ReadonlyArray<Extract<keyof NonNullable<this['properties']>, string>>;
|
readonly required?: ReadonlyArray<Extract<keyof NonNullable<this['properties']>, string>>;
|
||||||
readonly description?: string;
|
readonly description?: string;
|
||||||
|
@ -198,6 +200,7 @@ type UnionSchemaType<a extends readonly any[], X extends Schema = a[number]> = X
|
||||||
//type UnionObjectSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? ObjectSchemaType<X> : never;
|
//type UnionObjectSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? ObjectSchemaType<X> : never;
|
||||||
type UnionObjType<s extends Obj, a extends readonly any[], X extends ReadonlyArray<keyof s> = a[number]> = X extends any ? ObjType<s, X> : never;
|
type UnionObjType<s extends Obj, a extends readonly any[], X extends ReadonlyArray<keyof s> = a[number]> = X extends any ? ObjType<s, X> : never;
|
||||||
type ArrayUnion<T> = T extends any ? Array<T> : never;
|
type ArrayUnion<T> = T extends any ? Array<T> : never;
|
||||||
|
type ArrayToTuple<X extends ReadonlyArray<Schema>> = { [K in keyof X]: SchemaType<X[K]> };
|
||||||
|
|
||||||
type ObjectSchemaTypeDef<p extends Schema> =
|
type ObjectSchemaTypeDef<p extends Schema> =
|
||||||
p['ref'] extends keyof typeof refs ? Packed<p['ref']> :
|
p['ref'] extends keyof typeof refs ? Packed<p['ref']> :
|
||||||
|
@ -232,6 +235,12 @@ export type SchemaTypeDef<p extends Schema> =
|
||||||
p['items']['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<NonNullable<p['items']['allOf']>>>[] :
|
p['items']['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<NonNullable<p['items']['allOf']>>>[] :
|
||||||
never
|
never
|
||||||
) :
|
) :
|
||||||
|
p['prefixItems'] extends ReadonlyArray<Schema> ? (
|
||||||
|
p['items'] extends NonNullable<Schema> ? [...ArrayToTuple<p['prefixItems']>, ...SchemaType<p['items']>[]] :
|
||||||
|
p['items'] extends false ? ArrayToTuple<p['prefixItems']> :
|
||||||
|
p['unevaluatedItems'] extends false ? ArrayToTuple<p['prefixItems']> :
|
||||||
|
[...ArrayToTuple<p['prefixItems']>, ...unknown[]]
|
||||||
|
) :
|
||||||
p['items'] extends NonNullable<Schema> ? SchemaType<p['items']>[] :
|
p['items'] extends NonNullable<Schema> ? SchemaType<p['items']>[] :
|
||||||
any[]
|
any[]
|
||||||
) :
|
) :
|
||||||
|
|
|
@ -627,6 +627,11 @@ export class MiMeta {
|
||||||
})
|
})
|
||||||
public perUserListTimelineCacheMax: number;
|
public perUserListTimelineCacheMax: number;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public enableReactionsBuffering: boolean;
|
||||||
|
|
||||||
@Column('integer', {
|
@Column('integer', {
|
||||||
default: 0,
|
default: 0,
|
||||||
})
|
})
|
||||||
|
@ -682,4 +687,17 @@ export class MiMeta {
|
||||||
comment: 'An array of URL strings or regex that can be used to omit warnings about redirects to external sites. Separate them with spaces to specify AND, and enclose them with slashes to specify regular expressions. Each item is regarded as an OR.',
|
comment: 'An array of URL strings or regex that can be used to omit warnings about redirects to external sites. Separate them with spaces to specify AND, and enclose them with slashes to specify regular expressions. Each item is regarded as an OR.',
|
||||||
})
|
})
|
||||||
public trustedLinkUrlPatterns: string[];
|
public trustedLinkUrlPatterns: string[];
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 128,
|
||||||
|
default: 'all',
|
||||||
|
})
|
||||||
|
public federation: 'all' | 'specified' | 'none';
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 1024,
|
||||||
|
array: true,
|
||||||
|
default: '{}',
|
||||||
|
})
|
||||||
|
public federationHosts: string[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ import { MiUser } from './User.js';
|
||||||
import { MiNote } from './Note.js';
|
import { MiNote } from './Note.js';
|
||||||
import { MiAccessToken } from './AccessToken.js';
|
import { MiAccessToken } from './AccessToken.js';
|
||||||
import { MiRole } from './Role.js';
|
import { MiRole } from './Role.js';
|
||||||
|
import { MiDriveFile } from './DriveFile.js';
|
||||||
|
import { userExportableEntities } from '@/types.js';
|
||||||
|
|
||||||
export type MiNotification = {
|
export type MiNotification = {
|
||||||
type: 'note';
|
type: 'note';
|
||||||
|
@ -67,6 +69,7 @@ export type MiNotification = {
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
notifierId: MiUser['id'];
|
notifierId: MiUser['id'];
|
||||||
|
message: string | null;
|
||||||
} | {
|
} | {
|
||||||
type: 'roleAssigned';
|
type: 'roleAssigned';
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -77,6 +80,12 @@ export type MiNotification = {
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
achievement: string;
|
achievement: string;
|
||||||
|
} | {
|
||||||
|
type: 'exportCompleted';
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
exportedEntity: typeof userExportableEntities[number];
|
||||||
|
fileId: MiDriveFile['id'];
|
||||||
} | {
|
} | {
|
||||||
type: 'app';
|
type: 'app';
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -85,7 +94,7 @@ export type MiNotification = {
|
||||||
/**
|
/**
|
||||||
* アプリ通知のbody
|
* アプリ通知のbody
|
||||||
*/
|
*/
|
||||||
customBody: string | null;
|
customBody: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* アプリ通知のheader
|
* アプリ通知のheader
|
||||||
|
|
|
@ -179,6 +179,11 @@ export class MiUser {
|
||||||
})
|
})
|
||||||
public tags: string[];
|
public tags: string[];
|
||||||
|
|
||||||
|
@Column('integer', {
|
||||||
|
default: 0,
|
||||||
|
})
|
||||||
|
public score: number;
|
||||||
|
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: false,
|
default: false,
|
||||||
comment: 'Whether the User is suspended.',
|
comment: 'Whether the User is suspended.',
|
||||||
|
@ -341,6 +346,7 @@ export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toStr
|
||||||
export const passwordSchema = { type: 'string', minLength: 1 } as const;
|
export const passwordSchema = { type: 'string', minLength: 1 } as const;
|
||||||
export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
||||||
export const descriptionSchema = { type: 'string', minLength: 1, maxLength: 1500 } as const;
|
export const descriptionSchema = { type: 'string', minLength: 1, maxLength: 1500 } as const;
|
||||||
|
export const followedMessageSchema = { type: 'string', minLength: 1, maxLength: 256 } as const;
|
||||||
export const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
export const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
||||||
export const listenbrainzSchema = { type: "string", minLength: 1, maxLength: 128 } as const;
|
export const listenbrainzSchema = { type: "string", minLength: 1, maxLength: 128 } as const;
|
||||||
export const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const;
|
export const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const;
|
||||||
|
|
|
@ -49,6 +49,14 @@ export class MiUserProfile {
|
||||||
})
|
})
|
||||||
public description: string | null;
|
public description: string | null;
|
||||||
|
|
||||||
|
// フォローされた際のメッセージ
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 256, nullable: true,
|
||||||
|
})
|
||||||
|
public followedMessage: string | null;
|
||||||
|
|
||||||
|
// TODO: 鍵アカウントの場合の、フォローリクエスト受信時のメッセージも設定できるようにする
|
||||||
|
|
||||||
@Column('jsonb', {
|
@Column('jsonb', {
|
||||||
default: [],
|
default: [],
|
||||||
})
|
})
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { id } from './util/id.js';
|
||||||
import { MiUser } from './User.js';
|
import { MiUser } from './User.js';
|
||||||
|
|
||||||
export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction', 'edited'] as const;
|
export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction', 'edited'] as const;
|
||||||
|
export type WebhookEventTypes = typeof webhookEventTypes[number];
|
||||||
|
|
||||||
@Entity('webhook')
|
@Entity('webhook')
|
||||||
export class MiWebhook {
|
export class MiWebhook {
|
||||||
|
|
|
@ -281,6 +281,10 @@ export const packedMetaLiteSchema = {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
maxFileSize: {
|
||||||
|
type: 'number',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { notificationTypes } from '@/types.js';
|
import { ACHIEVEMENT_TYPES } from '@/core/AchievementService.js';
|
||||||
|
import { notificationTypes, userExportableEntities } from '@/types.js';
|
||||||
|
|
||||||
const baseSchema = {
|
const baseSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
@ -266,6 +267,10 @@ export const packedNotificationSchema = {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
format: 'id',
|
format: 'id',
|
||||||
},
|
},
|
||||||
|
message: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
@ -294,6 +299,27 @@ export const packedNotificationSchema = {
|
||||||
achievement: {
|
achievement: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
enum: ACHIEVEMENT_TYPES,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
...baseSchema.properties,
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['exportCompleted'],
|
||||||
|
},
|
||||||
|
exportedEntity: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: userExportableEntities,
|
||||||
|
},
|
||||||
|
fileId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
|
@ -311,11 +337,11 @@ export const packedNotificationSchema = {
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
|
|
|
@ -276,6 +276,26 @@ export const packedRolePoliciesSchema = {
|
||||||
type: 'integer',
|
type: 'integer',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
canImportAntennas: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
canImportBlocking: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
canImportFollowing: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
canImportMuting: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
canImportUserLists: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -126,10 +126,6 @@ export const packedUserLiteSchema = {
|
||||||
nullable: false, optional: true,
|
nullable: false, optional: true,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
isSilenced: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
noindex: {
|
noindex: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
|
@ -277,6 +273,10 @@ export const packedUserDetailedNotMeOnlySchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
},
|
},
|
||||||
|
isSilenced: {
|
||||||
|
type: 'boolean',
|
||||||
|
nullable: false, optional: false,
|
||||||
|
},
|
||||||
isSuspended: {
|
isSuspended: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
|
@ -412,6 +412,10 @@ export const packedUserDetailedNotMeOnlySchema = {
|
||||||
ref: 'RoleLite',
|
ref: 'RoleLite',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
followedMessage: {
|
||||||
|
type: 'string',
|
||||||
|
nullable: true, optional: true,
|
||||||
|
},
|
||||||
memo: {
|
memo: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
|
@ -484,6 +488,10 @@ export const packedMeDetailedOnlySchema = {
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
format: 'id',
|
format: 'id',
|
||||||
},
|
},
|
||||||
|
followedMessage: {
|
||||||
|
type: 'string',
|
||||||
|
nullable: true, optional: false,
|
||||||
|
},
|
||||||
isModerator: {
|
isModerator: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { InboxProcessorService } from './processors/InboxProcessorService.js';
|
||||||
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
|
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
|
||||||
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
|
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
|
||||||
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
|
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
|
||||||
|
import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js';
|
||||||
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
|
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
|
||||||
import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
||||||
import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
|
import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
|
||||||
|
@ -53,6 +54,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
|
||||||
ResyncChartsProcessorService,
|
ResyncChartsProcessorService,
|
||||||
CleanChartsProcessorService,
|
CleanChartsProcessorService,
|
||||||
CheckExpiredMutingsProcessorService,
|
CheckExpiredMutingsProcessorService,
|
||||||
|
BakeBufferedReactionsProcessorService,
|
||||||
CleanProcessorService,
|
CleanProcessorService,
|
||||||
DeleteDriveFilesProcessorService,
|
DeleteDriveFilesProcessorService,
|
||||||
ExportAccountDataProcessorService,
|
ExportAccountDataProcessorService,
|
||||||
|
|
|
@ -40,6 +40,7 @@ import { TickChartsProcessorService } from './processors/TickChartsProcessorServ
|
||||||
import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js';
|
import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js';
|
||||||
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
|
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
|
||||||
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
|
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
|
||||||
|
import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js';
|
||||||
import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
||||||
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
||||||
import { QueueLoggerService } from './QueueLoggerService.js';
|
import { QueueLoggerService } from './QueueLoggerService.js';
|
||||||
|
@ -122,6 +123,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
private cleanChartsProcessorService: CleanChartsProcessorService,
|
private cleanChartsProcessorService: CleanChartsProcessorService,
|
||||||
private aggregateRetentionProcessorService: AggregateRetentionProcessorService,
|
private aggregateRetentionProcessorService: AggregateRetentionProcessorService,
|
||||||
private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService,
|
private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService,
|
||||||
|
private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService,
|
||||||
private cleanProcessorService: CleanProcessorService,
|
private cleanProcessorService: CleanProcessorService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger;
|
this.logger = this.queueLoggerService.logger;
|
||||||
|
@ -151,6 +153,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
case 'cleanCharts': return this.cleanChartsProcessorService.process();
|
case 'cleanCharts': return this.cleanChartsProcessorService.process();
|
||||||
case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
|
case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
|
||||||
case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
|
case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
|
||||||
|
case 'bakeBufferedReactions': return this.bakeBufferedReactionsProcessorService.process();
|
||||||
case 'clean': return this.cleanProcessorService.process();
|
case 'clean': return this.cleanProcessorService.process();
|
||||||
default: throw new Error(`unrecognized job type ${job.name} for system`);
|
default: throw new Error(`unrecognized job type ${job.name} for system`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import type Logger from '@/logger.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
|
||||||
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
|
import type * as Bull from 'bullmq';
|
||||||
|
import { MiMeta } from '@/models/_.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BakeBufferedReactionsProcessorService {
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
|
|
||||||
|
private reactionsBufferingService: ReactionsBufferingService,
|
||||||
|
private queueLoggerService: QueueLoggerService,
|
||||||
|
) {
|
||||||
|
this.logger = this.queueLoggerService.logger.createSubLogger('bake-buffered-reactions');
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async process(): Promise<void> {
|
||||||
|
if (!this.meta.enableReactionsBuffering) {
|
||||||
|
this.logger.info('Reactions buffering is disabled. Skipping...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info('Baking buffered reactions...');
|
||||||
|
|
||||||
|
await this.reactionsBufferingService.bake();
|
||||||
|
|
||||||
|
this.logger.succ('All buffered reactions baked.');
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,9 +7,8 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as Bull from 'bullmq';
|
import * as Bull from 'bullmq';
|
||||||
import { Not } from 'typeorm';
|
import { Not } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { InstancesRepository } from '@/models/_.js';
|
import type { InstancesRepository, MiMeta } from '@/models/_.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { ApRequestService } from '@/core/activitypub/ApRequestService.js';
|
import { ApRequestService } from '@/core/activitypub/ApRequestService.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
|
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
|
||||||
|
@ -31,10 +30,12 @@ export class DeliverProcessorService {
|
||||||
private latest: string | null;
|
private latest: string | null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
|
|
||||||
@Inject(DI.instancesRepository)
|
@Inject(DI.instancesRepository)
|
||||||
private instancesRepository: InstancesRepository,
|
private instancesRepository: InstancesRepository,
|
||||||
|
|
||||||
private metaService: MetaService,
|
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private federatedInstanceService: FederatedInstanceService,
|
private federatedInstanceService: FederatedInstanceService,
|
||||||
private fetchInstanceMetadataService: FetchInstanceMetadataService,
|
private fetchInstanceMetadataService: FetchInstanceMetadataService,
|
||||||
|
@ -52,9 +53,7 @@ export class DeliverProcessorService {
|
||||||
public async process(job: Bull.Job<DeliverJobData>): Promise<string> {
|
public async process(job: Bull.Job<DeliverJobData>): Promise<string> {
|
||||||
const { host } = new URL(job.data.to);
|
const { host } = new URL(job.data.to);
|
||||||
|
|
||||||
// ブロックしてたら中断
|
if (!this.utilityService.isFederationAllowedUri(job.data.to)) {
|
||||||
const meta = await this.metaService.fetch();
|
|
||||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.toPuny(host))) {
|
|
||||||
return 'skip (blocked)';
|
return 'skip (blocked)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +87,7 @@ export class DeliverProcessorService {
|
||||||
this.apRequestChart.deliverSucc();
|
this.apRequestChart.deliverSucc();
|
||||||
this.federationChart.deliverd(i.host, true);
|
this.federationChart.deliverd(i.host, true);
|
||||||
|
|
||||||
if (meta.enableChartsForFederatedInstances) {
|
if (this.meta.enableChartsForFederatedInstances) {
|
||||||
this.instanceChart.requestSent(i.host, true);
|
this.instanceChart.requestSent(i.host, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -120,7 +119,7 @@ export class DeliverProcessorService {
|
||||||
this.apRequestChart.deliverFail();
|
this.apRequestChart.deliverFail();
|
||||||
this.federationChart.deliverd(i.host, false);
|
this.federationChart.deliverd(i.host, false);
|
||||||
|
|
||||||
if (meta.enableChartsForFederatedInstances) {
|
if (this.meta.enableChartsForFederatedInstances) {
|
||||||
this.instanceChart.requestSent(i.host, false);
|
this.instanceChart.requestSent(i.host, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { DriveService } from '@/core/DriveService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { createTemp } from '@/misc/create-temp.js';
|
import { createTemp } from '@/misc/create-temp.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type { DBExportAntennasData } from '../types.js';
|
import type { DBExportAntennasData } from '../types.js';
|
||||||
import type * as Bull from 'bullmq';
|
import type * as Bull from 'bullmq';
|
||||||
|
@ -35,6 +36,7 @@ export class ExportAntennasProcessorService {
|
||||||
private driveService: DriveService,
|
private driveService: DriveService,
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private queueLoggerService: QueueLoggerService,
|
private queueLoggerService: QueueLoggerService,
|
||||||
|
private notificationService: NotificationService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger.createSubLogger('export-antennas');
|
this.logger = this.queueLoggerService.logger.createSubLogger('export-antennas');
|
||||||
}
|
}
|
||||||
|
@ -95,6 +97,11 @@ export class ExportAntennasProcessorService {
|
||||||
const fileName = 'antennas-' + DateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json';
|
const fileName = 'antennas-' + DateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json';
|
||||||
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
|
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
|
||||||
this.logger.succ('Exported to: ' + driveFile.id);
|
this.logger.succ('Exported to: ' + driveFile.id);
|
||||||
|
|
||||||
|
this.notificationService.createNotification(user.id, 'exportCompleted', {
|
||||||
|
exportedEntity: 'antenna',
|
||||||
|
fileId: driveFile.id,
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import type Logger from '@/logger.js';
|
||||||
import { DriveService } from '@/core/DriveService.js';
|
import { DriveService } from '@/core/DriveService.js';
|
||||||
import { createTemp } from '@/misc/create-temp.js';
|
import { createTemp } from '@/misc/create-temp.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type * as Bull from 'bullmq';
|
import type * as Bull from 'bullmq';
|
||||||
|
@ -30,6 +31,7 @@ export class ExportBlockingProcessorService {
|
||||||
private blockingsRepository: BlockingsRepository,
|
private blockingsRepository: BlockingsRepository,
|
||||||
|
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
|
private notificationService: NotificationService,
|
||||||
private driveService: DriveService,
|
private driveService: DriveService,
|
||||||
private queueLoggerService: QueueLoggerService,
|
private queueLoggerService: QueueLoggerService,
|
||||||
) {
|
) {
|
||||||
|
@ -109,6 +111,11 @@ export class ExportBlockingProcessorService {
|
||||||
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
|
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
|
||||||
|
|
||||||
this.logger.succ(`Exported to: ${driveFile.id}`);
|
this.logger.succ(`Exported to: ${driveFile.id}`);
|
||||||
|
|
||||||
|
this.notificationService.createNotification(user.id, 'exportCompleted', {
|
||||||
|
exportedEntity: 'blocking',
|
||||||
|
fileId: driveFile.id,
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { bindThis } from '@/decorators.js';
|
||||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||||
import { Packed } from '@/misc/json-schema.js';
|
import { Packed } from '@/misc/json-schema.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type * as Bull from 'bullmq';
|
import type * as Bull from 'bullmq';
|
||||||
import type { DbJobDataWithUser } from '../types.js';
|
import type { DbJobDataWithUser } from '../types.js';
|
||||||
|
@ -43,6 +44,7 @@ export class ExportClipsProcessorService {
|
||||||
private driveService: DriveService,
|
private driveService: DriveService,
|
||||||
private queueLoggerService: QueueLoggerService,
|
private queueLoggerService: QueueLoggerService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
|
private notificationService: NotificationService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger.createSubLogger('export-clips');
|
this.logger = this.queueLoggerService.logger.createSubLogger('export-clips');
|
||||||
}
|
}
|
||||||
|
@ -79,6 +81,11 @@ export class ExportClipsProcessorService {
|
||||||
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
|
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
|
||||||
|
|
||||||
this.logger.succ(`Exported to: ${driveFile.id}`);
|
this.logger.succ(`Exported to: ${driveFile.id}`);
|
||||||
|
|
||||||
|
this.notificationService.createNotification(user.id, 'exportCompleted', {
|
||||||
|
exportedEntity: 'clip',
|
||||||
|
fileId: driveFile.id,
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import type Logger from '@/logger.js';
|
||||||
import { DriveService } from '@/core/DriveService.js';
|
import { DriveService } from '@/core/DriveService.js';
|
||||||
import { createTemp, createTempDir } from '@/misc/create-temp.js';
|
import { createTemp, createTempDir } from '@/misc/create-temp.js';
|
||||||
import { DownloadService } from '@/core/DownloadService.js';
|
import { DownloadService } from '@/core/DownloadService.js';
|
||||||
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type * as Bull from 'bullmq';
|
import type * as Bull from 'bullmq';
|
||||||
|
@ -37,6 +38,7 @@ export class ExportCustomEmojisProcessorService {
|
||||||
private driveService: DriveService,
|
private driveService: DriveService,
|
||||||
private downloadService: DownloadService,
|
private downloadService: DownloadService,
|
||||||
private queueLoggerService: QueueLoggerService,
|
private queueLoggerService: QueueLoggerService,
|
||||||
|
private notificationService: NotificationService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger.createSubLogger('export-custom-emojis');
|
this.logger = this.queueLoggerService.logger.createSubLogger('export-custom-emojis');
|
||||||
}
|
}
|
||||||
|
@ -134,6 +136,12 @@ export class ExportCustomEmojisProcessorService {
|
||||||
const driveFile = await this.driveService.addFile({ user, path: archivePath, name: fileName, force: true });
|
const driveFile = await this.driveService.addFile({ user, path: archivePath, name: fileName, force: true });
|
||||||
|
|
||||||
this.logger.succ(`Exported to: ${driveFile.id}`);
|
this.logger.succ(`Exported to: ${driveFile.id}`);
|
||||||
|
|
||||||
|
this.notificationService.createNotification(user.id, 'exportCompleted', {
|
||||||
|
exportedEntity: 'customEmoji',
|
||||||
|
fileId: driveFile.id,
|
||||||
|
});
|
||||||
|
|
||||||
cleanup();
|
cleanup();
|
||||||
archiveCleanup();
|
archiveCleanup();
|
||||||
resolve();
|
resolve();
|
||||||
|
|
|
@ -16,6 +16,7 @@ import type { MiPoll } from '@/models/Poll.js';
|
||||||
import type { MiNote } from '@/models/Note.js';
|
import type { MiNote } from '@/models/Note.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type * as Bull from 'bullmq';
|
import type * as Bull from 'bullmq';
|
||||||
import type { DbJobDataWithUser } from '../types.js';
|
import type { DbJobDataWithUser } from '../types.js';
|
||||||
|
@ -37,6 +38,7 @@ export class ExportFavoritesProcessorService {
|
||||||
private driveService: DriveService,
|
private driveService: DriveService,
|
||||||
private queueLoggerService: QueueLoggerService,
|
private queueLoggerService: QueueLoggerService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
|
private notificationService: NotificationService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger.createSubLogger('export-favorites');
|
this.logger = this.queueLoggerService.logger.createSubLogger('export-favorites');
|
||||||
}
|
}
|
||||||
|
@ -123,6 +125,11 @@ export class ExportFavoritesProcessorService {
|
||||||
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
|
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
|
||||||
|
|
||||||
this.logger.succ(`Exported to: ${driveFile.id}`);
|
this.logger.succ(`Exported to: ${driveFile.id}`);
|
||||||
|
|
||||||
|
this.notificationService.createNotification(user.id, 'exportCompleted', {
|
||||||
|
exportedEntity: 'favorite',
|
||||||
|
fileId: driveFile.id,
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { DriveService } from '@/core/DriveService.js';
|
||||||
import { createTemp } from '@/misc/create-temp.js';
|
import { createTemp } from '@/misc/create-temp.js';
|
||||||
import type { MiFollowing } from '@/models/Following.js';
|
import type { MiFollowing } from '@/models/Following.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type * as Bull from 'bullmq';
|
import type * as Bull from 'bullmq';
|
||||||
|
@ -36,6 +37,7 @@ export class ExportFollowingProcessorService {
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private driveService: DriveService,
|
private driveService: DriveService,
|
||||||
private queueLoggerService: QueueLoggerService,
|
private queueLoggerService: QueueLoggerService,
|
||||||
|
private notificationService: NotificationService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger.createSubLogger('export-following');
|
this.logger = this.queueLoggerService.logger.createSubLogger('export-following');
|
||||||
}
|
}
|
||||||
|
@ -113,6 +115,11 @@ export class ExportFollowingProcessorService {
|
||||||
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
|
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
|
||||||
|
|
||||||
this.logger.succ(`Exported to: ${driveFile.id}`);
|
this.logger.succ(`Exported to: ${driveFile.id}`);
|
||||||
|
|
||||||
|
this.notificationService.createNotification(user.id, 'exportCompleted', {
|
||||||
|
exportedEntity: 'following',
|
||||||
|
fileId: driveFile.id,
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import type Logger from '@/logger.js';
|
||||||
import { DriveService } from '@/core/DriveService.js';
|
import { DriveService } from '@/core/DriveService.js';
|
||||||
import { createTemp } from '@/misc/create-temp.js';
|
import { createTemp } from '@/misc/create-temp.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type * as Bull from 'bullmq';
|
import type * as Bull from 'bullmq';
|
||||||
|
@ -32,6 +33,7 @@ export class ExportMutingProcessorService {
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private driveService: DriveService,
|
private driveService: DriveService,
|
||||||
private queueLoggerService: QueueLoggerService,
|
private queueLoggerService: QueueLoggerService,
|
||||||
|
private notificationService: NotificationService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger.createSubLogger('export-muting');
|
this.logger = this.queueLoggerService.logger.createSubLogger('export-muting');
|
||||||
}
|
}
|
||||||
|
@ -110,6 +112,11 @@ export class ExportMutingProcessorService {
|
||||||
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
|
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
|
||||||
|
|
||||||
this.logger.succ(`Exported to: ${driveFile.id}`);
|
this.logger.succ(`Exported to: ${driveFile.id}`);
|
||||||
|
|
||||||
|
this.notificationService.createNotification(user.id, 'exportCompleted', {
|
||||||
|
exportedEntity: 'muting',
|
||||||
|
fileId: driveFile.id,
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { bindThis } from '@/decorators.js';
|
||||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||||
import { Packed } from '@/misc/json-schema.js';
|
import { Packed } from '@/misc/json-schema.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { JsonArrayStream } from '@/misc/JsonArrayStream.js';
|
import { JsonArrayStream } from '@/misc/JsonArrayStream.js';
|
||||||
import { FileWriterStream } from '@/misc/FileWriterStream.js';
|
import { FileWriterStream } from '@/misc/FileWriterStream.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
|
@ -112,6 +113,7 @@ export class ExportNotesProcessorService {
|
||||||
private queueLoggerService: QueueLoggerService,
|
private queueLoggerService: QueueLoggerService,
|
||||||
private driveFileEntityService: DriveFileEntityService,
|
private driveFileEntityService: DriveFileEntityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
|
private notificationService: NotificationService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger.createSubLogger('export-notes');
|
this.logger = this.queueLoggerService.logger.createSubLogger('export-notes');
|
||||||
}
|
}
|
||||||
|
@ -150,6 +152,11 @@ export class ExportNotesProcessorService {
|
||||||
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
|
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
|
||||||
|
|
||||||
this.logger.succ(`Exported to: ${driveFile.id}`);
|
this.logger.succ(`Exported to: ${driveFile.id}`);
|
||||||
|
|
||||||
|
this.notificationService.createNotification(user.id, 'exportCompleted', {
|
||||||
|
exportedEntity: 'note',
|
||||||
|
fileId: driveFile.id,
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import type Logger from '@/logger.js';
|
||||||
import { DriveService } from '@/core/DriveService.js';
|
import { DriveService } from '@/core/DriveService.js';
|
||||||
import { createTemp } from '@/misc/create-temp.js';
|
import { createTemp } from '@/misc/create-temp.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type * as Bull from 'bullmq';
|
import type * as Bull from 'bullmq';
|
||||||
|
@ -35,6 +36,7 @@ export class ExportUserListsProcessorService {
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private driveService: DriveService,
|
private driveService: DriveService,
|
||||||
private queueLoggerService: QueueLoggerService,
|
private queueLoggerService: QueueLoggerService,
|
||||||
|
private notificationService: NotificationService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger.createSubLogger('export-user-lists');
|
this.logger = this.queueLoggerService.logger.createSubLogger('export-user-lists');
|
||||||
}
|
}
|
||||||
|
@ -89,6 +91,11 @@ export class ExportUserListsProcessorService {
|
||||||
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
|
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
|
||||||
|
|
||||||
this.logger.succ(`Exported to: ${driveFile.id}`);
|
this.logger.succ(`Exported to: ${driveFile.id}`);
|
||||||
|
|
||||||
|
this.notificationService.createNotification(user.id, 'exportCompleted', {
|
||||||
|
exportedEntity: 'userList',
|
||||||
|
fileId: driveFile.id,
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,6 +88,7 @@ export class ImportCustomEmojisProcessorService {
|
||||||
await this.emojisRepository.delete({
|
await this.emojisRepository.delete({
|
||||||
name: nameNfc,
|
name: nameNfc,
|
||||||
});
|
});
|
||||||
|
try {
|
||||||
const driveFile = await this.driveService.addFile({
|
const driveFile = await this.driveService.addFile({
|
||||||
user: null,
|
user: null,
|
||||||
path: emojiPath,
|
path: emojiPath,
|
||||||
|
@ -105,6 +106,12 @@ export class ImportCustomEmojisProcessorService {
|
||||||
localOnly: emojiInfo.localOnly,
|
localOnly: emojiInfo.localOnly,
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction: [],
|
roleIdsThatCanBeUsedThisEmojiAsReaction: [],
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error || typeof e === 'string') {
|
||||||
|
this.logger.error(`couldn't import ${emojiPath} for ${emojiInfo.name}: ${e}`);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup();
|
cleanup();
|
||||||
|
|
|
@ -4,11 +4,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { URL } from 'node:url';
|
import { URL } from 'node:url';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import httpSignature from '@peertube/http-signature';
|
import httpSignature from '@peertube/http-signature';
|
||||||
import * as Bull from 'bullmq';
|
import * as Bull from 'bullmq';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
|
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
|
||||||
import InstanceChart from '@/core/chart/charts/instance.js';
|
import InstanceChart from '@/core/chart/charts/instance.js';
|
||||||
|
@ -26,16 +25,28 @@ import { JsonLdService } from '@/core/activitypub/JsonLdService.js';
|
||||||
import { ApInboxService } from '@/core/activitypub/ApInboxService.js';
|
import { ApInboxService } from '@/core/activitypub/ApInboxService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
|
import { CollapsedQueue } from '@/misc/collapsed-queue.js';
|
||||||
|
import { MiNote } from '@/models/Note.js';
|
||||||
|
import { MiMeta } from '@/models/Meta.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type { InboxJobData } from '../types.js';
|
import type { InboxJobData } from '../types.js';
|
||||||
|
|
||||||
|
type UpdateInstanceJob = {
|
||||||
|
latestRequestReceivedAt: Date,
|
||||||
|
shouldUnsuspend: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class InboxProcessorService {
|
export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
private updateInstanceQueue: CollapsedQueue<MiNote['id'], UpdateInstanceJob>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
|
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private metaService: MetaService,
|
|
||||||
private apInboxService: ApInboxService,
|
private apInboxService: ApInboxService,
|
||||||
private federatedInstanceService: FederatedInstanceService,
|
private federatedInstanceService: FederatedInstanceService,
|
||||||
private fetchInstanceMetadataService: FetchInstanceMetadataService,
|
private fetchInstanceMetadataService: FetchInstanceMetadataService,
|
||||||
|
@ -48,6 +59,7 @@ export class InboxProcessorService {
|
||||||
private queueLoggerService: QueueLoggerService,
|
private queueLoggerService: QueueLoggerService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger.createSubLogger('inbox');
|
this.logger = this.queueLoggerService.logger.createSubLogger('inbox');
|
||||||
|
this.updateInstanceQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseUpdateInstanceJobs, this.performUpdateInstance);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -63,9 +75,7 @@ export class InboxProcessorService {
|
||||||
|
|
||||||
const host = this.utilityService.toPuny(new URL(signature.keyId).hostname);
|
const host = this.utilityService.toPuny(new URL(signature.keyId).hostname);
|
||||||
|
|
||||||
// ブロックしてたら中断
|
if (!this.utilityService.isFederationAllowedHost(host)) {
|
||||||
const meta = await this.metaService.fetch();
|
|
||||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, host)) {
|
|
||||||
return `Blocked request: ${host}`;
|
return `Blocked request: ${host}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,9 +179,8 @@ export class InboxProcessorService {
|
||||||
throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`);
|
throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ブロックしてたら中断
|
|
||||||
const ldHost = this.utilityService.extractDbHost(authUser.user.uri);
|
const ldHost = this.utilityService.extractDbHost(authUser.user.uri);
|
||||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) {
|
if (!this.utilityService.isFederationAllowedHost(ldHost)) {
|
||||||
throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`);
|
throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -190,11 +199,9 @@ export class InboxProcessorService {
|
||||||
|
|
||||||
// Update stats
|
// Update stats
|
||||||
this.federatedInstanceService.fetch(authUser.user.host).then(i => {
|
this.federatedInstanceService.fetch(authUser.user.host).then(i => {
|
||||||
this.federatedInstanceService.update(i.id, {
|
this.updateInstanceQueue.enqueue(i.id, {
|
||||||
latestRequestReceivedAt: new Date(),
|
latestRequestReceivedAt: new Date(),
|
||||||
isNotResponding: false,
|
shouldUnsuspend: i.suspensionState === 'autoSuspendedForNotResponding',
|
||||||
// もしサーバーが死んでるために配信が止まっていた場合には自動的に復活させてあげる
|
|
||||||
suspensionState: i.suspensionState === 'autoSuspendedForNotResponding' ? 'none' : undefined,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
|
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
|
||||||
|
@ -202,7 +209,7 @@ export class InboxProcessorService {
|
||||||
this.apRequestChart.inbox();
|
this.apRequestChart.inbox();
|
||||||
this.federationChart.inbox(i.host);
|
this.federationChart.inbox(i.host);
|
||||||
|
|
||||||
if (meta.enableChartsForFederatedInstances) {
|
if (this.meta.enableChartsForFederatedInstances) {
|
||||||
this.instanceChart.requestReceived(i.host);
|
this.instanceChart.requestReceived(i.host);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -230,4 +237,36 @@ export class InboxProcessorService {
|
||||||
}
|
}
|
||||||
return 'ok';
|
return 'ok';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public collapseUpdateInstanceJobs(oldJob: UpdateInstanceJob, newJob: UpdateInstanceJob) {
|
||||||
|
const latestRequestReceivedAt = oldJob.latestRequestReceivedAt < newJob.latestRequestReceivedAt
|
||||||
|
? newJob.latestRequestReceivedAt
|
||||||
|
: oldJob.latestRequestReceivedAt;
|
||||||
|
const shouldUnsuspend = oldJob.shouldUnsuspend || newJob.shouldUnsuspend;
|
||||||
|
return {
|
||||||
|
latestRequestReceivedAt,
|
||||||
|
shouldUnsuspend,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async performUpdateInstance(id: string, job: UpdateInstanceJob) {
|
||||||
|
await this.federatedInstanceService.update(id, {
|
||||||
|
latestRequestReceivedAt: new Date(),
|
||||||
|
isNotResponding: false,
|
||||||
|
// もしサーバーが死んでるために配信が止まっていた場合には自動的に復活させてあげる
|
||||||
|
suspensionState: job.shouldUnsuspend ? 'none' : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async dispose(): Promise<void> {
|
||||||
|
await this.updateInstanceQueue.performAllNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
async onApplicationShutdown(signal?: string) {
|
||||||
|
await this.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@ export class FileServerService {
|
||||||
.catch(err => this.errorHandler(request, reply, err));
|
.catch(err => this.errorHandler(request, reply, err));
|
||||||
});
|
});
|
||||||
fastify.get<{ Params: { key: string; } }>('/files/:key/*', async (request, reply) => {
|
fastify.get<{ Params: { key: string; } }>('/files/:key/*', async (request, reply) => {
|
||||||
return await reply.redirect(301, `${this.config.url}/files/${request.params.key}`);
|
return await reply.redirect(`${this.config.url}/files/${request.params.key}`, 301);
|
||||||
});
|
});
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
@ -145,12 +145,12 @@ export class FileServerService {
|
||||||
url.searchParams.set('static', '1');
|
url.searchParams.set('static', '1');
|
||||||
|
|
||||||
file.cleanup();
|
file.cleanup();
|
||||||
return await reply.redirect(301, url.toString());
|
return await reply.redirect(url.toString(), 301);
|
||||||
} else if (file.mime.startsWith('video/')) {
|
} else if (file.mime.startsWith('video/')) {
|
||||||
const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url);
|
const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url);
|
||||||
if (externalThumbnail) {
|
if (externalThumbnail) {
|
||||||
file.cleanup();
|
file.cleanup();
|
||||||
return await reply.redirect(301, externalThumbnail);
|
return await reply.redirect(externalThumbnail, 301);
|
||||||
}
|
}
|
||||||
|
|
||||||
image = await this.videoProcessingService.generateVideoThumbnail(file.path);
|
image = await this.videoProcessingService.generateVideoThumbnail(file.path);
|
||||||
|
@ -165,7 +165,7 @@ export class FileServerService {
|
||||||
url.searchParams.set('url', file.url);
|
url.searchParams.set('url', file.url);
|
||||||
|
|
||||||
file.cleanup();
|
file.cleanup();
|
||||||
return await reply.redirect(301, url.toString());
|
return await reply.redirect(url.toString(), 301);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -312,8 +312,8 @@ export class FileServerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
return await reply.redirect(
|
return await reply.redirect(
|
||||||
301,
|
|
||||||
url.toString(),
|
url.toString(),
|
||||||
|
301,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,9 @@ export class HealthServerService {
|
||||||
@Inject(DI.redisForTimelines)
|
@Inject(DI.redisForTimelines)
|
||||||
private redisForTimelines: Redis.Redis,
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
|
@Inject(DI.redisForReactions)
|
||||||
|
private redisForReactions: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
|
@ -43,6 +46,7 @@ export class HealthServerService {
|
||||||
this.redisForPub.ping(),
|
this.redisForPub.ping(),
|
||||||
this.redisForSub.ping(),
|
this.redisForSub.ping(),
|
||||||
this.redisForTimelines.ping(),
|
this.redisForTimelines.ping(),
|
||||||
|
this.redisForReactions.ping(),
|
||||||
this.db.query('SELECT 1'),
|
this.db.query('SELECT 1'),
|
||||||
...(this.meilisearch ? [this.meilisearch.health()] : []),
|
...(this.meilisearch ? [this.meilisearch.health()] : []),
|
||||||
]).then(() => 200, () => 503));
|
]).then(() => 200, () => 503));
|
||||||
|
|
|
@ -49,6 +49,7 @@ import { MastodonApiServerService } from './api/mastodon/MastodonApiServerServic
|
||||||
import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js';
|
import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js';
|
||||||
import { ReversiChannelService } from './api/stream/channels/reversi.js';
|
import { ReversiChannelService } from './api/stream/channels/reversi.js';
|
||||||
import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js';
|
import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js';
|
||||||
|
import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -74,6 +75,7 @@ import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js
|
||||||
AuthenticateService,
|
AuthenticateService,
|
||||||
RateLimiterService,
|
RateLimiterService,
|
||||||
SigninApiService,
|
SigninApiService,
|
||||||
|
SigninWithPasskeyApiService,
|
||||||
SigninService,
|
SigninService,
|
||||||
SignupApiService,
|
SignupApiService,
|
||||||
StreamingApiServerService,
|
StreamingApiServerService,
|
||||||
|
|
|
@ -13,7 +13,7 @@ import fastifyRawBody from 'fastify-raw-body';
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { EmojisRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
import type { EmojisRepository, MiMeta, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import * as Acct from '@/misc/acct.js';
|
import * as Acct from '@/misc/acct.js';
|
||||||
|
@ -21,7 +21,6 @@ import { genIdenticon } from '@/misc/gen-identicon.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { ActivityPubServerService } from './ActivityPubServerService.js';
|
import { ActivityPubServerService } from './ActivityPubServerService.js';
|
||||||
import { NodeinfoServerService } from './NodeinfoServerService.js';
|
import { NodeinfoServerService } from './NodeinfoServerService.js';
|
||||||
import { ApiServerService } from './api/ApiServerService.js';
|
import { ApiServerService } from './api/ApiServerService.js';
|
||||||
|
@ -45,6 +44,9 @@ export class ServerService implements OnApplicationShutdown {
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@ -54,7 +56,6 @@ export class ServerService implements OnApplicationShutdown {
|
||||||
@Inject(DI.emojisRepository)
|
@Inject(DI.emojisRepository)
|
||||||
private emojisRepository: EmojisRepository,
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
private metaService: MetaService,
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private apiServerService: ApiServerService,
|
private apiServerService: ApiServerService,
|
||||||
private openApiServerService: OpenApiServerService,
|
private openApiServerService: OpenApiServerService,
|
||||||
|
@ -167,8 +168,8 @@ export class ServerService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
|
|
||||||
return await reply.redirect(
|
return await reply.redirect(
|
||||||
301,
|
|
||||||
url.toString(),
|
url.toString(),
|
||||||
|
301,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -195,7 +196,7 @@ export class ServerService implements OnApplicationShutdown {
|
||||||
reply.header('Content-Type', 'image/png');
|
reply.header('Content-Type', 'image/png');
|
||||||
reply.header('Cache-Control', 'public, max-age=86400');
|
reply.header('Cache-Control', 'public, max-age=86400');
|
||||||
|
|
||||||
if ((await this.metaService.fetch()).enableIdenticonGeneration) {
|
if (this.meta.enableIdenticonGeneration) {
|
||||||
return await genIdenticon(request.params.x);
|
return await genIdenticon(request.params.x);
|
||||||
} else {
|
} else {
|
||||||
return reply.redirect('/static-assets/avatar.png');
|
return reply.redirect('/static-assets/avatar.png');
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue