Compare commits
36 Commits
2023.12.0-
...
2023.11.1
Author | SHA1 | Date | |
---|---|---|---|
![]() |
9784d10c62 | ||
![]() |
0c2dd33593 | ||
![]() |
3043b5256d | ||
![]() |
7e7138c0eb | ||
![]() |
f964ef163b | ||
![]() |
0e6cd577cc | ||
![]() |
7adc8fcaf5 | ||
![]() |
e57b536767 | ||
![]() |
f32915b515 | ||
![]() |
a8d45d4b0d | ||
![]() |
4e24aff408 | ||
![]() |
e64a81aa1d | ||
![]() |
7093662ce5 | ||
![]() |
32c741154d | ||
![]() |
407a965c1d | ||
![]() |
de6348e8a0 | ||
![]() |
9ad57324db | ||
![]() |
94690c835e | ||
![]() |
c5d2dba28d | ||
![]() |
272e0c874f | ||
![]() |
d429f810a9 | ||
![]() |
75b28d6782 | ||
![]() |
8b1362ab03 | ||
![]() |
a096f621cf | ||
![]() |
f54a9542bb | ||
![]() |
a52bbc7c8d | ||
![]() |
59768bdf3f | ||
![]() |
1e67e9c661 | ||
![]() |
ae517a99a7 | ||
![]() |
b23a9b1a88 | ||
![]() |
5bd68aa3e0 | ||
![]() |
647ce174b3 | ||
![]() |
02c8fd9de5 | ||
![]() |
1ba49b614d | ||
![]() |
40de14415c | ||
![]() |
7c9330a02f |
@@ -8,7 +8,7 @@
|
|||||||
"version": "8.9.2"
|
"version": "8.9.2"
|
||||||
},
|
},
|
||||||
"ghcr.io/devcontainers/features/node:1": {
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
"version": "20.10.0"
|
"version": "20.5.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"forwardPorts": [3000],
|
"forwardPorts": [3000],
|
||||||
|
2
.github/workflows/dockle.yml
vendored
2
.github/workflows/dockle.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
|||||||
sudo dpkg -i dockle.deb
|
sudo dpkg -i dockle.deb
|
||||||
- run: |
|
- run: |
|
||||||
cp .config/docker_example.env .config/docker.env
|
cp .config/docker_example.env .config/docker.env
|
||||||
cp ./docker-compose_example.yml ./docker-compose.yml
|
cp ./docker-compose.yml.example ./docker-compose.yml
|
||||||
- run: |
|
- run: |
|
||||||
docker compose up -d web
|
docker compose up -d web
|
||||||
docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" misskey-web:latest
|
docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" misskey-web:latest
|
||||||
|
147
.github/workflows/get-api-diff.yml
vendored
147
.github/workflows/get-api-diff.yml
vendored
@@ -6,30 +6,37 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
- develop
|
- develop
|
||||||
paths:
|
|
||||||
- packages/backend/**
|
|
||||||
- .github/workflows/get-api-diff.yml
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
get-from-misskey:
|
get-base:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [20.10.0]
|
node-version: [20.5.1]
|
||||||
api-json-name: [api-base.json, api-head.json]
|
|
||||||
include:
|
services:
|
||||||
- api-json-name: api-base.json
|
db:
|
||||||
ref: ${{ github.base_ref }}
|
image: postgres:13
|
||||||
- api-json-name: api-head.json
|
ports:
|
||||||
ref: refs/pull/${{ github.event.number }}/merge
|
- 5432:5432
|
||||||
|
env:
|
||||||
|
POSTGRES_DB: misskey
|
||||||
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
|
POSTGRES_USER: example-misskey-user
|
||||||
|
POSTGRESS_PASS: example-misskey-pass
|
||||||
|
redis:
|
||||||
|
image: redis:7
|
||||||
|
ports:
|
||||||
|
- 6379:6379
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.1.1
|
- uses: actions/checkout@v4.1.1
|
||||||
with:
|
with:
|
||||||
ref: ${{ matrix.ref }}
|
repository: ${{ github.event.pull_request.base.repo.full_name }}
|
||||||
|
ref: ${{ github.base_ref }}
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v2
|
uses: pnpm/action-setup@v2
|
||||||
@@ -49,15 +56,121 @@ jobs:
|
|||||||
run: cp .config/example.yml .config/default.yml
|
run: cp .config/example.yml .config/default.yml
|
||||||
- name: Build
|
- name: Build
|
||||||
run: pnpm build
|
run: pnpm build
|
||||||
- name: Generate API JSON
|
- name : Migrate
|
||||||
run: pnpm --filter backend generate-api-json
|
run: pnpm migrate
|
||||||
- name: Copy API.json
|
- name: Launch misskey
|
||||||
run: cp packages/backend/built/api.json ${{ matrix.api-json-name }}
|
run: |
|
||||||
|
screen -S misskey -dm pnpm run dev
|
||||||
|
sleep 30s
|
||||||
|
- name: Wait for Misskey to be ready
|
||||||
|
run: |
|
||||||
|
MAX_RETRIES=12
|
||||||
|
RETRY_DELAY=5
|
||||||
|
count=0
|
||||||
|
until $(curl --output /dev/null --silent --head --fail http://localhost:3000) || [[ $count -eq $MAX_RETRIES ]]; do
|
||||||
|
printf '.'
|
||||||
|
sleep $RETRY_DELAY
|
||||||
|
count=$((count + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ $count -eq $MAX_RETRIES ]]; then
|
||||||
|
echo "Failed to connect to Misskey after $MAX_RETRIES attempts."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
- id: fetch
|
||||||
|
name: Get api.json from Misskey
|
||||||
|
run: |
|
||||||
|
RESULT=$(curl --retry 5 --retry-delay 5 --retry-max-time 60 http://localhost:3000/api.json)
|
||||||
|
echo $RESULT > api-base.json
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: api-artifact
|
name: api-artifact
|
||||||
path: ${{ matrix.api-json-name }}
|
path: api-base.json
|
||||||
|
- name: Kill Misskey Job
|
||||||
|
run: screen -S misskey -X quit
|
||||||
|
|
||||||
|
get-head:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [20.5.1]
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:13
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
env:
|
||||||
|
POSTGRES_DB: misskey
|
||||||
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
|
POSTGRES_USER: example-misskey-user
|
||||||
|
POSTGRESS_PASS: example-misskey-pass
|
||||||
|
redis:
|
||||||
|
image: redis:7
|
||||||
|
ports:
|
||||||
|
- 6379:6379
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4.1.1
|
||||||
|
with:
|
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
ref: ${{ github.head_ref }}
|
||||||
|
submodules: true
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 8
|
||||||
|
run_install: false
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v4.0.0
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'pnpm'
|
||||||
|
- run: corepack enable
|
||||||
|
- run: pnpm i --frozen-lockfile
|
||||||
|
- name: Check pnpm-lock.yaml
|
||||||
|
run: git diff --exit-code pnpm-lock.yaml
|
||||||
|
- name: Copy Configure
|
||||||
|
run: cp .config/example.yml .config/default.yml
|
||||||
|
- name: Build
|
||||||
|
run: pnpm build
|
||||||
|
- name : Migrate
|
||||||
|
run: pnpm migrate
|
||||||
|
- name: Launch misskey
|
||||||
|
run: |
|
||||||
|
screen -S misskey -dm pnpm run dev
|
||||||
|
sleep 30s
|
||||||
|
- name: Wait for Misskey to be ready
|
||||||
|
run: |
|
||||||
|
MAX_RETRIES=12
|
||||||
|
RETRY_DELAY=5
|
||||||
|
count=0
|
||||||
|
until $(curl --output /dev/null --silent --head --fail http://localhost:3000) || [[ $count -eq $MAX_RETRIES ]]; do
|
||||||
|
printf '.'
|
||||||
|
sleep $RETRY_DELAY
|
||||||
|
count=$((count + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ $count -eq $MAX_RETRIES ]]; then
|
||||||
|
echo "Failed to connect to Misskey after $MAX_RETRIES attempts."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
- id: fetch
|
||||||
|
name: Get api.json from Misskey
|
||||||
|
run: |
|
||||||
|
RESULT=$(curl --retry 5 --retry-delay 5 --retry-max-time 60 http://localhost:3000/api.json)
|
||||||
|
echo $RESULT > api-head.json
|
||||||
|
- name: Upload Artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: api-artifact
|
||||||
|
path: api-head.json
|
||||||
|
- name: Kill Misskey Job
|
||||||
|
run: screen -S misskey -X quit
|
||||||
|
|
||||||
save-pr-number:
|
save-pr-number:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
2
.github/workflows/test-backend.yml
vendored
2
.github/workflows/test-backend.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [20.10.0]
|
node-version: [20.5.1]
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
|
4
.github/workflows/test-frontend.yml
vendored
4
.github/workflows/test-frontend.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [20.10.0]
|
node-version: [20.5.1]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.1.1
|
- uses: actions/checkout@v4.1.1
|
||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [20.10.0]
|
node-version: [20.5.1]
|
||||||
browser: [chrome]
|
browser: [chrome]
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
2
.github/workflows/test-misskey-js.yml
vendored
2
.github/workflows/test-misskey-js.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [20.10.0]
|
node-version: [20.5.1]
|
||||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
2
.github/workflows/test-production.yml
vendored
2
.github/workflows/test-production.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [20.10.0]
|
node-version: [20.5.1]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.1.1
|
- uses: actions/checkout@v4.1.1
|
||||||
|
@@ -1 +1 @@
|
|||||||
20.10.0
|
20.5.1
|
||||||
|
55
CHANGELOG.md
55
CHANGELOG.md
@@ -5,70 +5,19 @@
|
|||||||
-
|
-
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
- Fix: ページ一覧ページの表示がモバイル環境において崩れているのを修正
|
-
|
||||||
- Fix: MFMでルビの中のテキストがnyaizeされない問題を修正
|
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
-
|
-
|
||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## 2023.x.x (unreleased)
|
|
||||||
|
|
||||||
### General
|
|
||||||
- Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed)
|
|
||||||
- Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83)
|
|
||||||
- Feat: TL上からノートが見えなくなるワードミュートであるハードミュートを追加
|
|
||||||
- Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正
|
|
||||||
|
|
||||||
### Client
|
|
||||||
- Feat: 今日誕生日のフォロー中のユーザーを一覧表示できるウィジェットを追加
|
|
||||||
- Feat: データセーバーでコードハイライトの読み込みを削減できるように
|
|
||||||
- Enhance: 投稿フォームの絵文字ピッカーをリアクション時に使用するものと同じのを使用するように #12336
|
|
||||||
- Enhance: 絵文字のオートコンプリート機能強化 #12364
|
|
||||||
- Enhance: ユーザーのRawデータを表示するページが復活
|
|
||||||
- Enhance: リアクション選択時に音を鳴らせるように
|
|
||||||
- Enhance: サウンドにドライブのファイルを使用できるように
|
|
||||||
- Enhance: ナビゲーションバーに項目「キャッシュを削除」を追加
|
|
||||||
- Enhance: Shareページで投稿を完了すると、親ウィンドウ(親フレーム)にpostMessageするように
|
|
||||||
- Enhance: チャンネル、クリップ、ページ、Play、ギャラリーにURLのコピーボタンを設置 #11305
|
|
||||||
- Enhance: ノートプレビューに「内容を隠す」が反映されるように
|
|
||||||
- Enhance: データセーバーの適用範囲を個別で設定できるように
|
|
||||||
- 従来のデータセーバーの設定はリセットされます
|
|
||||||
- fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正
|
|
||||||
- Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367
|
|
||||||
- Enhance: 絵文字の詳細ページに記載される情報を追加
|
|
||||||
- Fix: コードエディタが正しく表示されない問題を修正
|
|
||||||
- Fix: プロフィールの「ファイル」にセンシティブな画像がある際のデザインを修正
|
|
||||||
- Fix: 一度に大量の通知が入った際に通知音が音割れする問題を修正
|
|
||||||
- Fix: 共有機能をサポートしていないブラウザの場合は共有ボタンを非表示にする #11305
|
|
||||||
- Fix: 通知のグルーピング設定を変更してもリロードされるまで表示が変わらない問題を修正 #12470
|
|
||||||
- Fix: 長い名前のチャンネルにおける投稿フォームの表示が崩れる問題を修正
|
|
||||||
|
|
||||||
### Server
|
|
||||||
- Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように
|
|
||||||
- Fix: 時間経過により無効化されたアンテナを再有効化したとき、サーバ再起動までその状況が反映されないのを修正 #12303
|
|
||||||
- Fix: ロールタイムラインが保存されない問題を修正
|
|
||||||
- Fix: api.jsonの生成ロジックを改善 #12402
|
|
||||||
- Fix: 招待コードが使い回せる問題を修正
|
|
||||||
- Fix: 特定の条件下でチャンネルやユーザーのノート一覧に最新のノートが表示されなくなる問題を修正
|
|
||||||
- Fix: 何もノートしていないユーザーのフィードにアクセスするとエラーになる問題を修正
|
|
||||||
- Fix: リストタイムラインにてミュートが機能しないケースがある問題と、チャンネル投稿がストリーミングで流れてきてしまう問題を修正 #10443
|
|
||||||
- Fix: 「みつける」のなかにミュートしたユーザが現れてしまう問題を修正 #12383
|
|
||||||
- Fix: Social/Local/Home Timelineにてインスタンスミュートが効かない問題
|
|
||||||
- Fix: ユーザのノート一覧にてインスタンスミュートが効かない問題
|
|
||||||
- Fix: チャンネルのノート一覧にてインスタンスミュートが効かない問題
|
|
||||||
|
|
||||||
## 2023.11.1
|
## 2023.11.1
|
||||||
|
|
||||||
### Note
|
|
||||||
- 悪意のある第三者がリモートユーザーになりすました任意のアクティビティを受け取れてしまう問題を修正しました。詳しくは[GitHub security advisory](https://github.com/misskey-dev/misskey/security/advisories/GHSA-3f39-6537-3cgc)をご覧ください。
|
|
||||||
|
|
||||||
### General
|
### General
|
||||||
- Feat: 管理者がコントロールパネルからメールアドレスの照会を行えるようになりました
|
- Feat: 管理者がコントロールパネルからメールアドレスの照会を行えるようになりました
|
||||||
- Enhance: ローカリゼーションの更新
|
- Enhance: ローカリゼーションの更新
|
||||||
- Enhance: 依存関係の更新
|
- Enhance: 依存関係の更新
|
||||||
- Enhance: json-schema(OpenAPIの戻り値として使用されるスキーマ定義)を出来る限り最新化 #12311
|
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
- Enhance: MFMでルビを振れるように
|
- Enhance: MFMでルビを振れるように
|
||||||
@@ -77,7 +26,6 @@
|
|||||||
- 例: `$[unixtime 1701356400]`
|
- 例: `$[unixtime 1701356400]`
|
||||||
- Enhance: プラグインでエラーが発生した場合のハンドリングを強化
|
- Enhance: プラグインでエラーが発生した場合のハンドリングを強化
|
||||||
- Enhance: 細かなUIのブラッシュアップ
|
- Enhance: 細かなUIのブラッシュアップ
|
||||||
- Enhance: サウンド設定に「サウンドを出力しない」と「Misskeyがアクティブな時のみサウンドを出力する」を追加
|
|
||||||
- Fix: 効果音が再生されるとデバイスで再生している動画や音声が停止する問題を修正 #12339
|
- Fix: 効果音が再生されるとデバイスで再生している動画や音声が停止する問題を修正 #12339
|
||||||
- Fix: デッキに表示されたチャンネルの表示先チャンネルを切り替えた際、即座に反映されない問題を修正 #12236
|
- Fix: デッキに表示されたチャンネルの表示先チャンネルを切り替えた際、即座に反映されない問題を修正 #12236
|
||||||
- Fix: プラグインでノートの表示を書き換えられない問題を修正
|
- Fix: プラグインでノートの表示を書き換えられない問題を修正
|
||||||
@@ -187,7 +135,6 @@
|
|||||||
### Client
|
### Client
|
||||||
- Enhance: TLの返信表示オプションを記憶するように
|
- Enhance: TLの返信表示オプションを記憶するように
|
||||||
- Enhance: 投稿されてから時間が経過しているノートであることを視覚的に分かりやすく
|
- Enhance: 投稿されてから時間が経過しているノートであることを視覚的に分かりやすく
|
||||||
- Feat: 絵文字ピッカーのカテゴリに「/」を入れることでフォルダ分け表示できるように
|
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
- Enhance: タイムライン取得時のパフォーマンスを向上
|
- Enhance: タイムライン取得時のパフォーマンスを向上
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
# syntax = docker/dockerfile:1.4
|
# syntax = docker/dockerfile:1.4
|
||||||
|
|
||||||
ARG NODE_VERSION=20.10.0-bullseye
|
ARG NODE_VERSION=20.5.1-bullseye
|
||||||
|
|
||||||
# build assets & compile TypeScript
|
# build assets & compile TypeScript
|
||||||
|
|
||||||
@@ -67,8 +67,8 @@ RUN apt-get update \
|
|||||||
&& corepack enable \
|
&& corepack enable \
|
||||||
&& groupadd -g "${GID}" misskey \
|
&& groupadd -g "${GID}" misskey \
|
||||||
&& useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey \
|
&& useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey \
|
||||||
&& find / -type d -path /sys -prune -o -type d -path /proc -prune -o -type f -perm /u+s -ignore_readdir_race -exec chmod u-s {} \; \
|
&& find / -type d -path /proc -prune -o -type f -perm /u+s -ignore_readdir_race -exec chmod u-s {} \; \
|
||||||
&& find / -type d -path /sys -prune -o -type d -path /proc -prune -o -type f -perm /g+s -ignore_readdir_race -exec chmod g-s {} \; \
|
&& find / -type d -path /proc -prune -o -type f -perm /g+s -ignore_readdir_race -exec chmod g-s {} \; \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists
|
&& rm -rf /var/lib/apt/lists
|
||||||
|
|
||||||
|
@@ -564,10 +564,6 @@ output: "Output"
|
|||||||
script: "Script"
|
script: "Script"
|
||||||
disablePagesScript: "Disable AiScript on Pages"
|
disablePagesScript: "Disable AiScript on Pages"
|
||||||
updateRemoteUser: "Update remote user information"
|
updateRemoteUser: "Update remote user information"
|
||||||
unsetUserAvatar: "Delete user icon"
|
|
||||||
unsetUserAvatarConfirm: "Are you sure that you want to delete this user's icon?"
|
|
||||||
unsetUserBanner: "Delete user banner"
|
|
||||||
unsetUserBannerConfirm: "Are you sure that you want to delete this user's banner?"
|
|
||||||
deleteAllFiles: "Delete all files"
|
deleteAllFiles: "Delete all files"
|
||||||
deleteAllFilesConfirm: "Are you sure that you want to delete all files?"
|
deleteAllFilesConfirm: "Are you sure that you want to delete all files?"
|
||||||
removeAllFollowing: "Unfollow all followed users"
|
removeAllFollowing: "Unfollow all followed users"
|
||||||
|
@@ -56,18 +56,6 @@ export default function generateDTS() {
|
|||||||
ts.NodeFlags.Const | ts.NodeFlags.Ambient | ts.NodeFlags.ContextFlags,
|
ts.NodeFlags.Const | ts.NodeFlags.Ambient | ts.NodeFlags.ContextFlags,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ts.factory.createFunctionDeclaration(
|
|
||||||
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
|
|
||||||
undefined,
|
|
||||||
ts.factory.createIdentifier('build'),
|
|
||||||
undefined,
|
|
||||||
[],
|
|
||||||
ts.factory.createTypeReferenceNode(
|
|
||||||
ts.factory.createIdentifier('Locale'),
|
|
||||||
undefined,
|
|
||||||
),
|
|
||||||
undefined,
|
|
||||||
),
|
|
||||||
ts.factory.createExportDefault(ts.factory.createIdentifier('locales')),
|
ts.factory.createExportDefault(ts.factory.createIdentifier('locales')),
|
||||||
];
|
];
|
||||||
const printed = ts.createPrinter({
|
const printed = ts.createPrinter({
|
||||||
|
47
locales/index.d.ts
vendored
47
locales/index.d.ts
vendored
@@ -314,7 +314,6 @@ export interface Locale {
|
|||||||
"createFolder": string;
|
"createFolder": string;
|
||||||
"renameFolder": string;
|
"renameFolder": string;
|
||||||
"deleteFolder": string;
|
"deleteFolder": string;
|
||||||
"folder": string;
|
|
||||||
"addFile": string;
|
"addFile": string;
|
||||||
"emptyDrive": string;
|
"emptyDrive": string;
|
||||||
"emptyFolder": string;
|
"emptyFolder": string;
|
||||||
@@ -441,6 +440,7 @@ export interface Locale {
|
|||||||
"notFound": string;
|
"notFound": string;
|
||||||
"notFoundDescription": string;
|
"notFoundDescription": string;
|
||||||
"uploadFolder": string;
|
"uploadFolder": string;
|
||||||
|
"cacheClear": string;
|
||||||
"markAsReadAllNotifications": string;
|
"markAsReadAllNotifications": string;
|
||||||
"markAsReadAllUnreadNotes": string;
|
"markAsReadAllUnreadNotes": string;
|
||||||
"markAsReadAllTalkMessages": string;
|
"markAsReadAllTalkMessages": string;
|
||||||
@@ -547,8 +547,6 @@ export interface Locale {
|
|||||||
"popout": string;
|
"popout": string;
|
||||||
"volume": string;
|
"volume": string;
|
||||||
"masterVolume": string;
|
"masterVolume": string;
|
||||||
"notUseSound": string;
|
|
||||||
"useSoundOnlyWhenActive": string;
|
|
||||||
"details": string;
|
"details": string;
|
||||||
"chooseEmoji": string;
|
"chooseEmoji": string;
|
||||||
"unableToProcess": string;
|
"unableToProcess": string;
|
||||||
@@ -569,10 +567,6 @@ export interface Locale {
|
|||||||
"script": string;
|
"script": string;
|
||||||
"disablePagesScript": string;
|
"disablePagesScript": string;
|
||||||
"updateRemoteUser": string;
|
"updateRemoteUser": string;
|
||||||
"unsetUserAvatar": string;
|
|
||||||
"unsetUserAvatarConfirm": string;
|
|
||||||
"unsetUserBanner": string;
|
|
||||||
"unsetUserBannerConfirm": string;
|
|
||||||
"deleteAllFiles": string;
|
"deleteAllFiles": string;
|
||||||
"deleteAllFilesConfirm": string;
|
"deleteAllFilesConfirm": string;
|
||||||
"removeAllFollowing": string;
|
"removeAllFollowing": string;
|
||||||
@@ -644,7 +638,6 @@ export interface Locale {
|
|||||||
"smtpSecureInfo": string;
|
"smtpSecureInfo": string;
|
||||||
"testEmail": string;
|
"testEmail": string;
|
||||||
"wordMute": string;
|
"wordMute": string;
|
||||||
"hardWordMute": string;
|
|
||||||
"regexpError": string;
|
"regexpError": string;
|
||||||
"regexpErrorDescription": string;
|
"regexpErrorDescription": string;
|
||||||
"instanceMute": string;
|
"instanceMute": string;
|
||||||
@@ -1030,8 +1023,6 @@ export interface Locale {
|
|||||||
"sensitiveWords": string;
|
"sensitiveWords": string;
|
||||||
"sensitiveWordsDescription": string;
|
"sensitiveWordsDescription": string;
|
||||||
"sensitiveWordsDescription2": string;
|
"sensitiveWordsDescription2": string;
|
||||||
"hiddenTags": string;
|
|
||||||
"hiddenTagsDescription": string;
|
|
||||||
"notesSearchNotAvailable": string;
|
"notesSearchNotAvailable": string;
|
||||||
"license": string;
|
"license": string;
|
||||||
"unfavoriteConfirm": string;
|
"unfavoriteConfirm": string;
|
||||||
@@ -1044,7 +1035,6 @@ export interface Locale {
|
|||||||
"enableChartsForFederatedInstances": string;
|
"enableChartsForFederatedInstances": string;
|
||||||
"showClipButtonInNoteFooter": string;
|
"showClipButtonInNoteFooter": string;
|
||||||
"reactionsDisplaySize": string;
|
"reactionsDisplaySize": string;
|
||||||
"limitWidthOfReaction": string;
|
|
||||||
"noteIdOrUrl": string;
|
"noteIdOrUrl": string;
|
||||||
"video": string;
|
"video": string;
|
||||||
"videos": string;
|
"videos": string;
|
||||||
@@ -1171,8 +1161,6 @@ export interface Locale {
|
|||||||
"signupPendingError": string;
|
"signupPendingError": string;
|
||||||
"cwNotationRequired": string;
|
"cwNotationRequired": string;
|
||||||
"doReaction": string;
|
"doReaction": string;
|
||||||
"code": string;
|
|
||||||
"reloadRequiredToApplySettings": string;
|
|
||||||
"_announcement": {
|
"_announcement": {
|
||||||
"forExistingUsers": string;
|
"forExistingUsers": string;
|
||||||
"forExistingUsersDescription": string;
|
"forExistingUsersDescription": string;
|
||||||
@@ -1649,9 +1637,7 @@ export interface Locale {
|
|||||||
"assignTarget": string;
|
"assignTarget": string;
|
||||||
"descriptionOfAssignTarget": string;
|
"descriptionOfAssignTarget": string;
|
||||||
"manual": string;
|
"manual": string;
|
||||||
"manualRoles": string;
|
|
||||||
"conditional": string;
|
"conditional": string;
|
||||||
"conditionalRoles": string;
|
|
||||||
"condition": string;
|
"condition": string;
|
||||||
"isConditionalRole": string;
|
"isConditionalRole": string;
|
||||||
"isPublic": string;
|
"isPublic": string;
|
||||||
@@ -1949,15 +1935,6 @@ export interface Locale {
|
|||||||
"notification": string;
|
"notification": string;
|
||||||
"antenna": string;
|
"antenna": string;
|
||||||
"channel": string;
|
"channel": string;
|
||||||
"reaction": string;
|
|
||||||
};
|
|
||||||
"_soundSettings": {
|
|
||||||
"driveFile": string;
|
|
||||||
"driveFileWarn": string;
|
|
||||||
"driveFileTypeWarn": string;
|
|
||||||
"driveFileTypeWarnDescription": string;
|
|
||||||
"driveFileDurationWarn": string;
|
|
||||||
"driveFileDurationWarnDescription": string;
|
|
||||||
};
|
};
|
||||||
"_ago": {
|
"_ago": {
|
||||||
"future": string;
|
"future": string;
|
||||||
@@ -2112,7 +2089,6 @@ export interface Locale {
|
|||||||
"chooseList": string;
|
"chooseList": string;
|
||||||
};
|
};
|
||||||
"clicker": string;
|
"clicker": string;
|
||||||
"birthdayFollowings": string;
|
|
||||||
};
|
};
|
||||||
"_cw": {
|
"_cw": {
|
||||||
"hide": string;
|
"hide": string;
|
||||||
@@ -2437,8 +2413,6 @@ export interface Locale {
|
|||||||
"createAvatarDecoration": string;
|
"createAvatarDecoration": string;
|
||||||
"updateAvatarDecoration": string;
|
"updateAvatarDecoration": string;
|
||||||
"deleteAvatarDecoration": string;
|
"deleteAvatarDecoration": string;
|
||||||
"unsetUserAvatar": string;
|
|
||||||
"unsetUserBanner": string;
|
|
||||||
};
|
};
|
||||||
"_fileViewer": {
|
"_fileViewer": {
|
||||||
"title": string;
|
"title": string;
|
||||||
@@ -2504,27 +2478,8 @@ export interface Locale {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
"_dataSaver": {
|
|
||||||
"_media": {
|
|
||||||
"title": string;
|
|
||||||
"description": string;
|
|
||||||
};
|
|
||||||
"_avatar": {
|
|
||||||
"title": string;
|
|
||||||
"description": string;
|
|
||||||
};
|
|
||||||
"_urlPreview": {
|
|
||||||
"title": string;
|
|
||||||
"description": string;
|
|
||||||
};
|
|
||||||
"_code": {
|
|
||||||
"title": string;
|
|
||||||
"description": string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
declare const locales: {
|
declare const locales: {
|
||||||
[lang: string]: Locale;
|
[lang: string]: Locale;
|
||||||
};
|
};
|
||||||
export function build(): Locale;
|
|
||||||
export default locales;
|
export default locales;
|
||||||
|
@@ -51,37 +51,33 @@ const primaries = {
|
|||||||
// 何故か文字列にバックスペース文字が混入することがあり、YAMLが壊れるので取り除く
|
// 何故か文字列にバックスペース文字が混入することがあり、YAMLが壊れるので取り除く
|
||||||
const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
|
const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
|
||||||
|
|
||||||
export function build() {
|
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {});
|
||||||
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {});
|
|
||||||
|
|
||||||
// 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す
|
// 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す
|
||||||
const removeEmpty = (obj) => {
|
const removeEmpty = (obj) => {
|
||||||
for (const [k, v] of Object.entries(obj)) {
|
for (const [k, v] of Object.entries(obj)) {
|
||||||
if (v === '') {
|
if (v === '') {
|
||||||
delete obj[k];
|
delete obj[k];
|
||||||
} else if (typeof v === 'object') {
|
} else if (typeof v === 'object') {
|
||||||
removeEmpty(v);
|
removeEmpty(v);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return obj;
|
}
|
||||||
};
|
return obj;
|
||||||
removeEmpty(locales);
|
};
|
||||||
|
removeEmpty(locales);
|
||||||
|
|
||||||
return Object.entries(locales)
|
export default Object.entries(locales)
|
||||||
.reduce((a, [k, v]) => (a[k] = (() => {
|
.reduce((a, [k ,v]) => (a[k] = (() => {
|
||||||
const [lang] = k.split('-');
|
const [lang] = k.split('-');
|
||||||
switch (k) {
|
switch (k) {
|
||||||
case 'ja-JP': return v;
|
case 'ja-JP': return v;
|
||||||
case 'ja-KS':
|
case 'ja-KS':
|
||||||
case 'en-US': return merge(locales['ja-JP'], v);
|
case 'en-US': return merge(locales['ja-JP'], v);
|
||||||
default: return merge(
|
default: return merge(
|
||||||
locales['ja-JP'],
|
locales['ja-JP'],
|
||||||
locales['en-US'],
|
locales['en-US'],
|
||||||
locales[`${lang}-${primaries[lang]}`] ?? {},
|
locales[`${lang}-${primaries[lang]}`] ?? {},
|
||||||
v
|
v
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})(), a), {});
|
})(), a), {});
|
||||||
}
|
|
||||||
|
|
||||||
export default build();
|
|
||||||
|
@@ -311,7 +311,6 @@ folderName: "フォルダー名"
|
|||||||
createFolder: "フォルダーを作成"
|
createFolder: "フォルダーを作成"
|
||||||
renameFolder: "フォルダー名を変更"
|
renameFolder: "フォルダー名を変更"
|
||||||
deleteFolder: "フォルダーを削除"
|
deleteFolder: "フォルダーを削除"
|
||||||
folder: "フォルダー"
|
|
||||||
addFile: "ファイルを追加"
|
addFile: "ファイルを追加"
|
||||||
emptyDrive: "ドライブは空です"
|
emptyDrive: "ドライブは空です"
|
||||||
emptyFolder: "フォルダーは空です"
|
emptyFolder: "フォルダーは空です"
|
||||||
@@ -438,6 +437,7 @@ share: "共有"
|
|||||||
notFound: "見つかりません"
|
notFound: "見つかりません"
|
||||||
notFoundDescription: "指定されたURLに該当するページはありませんでした。"
|
notFoundDescription: "指定されたURLに該当するページはありませんでした。"
|
||||||
uploadFolder: "既定アップロード先"
|
uploadFolder: "既定アップロード先"
|
||||||
|
cacheClear: "キャッシュを削除"
|
||||||
markAsReadAllNotifications: "すべての通知を既読にする"
|
markAsReadAllNotifications: "すべての通知を既読にする"
|
||||||
markAsReadAllUnreadNotes: "すべての投稿を既読にする"
|
markAsReadAllUnreadNotes: "すべての投稿を既読にする"
|
||||||
markAsReadAllTalkMessages: "すべてのチャットを既読にする"
|
markAsReadAllTalkMessages: "すべてのチャットを既読にする"
|
||||||
@@ -544,8 +544,6 @@ showInPage: "ページで表示"
|
|||||||
popout: "ポップアウト"
|
popout: "ポップアウト"
|
||||||
volume: "音量"
|
volume: "音量"
|
||||||
masterVolume: "マスター音量"
|
masterVolume: "マスター音量"
|
||||||
notUseSound: "サウンドを出力しない"
|
|
||||||
useSoundOnlyWhenActive: "Misskeyがアクティブな時のみサウンドを出力する"
|
|
||||||
details: "詳細"
|
details: "詳細"
|
||||||
chooseEmoji: "絵文字を選択"
|
chooseEmoji: "絵文字を選択"
|
||||||
unableToProcess: "操作を完了できません"
|
unableToProcess: "操作を完了できません"
|
||||||
@@ -566,10 +564,6 @@ output: "出力"
|
|||||||
script: "スクリプト"
|
script: "スクリプト"
|
||||||
disablePagesScript: "Pagesのスクリプトを無効にする"
|
disablePagesScript: "Pagesのスクリプトを無効にする"
|
||||||
updateRemoteUser: "リモートユーザー情報の更新"
|
updateRemoteUser: "リモートユーザー情報の更新"
|
||||||
unsetUserAvatar: "アイコンを解除"
|
|
||||||
unsetUserAvatarConfirm: "アイコンを解除しますか?"
|
|
||||||
unsetUserBanner: "バナーを解除"
|
|
||||||
unsetUserBannerConfirm: "バナーを解除しますか?"
|
|
||||||
deleteAllFiles: "すべてのファイルを削除"
|
deleteAllFiles: "すべてのファイルを削除"
|
||||||
deleteAllFilesConfirm: "すべてのファイルを削除しますか?"
|
deleteAllFilesConfirm: "すべてのファイルを削除しますか?"
|
||||||
removeAllFollowing: "フォローを全解除"
|
removeAllFollowing: "フォローを全解除"
|
||||||
@@ -641,7 +635,6 @@ smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する"
|
|||||||
smtpSecureInfo: "STARTTLS使用時はオフにします。"
|
smtpSecureInfo: "STARTTLS使用時はオフにします。"
|
||||||
testEmail: "配信テスト"
|
testEmail: "配信テスト"
|
||||||
wordMute: "ワードミュート"
|
wordMute: "ワードミュート"
|
||||||
hardWordMute: "ハードワードミュート"
|
|
||||||
regexpError: "正規表現エラー"
|
regexpError: "正規表現エラー"
|
||||||
regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが発生しました:"
|
regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが発生しました:"
|
||||||
instanceMute: "サーバーミュート"
|
instanceMute: "サーバーミュート"
|
||||||
@@ -1027,8 +1020,6 @@ resetPasswordConfirm: "パスワードリセットしますか?"
|
|||||||
sensitiveWords: "センシティブワード"
|
sensitiveWords: "センシティブワード"
|
||||||
sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。"
|
sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。"
|
||||||
sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。"
|
sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。"
|
||||||
hiddenTags: "非表示ハッシュタグ"
|
|
||||||
hiddenTagsDescription: "設定したタグをトレンドに表示させないようにします。改行で区切って複数設定できます。"
|
|
||||||
notesSearchNotAvailable: "ノート検索は利用できません。"
|
notesSearchNotAvailable: "ノート検索は利用できません。"
|
||||||
license: "ライセンス"
|
license: "ライセンス"
|
||||||
unfavoriteConfirm: "お気に入り解除しますか?"
|
unfavoriteConfirm: "お気に入り解除しますか?"
|
||||||
@@ -1041,7 +1032,6 @@ enableChartsForRemoteUser: "リモートユーザーのチャートを生成"
|
|||||||
enableChartsForFederatedInstances: "リモートサーバーのチャートを生成"
|
enableChartsForFederatedInstances: "リモートサーバーのチャートを生成"
|
||||||
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
|
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
|
||||||
reactionsDisplaySize: "リアクションの表示サイズ"
|
reactionsDisplaySize: "リアクションの表示サイズ"
|
||||||
limitWidthOfReaction: "リアクションの最大横幅を制限し、縮小して表示する"
|
|
||||||
noteIdOrUrl: "ノートIDまたはURL"
|
noteIdOrUrl: "ノートIDまたはURL"
|
||||||
video: "動画"
|
video: "動画"
|
||||||
videos: "動画"
|
videos: "動画"
|
||||||
@@ -1168,8 +1158,6 @@ useGroupedNotifications: "通知をグルーピングして表示する"
|
|||||||
signupPendingError: "メールアドレスの確認中に問題が発生しました。リンクの有効期限が切れている可能性があります。"
|
signupPendingError: "メールアドレスの確認中に問題が発生しました。リンクの有効期限が切れている可能性があります。"
|
||||||
cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述が必要です。"
|
cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述が必要です。"
|
||||||
doReaction: "リアクションする"
|
doReaction: "リアクションする"
|
||||||
code: "コード"
|
|
||||||
reloadRequiredToApplySettings: "設定の反映にはリロードが必要です。"
|
|
||||||
|
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "既存ユーザーのみ"
|
forExistingUsers: "既存ユーザーのみ"
|
||||||
@@ -1559,9 +1547,7 @@ _role:
|
|||||||
assignTarget: "アサイン"
|
assignTarget: "アサイン"
|
||||||
descriptionOfAssignTarget: "<b>マニュアル</b>は誰がこのロールに含まれるかを手動で管理します。\n<b>コンディショナル</b>は条件を設定し、それに合致するユーザーが自動で含まれるようになります。"
|
descriptionOfAssignTarget: "<b>マニュアル</b>は誰がこのロールに含まれるかを手動で管理します。\n<b>コンディショナル</b>は条件を設定し、それに合致するユーザーが自動で含まれるようになります。"
|
||||||
manual: "マニュアル"
|
manual: "マニュアル"
|
||||||
manualRoles: "マニュアルロール"
|
|
||||||
conditional: "コンディショナル"
|
conditional: "コンディショナル"
|
||||||
conditionalRoles: "コンディショナルロール"
|
|
||||||
condition: "条件"
|
condition: "条件"
|
||||||
isConditionalRole: "これはコンディショナルロールです。"
|
isConditionalRole: "これはコンディショナルロールです。"
|
||||||
isPublic: "公開ロール"
|
isPublic: "公開ロール"
|
||||||
@@ -1854,15 +1840,6 @@ _sfx:
|
|||||||
notification: "通知"
|
notification: "通知"
|
||||||
antenna: "アンテナ受信"
|
antenna: "アンテナ受信"
|
||||||
channel: "チャンネル通知"
|
channel: "チャンネル通知"
|
||||||
reaction: "リアクション選択時"
|
|
||||||
|
|
||||||
_soundSettings:
|
|
||||||
driveFile: "ドライブの音声を使用"
|
|
||||||
driveFileWarn: "ドライブのファイルを選択してください"
|
|
||||||
driveFileTypeWarn: "このファイルは対応していません"
|
|
||||||
driveFileTypeWarnDescription: "音声ファイルを選択してください"
|
|
||||||
driveFileDurationWarn: "音声が長すぎます"
|
|
||||||
driveFileDurationWarnDescription: "長い音声を使用するとMisskeyの使用に支障をきたす可能性があります。それでも続行しますか?"
|
|
||||||
|
|
||||||
_ago:
|
_ago:
|
||||||
future: "未来"
|
future: "未来"
|
||||||
@@ -1874,7 +1851,7 @@ _ago:
|
|||||||
weeksAgo: "{n}週間前"
|
weeksAgo: "{n}週間前"
|
||||||
monthsAgo: "{n}ヶ月前"
|
monthsAgo: "{n}ヶ月前"
|
||||||
yearsAgo: "{n}年前"
|
yearsAgo: "{n}年前"
|
||||||
invalid: "日時の解析に失敗"
|
invalid: "ありません"
|
||||||
|
|
||||||
_timeIn:
|
_timeIn:
|
||||||
seconds: "{n}秒後"
|
seconds: "{n}秒後"
|
||||||
@@ -2016,7 +1993,6 @@ _widgets:
|
|||||||
_userList:
|
_userList:
|
||||||
chooseList: "リストを選択"
|
chooseList: "リストを選択"
|
||||||
clicker: "クリッカー"
|
clicker: "クリッカー"
|
||||||
birthdayFollowings: "今日誕生日のユーザー"
|
|
||||||
|
|
||||||
_cw:
|
_cw:
|
||||||
hide: "隠す"
|
hide: "隠す"
|
||||||
@@ -2338,8 +2314,6 @@ _moderationLogTypes:
|
|||||||
createAvatarDecoration: "アイコンデコレーションを作成"
|
createAvatarDecoration: "アイコンデコレーションを作成"
|
||||||
updateAvatarDecoration: "アイコンデコレーションを更新"
|
updateAvatarDecoration: "アイコンデコレーションを更新"
|
||||||
deleteAvatarDecoration: "アイコンデコレーションを削除"
|
deleteAvatarDecoration: "アイコンデコレーションを削除"
|
||||||
unsetUserAvatar: "ユーザーのアイコンを解除"
|
|
||||||
unsetUserBanner: "ユーザーのバナーを解除"
|
|
||||||
|
|
||||||
_fileViewer:
|
_fileViewer:
|
||||||
title: "ファイルの詳細"
|
title: "ファイルの詳細"
|
||||||
@@ -2391,17 +2365,3 @@ _externalResourceInstaller:
|
|||||||
_themeInstallFailed:
|
_themeInstallFailed:
|
||||||
title: "テーマのインストールに失敗しました"
|
title: "テーマのインストールに失敗しました"
|
||||||
description: "テーマのインストール中に問題が発生しました。もう一度お試しください。エラーの詳細はJavascriptコンソールをご覧ください。"
|
description: "テーマのインストール中に問題が発生しました。もう一度お試しください。エラーの詳細はJavascriptコンソールをご覧ください。"
|
||||||
|
|
||||||
_dataSaver:
|
|
||||||
_media:
|
|
||||||
title: "メディアの読み込み"
|
|
||||||
description: "画像・動画が自動で読み込まれるのを防止します。隠れている画像・動画はタップすると読み込まれます。"
|
|
||||||
_avatar:
|
|
||||||
title: "アイコン画像"
|
|
||||||
description: "アイコン画像のアニメーションが停止します。アニメーション画像は通常の画像よりファイルサイズが大きいことがあるので、データ通信量をさらに削減できます。"
|
|
||||||
_urlPreview:
|
|
||||||
title: "URLプレビューのサムネイル"
|
|
||||||
description: "URLプレビューのサムネイル画像が読み込まれなくなります。"
|
|
||||||
_code:
|
|
||||||
title: "コードハイライト"
|
|
||||||
description: "MFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。"
|
|
||||||
|
18
package.json
18
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "2023.12.0-beta.2",
|
"version": "2023.11.1",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -18,7 +18,6 @@
|
|||||||
"build-assets": "node ./scripts/build-assets.mjs",
|
"build-assets": "node ./scripts/build-assets.mjs",
|
||||||
"build": "pnpm build-pre && pnpm -r build && pnpm build-assets",
|
"build": "pnpm build-pre && pnpm -r build && pnpm build-assets",
|
||||||
"build-storybook": "pnpm --filter frontend build-storybook",
|
"build-storybook": "pnpm --filter frontend build-storybook",
|
||||||
"build-misskey-js-with-types": "pnpm --filter backend build && pnpm --filter backend generate-api-json && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build",
|
|
||||||
"start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js",
|
"start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js",
|
||||||
"start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js",
|
"start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js",
|
||||||
"init": "pnpm migrate",
|
"init": "pnpm migrate",
|
||||||
@@ -48,18 +47,17 @@
|
|||||||
"execa": "8.0.1",
|
"execa": "8.0.1",
|
||||||
"cssnano": "6.0.1",
|
"cssnano": "6.0.1",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"postcss": "8.4.32",
|
"postcss": "8.4.31",
|
||||||
"terser": "5.24.0",
|
"terser": "5.24.0",
|
||||||
"typescript": "5.3.2"
|
"typescript": "5.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "6.13.1",
|
"@typescript-eslint/eslint-plugin": "6.11.0",
|
||||||
"@typescript-eslint/parser": "6.13.1",
|
"@typescript-eslint/parser": "6.11.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "13.6.0",
|
"cypress": "13.5.1",
|
||||||
"eslint": "8.55.0",
|
"eslint": "8.53.0",
|
||||||
"start-server-and-test": "2.0.3",
|
"start-server-and-test": "2.0.3"
|
||||||
"ncp": "2.0.0"
|
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@tensorflow/tfjs-core": "4.4.0"
|
"@tensorflow/tfjs-core": "4.4.0"
|
||||||
|
@@ -1,8 +0,0 @@
|
|||||||
import { loadConfig } from './built/config.js'
|
|
||||||
import { genOpenapiSpec } from './built/server/api/openapi/gen-spec.js'
|
|
||||||
import { writeFileSync } from "node:fs";
|
|
||||||
|
|
||||||
const config = loadConfig();
|
|
||||||
const spec = genOpenapiSpec(config);
|
|
||||||
|
|
||||||
writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8');
|
|
@@ -1,18 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class SupportVerifyMailApi1700303245007 {
|
|
||||||
name = 'SupportVerifyMailApi1700303245007'
|
|
||||||
|
|
||||||
async up(queryRunner) {
|
|
||||||
await queryRunner.query(`ALTER TABLE "meta" ADD "verifymailAuthKey" character varying(1024)`);
|
|
||||||
await queryRunner.query(`ALTER TABLE "meta" ADD "enableVerifymailApi" boolean NOT NULL DEFAULT false`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async down(queryRunner) {
|
|
||||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableVerifymailApi"`);
|
|
||||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "verifymailAuthKey"`);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,11 +0,0 @@
|
|||||||
export class HardMute1700383825690 {
|
|
||||||
name = 'HardMute1700383825690'
|
|
||||||
|
|
||||||
async up(queryRunner) {
|
|
||||||
await queryRunner.query(`ALTER TABLE "user_profile" ADD "hardMutedWords" jsonb NOT NULL DEFAULT '[]'`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async down(queryRunner) {
|
|
||||||
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "hardMutedWords"`);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,16 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class AddBdayIndex1700902349231 {
|
|
||||||
name = 'AddBdayIndex1700902349231'
|
|
||||||
|
|
||||||
async up(queryRunner) {
|
|
||||||
await queryRunner.query(`CREATE INDEX "IDX_de22cd2b445eee31ae51cdbe99" ON "user_profile" (SUBSTR("birthday", 6, 5))`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async down(queryRunner) {
|
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_de22cd2b445eee31ae51cdbe99"`);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -7,8 +7,8 @@
|
|||||||
"node": ">=18.16.0"
|
"node": ">=18.16.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./built/boot/entry.js",
|
"start": "node ./built/index.js",
|
||||||
"start:test": "NODE_ENV=test node ./built/boot/entry.js",
|
"start:test": "NODE_ENV=test node ./built/index.js",
|
||||||
"migrate": "pnpm typeorm migration:run -d ormconfig.js",
|
"migrate": "pnpm typeorm migration:run -d ormconfig.js",
|
||||||
"revert": "pnpm typeorm migration:revert -d ormconfig.js",
|
"revert": "pnpm typeorm migration:revert -d ormconfig.js",
|
||||||
"check:connect": "node ./check_connect.js",
|
"check:connect": "node ./check_connect.js",
|
||||||
@@ -23,8 +23,7 @@
|
|||||||
"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit",
|
"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit",
|
||||||
"jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache",
|
"jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache",
|
||||||
"test": "pnpm jest",
|
"test": "pnpm jest",
|
||||||
"test-and-coverage": "pnpm jest-and-coverage",
|
"test-and-coverage": "pnpm jest-and-coverage"
|
||||||
"generate-api-json": "node ./generate_api_json.js"
|
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@swc/core-android-arm64": "1.3.11",
|
"@swc/core-android-arm64": "1.3.11",
|
||||||
@@ -60,9 +59,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "3.412.0",
|
"@aws-sdk/client-s3": "3.412.0",
|
||||||
"@aws-sdk/lib-storage": "3.412.0",
|
"@aws-sdk/lib-storage": "3.412.0",
|
||||||
"@bull-board/api": "5.10.1",
|
"@bull-board/api": "5.9.1",
|
||||||
"@bull-board/fastify": "5.10.1",
|
"@bull-board/fastify": "5.9.1",
|
||||||
"@bull-board/ui": "5.10.1",
|
"@bull-board/ui": "5.9.1",
|
||||||
"@discordapp/twemoji": "14.1.2",
|
"@discordapp/twemoji": "14.1.2",
|
||||||
"@fastify/accepts": "4.2.0",
|
"@fastify/accepts": "4.2.0",
|
||||||
"@fastify/cookie": "9.2.0",
|
"@fastify/cookie": "9.2.0",
|
||||||
@@ -72,15 +71,15 @@
|
|||||||
"@fastify/multipart": "8.0.0",
|
"@fastify/multipart": "8.0.0",
|
||||||
"@fastify/static": "6.12.0",
|
"@fastify/static": "6.12.0",
|
||||||
"@fastify/view": "8.2.0",
|
"@fastify/view": "8.2.0",
|
||||||
"@nestjs/common": "10.2.10",
|
"@nestjs/common": "10.2.8",
|
||||||
"@nestjs/core": "10.2.10",
|
"@nestjs/core": "10.2.8",
|
||||||
"@nestjs/testing": "10.2.10",
|
"@nestjs/testing": "10.2.8",
|
||||||
"@peertube/http-signature": "1.7.0",
|
"@peertube/http-signature": "1.7.0",
|
||||||
"@simplewebauthn/server": "8.3.5",
|
"@simplewebauthn/server": "8.3.5",
|
||||||
"@sinonjs/fake-timers": "11.2.2",
|
"@sinonjs/fake-timers": "11.2.2",
|
||||||
"@smithy/node-http-handler": "2.1.10",
|
"@smithy/node-http-handler": "2.1.5",
|
||||||
"@swc/cli": "0.1.63",
|
"@swc/cli": "0.1.63",
|
||||||
"@swc/core": "1.3.100",
|
"@swc/core": "1.3.96",
|
||||||
"accepts": "1.3.8",
|
"accepts": "1.3.8",
|
||||||
"ajv": "8.12.0",
|
"ajv": "8.12.0",
|
||||||
"archiver": "6.0.1",
|
"archiver": "6.0.1",
|
||||||
@@ -88,7 +87,7 @@
|
|||||||
"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.2",
|
||||||
"bullmq": "4.14.4",
|
"bullmq": "4.13.3",
|
||||||
"cacheable-lookup": "7.0.0",
|
"cacheable-lookup": "7.0.0",
|
||||||
"cbor": "9.0.1",
|
"cbor": "9.0.1",
|
||||||
"chalk": "5.3.0",
|
"chalk": "5.3.0",
|
||||||
@@ -114,17 +113,17 @@
|
|||||||
"ipaddr.js": "2.1.0",
|
"ipaddr.js": "2.1.0",
|
||||||
"is-svg": "5.0.0",
|
"is-svg": "5.0.0",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"jsdom": "23.0.1",
|
"jsdom": "22.1.0",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"jsonld": "8.3.1",
|
"jsonld": "8.3.1",
|
||||||
"jsrsasign": "10.9.0",
|
"jsrsasign": "10.8.6",
|
||||||
"meilisearch": "0.36.0",
|
"meilisearch": "0.35.0",
|
||||||
"mfm-js": "0.23.3",
|
"mfm-js": "0.23.3",
|
||||||
"microformats-parser": "1.5.2",
|
"microformats-parser": "1.5.2",
|
||||||
"mime-types": "2.1.35",
|
"mime-types": "2.1.35",
|
||||||
"misskey-js": "workspace:*",
|
"misskey-js": "workspace:*",
|
||||||
"ms": "3.0.0-canary.1",
|
"ms": "3.0.0-canary.1",
|
||||||
"nanoid": "5.0.4",
|
"nanoid": "5.0.3",
|
||||||
"nested-property": "4.0.0",
|
"nested-property": "4.0.0",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
"nodemailer": "6.9.7",
|
"nodemailer": "6.9.7",
|
||||||
@@ -145,7 +144,7 @@
|
|||||||
"qrcode": "1.5.3",
|
"qrcode": "1.5.3",
|
||||||
"random-seed": "0.3.0",
|
"random-seed": "0.3.0",
|
||||||
"ratelimiter": "3.4.1",
|
"ratelimiter": "3.4.1",
|
||||||
"re2": "1.20.9",
|
"re2": "1.20.8",
|
||||||
"redis-lock": "0.1.4",
|
"redis-lock": "0.1.4",
|
||||||
"reflect-metadata": "0.1.13",
|
"reflect-metadata": "0.1.13",
|
||||||
"rename": "1.0.4",
|
"rename": "1.0.4",
|
||||||
@@ -159,14 +158,14 @@
|
|||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"stringz": "2.1.0",
|
"stringz": "2.1.0",
|
||||||
"summaly": "github:misskey-dev/summaly",
|
"summaly": "github:misskey-dev/summaly",
|
||||||
"systeminformation": "5.21.20",
|
"systeminformation": "5.21.17",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"tmp": "0.2.1",
|
"tmp": "0.2.1",
|
||||||
"tsc-alias": "1.8.8",
|
"tsc-alias": "1.8.8",
|
||||||
"tsconfig-paths": "4.2.0",
|
"tsconfig-paths": "4.2.0",
|
||||||
"twemoji-parser": "14.0.0",
|
"twemoji-parser": "14.0.0",
|
||||||
"typeorm": "0.3.17",
|
"typeorm": "0.3.17",
|
||||||
"typescript": "5.3.2",
|
"typescript": "5.2.2",
|
||||||
"ulid": "2.3.0",
|
"ulid": "2.3.0",
|
||||||
"vary": "1.1.2",
|
"vary": "1.1.2",
|
||||||
"web-push": "3.6.6",
|
"web-push": "3.6.6",
|
||||||
@@ -178,7 +177,7 @@
|
|||||||
"@simplewebauthn/typescript-types": "8.3.4",
|
"@simplewebauthn/typescript-types": "8.3.4",
|
||||||
"@swc/jest": "0.2.29",
|
"@swc/jest": "0.2.29",
|
||||||
"@types/accepts": "1.3.7",
|
"@types/accepts": "1.3.7",
|
||||||
"@types/archiver": "6.0.2",
|
"@types/archiver": "6.0.1",
|
||||||
"@types/bcryptjs": "2.4.6",
|
"@types/bcryptjs": "2.4.6",
|
||||||
"@types/body-parser": "1.19.5",
|
"@types/body-parser": "1.19.5",
|
||||||
"@types/cbor": "6.0.0",
|
"@types/cbor": "6.0.0",
|
||||||
@@ -186,28 +185,28 @@
|
|||||||
"@types/content-disposition": "0.5.8",
|
"@types/content-disposition": "0.5.8",
|
||||||
"@types/fluent-ffmpeg": "2.1.24",
|
"@types/fluent-ffmpeg": "2.1.24",
|
||||||
"@types/http-link-header": "1.0.5",
|
"@types/http-link-header": "1.0.5",
|
||||||
"@types/jest": "29.5.10",
|
"@types/jest": "29.5.8",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/jsdom": "21.1.6",
|
"@types/jsdom": "21.1.5",
|
||||||
"@types/jsonld": "1.5.13",
|
"@types/jsonld": "1.5.12",
|
||||||
"@types/jsrsasign": "10.5.12",
|
"@types/jsrsasign": "10.5.12",
|
||||||
"@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.10.3",
|
"@types/node": "20.9.1",
|
||||||
"@types/node-fetch": "3.0.3",
|
"@types/node-fetch": "3.0.3",
|
||||||
"@types/nodemailer": "6.4.14",
|
"@types/nodemailer": "6.4.14",
|
||||||
"@types/oauth": "0.9.4",
|
"@types/oauth": "0.9.4",
|
||||||
"@types/oauth2orize": "1.11.3",
|
"@types/oauth2orize": "1.11.3",
|
||||||
"@types/oauth2orize-pkce": "0.1.2",
|
"@types/oauth2orize-pkce": "0.1.2",
|
||||||
"@types/pg": "8.10.9",
|
"@types/pg": "8.10.9",
|
||||||
"@types/pug": "2.0.10",
|
"@types/pug": "2.0.9",
|
||||||
"@types/punycode": "2.1.3",
|
"@types/punycode": "2.1.2",
|
||||||
"@types/qrcode": "1.5.5",
|
"@types/qrcode": "1.5.5",
|
||||||
"@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.9.5",
|
"@types/sanitize-html": "2.9.4",
|
||||||
"@types/semver": "7.5.6",
|
"@types/semver": "7.5.5",
|
||||||
"@types/sharp": "0.32.0",
|
"@types/sharp": "0.32.0",
|
||||||
"@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",
|
||||||
@@ -215,12 +214,12 @@
|
|||||||
"@types/tmp": "0.2.6",
|
"@types/tmp": "0.2.6",
|
||||||
"@types/vary": "1.1.3",
|
"@types/vary": "1.1.3",
|
||||||
"@types/web-push": "3.6.3",
|
"@types/web-push": "3.6.3",
|
||||||
"@types/ws": "8.5.10",
|
"@types/ws": "8.5.9",
|
||||||
"@typescript-eslint/eslint-plugin": "6.13.1",
|
"@typescript-eslint/eslint-plugin": "6.11.0",
|
||||||
"@typescript-eslint/parser": "6.13.1",
|
"@typescript-eslint/parser": "6.11.0",
|
||||||
"aws-sdk-client-mock": "3.0.0",
|
"aws-sdk-client-mock": "3.0.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint": "8.55.0",
|
"eslint": "8.53.0",
|
||||||
"eslint-plugin-import": "2.29.0",
|
"eslint-plugin-import": "2.29.0",
|
||||||
"execa": "8.0.1",
|
"execa": "8.0.1",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
|
@@ -16,7 +16,7 @@ import type { AntennasRepository, UserListMembershipsRepository } from '@/models
|
|||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -39,7 +39,7 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private fanoutTimelineService: FanoutTimelineService,
|
private funoutTimelineService: FunoutTimelineService,
|
||||||
) {
|
) {
|
||||||
this.antennasFetched = false;
|
this.antennasFetched = false;
|
||||||
this.antennas = [];
|
this.antennas = [];
|
||||||
@@ -60,21 +60,11 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
lastUsedAt: new Date(body.lastUsedAt),
|
lastUsedAt: new Date(body.lastUsedAt),
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'antennaUpdated': {
|
case 'antennaUpdated':
|
||||||
const idx = this.antennas.findIndex(a => a.id === body.id);
|
this.antennas[this.antennas.findIndex(a => a.id === body.id)] = {
|
||||||
if (idx >= 0) {
|
...body,
|
||||||
this.antennas[idx] = {
|
lastUsedAt: new Date(body.lastUsedAt),
|
||||||
...body,
|
};
|
||||||
lastUsedAt: new Date(body.lastUsedAt),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// サーバ起動時にactiveじゃなかった場合、リストに持っていないので追加する必要あり
|
|
||||||
this.antennas.push({
|
|
||||||
...body,
|
|
||||||
lastUsedAt: new Date(body.lastUsedAt),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'antennaDeleted':
|
case 'antennaDeleted':
|
||||||
this.antennas = this.antennas.filter(a => a.id !== body.id);
|
this.antennas = this.antennas.filter(a => a.id !== body.id);
|
||||||
@@ -94,7 +84,7 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
const redisPipeline = this.redisForTimelines.pipeline();
|
const redisPipeline = this.redisForTimelines.pipeline();
|
||||||
|
|
||||||
for (const antenna of matchedAntennas) {
|
for (const antenna of matchedAntennas) {
|
||||||
this.fanoutTimelineService.push(`antennaTimeline:${antenna.id}`, note.id, 200, redisPipeline);
|
this.funoutTimelineService.push(`antennaTimeline:${antenna.id}`, note.id, 200, redisPipeline);
|
||||||
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
|
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -4,7 +4,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
|
|
||||||
import { AccountMoveService } from './AccountMoveService.js';
|
import { AccountMoveService } from './AccountMoveService.js';
|
||||||
import { AccountUpdateService } from './AccountUpdateService.js';
|
import { AccountUpdateService } from './AccountUpdateService.js';
|
||||||
import { AiService } from './AiService.js';
|
import { AiService } from './AiService.js';
|
||||||
@@ -63,7 +62,7 @@ import { FileInfoService } from './FileInfoService.js';
|
|||||||
import { SearchService } from './SearchService.js';
|
import { SearchService } from './SearchService.js';
|
||||||
import { ClipService } from './ClipService.js';
|
import { ClipService } from './ClipService.js';
|
||||||
import { FeaturedService } from './FeaturedService.js';
|
import { FeaturedService } from './FeaturedService.js';
|
||||||
import { FanoutTimelineService } from './FanoutTimelineService.js';
|
import { FunoutTimelineService } from './FunoutTimelineService.js';
|
||||||
import { ChannelFollowingService } from './ChannelFollowingService.js';
|
import { ChannelFollowingService } from './ChannelFollowingService.js';
|
||||||
import { RegistryApiService } from './RegistryApiService.js';
|
import { RegistryApiService } from './RegistryApiService.js';
|
||||||
import { ChartLoggerService } from './chart/ChartLoggerService.js';
|
import { ChartLoggerService } from './chart/ChartLoggerService.js';
|
||||||
@@ -195,8 +194,7 @@ const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: Fi
|
|||||||
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
|
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
|
||||||
const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService };
|
const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService };
|
||||||
const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService };
|
const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService };
|
||||||
const $FanoutTimelineService: Provider = { provide: 'FanoutTimelineService', useExisting: FanoutTimelineService };
|
const $FunoutTimelineService: Provider = { provide: 'FunoutTimelineService', useExisting: FunoutTimelineService };
|
||||||
const $FanoutTimelineEndpointService: Provider = { provide: 'FanoutTimelineEndpointService', useExisting: FanoutTimelineEndpointService };
|
|
||||||
const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService };
|
const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService };
|
||||||
const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService };
|
const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService };
|
||||||
|
|
||||||
@@ -332,8 +330,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
SearchService,
|
SearchService,
|
||||||
ClipService,
|
ClipService,
|
||||||
FeaturedService,
|
FeaturedService,
|
||||||
FanoutTimelineService,
|
FunoutTimelineService,
|
||||||
FanoutTimelineEndpointService,
|
|
||||||
ChannelFollowingService,
|
ChannelFollowingService,
|
||||||
RegistryApiService,
|
RegistryApiService,
|
||||||
ChartLoggerService,
|
ChartLoggerService,
|
||||||
@@ -462,8 +459,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
$SearchService,
|
$SearchService,
|
||||||
$ClipService,
|
$ClipService,
|
||||||
$FeaturedService,
|
$FeaturedService,
|
||||||
$FanoutTimelineService,
|
$FunoutTimelineService,
|
||||||
$FanoutTimelineEndpointService,
|
|
||||||
$ChannelFollowingService,
|
$ChannelFollowingService,
|
||||||
$RegistryApiService,
|
$RegistryApiService,
|
||||||
$ChartLoggerService,
|
$ChartLoggerService,
|
||||||
@@ -593,8 +589,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
SearchService,
|
SearchService,
|
||||||
ClipService,
|
ClipService,
|
||||||
FeaturedService,
|
FeaturedService,
|
||||||
FanoutTimelineService,
|
FunoutTimelineService,
|
||||||
FanoutTimelineEndpointService,
|
|
||||||
ChannelFollowingService,
|
ChannelFollowingService,
|
||||||
RegistryApiService,
|
RegistryApiService,
|
||||||
FederationChart,
|
FederationChart,
|
||||||
@@ -722,8 +717,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
$SearchService,
|
$SearchService,
|
||||||
$ClipService,
|
$ClipService,
|
||||||
$FeaturedService,
|
$FeaturedService,
|
||||||
$FanoutTimelineService,
|
$FunoutTimelineService,
|
||||||
$FanoutTimelineEndpointService,
|
|
||||||
$ChannelFollowingService,
|
$ChannelFollowingService,
|
||||||
$RegistryApiService,
|
$RegistryApiService,
|
||||||
$FederationChart,
|
$FederationChart,
|
||||||
|
@@ -3,11 +3,9 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { URLSearchParams } from 'node:url';
|
|
||||||
import * as nodemailer from 'nodemailer';
|
import * as nodemailer from 'nodemailer';
|
||||||
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 { SubOutputFormat } from 'deep-email-validator/dist/output/output.js';
|
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.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';
|
||||||
@@ -15,7 +13,6 @@ import type Logger from '@/logger.js';
|
|||||||
import type { UserProfilesRepository } from '@/models/_.js';
|
import type { 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';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EmailService {
|
export class EmailService {
|
||||||
@@ -30,7 +27,6 @@ export class EmailService {
|
|||||||
|
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
private httpRequestService: HttpRequestService,
|
|
||||||
) {
|
) {
|
||||||
this.logger = this.loggerService.getLogger('email');
|
this.logger = this.loggerService.getLogger('email');
|
||||||
}
|
}
|
||||||
@@ -164,25 +160,14 @@ export class EmailService {
|
|||||||
email: emailAddress,
|
email: emailAddress,
|
||||||
});
|
});
|
||||||
|
|
||||||
const verifymailApi = meta.enableVerifymailApi && meta.verifymailAuthKey != null;
|
const validated = meta.enableActiveEmailValidation ? await validateEmail({
|
||||||
let validated;
|
email: emailAddress,
|
||||||
|
validateRegex: true,
|
||||||
if (meta.enableActiveEmailValidation && meta.verifymailAuthKey) {
|
validateMx: true,
|
||||||
if (verifymailApi) {
|
validateTypo: false, // TLDを見ているみたいだけどclubとか弾かれるので
|
||||||
validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey);
|
validateDisposable: true, // 捨てアドかどうかチェック
|
||||||
} else {
|
validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので
|
||||||
validated = meta.enableActiveEmailValidation ? await validateEmail({
|
}) : { valid: true, reason: null };
|
||||||
email: emailAddress,
|
|
||||||
validateRegex: true,
|
|
||||||
validateMx: true,
|
|
||||||
validateTypo: false, // TLDを見ているみたいだけどclubとか弾かれるので
|
|
||||||
validateDisposable: true, // 捨てアドかどうかチェック
|
|
||||||
validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので
|
|
||||||
}) : { valid: true, reason: null };
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
validated = { valid: true, reason: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
const available = exist === 0 && validated.valid;
|
const available = exist === 0 && validated.valid;
|
||||||
|
|
||||||
@@ -197,65 +182,4 @@ export class EmailService {
|
|||||||
null,
|
null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async verifyMail(emailAddress: string, verifymailAuthKey: string): Promise<{
|
|
||||||
valid: boolean;
|
|
||||||
reason: 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | null;
|
|
||||||
}> {
|
|
||||||
const endpoint = 'https://verifymail.io/api/' + emailAddress + '?key=' + verifymailAuthKey;
|
|
||||||
const res = await this.httpRequestService.send(endpoint, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
Accept: 'application/json, */*',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const json = (await res.json()) as {
|
|
||||||
block: boolean;
|
|
||||||
catch_all: boolean;
|
|
||||||
deliverable_email: boolean;
|
|
||||||
disposable: boolean;
|
|
||||||
domain: string;
|
|
||||||
email_address: string;
|
|
||||||
email_provider: string;
|
|
||||||
mx: boolean;
|
|
||||||
mx_fallback: boolean;
|
|
||||||
mx_host: string[];
|
|
||||||
mx_ip: string[];
|
|
||||||
mx_priority: { [key: string]: number };
|
|
||||||
privacy: boolean;
|
|
||||||
related_domains: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
if (json.email_address === undefined) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
reason: 'format',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (json.deliverable_email !== undefined && !json.deliverable_email) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
reason: 'smtp',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (json.disposable) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
reason: 'disposable',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (json.mx !== undefined && !json.mx) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
reason: 'mx',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
valid: true,
|
|
||||||
reason: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,170 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
|
||||||
import type { MiUser } from '@/models/User.js';
|
|
||||||
import type { MiNote } from '@/models/Note.js';
|
|
||||||
import { Packed } from '@/misc/json-schema.js';
|
|
||||||
import type { NotesRepository } from '@/models/_.js';
|
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
|
||||||
import { FanoutTimelineName, FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
|
||||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
|
||||||
import { isPureRenote } from '@/misc/is-pure-renote.js';
|
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
|
||||||
import { isReply } from '@/misc/is-reply.js';
|
|
||||||
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
|
||||||
|
|
||||||
type TimelineOptions = {
|
|
||||||
untilId: string | null,
|
|
||||||
sinceId: string | null,
|
|
||||||
limit: number,
|
|
||||||
allowPartial: boolean,
|
|
||||||
me?: { id: MiUser['id'] } | undefined | null,
|
|
||||||
useDbFallback: boolean,
|
|
||||||
redisTimelines: FanoutTimelineName[],
|
|
||||||
noteFilter?: (note: MiNote) => boolean,
|
|
||||||
alwaysIncludeMyNotes?: boolean;
|
|
||||||
ignoreAuthorFromMute?: boolean;
|
|
||||||
excludeNoFiles?: boolean;
|
|
||||||
excludeReplies?: boolean;
|
|
||||||
excludePureRenotes: boolean;
|
|
||||||
dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise<MiNote[]>,
|
|
||||||
};
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class FanoutTimelineEndpointService {
|
|
||||||
constructor(
|
|
||||||
@Inject(DI.notesRepository)
|
|
||||||
private notesRepository: NotesRepository,
|
|
||||||
|
|
||||||
private noteEntityService: NoteEntityService,
|
|
||||||
private cacheService: CacheService,
|
|
||||||
private fanoutTimelineService: FanoutTimelineService,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
async timeline(ps: TimelineOptions): Promise<Packed<'Note'>[]> {
|
|
||||||
return await this.noteEntityService.packMany(await this.getMiNotes(ps), ps.me);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private async getMiNotes(ps: TimelineOptions): Promise<MiNote[]> {
|
|
||||||
let noteIds: string[];
|
|
||||||
let shouldFallbackToDb = false;
|
|
||||||
|
|
||||||
// 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える
|
|
||||||
if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]);
|
|
||||||
|
|
||||||
const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId);
|
|
||||||
|
|
||||||
const redisResultIds = Array.from(new Set(redisResult.flat(1)));
|
|
||||||
|
|
||||||
redisResultIds.sort((a, b) => a > b ? -1 : 1);
|
|
||||||
noteIds = redisResultIds.slice(0, ps.limit);
|
|
||||||
|
|
||||||
shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
|
|
||||||
|
|
||||||
if (!shouldFallbackToDb) {
|
|
||||||
let filter = ps.noteFilter ?? (_note => true);
|
|
||||||
|
|
||||||
if (ps.alwaysIncludeMyNotes && ps.me) {
|
|
||||||
const me = ps.me;
|
|
||||||
const parentFilter = filter;
|
|
||||||
filter = (note) => note.userId === me.id || parentFilter(note);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.excludeNoFiles) {
|
|
||||||
const parentFilter = filter;
|
|
||||||
filter = (note) => note.fileIds.length !== 0 && parentFilter(note);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.excludeReplies) {
|
|
||||||
const parentFilter = filter;
|
|
||||||
filter = (note) => !isReply(note, ps.me?.id) && parentFilter(note);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.excludePureRenotes) {
|
|
||||||
const parentFilter = filter;
|
|
||||||
filter = (note) => !isPureRenote(note) && parentFilter(note);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.me) {
|
|
||||||
const me = ps.me;
|
|
||||||
const [
|
|
||||||
userIdsWhoMeMuting,
|
|
||||||
userIdsWhoMeMutingRenotes,
|
|
||||||
userIdsWhoBlockingMe,
|
|
||||||
userMutedInstances,
|
|
||||||
] = await Promise.all([
|
|
||||||
this.cacheService.userMutingsCache.fetch(ps.me.id),
|
|
||||||
this.cacheService.renoteMutingsCache.fetch(ps.me.id),
|
|
||||||
this.cacheService.userBlockedCache.fetch(ps.me.id),
|
|
||||||
this.cacheService.userProfileCache.fetch(me.id).then(p => new Set(p.mutedInstances)),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const parentFilter = filter;
|
|
||||||
filter = (note) => {
|
|
||||||
if (isUserRelated(note, userIdsWhoBlockingMe, ps.ignoreAuthorFromMute)) return false;
|
|
||||||
if (isUserRelated(note, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false;
|
|
||||||
if (isPureRenote(note) && isUserRelated(note, userIdsWhoMeMutingRenotes, ps.ignoreAuthorFromMute)) return false;
|
|
||||||
if (isInstanceMuted(note, userMutedInstances)) return false;
|
|
||||||
|
|
||||||
return parentFilter(note);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const redisTimeline: MiNote[] = [];
|
|
||||||
let readFromRedis = 0;
|
|
||||||
let lastSuccessfulRate = 1; // rateをキャッシュする?
|
|
||||||
|
|
||||||
while ((redisResultIds.length - readFromRedis) !== 0) {
|
|
||||||
const remainingToRead = ps.limit - redisTimeline.length;
|
|
||||||
|
|
||||||
// DBからの取り直しを減らす初回と同じ割合以上で成功すると仮定するが、クエリの長さを考えて三倍まで
|
|
||||||
const countToGet = remainingToRead * Math.ceil(Math.min(1.1 / lastSuccessfulRate, 3));
|
|
||||||
noteIds = redisResultIds.slice(readFromRedis, readFromRedis + countToGet);
|
|
||||||
|
|
||||||
readFromRedis += noteIds.length;
|
|
||||||
|
|
||||||
const gotFromDb = await this.getAndFilterFromDb(noteIds, filter);
|
|
||||||
redisTimeline.push(...gotFromDb);
|
|
||||||
lastSuccessfulRate = gotFromDb.length / noteIds.length;
|
|
||||||
|
|
||||||
if (ps.allowPartial ? redisTimeline.length !== 0 : redisTimeline.length >= ps.limit) {
|
|
||||||
// 十分Redisからとれた
|
|
||||||
return redisTimeline.slice(0, ps.limit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// まだ足りない分はDBにフォールバック
|
|
||||||
const remainingToRead = ps.limit - redisTimeline.length;
|
|
||||||
const gotFromDb = await ps.dbFallback(noteIds[noteIds.length - 1], ps.sinceId, remainingToRead);
|
|
||||||
redisTimeline.push(...gotFromDb);
|
|
||||||
return redisTimeline;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await ps.dbFallback(ps.untilId, ps.sinceId, ps.limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getAndFilterFromDb(noteIds: string[], noteFilter: (note: MiNote) => boolean): Promise<MiNote[]> {
|
|
||||||
const query = this.notesRepository.createQueryBuilder('note')
|
|
||||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
|
||||||
.innerJoinAndSelect('note.user', 'user')
|
|
||||||
.leftJoinAndSelect('note.reply', 'reply')
|
|
||||||
.leftJoinAndSelect('note.renote', 'renote')
|
|
||||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
|
||||||
.leftJoinAndSelect('note.channel', 'channel');
|
|
||||||
|
|
||||||
const notes = (await query.getMany()).filter(noteFilter);
|
|
||||||
|
|
||||||
notes.sort((a, b) => a.id > b.id ? -1 : 1);
|
|
||||||
|
|
||||||
return notes;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -5,12 +5,11 @@
|
|||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import type { MiGalleryPost, MiNote, MiUser } from '@/models/_.js';
|
import type { MiNote, MiUser } from '@/models/_.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
|
||||||
const GLOBAL_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3日ごと
|
const GLOBAL_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3日ごと
|
||||||
export const GALLERY_POSTS_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3日ごと
|
|
||||||
const PER_USER_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 7; // 1週間ごと
|
const PER_USER_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 7; // 1週間ごと
|
||||||
const HASHTAG_RANKING_WINDOW = 1000 * 60 * 60; // 1時間ごと
|
const HASHTAG_RANKING_WINDOW = 1000 * 60 * 60; // 1時間ごと
|
||||||
|
|
||||||
@@ -80,11 +79,6 @@ export class FeaturedService {
|
|||||||
return this.updateRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, noteId, score);
|
return this.updateRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, noteId, score);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public updateGalleryPostsRanking(galleryPostId: MiGalleryPost['id'], score = 1): Promise<void> {
|
|
||||||
return this.updateRankingOf('featuredGalleryPostsRanking', GALLERY_POSTS_RANKING_WINDOW, galleryPostId, score);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public updateInChannelNotesRanking(channelId: MiNote['channelId'], noteId: MiNote['id'], score = 1): Promise<void> {
|
public updateInChannelNotesRanking(channelId: MiNote['channelId'], noteId: MiNote['id'], score = 1): Promise<void> {
|
||||||
return this.updateRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, noteId, score);
|
return this.updateRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, noteId, score);
|
||||||
@@ -105,11 +99,6 @@ export class FeaturedService {
|
|||||||
return this.getRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, threshold);
|
return this.getRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, threshold);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public getGalleryPostsRanking(threshold: number): Promise<MiGalleryPost['id'][]> {
|
|
||||||
return this.getRankingOf('featuredGalleryPostsRanking', GALLERY_POSTS_RANKING_WINDOW, threshold);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public getInChannelNotesRanking(channelId: MiNote['channelId'], threshold: number): Promise<MiNote['id'][]> {
|
public getInChannelNotesRanking(channelId: MiNote['channelId'], threshold: number): Promise<MiNote['id'][]> {
|
||||||
return this.getRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, threshold);
|
return this.getRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, threshold);
|
||||||
|
@@ -9,37 +9,8 @@ import { DI } from '@/di-symbols.js';
|
|||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
|
||||||
export type FanoutTimelineName =
|
|
||||||
// home timeline
|
|
||||||
| `homeTimeline:${string}`
|
|
||||||
| `homeTimelineWithFiles:${string}` // only notes with files are included
|
|
||||||
// local timeline
|
|
||||||
| `localTimeline` // replies are not included
|
|
||||||
| `localTimelineWithFiles` // only non-reply notes with files are included
|
|
||||||
| `localTimelineWithReplies` // only replies are included
|
|
||||||
| `localTimelineWithReplyTo:${string}` // Only replies to specific local user are included. Parameter is reply user id.
|
|
||||||
|
|
||||||
// antenna
|
|
||||||
| `antennaTimeline:${string}`
|
|
||||||
|
|
||||||
// user timeline
|
|
||||||
| `userTimeline:${string}` // replies are not included
|
|
||||||
| `userTimelineWithFiles:${string}` // only non-reply notes with files are included
|
|
||||||
| `userTimelineWithReplies:${string}` // only replies are included
|
|
||||||
| `userTimelineWithChannel:${string}` // only channel notes are included, replies are included
|
|
||||||
|
|
||||||
// user list timelines
|
|
||||||
| `userListTimeline:${string}`
|
|
||||||
| `userListTimelineWithFiles:${string}` // only notes with files are included
|
|
||||||
|
|
||||||
// channel timelines
|
|
||||||
| `channelTimeline:${string}` // replies are included
|
|
||||||
|
|
||||||
// role timelines
|
|
||||||
| `roleTimeline:${string}` // any notes are included
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FanoutTimelineService {
|
export class FunoutTimelineService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redisForTimelines)
|
@Inject(DI.redisForTimelines)
|
||||||
private redisForTimelines: Redis.Redis,
|
private redisForTimelines: Redis.Redis,
|
||||||
@@ -49,7 +20,7 @@ export class FanoutTimelineService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public push(tl: FanoutTimelineName, id: string, maxlen: number, pipeline: Redis.ChainableCommander) {
|
public push(tl: string, id: string, maxlen: number, pipeline: Redis.ChainableCommander) {
|
||||||
// リモートから遅れて届いた(もしくは後から追加された)投稿日時が古い投稿が追加されるとページネーション時に問題を引き起こすため、
|
// リモートから遅れて届いた(もしくは後から追加された)投稿日時が古い投稿が追加されるとページネーション時に問題を引き起こすため、
|
||||||
// 3分以内に投稿されたものでない場合、Redisにある最古のIDより新しい場合のみ追加する
|
// 3分以内に投稿されたものでない場合、Redisにある最古のIDより新しい場合のみ追加する
|
||||||
if (this.idService.parse(id).date.getTime() > Date.now() - 1000 * 60 * 3) {
|
if (this.idService.parse(id).date.getTime() > Date.now() - 1000 * 60 * 3) {
|
||||||
@@ -70,7 +41,7 @@ export class FanoutTimelineService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public get(name: FanoutTimelineName, untilId?: string | null, sinceId?: string | null) {
|
public get(name: string, untilId?: string | null, sinceId?: string | null) {
|
||||||
if (untilId && sinceId) {
|
if (untilId && sinceId) {
|
||||||
return this.redisForTimelines.lrange('list:' + name, 0, -1)
|
return this.redisForTimelines.lrange('list:' + name, 0, -1)
|
||||||
.then(ids => ids.filter(id => id < untilId && id > sinceId).sort((a, b) => a > b ? -1 : 1));
|
.then(ids => ids.filter(id => id < untilId && id > sinceId).sort((a, b) => a > b ? -1 : 1));
|
||||||
@@ -87,7 +58,7 @@ export class FanoutTimelineService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public getMulti(name: FanoutTimelineName[], untilId?: string | null, sinceId?: string | null): Promise<string[][]> {
|
public getMulti(name: string[], untilId?: string | null, sinceId?: string | null): Promise<string[][]> {
|
||||||
const pipeline = this.redisForTimelines.pipeline();
|
const pipeline = this.redisForTimelines.pipeline();
|
||||||
for (const n of name) {
|
for (const n of name) {
|
||||||
pipeline.lrange('list:' + n, 0, -1);
|
pipeline.lrange('list:' + n, 0, -1);
|
||||||
@@ -108,7 +79,7 @@ export class FanoutTimelineService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public purge(name: FanoutTimelineName) {
|
public purge(name: string) {
|
||||||
return this.redisForTimelines.del('list:' + name);
|
return this.redisForTimelines.del('list:' + name);
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -7,11 +7,11 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import { ulid } from 'ulid';
|
import { ulid } from 'ulid';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { genAid, isSafeAidT, parseAid } from '@/misc/id/aid.js';
|
import { genAid, parseAid } from '@/misc/id/aid.js';
|
||||||
import { genAidx, isSafeAidxT, parseAidx } from '@/misc/id/aidx.js';
|
import { genAidx, parseAidx } from '@/misc/id/aidx.js';
|
||||||
import { genMeid, isSafeMeidT, parseMeid } from '@/misc/id/meid.js';
|
import { genMeid, parseMeid } from '@/misc/id/meid.js';
|
||||||
import { genMeidg, isSafeMeidgT, parseMeidg } from '@/misc/id/meidg.js';
|
import { genMeidg, parseMeidg } from '@/misc/id/meidg.js';
|
||||||
import { genObjectId, isSafeObjectIdT, parseObjectId } from '@/misc/id/object-id.js';
|
import { genObjectId, parseObjectId } from '@/misc/id/object-id.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { parseUlid } from '@/misc/id/ulid.js';
|
import { parseUlid } from '@/misc/id/ulid.js';
|
||||||
|
|
||||||
@@ -26,19 +26,6 @@ export class IdService {
|
|||||||
this.method = config.id.toLowerCase();
|
this.method = config.id.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public isSafeT(t: number): boolean {
|
|
||||||
switch (this.method) {
|
|
||||||
case 'aid': return isSafeAidT(t);
|
|
||||||
case 'aidx': return isSafeAidxT(t);
|
|
||||||
case 'meid': return isSafeMeidT(t);
|
|
||||||
case 'meidg': return isSafeMeidgT(t);
|
|
||||||
case 'ulid': return t > 0;
|
|
||||||
case 'objectid': return isSafeObjectIdT(t);
|
|
||||||
default: throw new Error('unrecognized id generation method');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 時間を元にIDを生成します(省略時は現在日時)
|
* 時間を元にIDを生成します(省略時は現在日時)
|
||||||
* @param time 日時
|
* @param time 日時
|
||||||
|
@@ -250,12 +250,6 @@ export class MfmService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function fnDefault(node: mfm.MfmFn) {
|
|
||||||
const el = doc.createElement('i');
|
|
||||||
appendChildren(node.children, el);
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = {
|
const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = {
|
||||||
bold: (node) => {
|
bold: (node) => {
|
||||||
const el = doc.createElement('b');
|
const el = doc.createElement('b');
|
||||||
@@ -282,68 +276,17 @@ export class MfmService {
|
|||||||
},
|
},
|
||||||
|
|
||||||
fn: (node) => {
|
fn: (node) => {
|
||||||
switch (node.props.name) {
|
if (node.props.name === 'unixtime') {
|
||||||
case 'unixtime': {
|
const text = node.children[0]!.type === 'text' ? node.children[0].props.text : '';
|
||||||
const text = node.children[0].type === 'text' ? node.children[0].props.text : '';
|
const date = new Date(parseInt(text, 10) * 1000);
|
||||||
try {
|
const el = doc.createElement('time');
|
||||||
const date = new Date(parseInt(text, 10) * 1000);
|
el.setAttribute('datetime', date.toISOString());
|
||||||
const el = doc.createElement('time');
|
el.textContent = date.toISOString();
|
||||||
el.setAttribute('datetime', date.toISOString());
|
return el;
|
||||||
el.textContent = date.toISOString();
|
} else {
|
||||||
return el;
|
const el = doc.createElement('i');
|
||||||
} catch (err) {
|
appendChildren(node.children, el);
|
||||||
return fnDefault(node);
|
return el;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'ruby': {
|
|
||||||
if (node.children.length === 1) {
|
|
||||||
const child = node.children[0];
|
|
||||||
const text = child.type === 'text' ? child.props.text : '';
|
|
||||||
const rubyEl = doc.createElement('ruby');
|
|
||||||
const rtEl = doc.createElement('rt');
|
|
||||||
|
|
||||||
// ruby未対応のHTMLサニタイザーを通したときにルビが「劉備(りゅうび)」となるようにする
|
|
||||||
const rpStartEl = doc.createElement('rp');
|
|
||||||
rpStartEl.appendChild(doc.createTextNode('('));
|
|
||||||
const rpEndEl = doc.createElement('rp');
|
|
||||||
rpEndEl.appendChild(doc.createTextNode(')'));
|
|
||||||
|
|
||||||
rubyEl.appendChild(doc.createTextNode(text.split(' ')[0]));
|
|
||||||
rtEl.appendChild(doc.createTextNode(text.split(' ')[1]));
|
|
||||||
rubyEl.appendChild(rpStartEl);
|
|
||||||
rubyEl.appendChild(rtEl);
|
|
||||||
rubyEl.appendChild(rpEndEl);
|
|
||||||
return rubyEl;
|
|
||||||
} else {
|
|
||||||
const rt = node.children.at(-1);
|
|
||||||
|
|
||||||
if (!rt) {
|
|
||||||
return fnDefault(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = rt.type === 'text' ? rt.props.text : '';
|
|
||||||
const rubyEl = doc.createElement('ruby');
|
|
||||||
const rtEl = doc.createElement('rt');
|
|
||||||
|
|
||||||
// ruby未対応のHTMLサニタイザーを通したときにルビが「劉備(りゅうび)」となるようにする
|
|
||||||
const rpStartEl = doc.createElement('rp');
|
|
||||||
rpStartEl.appendChild(doc.createTextNode('('));
|
|
||||||
const rpEndEl = doc.createElement('rp');
|
|
||||||
rpEndEl.appendChild(doc.createTextNode(')'));
|
|
||||||
|
|
||||||
appendChildren(node.children.slice(0, node.children.length - 1), rubyEl);
|
|
||||||
rtEl.appendChild(doc.createTextNode(text.trim()));
|
|
||||||
rubyEl.appendChild(rpStartEl);
|
|
||||||
rubyEl.appendChild(rtEl);
|
|
||||||
rubyEl.appendChild(rpEndEl);
|
|
||||||
return rubyEl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
return fnDefault(node);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -54,10 +54,9 @@ import { RoleService } from '@/core/RoleService.js';
|
|||||||
import { MetaService } from '@/core/MetaService.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 { FunoutTimelineService } from '@/core/FunoutTimelineService.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';
|
||||||
import { isReply } from '@/misc/is-reply.js';
|
|
||||||
|
|
||||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||||
|
|
||||||
@@ -195,7 +194,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private fanoutTimelineService: FanoutTimelineService,
|
private funoutTimelineService: FunoutTimelineService,
|
||||||
private noteReadService: NoteReadService,
|
private noteReadService: NoteReadService,
|
||||||
private notificationService: NotificationService,
|
private notificationService: NotificationService,
|
||||||
private relayService: RelayService,
|
private relayService: RelayService,
|
||||||
@@ -844,9 +843,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
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.funoutTimelineService.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.funoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
|
||||||
|
|
||||||
const channelFollowings = await this.channelFollowingsRepository.find({
|
const channelFollowings = await this.channelFollowingsRepository.find({
|
||||||
where: {
|
where: {
|
||||||
@@ -856,9 +855,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.funoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, 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.funoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -892,13 +891,13 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
if (note.visibility === 'specified' && !note.visibleUserIds.some(v => v === following.followerId)) continue;
|
if (note.visibility === 'specified' && !note.visibleUserIds.some(v => v === following.followerId)) continue;
|
||||||
|
|
||||||
// 「自分自身への返信 or そのフォロワーへの返信」のどちらでもない場合
|
// 「自分自身への返信 or そのフォロワーへの返信」のどちらでもない場合
|
||||||
if (isReply(note, following.followerId)) {
|
if (note.replyId && !(note.replyUserId === note.userId || note.replyUserId === following.followerId)) {
|
||||||
if (!following.withReplies) continue;
|
if (!following.withReplies) continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
this.funoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, 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.funoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -910,43 +909,40 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
) continue;
|
) continue;
|
||||||
|
|
||||||
// 「自分自身への返信 or そのリストの作成者への返信」のどちらでもない場合
|
// 「自分自身への返信 or そのリストの作成者への返信」のどちらでもない場合
|
||||||
if (isReply(note, userListMembership.userListUserId)) {
|
if (note.replyId && !(note.replyUserId === note.userId || note.replyUserId === userListMembership.userListUserId)) {
|
||||||
if (!userListMembership.withReplies) continue;
|
if (!userListMembership.withReplies) continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r);
|
this.funoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, 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.funoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL
|
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL
|
||||||
this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
this.funoutTimelineService.push(`homeTimeline:${user.id}`, note.id, 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.funoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自分自身以外への返信
|
// 自分自身以外への返信
|
||||||
if (isReply(note)) {
|
if (note.replyId && note.replyUserId !== note.userId) {
|
||||||
this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
|
this.funoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : 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.funoutTimelineService.push('localTimelineWithReplies', note.id, 300, r);
|
||||||
if (note.replyUserHost == null) {
|
|
||||||
this.fanoutTimelineService.push(`localTimelineWithReplyTo:${note.replyUserId}`, note.id, 300 / 10, r);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
|
this.funoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : 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.funoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.visibility === 'public' && note.userHost == null) {
|
if (note.visibility === 'public' && note.userHost == null) {
|
||||||
this.fanoutTimelineService.push('localTimeline', note.id, 1000, r);
|
this.funoutTimelineService.push('localTimeline', note.id, 1000, r);
|
||||||
if (note.fileIds.length > 0) {
|
if (note.fileIds.length > 0) {
|
||||||
this.fanoutTimelineService.push('localTimelineWithFiles', note.id, 500, r);
|
this.funoutTimelineService.push('localTimelineWithFiles', note.id, 500, r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -77,7 +77,7 @@ export class NotePiningService {
|
|||||||
} as MiUserNotePining);
|
} as MiUserNotePining);
|
||||||
|
|
||||||
// Deliver to remote followers
|
// Deliver to remote followers
|
||||||
if (this.userEntityService.isLocalUser(user) && !note.localOnly && ['public', 'home'].includes(note.visibility)) {
|
if (this.userEntityService.isLocalUser(user)) {
|
||||||
this.deliverPinnedChange(user.id, note.id, true);
|
this.deliverPinnedChange(user.id, note.id, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,7 +105,7 @@ export class NotePiningService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Deliver to remote followers
|
// Deliver to remote followers
|
||||||
if (this.userEntityService.isLocalUser(user) && !note.localOnly && ['public', 'home'].includes(note.visibility)) {
|
if (this.userEntityService.isLocalUser(user)) {
|
||||||
this.deliverPinnedChange(user.id, noteId, false);
|
this.deliverPinnedChange(user.id, noteId, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -20,7 +20,7 @@ import { IdService } from '@/core/IdService.js';
|
|||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
export type RolePolicies = {
|
export type RolePolicies = {
|
||||||
@@ -87,9 +87,6 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
@Inject(DI.redis)
|
@Inject(DI.redis)
|
||||||
private redisClient: Redis.Redis,
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.redisForTimelines)
|
|
||||||
private redisForTimelines: Redis.Redis,
|
|
||||||
|
|
||||||
@Inject(DI.redisForSub)
|
@Inject(DI.redisForSub)
|
||||||
private redisForSub: Redis.Redis,
|
private redisForSub: Redis.Redis,
|
||||||
|
|
||||||
@@ -108,7 +105,7 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private moderationLogService: ModerationLogService,
|
private moderationLogService: ModerationLogService,
|
||||||
private fanoutTimelineService: FanoutTimelineService,
|
private funoutTimelineService: FunoutTimelineService,
|
||||||
) {
|
) {
|
||||||
//this.onMessage = this.onMessage.bind(this);
|
//this.onMessage = this.onMessage.bind(this);
|
||||||
|
|
||||||
@@ -479,10 +476,10 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
public async addNoteToRoleTimeline(note: Packed<'Note'>): Promise<void> {
|
public async addNoteToRoleTimeline(note: Packed<'Note'>): Promise<void> {
|
||||||
const roles = await this.getUserRoles(note.userId);
|
const roles = await this.getUserRoles(note.userId);
|
||||||
|
|
||||||
const redisPipeline = this.redisForTimelines.pipeline();
|
const redisPipeline = this.redisClient.pipeline();
|
||||||
|
|
||||||
for (const role of roles) {
|
for (const role of roles) {
|
||||||
this.fanoutTimelineService.push(`roleTimeline:${role.id}`, note.id, 1000, redisPipeline);
|
this.funoutTimelineService.push(`roleTimeline:${role.id}`, note.id, 1000, redisPipeline);
|
||||||
this.globalEventService.publishRoleTimelineStream(role.id, 'note', note);
|
this.globalEventService.publishRoleTimelineStream(role.id, 'note', note);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -29,7 +29,7 @@ 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 { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||||
import Logger from '../logger.js';
|
import Logger from '../logger.js';
|
||||||
|
|
||||||
const logger = new Logger('following/create');
|
const logger = new Logger('following/create');
|
||||||
@@ -84,7 +84,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||||||
private webhookService: WebhookService,
|
private webhookService: WebhookService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private accountMoveService: AccountMoveService,
|
private accountMoveService: AccountMoveService,
|
||||||
private fanoutTimelineService: FanoutTimelineService,
|
private funoutTimelineService: FunoutTimelineService,
|
||||||
private perUserFollowingChart: PerUserFollowingChart,
|
private perUserFollowingChart: PerUserFollowingChart,
|
||||||
private instanceChart: InstanceChart,
|
private instanceChart: InstanceChart,
|
||||||
) {
|
) {
|
||||||
@@ -304,6 +304,8 @@ export class UserFollowingService implements OnModuleInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.funoutTimelineService.purge(`homeTimeline:${follower.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish followed event
|
// Publish followed event
|
||||||
@@ -371,6 +373,8 @@ export class UserFollowingService implements OnModuleInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.funoutTimelineService.purge(`homeTimeline:${follower.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||||
|
@@ -306,15 +306,9 @@ export class ApInboxService {
|
|||||||
this.logger.info(`Creating the (Re)Note: ${uri}`);
|
this.logger.info(`Creating the (Re)Note: ${uri}`);
|
||||||
|
|
||||||
const activityAudience = await this.apAudienceService.parseAudience(actor, activity.to, activity.cc);
|
const activityAudience = await this.apAudienceService.parseAudience(actor, activity.to, activity.cc);
|
||||||
const createdAt = activity.published ? new Date(activity.published) : null;
|
|
||||||
|
|
||||||
if (createdAt && createdAt < this.idService.parse(renote.id).date) {
|
|
||||||
this.logger.warn('skip: malformed createdAt');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.noteCreateService.create(actor, {
|
await this.noteCreateService.create(actor, {
|
||||||
createdAt,
|
createdAt: activity.published ? new Date(activity.published) : null,
|
||||||
renote,
|
renote,
|
||||||
visibility: activityAudience.visibility,
|
visibility: activityAudience.visibility,
|
||||||
visibleUsers: activityAudience.visibleUsers,
|
visibleUsers: activityAudience.visibleUsers,
|
||||||
|
@@ -92,10 +92,6 @@ export class ApNoteService {
|
|||||||
return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
|
return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) {
|
|
||||||
return new Error('invalid Note: published timestamp is malformed');
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -198,14 +198,12 @@ export class NotificationEntityService implements OnModuleInit {
|
|||||||
});
|
});
|
||||||
} else if (notification.type === 'renote:grouped') {
|
} else if (notification.type === 'renote:grouped') {
|
||||||
const users = await Promise.all(notification.userIds.map(userId => {
|
const users = await Promise.all(notification.userIds.map(userId => {
|
||||||
const packedUser = hint?.packedUsers != null ? hint.packedUsers.get(userId) : null;
|
const user = hint?.packedUsers != null
|
||||||
if (packedUser) {
|
? hint.packedUsers.get(userId)
|
||||||
return packedUser;
|
: this.userEntityService.pack(userId!, { id: meId }, {
|
||||||
}
|
detail: false,
|
||||||
|
});
|
||||||
return this.userEntityService.pack(userId, { id: meId }, {
|
return user;
|
||||||
detail: false,
|
|
||||||
});
|
|
||||||
}));
|
}));
|
||||||
return await awaitAll({
|
return await awaitAll({
|
||||||
id: notification.id,
|
id: notification.id,
|
||||||
|
@@ -473,7 +473,6 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
||||||
unreadNotificationsCount: notificationsInfo?.unreadCount,
|
unreadNotificationsCount: notificationsInfo?.unreadCount,
|
||||||
mutedWords: profile!.mutedWords,
|
mutedWords: profile!.mutedWords,
|
||||||
hardMutedWords: profile!.hardMutedWords,
|
|
||||||
mutedInstances: profile!.mutedInstances,
|
mutedInstances: profile!.mutedInstances,
|
||||||
mutingNotificationTypes: [], // 後方互換性のため
|
mutingNotificationTypes: [], // 後方互換性のため
|
||||||
notificationRecieveConfig: profile!.notificationRecieveConfig,
|
notificationRecieveConfig: profile!.notificationRecieveConfig,
|
||||||
|
@@ -34,7 +34,3 @@ export function parseAid(id: string): { date: Date; } {
|
|||||||
const time = parseInt(id.slice(0, 8), 36) + TIME2000;
|
const time = parseInt(id.slice(0, 8), 36) + TIME2000;
|
||||||
return { date: new Date(time) };
|
return { date: new Date(time) };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSafeAidT(t: number): boolean {
|
|
||||||
return t > TIME2000;
|
|
||||||
}
|
|
||||||
|
@@ -41,7 +41,3 @@ export function parseAidx(id: string): { date: Date; } {
|
|||||||
const time = parseInt(id.slice(0, TIME_LENGTH), 36) + TIME2000;
|
const time = parseInt(id.slice(0, TIME_LENGTH), 36) + TIME2000;
|
||||||
return { date: new Date(time) };
|
return { date: new Date(time) };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSafeAidxT(t: number): boolean {
|
|
||||||
return t > TIME2000;
|
|
||||||
}
|
|
||||||
|
@@ -38,7 +38,3 @@ export function parseMeid(id: string): { date: Date; } {
|
|||||||
date: new Date(parseInt(id.slice(0, 12), 16) - 0x800000000000),
|
date: new Date(parseInt(id.slice(0, 12), 16) - 0x800000000000),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSafeMeidT(t: number): boolean {
|
|
||||||
return t > 0;
|
|
||||||
}
|
|
||||||
|
@@ -38,7 +38,3 @@ export function parseMeidg(id: string): { date: Date; } {
|
|||||||
date: new Date(parseInt(id.slice(1, 12), 16)),
|
date: new Date(parseInt(id.slice(1, 12), 16)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSafeMeidgT(t: number): boolean {
|
|
||||||
return t > 0;
|
|
||||||
}
|
|
||||||
|
@@ -38,7 +38,3 @@ export function parseObjectId(id: string): { date: Date; } {
|
|||||||
date: new Date(parseInt(id.slice(0, 8), 16) * 1000),
|
date: new Date(parseInt(id.slice(0, 8), 16) * 1000),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSafeObjectIdT(t: number): boolean {
|
|
||||||
return t > 0;
|
|
||||||
}
|
|
||||||
|
@@ -3,13 +3,12 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MiNote } from '@/models/Note.js';
|
|
||||||
import type { Packed } from './json-schema.js';
|
import type { Packed } from './json-schema.js';
|
||||||
|
|
||||||
export function isInstanceMuted(note: Packed<'Note'> | MiNote, mutedInstances: Set<string>): boolean {
|
export function isInstanceMuted(note: Packed<'Note'>, mutedInstances: Set<string>): boolean {
|
||||||
if (mutedInstances.has(note.user?.host ?? '')) return true;
|
if (mutedInstances.has(note.user.host ?? '')) return true;
|
||||||
if (mutedInstances.has(note.reply?.user?.host ?? '')) return true;
|
if (mutedInstances.has(note.reply?.user.host ?? '')) return true;
|
||||||
if (mutedInstances.has(note.renote?.user?.host ?? '')) return true;
|
if (mutedInstances.has(note.renote?.user.host ?? '')) return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { MiUser } from '@/models/User.js';
|
|
||||||
|
|
||||||
export function isReply(note: any, viewerId?: MiUser['id'] | undefined | null): boolean {
|
|
||||||
return note.replyId && note.replyUserId !== note.userId && note.replyUserId !== viewerId;
|
|
||||||
}
|
|
@@ -36,7 +36,6 @@ import { packedGalleryPostSchema } from '@/models/json-schema/gallery-post.js';
|
|||||||
import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/json-schema/emoji.js';
|
import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/json-schema/emoji.js';
|
||||||
import { packedFlashSchema } from '@/models/json-schema/flash.js';
|
import { packedFlashSchema } from '@/models/json-schema/flash.js';
|
||||||
import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
|
import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
|
||||||
import { packedSigninSchema } from '@/models/json-schema/signin.js';
|
|
||||||
|
|
||||||
export const refs = {
|
export const refs = {
|
||||||
UserLite: packedUserLiteSchema,
|
UserLite: packedUserLiteSchema,
|
||||||
@@ -72,7 +71,6 @@ export const refs = {
|
|||||||
EmojiSimple: packedEmojiSimpleSchema,
|
EmojiSimple: packedEmojiSimpleSchema,
|
||||||
EmojiDetailed: packedEmojiDetailedSchema,
|
EmojiDetailed: packedEmojiDetailedSchema,
|
||||||
Flash: packedFlashSchema,
|
Flash: packedFlashSchema,
|
||||||
Signin: packedSigninSchema,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
|
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
|
||||||
|
@@ -446,17 +446,6 @@ export class MiMeta {
|
|||||||
})
|
})
|
||||||
public enableActiveEmailValidation: boolean;
|
public enableActiveEmailValidation: boolean;
|
||||||
|
|
||||||
@Column('boolean', {
|
|
||||||
default: false,
|
|
||||||
})
|
|
||||||
public enableVerifymailApi: boolean;
|
|
||||||
|
|
||||||
@Column('varchar', {
|
|
||||||
length: 1024,
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
public verifymailAuthKey: string | null;
|
|
||||||
|
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: true,
|
default: true,
|
||||||
})
|
})
|
||||||
|
@@ -29,7 +29,6 @@ export class MiUserProfile {
|
|||||||
})
|
})
|
||||||
public location: string | null;
|
public location: string | null;
|
||||||
|
|
||||||
@Index()
|
|
||||||
@Column('char', {
|
@Column('char', {
|
||||||
length: 10, nullable: true,
|
length: 10, nullable: true,
|
||||||
comment: 'The birthday (YYYY-MM-DD) of the User.',
|
comment: 'The birthday (YYYY-MM-DD) of the User.',
|
||||||
@@ -216,12 +215,7 @@ export class MiUserProfile {
|
|||||||
@Column('jsonb', {
|
@Column('jsonb', {
|
||||||
default: [],
|
default: [],
|
||||||
})
|
})
|
||||||
public mutedWords: (string[] | string)[];
|
public mutedWords: string[][];
|
||||||
|
|
||||||
@Column('jsonb', {
|
|
||||||
default: [],
|
|
||||||
})
|
|
||||||
public hardMutedWords: (string[] | string)[];
|
|
||||||
|
|
||||||
@Column('jsonb', {
|
@Column('jsonb', {
|
||||||
default: [],
|
default: [],
|
||||||
|
@@ -42,18 +42,14 @@ export const packedAnnouncementSchema = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
needConfirmationToRead: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
silence: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
forYou: {
|
forYou: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
needConfirmationToRead: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
isRead: {
|
isRead: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: true, nullable: false,
|
optional: true, nullable: false,
|
||||||
|
@@ -19,7 +19,7 @@ export const packedChannelSchema = {
|
|||||||
},
|
},
|
||||||
lastNotedAt: {
|
lastNotedAt: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
nullable: true, optional: false,
|
optional: false, nullable: true,
|
||||||
format: 'date-time',
|
format: 'date-time',
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
@@ -27,19 +27,39 @@ export const packedChannelSchema = {
|
|||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
},
|
|
||||||
userId: {
|
|
||||||
type: 'string',
|
type: 'string',
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
format: 'id',
|
|
||||||
},
|
},
|
||||||
bannerUrl: {
|
bannerUrl: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
format: 'url',
|
format: 'url',
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
},
|
},
|
||||||
|
isArchived: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
notesCount: {
|
||||||
|
type: 'number',
|
||||||
|
nullable: false, optional: false,
|
||||||
|
},
|
||||||
|
usersCount: {
|
||||||
|
type: 'number',
|
||||||
|
nullable: false, optional: false,
|
||||||
|
},
|
||||||
|
isFollowing: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
},
|
||||||
|
isFavorited: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: 'string',
|
||||||
|
nullable: true, optional: false,
|
||||||
|
format: 'id',
|
||||||
|
},
|
||||||
pinnedNoteIds: {
|
pinnedNoteIds: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
@@ -52,18 +72,6 @@ export const packedChannelSchema = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
isArchived: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
usersCount: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
notesCount: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
isSensitive: {
|
isSensitive: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
@@ -72,22 +80,5 @@ export const packedChannelSchema = {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
isFollowing: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: true, nullable: false,
|
|
||||||
},
|
|
||||||
isFavorited: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: true, nullable: false,
|
|
||||||
},
|
|
||||||
pinnedNotes: {
|
|
||||||
type: 'array',
|
|
||||||
optional: true, nullable: false,
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
ref: 'Note',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@@ -44,13 +44,13 @@ export const packedClipSchema = {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
favoritedCount: {
|
|
||||||
type: 'number',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
isFavorited: {
|
isFavorited: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: true, nullable: false,
|
optional: true, nullable: false,
|
||||||
},
|
},
|
||||||
|
favoritedCount: {
|
||||||
|
type: 'number',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@@ -74,7 +74,7 @@ export const packedDriveFileSchema = {
|
|||||||
},
|
},
|
||||||
url: {
|
url: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: true,
|
||||||
format: 'url',
|
format: 'url',
|
||||||
},
|
},
|
||||||
thumbnailUrl: {
|
thumbnailUrl: {
|
||||||
|
@@ -21,12 +21,6 @@ export const packedDriveFolderSchema = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
parentId: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
format: 'id',
|
|
||||||
example: 'xxxxxxxxxx',
|
|
||||||
},
|
|
||||||
foldersCount: {
|
foldersCount: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
optional: true, nullable: false,
|
optional: true, nullable: false,
|
||||||
@@ -35,6 +29,12 @@ export const packedDriveFolderSchema = {
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
optional: true, nullable: false,
|
optional: true, nullable: false,
|
||||||
},
|
},
|
||||||
|
parentId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
format: 'id',
|
||||||
|
example: 'xxxxxxxxxx',
|
||||||
|
},
|
||||||
parent: {
|
parent: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: true, nullable: true,
|
optional: true, nullable: true,
|
||||||
|
@@ -79,10 +79,6 @@ export const packedFederationInstanceSchema = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
isSilenced: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
iconUrl: {
|
iconUrl: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
@@ -97,6 +93,11 @@ export const packedFederationInstanceSchema = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
|
isSilenced: {
|
||||||
|
type: "boolean",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
infoUpdatedAt: {
|
infoUpdatedAt: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
|
@@ -22,16 +22,6 @@ export const packedFlashSchema = {
|
|||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
format: 'date-time',
|
format: 'date-time',
|
||||||
},
|
},
|
||||||
userId: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
format: 'id',
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
type: 'object',
|
|
||||||
ref: 'UserLite',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
title: {
|
title: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
@@ -44,6 +34,16 @@ export const packedFlashSchema = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
userId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'id',
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'UserLite',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
likedCount: {
|
likedCount: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
|
@@ -22,16 +22,16 @@ export const packedFollowingSchema = {
|
|||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
format: 'id',
|
format: 'id',
|
||||||
},
|
},
|
||||||
followerId: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
format: 'id',
|
|
||||||
},
|
|
||||||
followee: {
|
followee: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: true, nullable: false,
|
optional: true, nullable: false,
|
||||||
ref: 'UserDetailed',
|
ref: 'UserDetailed',
|
||||||
},
|
},
|
||||||
|
followerId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'id',
|
||||||
|
},
|
||||||
follower: {
|
follower: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: true, nullable: false,
|
optional: true, nullable: false,
|
||||||
|
@@ -22,6 +22,14 @@ export const packedGalleryPostSchema = {
|
|||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
format: 'date-time',
|
format: 'date-time',
|
||||||
},
|
},
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
userId: {
|
userId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
@@ -32,14 +40,6 @@ export const packedGalleryPostSchema = {
|
|||||||
ref: 'UserLite',
|
ref: 'UserLite',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
title: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
},
|
|
||||||
fileIds: {
|
fileIds: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: true, nullable: false,
|
optional: true, nullable: false,
|
||||||
@@ -70,13 +70,5 @@ export const packedGalleryPostSchema = {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
likedCount: {
|
|
||||||
type: 'number',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
isLiked: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: true, nullable: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@@ -127,26 +127,22 @@ export const packedNoteSchema = {
|
|||||||
channel: {
|
channel: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: true, nullable: true,
|
optional: true, nullable: true,
|
||||||
properties: {
|
items: {
|
||||||
id: {
|
type: 'object',
|
||||||
type: 'string',
|
optional: false, nullable: false,
|
||||||
optional: false, nullable: false,
|
properties: {
|
||||||
},
|
id: {
|
||||||
name: {
|
type: 'string',
|
||||||
type: 'string',
|
optional: false, nullable: false,
|
||||||
optional: false, nullable: false,
|
},
|
||||||
},
|
name: {
|
||||||
color: {
|
type: 'string',
|
||||||
type: 'string',
|
optional: false, nullable: true,
|
||||||
optional: false, nullable: false,
|
},
|
||||||
},
|
isSensitive: {
|
||||||
isSensitive: {
|
type: 'boolean',
|
||||||
type: 'boolean',
|
optional: true, nullable: false,
|
||||||
optional: false, nullable: false,
|
},
|
||||||
},
|
|
||||||
allowRenoteToExternal: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -186,10 +182,6 @@ export const packedNoteSchema = {
|
|||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
clippedCount: {
|
|
||||||
type: 'number',
|
|
||||||
optional: true, nullable: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
myReaction: {
|
myReaction: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
@@ -42,9 +42,13 @@ export const packedNotificationSchema = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: true, nullable: true,
|
||||||
},
|
},
|
||||||
achievement: {
|
choice: {
|
||||||
type: 'string',
|
type: 'number',
|
||||||
optional: true, nullable: false,
|
optional: true, nullable: true,
|
||||||
|
},
|
||||||
|
invitation: {
|
||||||
|
type: 'object',
|
||||||
|
optional: true, nullable: true,
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@@ -77,14 +81,14 @@ export const packedNotificationSchema = {
|
|||||||
required: ['user', 'reaction'],
|
required: ['user', 'reaction'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
users: {
|
},
|
||||||
type: 'array',
|
users: {
|
||||||
optional: true, nullable: true,
|
type: 'array',
|
||||||
items: {
|
optional: true, nullable: true,
|
||||||
type: 'object',
|
items: {
|
||||||
ref: 'UserLite',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
ref: 'UserLite',
|
||||||
},
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@@ -22,32 +22,6 @@ export const packedPageSchema = {
|
|||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
format: 'date-time',
|
format: 'date-time',
|
||||||
},
|
},
|
||||||
userId: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
format: 'id',
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
type: 'object',
|
|
||||||
ref: 'UserLite',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
type: 'array',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
variables: {
|
|
||||||
type: 'array',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
title: {
|
title: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
@@ -60,47 +34,23 @@ export const packedPageSchema = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
hideTitleWhenPinned: {
|
content: {
|
||||||
type: 'boolean',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
alignCenter: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
font: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
script: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
eyeCatchingImageId: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
},
|
|
||||||
eyeCatchingImage: {
|
|
||||||
type: 'object',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
ref: 'DriveFile',
|
|
||||||
},
|
|
||||||
attachedFiles: {
|
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
ref: 'DriveFile',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
likedCount: {
|
variables: {
|
||||||
type: 'number',
|
type: 'array',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
isLiked: {
|
userId: {
|
||||||
type: 'boolean',
|
type: 'string',
|
||||||
optional: true, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
format: 'id',
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'UserLite',
|
||||||
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@@ -1,26 +0,0 @@
|
|||||||
export const packedSigninSchema = {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
id: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
format: 'date-time',
|
|
||||||
},
|
|
||||||
ip: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
type: 'object',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
success: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as const;
|
|
@@ -3,18 +3,6 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const notificationRecieveConfig = {
|
|
||||||
type: 'object',
|
|
||||||
nullable: false, optional: true,
|
|
||||||
properties: {
|
|
||||||
type: {
|
|
||||||
type: 'string',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
enum: ['all', 'following', 'follower', 'mutualFollow', 'list', 'never'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const packedUserLiteSchema = {
|
export const packedUserLiteSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
@@ -61,6 +49,11 @@ export const packedUserLiteSchema = {
|
|||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
format: 'id',
|
format: 'id',
|
||||||
},
|
},
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'url',
|
||||||
|
nullable: false, optional: false,
|
||||||
|
},
|
||||||
angle: {
|
angle: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
nullable: false, optional: true,
|
nullable: false, optional: true,
|
||||||
@@ -69,14 +62,19 @@ export const packedUserLiteSchema = {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: true,
|
nullable: false, optional: true,
|
||||||
},
|
},
|
||||||
url: {
|
|
||||||
type: 'string',
|
|
||||||
format: 'url',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
isAdmin: {
|
||||||
|
type: 'boolean',
|
||||||
|
nullable: false, optional: true,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isModerator: {
|
||||||
|
type: 'boolean',
|
||||||
|
nullable: false, optional: true,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
isBot: {
|
isBot: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: true,
|
nullable: false, optional: true,
|
||||||
@@ -85,67 +83,12 @@ export const packedUserLiteSchema = {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: true,
|
nullable: false, optional: true,
|
||||||
},
|
},
|
||||||
instance: {
|
|
||||||
type: 'object',
|
|
||||||
nullable: false, optional: true,
|
|
||||||
properties: {
|
|
||||||
name: {
|
|
||||||
type: 'string',
|
|
||||||
nullable: true, optional: false,
|
|
||||||
},
|
|
||||||
softwareName: {
|
|
||||||
type: 'string',
|
|
||||||
nullable: true, optional: false,
|
|
||||||
},
|
|
||||||
softwareVersion: {
|
|
||||||
type: 'string',
|
|
||||||
nullable: true, optional: false,
|
|
||||||
},
|
|
||||||
iconUrl: {
|
|
||||||
type: 'string',
|
|
||||||
nullable: true, optional: false,
|
|
||||||
},
|
|
||||||
faviconUrl: {
|
|
||||||
type: 'string',
|
|
||||||
nullable: true, optional: false,
|
|
||||||
},
|
|
||||||
themeColor: {
|
|
||||||
type: 'string',
|
|
||||||
nullable: true, optional: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emojis: {
|
|
||||||
type: 'object',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
onlineStatus: {
|
onlineStatus: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
nullable: false, optional: false,
|
format: 'url',
|
||||||
|
nullable: true, optional: false,
|
||||||
enum: ['unknown', 'online', 'active', 'offline'],
|
enum: ['unknown', 'online', 'active', 'offline'],
|
||||||
},
|
},
|
||||||
badgeRoles: {
|
|
||||||
type: 'array',
|
|
||||||
nullable: false, optional: true,
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
properties: {
|
|
||||||
name: {
|
|
||||||
type: 'string',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
iconUrl: {
|
|
||||||
type: 'string',
|
|
||||||
nullable: true, optional: false,
|
|
||||||
},
|
|
||||||
displayOrder: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -162,18 +105,21 @@ export const packedUserDetailedNotMeOnlySchema = {
|
|||||||
format: 'uri',
|
format: 'uri',
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
},
|
},
|
||||||
movedTo: {
|
movedToUri: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
format: 'uri',
|
format: 'uri',
|
||||||
nullable: true, optional: false,
|
nullable: true,
|
||||||
|
optional: false,
|
||||||
},
|
},
|
||||||
alsoKnownAs: {
|
alsoKnownAs: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
nullable: true, optional: false,
|
nullable: true,
|
||||||
|
optional: false,
|
||||||
items: {
|
items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
format: 'id',
|
format: 'id',
|
||||||
nullable: false, optional: false,
|
nullable: false,
|
||||||
|
optional: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
createdAt: {
|
createdAt: {
|
||||||
@@ -303,11 +249,6 @@ export const packedUserDetailedNotMeOnlySchema = {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
},
|
},
|
||||||
ffVisibility: {
|
|
||||||
type: 'string',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
enum: ['public', 'followers', 'private'],
|
|
||||||
},
|
|
||||||
twoFactorEnabled: {
|
twoFactorEnabled: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
@@ -323,57 +264,6 @@ export const packedUserDetailedNotMeOnlySchema = {
|
|||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
roles: {
|
|
||||||
type: 'array',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
properties: {
|
|
||||||
id: {
|
|
||||||
type: 'string',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
format: 'id',
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
type: 'string',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
type: 'string',
|
|
||||||
nullable: true, optional: false,
|
|
||||||
},
|
|
||||||
iconUrl: {
|
|
||||||
type: 'string',
|
|
||||||
nullable: true, optional: false,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: 'string',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
isModerator: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
isAdministrator: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
displayOrder: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
memo: {
|
|
||||||
type: 'string',
|
|
||||||
nullable: true, optional: false,
|
|
||||||
},
|
|
||||||
moderationNote: {
|
|
||||||
type: 'string',
|
|
||||||
nullable: false, optional: true,
|
|
||||||
},
|
|
||||||
//#region relations
|
//#region relations
|
||||||
isFollowing: {
|
isFollowing: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
@@ -407,10 +297,13 @@ export const packedUserDetailedNotMeOnlySchema = {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: true,
|
nullable: false, optional: true,
|
||||||
},
|
},
|
||||||
|
memo: {
|
||||||
|
type: 'string',
|
||||||
|
nullable: false, optional: true,
|
||||||
|
},
|
||||||
notify: {
|
notify: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
nullable: false, optional: true,
|
nullable: false, optional: true,
|
||||||
enum: ['normal', 'none'],
|
|
||||||
},
|
},
|
||||||
withReplies: {
|
withReplies: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
@@ -433,37 +326,29 @@ export const packedMeDetailedOnlySchema = {
|
|||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
format: 'id',
|
format: 'id',
|
||||||
},
|
},
|
||||||
isModerator: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: true, optional: false,
|
|
||||||
},
|
|
||||||
isAdmin: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: true, optional: false,
|
|
||||||
},
|
|
||||||
injectFeaturedNote: {
|
injectFeaturedNote: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: false,
|
nullable: true, optional: false,
|
||||||
},
|
},
|
||||||
receiveAnnouncementEmail: {
|
receiveAnnouncementEmail: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: false,
|
nullable: true, optional: false,
|
||||||
},
|
},
|
||||||
alwaysMarkNsfw: {
|
alwaysMarkNsfw: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: false,
|
nullable: true, optional: false,
|
||||||
},
|
},
|
||||||
autoSensitive: {
|
autoSensitive: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: false,
|
nullable: true, optional: false,
|
||||||
},
|
},
|
||||||
carefulBot: {
|
carefulBot: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: false,
|
nullable: true, optional: false,
|
||||||
},
|
},
|
||||||
autoAcceptFollowed: {
|
autoAcceptFollowed: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: false,
|
nullable: true, optional: false,
|
||||||
},
|
},
|
||||||
noCrawle: {
|
noCrawle: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
@@ -502,23 +387,10 @@ export const packedMeDetailedOnlySchema = {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
},
|
},
|
||||||
unreadAnnouncements: {
|
|
||||||
type: 'array',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
ref: 'Announcement',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
hasUnreadAntenna: {
|
hasUnreadAntenna: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
},
|
},
|
||||||
hasUnreadChannel: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
hasUnreadNotification: {
|
hasUnreadNotification: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
@@ -543,18 +415,6 @@ export const packedMeDetailedOnlySchema = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
hardMutedWords: {
|
|
||||||
type: 'array',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
items: {
|
|
||||||
type: 'array',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
items: {
|
|
||||||
type: 'string',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mutedInstances: {
|
mutedInstances: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
@@ -566,148 +426,15 @@ export const packedMeDetailedOnlySchema = {
|
|||||||
notificationRecieveConfig: {
|
notificationRecieveConfig: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
properties: {
|
|
||||||
app: notificationRecieveConfig,
|
|
||||||
quote: notificationRecieveConfig,
|
|
||||||
reply: notificationRecieveConfig,
|
|
||||||
follow: notificationRecieveConfig,
|
|
||||||
renote: notificationRecieveConfig,
|
|
||||||
mention: notificationRecieveConfig,
|
|
||||||
reaction: notificationRecieveConfig,
|
|
||||||
pollEnded: notificationRecieveConfig,
|
|
||||||
achievementEarned: notificationRecieveConfig,
|
|
||||||
receiveFollowRequest: notificationRecieveConfig,
|
|
||||||
followRequestAccepted: notificationRecieveConfig,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
emailNotificationTypes: {
|
emailNotificationTypes: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
nullable: false, optional: false,
|
nullable: true, optional: false,
|
||||||
items: {
|
items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
achievements: {
|
|
||||||
type: 'array',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
properties: {
|
|
||||||
name: {
|
|
||||||
type: 'string',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
unlockedAt: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
loggedInDays: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
policies: {
|
|
||||||
type: 'object',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
properties: {
|
|
||||||
gtlAvailable: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
ltlAvailable: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
canPublicNote: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
canInvite: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
inviteLimit: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
inviteLimitCycle: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
inviteExpirationTime: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
canManageCustomEmojis: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
canManageAvatarDecorations: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
canSearchNotes: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
canUseTranslator: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
canHideAds: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
driveCapacityMb: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
alwaysMarkNsfw: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
pinLimit: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
antennaLimit: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
wordMuteLimit: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
webhookLimit: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
clipLimit: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
noteEachClipsLimit: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
userListLimit: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
userEachUserListsLimit: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
rateLimitFactor: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
//#region secrets
|
//#region secrets
|
||||||
email: {
|
email: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@@ -723,23 +450,6 @@ export const packedMeDetailedOnlySchema = {
|
|||||||
items: {
|
items: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
properties: {
|
|
||||||
id: {
|
|
||||||
type: 'string',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
format: 'id',
|
|
||||||
example: 'xxxxxxxxxx',
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
type: 'string',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
lastUsed: {
|
|
||||||
type: 'string',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
format: 'date-time',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
//#endregion
|
//#endregion
|
||||||
@@ -801,13 +511,5 @@ export const packedUserSchema = {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
ref: 'UserDetailed',
|
ref: 'UserDetailed',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: 'object',
|
|
||||||
ref: 'UserDetailedNotMe',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'object',
|
|
||||||
ref: 'MeDetailed',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
@@ -370,9 +370,8 @@ export class ActivityPubServerService {
|
|||||||
order: { id: 'DESC' },
|
order: { id: 'DESC' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const pinnedNotes = (await Promise.all(pinings.map(pining =>
|
const pinnedNotes = await Promise.all(pinings.map(pining =>
|
||||||
this.notesRepository.findOneByOrFail({ id: pining.noteId }))))
|
this.notesRepository.findOneByOrFail({ id: pining.noteId })));
|
||||||
.filter(note => !note.localOnly && ['public', 'home'].includes(note.visibility));
|
|
||||||
|
|
||||||
const renderedNotes = await Promise.all(pinnedNotes.map(note => this.apRendererService.renderNote(note)));
|
const renderedNotes = await Promise.all(pinnedNotes.map(note => this.apRendererService.renderNote(note)));
|
||||||
|
|
||||||
|
@@ -96,11 +96,6 @@ export class NodeinfoServerService {
|
|||||||
metadata: {
|
metadata: {
|
||||||
nodeName: meta.name,
|
nodeName: meta.name,
|
||||||
nodeDescription: meta.description,
|
nodeDescription: meta.description,
|
||||||
nodeAdmins: [{
|
|
||||||
name: meta.maintainerName,
|
|
||||||
email: meta.maintainerEmail,
|
|
||||||
}],
|
|
||||||
// deprecated
|
|
||||||
maintainer: {
|
maintainer: {
|
||||||
name: meta.maintainerName,
|
name: meta.maintainerName,
|
||||||
email: meta.maintainerEmail,
|
email: meta.maintainerEmail,
|
||||||
|
@@ -119,8 +119,8 @@ export class ServerService implements OnApplicationShutdown {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = path.split('@')[0].replace(/\.webp$/i, '');
|
const name = path.split('@')[0].replace('.webp', '');
|
||||||
const host = path.split('@')[1]?.replace(/\.webp$/i, '');
|
const host = path.split('@')[1]?.replace('.webp', '');
|
||||||
|
|
||||||
const emoji = await this.emojisRepository.findOneBy({
|
const emoji = await this.emojisRepository.findOneBy({
|
||||||
// `@.` is the spec of ReactionService.decodeReaction
|
// `@.` is the spec of ReactionService.decodeReaction
|
||||||
|
@@ -24,8 +24,6 @@ import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-d
|
|||||||
import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
|
import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
|
||||||
import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
|
import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
|
||||||
import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
|
import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
|
||||||
import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js';
|
|
||||||
import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js';
|
|
||||||
import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
|
import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
|
||||||
import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
|
import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
|
||||||
import * as ep___admin_drive_files from './endpoints/admin/drive/files.js';
|
import * as ep___admin_drive_files from './endpoints/admin/drive/files.js';
|
||||||
@@ -385,8 +383,6 @@ const $admin_avatarDecorations_delete: Provider = { provide: 'ep:admin/avatar-de
|
|||||||
const $admin_avatarDecorations_list: Provider = { provide: 'ep:admin/avatar-decorations/list', useClass: ep___admin_avatarDecorations_list.default };
|
const $admin_avatarDecorations_list: Provider = { provide: 'ep:admin/avatar-decorations/list', useClass: ep___admin_avatarDecorations_list.default };
|
||||||
const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-decorations/update', useClass: ep___admin_avatarDecorations_update.default };
|
const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-decorations/update', useClass: ep___admin_avatarDecorations_update.default };
|
||||||
const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default };
|
const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default };
|
||||||
const $admin_unsetUserAvatar: Provider = { provide: 'ep:admin/unset-user-avatar', useClass: ep___admin_unsetUserAvatar.default };
|
|
||||||
const $admin_unsetUserBanner: Provider = { provide: 'ep:admin/unset-user-banner', useClass: ep___admin_unsetUserBanner.default };
|
|
||||||
const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default };
|
const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default };
|
||||||
const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default };
|
const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default };
|
||||||
const $admin_drive_files: Provider = { provide: 'ep:admin/drive/files', useClass: ep___admin_drive_files.default };
|
const $admin_drive_files: Provider = { provide: 'ep:admin/drive/files', useClass: ep___admin_drive_files.default };
|
||||||
@@ -750,8 +746,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||||||
$admin_avatarDecorations_list,
|
$admin_avatarDecorations_list,
|
||||||
$admin_avatarDecorations_update,
|
$admin_avatarDecorations_update,
|
||||||
$admin_deleteAllFilesOfAUser,
|
$admin_deleteAllFilesOfAUser,
|
||||||
$admin_unsetUserAvatar,
|
|
||||||
$admin_unsetUserBanner,
|
|
||||||
$admin_drive_cleanRemoteFiles,
|
$admin_drive_cleanRemoteFiles,
|
||||||
$admin_drive_cleanup,
|
$admin_drive_cleanup,
|
||||||
$admin_drive_files,
|
$admin_drive_files,
|
||||||
@@ -1109,8 +1103,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||||||
$admin_avatarDecorations_list,
|
$admin_avatarDecorations_list,
|
||||||
$admin_avatarDecorations_update,
|
$admin_avatarDecorations_update,
|
||||||
$admin_deleteAllFilesOfAUser,
|
$admin_deleteAllFilesOfAUser,
|
||||||
$admin_unsetUserAvatar,
|
|
||||||
$admin_unsetUserBanner,
|
|
||||||
$admin_drive_cleanRemoteFiles,
|
$admin_drive_cleanRemoteFiles,
|
||||||
$admin_drive_cleanup,
|
$admin_drive_cleanup,
|
||||||
$admin_drive_files,
|
$admin_drive_files,
|
||||||
|
@@ -126,7 +126,7 @@ export class SignupApiService {
|
|||||||
code: invitationCode,
|
code: invitationCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ticket == null || ticket.usedById != null) {
|
if (ticket == null) {
|
||||||
reply.code(400);
|
reply.code(400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@@ -24,8 +24,6 @@ import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-d
|
|||||||
import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
|
import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
|
||||||
import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
|
import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
|
||||||
import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
|
import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
|
||||||
import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js';
|
|
||||||
import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js';
|
|
||||||
import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
|
import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
|
||||||
import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
|
import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
|
||||||
import * as ep___admin_drive_files from './endpoints/admin/drive/files.js';
|
import * as ep___admin_drive_files from './endpoints/admin/drive/files.js';
|
||||||
@@ -383,8 +381,6 @@ const eps = [
|
|||||||
['admin/avatar-decorations/list', ep___admin_avatarDecorations_list],
|
['admin/avatar-decorations/list', ep___admin_avatarDecorations_list],
|
||||||
['admin/avatar-decorations/update', ep___admin_avatarDecorations_update],
|
['admin/avatar-decorations/update', ep___admin_avatarDecorations_update],
|
||||||
['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser],
|
['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser],
|
||||||
['admin/unset-user-avatar', ep___admin_unsetUserAvatar],
|
|
||||||
['admin/unset-user-banner', ep___admin_unsetUserBanner],
|
|
||||||
['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles],
|
['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles],
|
||||||
['admin/drive/cleanup', ep___admin_drive_cleanup],
|
['admin/drive/cleanup', ep___admin_drive_cleanup],
|
||||||
['admin/drive/files', ep___admin_drive_files],
|
['admin/drive/files', ep___admin_drive_files],
|
||||||
|
@@ -22,7 +22,7 @@ export const paramDef = {
|
|||||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||||
sinceId: { type: 'string', format: 'misskey:id' },
|
sinceId: { type: 'string', format: 'misskey:id' },
|
||||||
untilId: { type: 'string', format: 'misskey:id' },
|
untilId: { type: 'string', format: 'misskey:id' },
|
||||||
publishing: { type: 'boolean', default: null, nullable: true },
|
publishing: { type: 'boolean', default: false },
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
@@ -37,10 +37,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const query = this.queryService.makePaginationQuery(this.adsRepository.createQueryBuilder('ad'), ps.sinceId, ps.untilId);
|
const query = this.queryService.makePaginationQuery(this.adsRepository.createQueryBuilder('ad'), ps.sinceId, ps.untilId);
|
||||||
if (ps.publishing === true) {
|
if (ps.publishing) {
|
||||||
query.andWhere('ad.expiresAt > :now', { now: new Date() }).andWhere('ad.startsAt <= :now', { now: new Date() });
|
query.andWhere('ad.expiresAt > :now', { now: new Date() }).andWhere('ad.startsAt <= :now', { now: new Date() });
|
||||||
} else if (ps.publishing === false) {
|
|
||||||
query.andWhere('ad.expiresAt <= :now', { now: new Date() }).orWhere('ad.startsAt > :now', { now: new Date() });
|
|
||||||
}
|
}
|
||||||
const ads = await query.limit(ps.limit).getMany();
|
const ads = await query.limit(ps.limit).getMany();
|
||||||
|
|
||||||
|
@@ -6,10 +6,11 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { EmojisRepository } from '@/models/_.js';
|
import type { EmojisRepository } from '@/models/_.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { DriveService } from '@/core/DriveService.js';
|
import { DriveService } from '@/core/DriveService.js';
|
||||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
|
|
||||||
@@ -25,11 +26,6 @@ export const meta = {
|
|||||||
code: 'NO_SUCH_EMOJI',
|
code: 'NO_SUCH_EMOJI',
|
||||||
id: 'e2785b66-dca3-4087-9cac-b93c541cc425',
|
id: 'e2785b66-dca3-4087-9cac-b93c541cc425',
|
||||||
},
|
},
|
||||||
duplicateName: {
|
|
||||||
message: 'Duplicate name.',
|
|
||||||
code: 'DUPLICATE_NAME',
|
|
||||||
id: 'f7a3462c-4e6e-4069-8421-b9bd4f4c3975',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
@@ -60,12 +56,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.emojisRepository)
|
@Inject(DI.emojisRepository)
|
||||||
private emojisRepository: EmojisRepository,
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
private emojiEntityService: EmojiEntityService,
|
private emojiEntityService: EmojiEntityService,
|
||||||
private customEmojiService: CustomEmojiService,
|
private idService: IdService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
private driveService: DriveService,
|
private driveService: DriveService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const emoji = await this.emojisRepository.findOneBy({ id: ps.emojiId });
|
const emoji = await this.emojisRepository.findOneBy({ id: ps.emojiId });
|
||||||
|
|
||||||
if (emoji == null) {
|
if (emoji == null) {
|
||||||
throw new ApiError(meta.errors.noSuchEmoji);
|
throw new ApiError(meta.errors.noSuchEmoji);
|
||||||
}
|
}
|
||||||
@@ -76,27 +75,28 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
// Create file
|
// Create file
|
||||||
driveFile = await this.driveService.uploadFromUrl({ url: emoji.originalUrl, user: null, force: true });
|
driveFile = await this.driveService.uploadFromUrl({ url: emoji.originalUrl, user: null, force: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// TODO: need to return Drive Error
|
|
||||||
throw new ApiError();
|
throw new ApiError();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Duplication Check
|
const copied = await this.emojisRepository.insert({
|
||||||
const isDuplicate = await this.customEmojiService.checkDuplicate(emoji.name);
|
id: this.idService.gen(),
|
||||||
if (isDuplicate) throw new ApiError(meta.errors.duplicateName);
|
updatedAt: new Date(),
|
||||||
|
|
||||||
const addedEmoji = await this.customEmojiService.add({
|
|
||||||
driveFile,
|
|
||||||
name: emoji.name,
|
name: emoji.name,
|
||||||
category: emoji.category,
|
|
||||||
aliases: emoji.aliases,
|
|
||||||
host: null,
|
host: null,
|
||||||
|
aliases: [],
|
||||||
|
originalUrl: driveFile.url,
|
||||||
|
publicUrl: driveFile.webpublicUrl ?? driveFile.url,
|
||||||
|
type: driveFile.webpublicType ?? driveFile.type,
|
||||||
license: emoji.license,
|
license: emoji.license,
|
||||||
isSensitive: emoji.isSensitive,
|
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
localOnly: emoji.localOnly,
|
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction,
|
|
||||||
}, me);
|
|
||||||
|
|
||||||
return this.emojiEntityService.packDetailed(addedEmoji);
|
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
||||||
|
emoji: await this.emojiEntityService.packDetailed(copied.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: copied.id,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -33,7 +33,13 @@ export const meta = {
|
|||||||
items: {
|
items: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
ref: 'InviteCode',
|
properties: {
|
||||||
|
code: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
example: 'GR6S02ERUA5VR',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@@ -21,7 +21,6 @@ export const meta = {
|
|||||||
items: {
|
items: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
ref: 'InviteCode',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@@ -267,14 +267,6 @@ export const meta = {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
enableVerifymailApi: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
verifymailAuthKey: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
},
|
|
||||||
enableChartsForRemoteUser: {
|
enableChartsForRemoteUser: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
@@ -327,82 +319,6 @@ export const meta = {
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
backgroundImageUrl: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
},
|
|
||||||
deeplAuthKey: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
},
|
|
||||||
deeplIsPro: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
defaultDarkTheme: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
},
|
|
||||||
defaultLightTheme: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
},
|
|
||||||
disableRegistration: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
impressumUrl: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
},
|
|
||||||
maintainerEmail: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
},
|
|
||||||
maintainerName: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
},
|
|
||||||
objectStorageS3ForcePathStyle: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
privacyPolicyUrl: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
},
|
|
||||||
repositoryUrl: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
summalyProxy: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
},
|
|
||||||
themeColor: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
},
|
|
||||||
tosUrl: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
},
|
|
||||||
uri: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
version: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
@@ -505,8 +421,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
deeplIsPro: instance.deeplIsPro,
|
deeplIsPro: instance.deeplIsPro,
|
||||||
enableIpLogging: instance.enableIpLogging,
|
enableIpLogging: instance.enableIpLogging,
|
||||||
enableActiveEmailValidation: instance.enableActiveEmailValidation,
|
enableActiveEmailValidation: instance.enableActiveEmailValidation,
|
||||||
enableVerifymailApi: instance.enableVerifymailApi,
|
|
||||||
verifymailAuthKey: instance.verifymailAuthKey,
|
|
||||||
enableChartsForRemoteUser: instance.enableChartsForRemoteUser,
|
enableChartsForRemoteUser: instance.enableChartsForRemoteUser,
|
||||||
enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances,
|
enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances,
|
||||||
enableServerMachineStats: instance.enableServerMachineStats,
|
enableServerMachineStats: instance.enableServerMachineStats,
|
||||||
|
@@ -1,60 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
|
||||||
import type { UsersRepository } from '@/models/_.js';
|
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
|
||||||
|
|
||||||
export const meta = {
|
|
||||||
tags: ['admin'],
|
|
||||||
|
|
||||||
requireCredential: true,
|
|
||||||
requireModerator: true,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const paramDef = {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
userId: { type: 'string', format: 'misskey:id' },
|
|
||||||
},
|
|
||||||
required: ['userId'],
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
|
||||||
@Injectable()
|
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|
||||||
constructor(
|
|
||||||
@Inject(DI.usersRepository)
|
|
||||||
private usersRepository: UsersRepository,
|
|
||||||
|
|
||||||
private moderationLogService: ModerationLogService,
|
|
||||||
) {
|
|
||||||
super(meta, paramDef, async (ps, me) => {
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
|
||||||
|
|
||||||
if (user == null) {
|
|
||||||
throw new Error('user not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.avatarId == null) return;
|
|
||||||
|
|
||||||
await this.usersRepository.update(user.id, {
|
|
||||||
avatar: null,
|
|
||||||
avatarId: null,
|
|
||||||
avatarUrl: null,
|
|
||||||
avatarBlurhash: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.moderationLogService.log(me, 'unsetUserAvatar', {
|
|
||||||
userId: user.id,
|
|
||||||
userUsername: user.username,
|
|
||||||
userHost: user.host,
|
|
||||||
fileId: user.avatarId,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,60 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
|
||||||
import type { UsersRepository } from '@/models/_.js';
|
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
|
||||||
|
|
||||||
export const meta = {
|
|
||||||
tags: ['admin'],
|
|
||||||
|
|
||||||
requireCredential: true,
|
|
||||||
requireModerator: true,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const paramDef = {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
userId: { type: 'string', format: 'misskey:id' },
|
|
||||||
},
|
|
||||||
required: ['userId'],
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
|
||||||
@Injectable()
|
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|
||||||
constructor(
|
|
||||||
@Inject(DI.usersRepository)
|
|
||||||
private usersRepository: UsersRepository,
|
|
||||||
|
|
||||||
private moderationLogService: ModerationLogService,
|
|
||||||
) {
|
|
||||||
super(meta, paramDef, async (ps, me) => {
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
|
||||||
|
|
||||||
if (user == null) {
|
|
||||||
throw new Error('user not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.bannerId == null) return;
|
|
||||||
|
|
||||||
await this.usersRepository.update(user.id, {
|
|
||||||
banner: null,
|
|
||||||
bannerId: null,
|
|
||||||
bannerUrl: null,
|
|
||||||
bannerBlurhash: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.moderationLogService.log(me, 'unsetUserBanner', {
|
|
||||||
userId: user.id,
|
|
||||||
userUsername: user.username,
|
|
||||||
userHost: user.host,
|
|
||||||
fileId: user.bannerId,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@@ -113,8 +113,6 @@ export const paramDef = {
|
|||||||
objectStorageS3ForcePathStyle: { type: 'boolean' },
|
objectStorageS3ForcePathStyle: { type: 'boolean' },
|
||||||
enableIpLogging: { type: 'boolean' },
|
enableIpLogging: { type: 'boolean' },
|
||||||
enableActiveEmailValidation: { type: 'boolean' },
|
enableActiveEmailValidation: { type: 'boolean' },
|
||||||
enableVerifymailApi: { type: 'boolean' },
|
|
||||||
verifymailAuthKey: { type: 'string', nullable: true },
|
|
||||||
enableChartsForRemoteUser: { type: 'boolean' },
|
enableChartsForRemoteUser: { type: 'boolean' },
|
||||||
enableChartsForFederatedInstances: { type: 'boolean' },
|
enableChartsForFederatedInstances: { type: 'boolean' },
|
||||||
enableServerMachineStats: { type: 'boolean' },
|
enableServerMachineStats: { type: 'boolean' },
|
||||||
@@ -456,18 +454,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
set.enableActiveEmailValidation = ps.enableActiveEmailValidation;
|
set.enableActiveEmailValidation = ps.enableActiveEmailValidation;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ps.enableVerifymailApi !== undefined) {
|
|
||||||
set.enableVerifymailApi = ps.enableVerifymailApi;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.verifymailAuthKey !== undefined) {
|
|
||||||
if (ps.verifymailAuthKey === '') {
|
|
||||||
set.verifymailAuthKey = null;
|
|
||||||
} else {
|
|
||||||
set.verifymailAuthKey = ps.verifymailAuthKey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.enableChartsForRemoteUser !== undefined) {
|
if (ps.enableChartsForRemoteUser !== undefined) {
|
||||||
set.enableChartsForRemoteUser = ps.enableChartsForRemoteUser;
|
set.enableChartsForRemoteUser = ps.enableChartsForRemoteUser;
|
||||||
}
|
}
|
||||||
|
@@ -12,8 +12,7 @@ import { NoteReadService } from '@/core/NoteReadService.js';
|
|||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
@@ -71,8 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private noteReadService: NoteReadService,
|
private noteReadService: NoteReadService,
|
||||||
private fanoutTimelineService: FanoutTimelineService,
|
private funoutTimelineService: FunoutTimelineService,
|
||||||
private globalEventService: GlobalEventService,
|
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
||||||
@@ -87,18 +85,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
throw new ApiError(meta.errors.noSuchAntenna);
|
throw new ApiError(meta.errors.noSuchAntenna);
|
||||||
}
|
}
|
||||||
|
|
||||||
// falseだった場合はアンテナの配信先が増えたことを通知したい
|
this.antennasRepository.update(antenna.id, {
|
||||||
const needPublishEvent = !antenna.isActive;
|
isActive: true,
|
||||||
|
lastUsedAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
antenna.isActive = true;
|
let noteIds = await this.funoutTimelineService.get(`antennaTimeline:${antenna.id}`, untilId, sinceId);
|
||||||
antenna.lastUsedAt = new Date();
|
|
||||||
this.antennasRepository.update(antenna.id, antenna);
|
|
||||||
|
|
||||||
if (needPublishEvent) {
|
|
||||||
this.globalEventService.publishInternalEvent('antennaUpdated', antenna);
|
|
||||||
}
|
|
||||||
|
|
||||||
let noteIds = await this.fanoutTimelineService.get(`antennaTimeline:${antenna.id}`, untilId, sinceId);
|
|
||||||
noteIds = noteIds.slice(0, ps.limit);
|
noteIds = noteIds.slice(0, ps.limit);
|
||||||
if (noteIds.length === 0) {
|
if (noteIds.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
|
@@ -4,17 +4,17 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { ChannelsRepository, NotesRepository } from '@/models/_.js';
|
import type { ChannelsRepository, MiNote, NotesRepository } from '@/models/_.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||||
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
|
|
||||||
import { MiLocalUser } from '@/models/User.js';
|
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
@@ -50,7 +50,6 @@ export const paramDef = {
|
|||||||
untilId: { type: 'string', format: 'misskey:id' },
|
untilId: { type: 'string', format: 'misskey:id' },
|
||||||
sinceDate: { type: 'integer' },
|
sinceDate: { type: 'integer' },
|
||||||
untilDate: { type: 'integer' },
|
untilDate: { type: 'integer' },
|
||||||
allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default
|
|
||||||
},
|
},
|
||||||
required: ['channelId'],
|
required: ['channelId'],
|
||||||
} as const;
|
} as const;
|
||||||
@@ -58,6 +57,9 @@ export const paramDef = {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.redisForTimelines)
|
||||||
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
@@ -67,16 +69,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
|
private funoutTimelineService: FunoutTimelineService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
private activeUsersChart: ActiveUsersChart,
|
private activeUsersChart: ActiveUsersChart,
|
||||||
private metaService: MetaService,
|
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
||||||
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
|
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
|
||||||
|
const isRangeSpecified = untilId != null && sinceId != null;
|
||||||
const serverSettings = await this.metaService.fetch();
|
|
||||||
|
|
||||||
const channel = await this.channelsRepository.findOneBy({
|
const channel = await this.channelsRepository.findOneBy({
|
||||||
id: ps.channelId,
|
id: ps.channelId,
|
||||||
@@ -88,48 +88,64 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
|
|
||||||
if (me) this.activeUsersChart.read(me);
|
if (me) this.activeUsersChart.read(me);
|
||||||
|
|
||||||
if (!serverSettings.enableFanoutTimeline) {
|
if (isRangeSpecified || sinceId == null) {
|
||||||
return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id }, me), me);
|
const [
|
||||||
|
userIdsWhoMeMuting,
|
||||||
|
] = me ? await Promise.all([
|
||||||
|
this.cacheService.userMutingsCache.fetch(me.id),
|
||||||
|
]) : [new Set<string>()];
|
||||||
|
|
||||||
|
let noteIds = await this.funoutTimelineService.get(`channelTimeline:${channel.id}`, untilId, sinceId);
|
||||||
|
noteIds = noteIds.slice(0, ps.limit);
|
||||||
|
|
||||||
|
if (noteIds.length > 0) {
|
||||||
|
const query = this.notesRepository.createQueryBuilder('note')
|
||||||
|
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||||
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
|
.leftJoinAndSelect('note.reply', 'reply')
|
||||||
|
.leftJoinAndSelect('note.renote', 'renote')
|
||||||
|
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||||
|
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||||
|
.leftJoinAndSelect('note.channel', 'channel');
|
||||||
|
|
||||||
|
let timeline = await query.getMany();
|
||||||
|
|
||||||
|
timeline = timeline.filter(note => {
|
||||||
|
if (me && isUserRelated(note, userIdsWhoMeMuting)) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: フィルタで件数が減った場合の埋め合わせ処理
|
||||||
|
|
||||||
|
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||||
|
|
||||||
|
if (timeline.length > 0) {
|
||||||
|
return await this.noteEntityService.packMany(timeline, me);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.fanoutTimelineEndpointService.timeline({
|
//#region fallback to database
|
||||||
untilId,
|
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||||
sinceId,
|
.andWhere('note.channelId = :channelId', { channelId: channel.id })
|
||||||
limit: ps.limit,
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
allowPartial: ps.allowPartial,
|
.leftJoinAndSelect('note.reply', 'reply')
|
||||||
me,
|
.leftJoinAndSelect('note.renote', 'renote')
|
||||||
useDbFallback: true,
|
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||||
redisTimelines: [`channelTimeline:${channel.id}`],
|
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||||
excludePureRenotes: false,
|
.leftJoinAndSelect('note.channel', 'channel');
|
||||||
dbFallback: async (untilId, sinceId, limit) => {
|
|
||||||
return await this.getFromDb({ untilId, sinceId, limit, channelId: channel.id }, me);
|
if (me) {
|
||||||
},
|
this.queryService.generateMutedUserQuery(query, me);
|
||||||
});
|
this.queryService.generateBlockedUserQuery(query, me);
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
const timeline = await query.limit(ps.limit).getMany();
|
||||||
|
|
||||||
|
return await this.noteEntityService.packMany(timeline, me);
|
||||||
|
//#endregion
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getFromDb(ps: {
|
|
||||||
untilId: string | null,
|
|
||||||
sinceId: string | null,
|
|
||||||
limit: number,
|
|
||||||
channelId: string
|
|
||||||
}, me: MiLocalUser | null) {
|
|
||||||
//#region fallback to database
|
|
||||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
|
||||||
.andWhere('note.channelId = :channelId', { channelId: ps.channelId })
|
|
||||||
.innerJoinAndSelect('note.user', 'user')
|
|
||||||
.leftJoinAndSelect('note.reply', 'reply')
|
|
||||||
.leftJoinAndSelect('note.renote', 'renote')
|
|
||||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
|
||||||
.leftJoinAndSelect('note.channel', 'channel');
|
|
||||||
|
|
||||||
if (me) {
|
|
||||||
this.queryService.generateMutedUserQuery(query, me);
|
|
||||||
this.queryService.generateBlockedUserQuery(query, me);
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
return await query.limit(ps.limit).getMany();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -36,32 +36,13 @@ export const paramDef = {
|
|||||||
blocked: { type: 'boolean', nullable: true },
|
blocked: { type: 'boolean', nullable: true },
|
||||||
notResponding: { type: 'boolean', nullable: true },
|
notResponding: { type: 'boolean', nullable: true },
|
||||||
suspended: { type: 'boolean', nullable: true },
|
suspended: { type: 'boolean', nullable: true },
|
||||||
silenced: { type: 'boolean', nullable: true },
|
silenced: { type: "boolean", nullable: true },
|
||||||
federating: { type: 'boolean', nullable: true },
|
federating: { type: 'boolean', nullable: true },
|
||||||
subscribing: { type: 'boolean', nullable: true },
|
subscribing: { type: 'boolean', nullable: true },
|
||||||
publishing: { type: 'boolean', nullable: true },
|
publishing: { type: 'boolean', nullable: true },
|
||||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
|
||||||
offset: { type: 'integer', default: 0 },
|
offset: { type: 'integer', default: 0 },
|
||||||
sort: {
|
sort: { type: 'string' },
|
||||||
type: 'string',
|
|
||||||
nullable: true,
|
|
||||||
enum: [
|
|
||||||
'+pubSub',
|
|
||||||
'-pubSub',
|
|
||||||
'+notes',
|
|
||||||
'-notes',
|
|
||||||
'+users',
|
|
||||||
'-users',
|
|
||||||
'+following',
|
|
||||||
'-following',
|
|
||||||
'+followers',
|
|
||||||
'-followers',
|
|
||||||
'+firstRetrievedAt',
|
|
||||||
'-firstRetrievedAt',
|
|
||||||
'+latestRequestReceivedAt',
|
|
||||||
'-latestRequestReceivedAt',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
@@ -122,18 +103,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof ps.silenced === 'boolean') {
|
if (typeof ps.silenced === "boolean") {
|
||||||
const meta = await this.metaService.fetch(true);
|
const meta = await this.metaService.fetch(true);
|
||||||
|
|
||||||
if (ps.silenced) {
|
if (ps.silenced) {
|
||||||
if (meta.silencedHosts.length === 0) {
|
if (meta.silencedHosts.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
query.andWhere('instance.host IN (:...silences)', {
|
query.andWhere("instance.host IN (:...silences)", {
|
||||||
silences: meta.silencedHosts,
|
silences: meta.silencedHosts,
|
||||||
});
|
});
|
||||||
} else if (meta.silencedHosts.length > 0) {
|
} else if (meta.silencedHosts.length > 0) {
|
||||||
query.andWhere('instance.host NOT IN (:...silences)', {
|
query.andWhere("instance.host NOT IN (:...silences)", {
|
||||||
silences: meta.silencedHosts,
|
silences: meta.silencedHosts,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -16,9 +16,12 @@ export const meta = {
|
|||||||
requireCredential: false,
|
requireCredential: false,
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
oneOf: [{
|
||||||
optional: false, nullable: true,
|
type: 'object',
|
||||||
ref: 'FederationInstance',
|
ref: 'FederationInstance',
|
||||||
|
}, {
|
||||||
|
type: 'null',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@@ -8,7 +8,6 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
|||||||
import type { GalleryPostsRepository } from '@/models/_.js';
|
import type { GalleryPostsRepository } from '@/models/_.js';
|
||||||
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
|
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { FeaturedService } from '@/core/FeaturedService.js';
|
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['gallery'],
|
tags: ['gallery'],
|
||||||
@@ -28,49 +27,25 @@ export const meta = {
|
|||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {},
|
||||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
|
||||||
untilId: { type: 'string', format: 'misskey:id' },
|
|
||||||
},
|
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
private galleryPostsRankingCache: string[] = [];
|
|
||||||
private galleryPostsRankingCacheLastFetchedAt = 0;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.galleryPostsRepository)
|
@Inject(DI.galleryPostsRepository)
|
||||||
private galleryPostsRepository: GalleryPostsRepository,
|
private galleryPostsRepository: GalleryPostsRepository,
|
||||||
|
|
||||||
private galleryPostEntityService: GalleryPostEntityService,
|
private galleryPostEntityService: GalleryPostEntityService,
|
||||||
private featuredService: FeaturedService,
|
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
let postIds: string[];
|
|
||||||
if (this.galleryPostsRankingCacheLastFetchedAt !== 0 && (Date.now() - this.galleryPostsRankingCacheLastFetchedAt < 1000 * 60 * 30)) {
|
|
||||||
postIds = this.galleryPostsRankingCache;
|
|
||||||
} else {
|
|
||||||
postIds = await this.featuredService.getGalleryPostsRanking(100);
|
|
||||||
this.galleryPostsRankingCache = postIds;
|
|
||||||
this.galleryPostsRankingCacheLastFetchedAt = Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
postIds.sort((a, b) => a > b ? -1 : 1);
|
|
||||||
if (ps.untilId) {
|
|
||||||
postIds = postIds.filter(id => id < ps.untilId!);
|
|
||||||
}
|
|
||||||
postIds = postIds.slice(0, ps.limit);
|
|
||||||
|
|
||||||
if (postIds.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = this.galleryPostsRepository.createQueryBuilder('post')
|
const query = this.galleryPostsRepository.createQueryBuilder('post')
|
||||||
.where('post.id IN (:...postIds)', { postIds: postIds });
|
.andWhere('post.createdAt > :date', { date: new Date(Date.now() - (1000 * 60 * 60 * 24 * 3)) })
|
||||||
|
.andWhere('post.likedCount > 0')
|
||||||
|
.orderBy('post.likedCount', 'DESC');
|
||||||
|
|
||||||
const posts = await query.getMany();
|
const posts = await query.limit(10).getMany();
|
||||||
|
|
||||||
return await this.galleryPostEntityService.packMany(posts, me);
|
return await this.galleryPostEntityService.packMany(posts, me);
|
||||||
});
|
});
|
||||||
|
@@ -6,7 +6,6 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { GalleryLikesRepository, GalleryPostsRepository } from '@/models/_.js';
|
import type { GalleryLikesRepository, GalleryPostsRepository } from '@/models/_.js';
|
||||||
import { FeaturedService, GALLERY_POSTS_RANKING_WINDOW } from '@/core/FeaturedService.js';
|
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
@@ -58,7 +57,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
@Inject(DI.galleryLikesRepository)
|
@Inject(DI.galleryLikesRepository)
|
||||||
private galleryLikesRepository: GalleryLikesRepository,
|
private galleryLikesRepository: GalleryLikesRepository,
|
||||||
|
|
||||||
private featuredService: FeaturedService,
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
@@ -90,11 +88,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
userId: me.id,
|
userId: me.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// ランキング更新
|
|
||||||
if (Date.now() - this.idService.parse(post.id).date.getTime() < GALLERY_POSTS_RANKING_WINDOW) {
|
|
||||||
await this.featuredService.updateGalleryPostsRanking(post.id, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.galleryPostsRepository.increment({ id: post.id }, 'likedCount', 1);
|
this.galleryPostsRepository.increment({ id: post.id }, 'likedCount', 1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -6,8 +6,6 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { GalleryPostsRepository, GalleryLikesRepository } from '@/models/_.js';
|
import type { GalleryPostsRepository, GalleryLikesRepository } from '@/models/_.js';
|
||||||
import { FeaturedService, GALLERY_POSTS_RANKING_WINDOW } from '@/core/FeaturedService.js';
|
|
||||||
import { IdService } from '@/core/IdService.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
|
|
||||||
@@ -51,9 +49,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
|
|
||||||
@Inject(DI.galleryLikesRepository)
|
@Inject(DI.galleryLikesRepository)
|
||||||
private galleryLikesRepository: GalleryLikesRepository,
|
private galleryLikesRepository: GalleryLikesRepository,
|
||||||
|
|
||||||
private featuredService: FeaturedService,
|
|
||||||
private idService: IdService,
|
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const post = await this.galleryPostsRepository.findOneBy({ id: ps.postId });
|
const post = await this.galleryPostsRepository.findOneBy({ id: ps.postId });
|
||||||
@@ -73,11 +68,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
// Delete like
|
// Delete like
|
||||||
await this.galleryLikesRepository.delete(exist.id);
|
await this.galleryLikesRepository.delete(exist.id);
|
||||||
|
|
||||||
// ランキング更新
|
|
||||||
if (Date.now() - this.idService.parse(post.id).date.getTime() < GALLERY_POSTS_RANKING_WINDOW) {
|
|
||||||
await this.featuredService.updateGalleryPostsRanking(post.id, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.galleryPostsRepository.decrement({ id: post.id }, 'likedCount', 1);
|
this.galleryPostsRepository.decrement({ id: post.id }, 'likedCount', 1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -12,17 +12,8 @@ import { DI } from '@/di-symbols.js';
|
|||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
secure: true,
|
|
||||||
|
|
||||||
res: {
|
secure: true,
|
||||||
type: 'array',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
ref: 'Signin',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@@ -123,11 +123,6 @@ export const meta = {
|
|||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const muteWords = { type: 'array', items: { oneOf: [
|
|
||||||
{ type: 'array', items: { type: 'string' } },
|
|
||||||
{ type: 'string' }
|
|
||||||
] } } as const;
|
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
@@ -176,8 +171,7 @@ export const paramDef = {
|
|||||||
autoSensitive: { type: 'boolean' },
|
autoSensitive: { type: 'boolean' },
|
||||||
ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
|
ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
|
||||||
pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
|
pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
mutedWords: muteWords,
|
mutedWords: { type: 'array' },
|
||||||
hardMutedWords: muteWords,
|
|
||||||
mutedInstances: { type: 'array', items: {
|
mutedInstances: { type: 'array', items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
} },
|
} },
|
||||||
@@ -240,20 +234,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
if (ps.location !== undefined) profileUpdates.location = ps.location;
|
if (ps.location !== undefined) profileUpdates.location = ps.location;
|
||||||
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
|
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
|
||||||
if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility;
|
if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility;
|
||||||
|
if (ps.mutedWords !== undefined) {
|
||||||
function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) {
|
|
||||||
// TODO: ちゃんと数える
|
// TODO: ちゃんと数える
|
||||||
const length = JSON.stringify(mutedWords).length;
|
const length = JSON.stringify(ps.mutedWords).length;
|
||||||
if (length > limit) {
|
if (length > (await this.roleService.getUserPolicies(user.id)).wordMuteLimit) {
|
||||||
throw new ApiError(meta.errors.tooManyMutedWords);
|
throw new ApiError(meta.errors.tooManyMutedWords);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function validateMuteWordRegex(mutedWords: (string[] | string)[]) {
|
// validate regular expression syntax
|
||||||
for (const mutedWord of mutedWords) {
|
ps.mutedWords.filter(x => !Array.isArray(x)).forEach(x => {
|
||||||
if (typeof mutedWord !== "string") continue;
|
const regexp = x.match(/^\/(.+)\/(.*)$/);
|
||||||
|
|
||||||
const regexp = mutedWord.match(/^\/(.+)\/(.*)$/);
|
|
||||||
if (!regexp) throw new ApiError(meta.errors.invalidRegexp);
|
if (!regexp) throw new ApiError(meta.errors.invalidRegexp);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -261,21 +251,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new ApiError(meta.errors.invalidRegexp);
|
throw new ApiError(meta.errors.invalidRegexp);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.mutedWords !== undefined) {
|
|
||||||
checkMuteWordCount(ps.mutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
|
|
||||||
validateMuteWordRegex(ps.mutedWords);
|
|
||||||
|
|
||||||
profileUpdates.mutedWords = ps.mutedWords;
|
profileUpdates.mutedWords = ps.mutedWords;
|
||||||
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
|
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
|
||||||
}
|
}
|
||||||
if (ps.hardMutedWords !== undefined) {
|
|
||||||
checkMuteWordCount(ps.hardMutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
|
|
||||||
validateMuteWordRegex(ps.hardMutedWords);
|
|
||||||
profileUpdates.hardMutedWords = ps.hardMutedWords;
|
|
||||||
}
|
|
||||||
if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances;
|
if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances;
|
||||||
if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig;
|
if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig;
|
||||||
if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;
|
if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;
|
||||||
|
@@ -31,7 +31,13 @@ export const meta = {
|
|||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
ref: 'InviteCode',
|
properties: {
|
||||||
|
code: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
example: 'GR6S02ERUA5VR',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@@ -9,6 +9,7 @@ import type { RegistrationTicketsRepository } from '@/models/_.js';
|
|||||||
import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js';
|
import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['meta'],
|
tags: ['meta'],
|
||||||
@@ -22,7 +23,6 @@ export const meta = {
|
|||||||
items: {
|
items: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
ref: 'InviteCode',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@@ -250,33 +250,6 @@ export const meta = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
backgroundImageUrl: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
},
|
|
||||||
impressumUrl: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
},
|
|
||||||
logoImageUrl: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
},
|
|
||||||
privacyPolicyUrl: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
},
|
|
||||||
serverRules: {
|
|
||||||
type: 'array',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
items: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
themeColor: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@@ -262,7 +262,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
if (renote.channelId && renote.channelId !== ps.channelId) {
|
if (renote.channelId && renote.channelId !== ps.channelId) {
|
||||||
// チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック
|
// チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック
|
||||||
// リノートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する
|
// リノートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する
|
||||||
const renoteChannel = await this.channelsRepository.findOneBy({ id: renote.channelId });
|
const renoteChannel = await this.channelsRepository.findOneById(renote.channelId);
|
||||||
if (renoteChannel == null) {
|
if (renoteChannel == null) {
|
||||||
// リノートしたいノートが書き込まれているチャンネルが無い
|
// リノートしたいノートが書き込まれているチャンネルが無い
|
||||||
throw new ApiError(meta.errors.noSuchChannel);
|
throw new ApiError(meta.errors.noSuchChannel);
|
||||||
|
@@ -9,8 +9,6 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
|||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { FeaturedService } from '@/core/FeaturedService.js';
|
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['notes'],
|
tags: ['notes'],
|
||||||
@@ -49,7 +47,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
private cacheService: CacheService,
|
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private featuredService: FeaturedService,
|
private featuredService: FeaturedService,
|
||||||
) {
|
) {
|
||||||
@@ -67,24 +64,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (noteIds.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
noteIds.sort((a, b) => a > b ? -1 : 1);
|
noteIds.sort((a, b) => a > b ? -1 : 1);
|
||||||
if (ps.untilId) {
|
if (ps.untilId) {
|
||||||
noteIds = noteIds.filter(id => id < ps.untilId!);
|
noteIds = noteIds.filter(id => id < ps.untilId!);
|
||||||
}
|
}
|
||||||
noteIds = noteIds.slice(0, ps.limit);
|
noteIds = noteIds.slice(0, ps.limit);
|
||||||
|
|
||||||
if (noteIds.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const [
|
|
||||||
userIdsWhoMeMuting,
|
|
||||||
userIdsWhoBlockingMe,
|
|
||||||
] = me ? await Promise.all([
|
|
||||||
this.cacheService.userMutingsCache.fetch(me.id),
|
|
||||||
this.cacheService.userBlockedCache.fetch(me.id),
|
|
||||||
]) : [new Set<string>(), new Set<string>()];
|
|
||||||
|
|
||||||
const query = this.notesRepository.createQueryBuilder('note')
|
const query = this.notesRepository.createQueryBuilder('note')
|
||||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||||
.innerJoinAndSelect('note.user', 'user')
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
@@ -94,15 +83,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||||
.leftJoinAndSelect('note.channel', 'channel');
|
.leftJoinAndSelect('note.channel', 'channel');
|
||||||
|
|
||||||
const notes = (await query.getMany()).filter(note => {
|
const notes = await query.getMany();
|
||||||
if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
|
||||||
if (me && isUserRelated(note, userIdsWhoMeMuting)) return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
notes.sort((a, b) => a.id > b.id ? -1 : 1);
|
notes.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||||
|
|
||||||
|
// TODO: ミュート等考慮
|
||||||
|
|
||||||
return await this.noteEntityService.packMany(notes, me);
|
return await this.noteEntityService.packMany(notes, me);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -5,20 +5,20 @@
|
|||||||
|
|
||||||
import { Brackets } from 'typeorm';
|
import { Brackets } from 'typeorm';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { NotesRepository, ChannelFollowingsRepository } from '@/models/_.js';
|
import type { NotesRepository, FollowingsRepository, MiNote, ChannelFollowingsRepository } from '@/models/_.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { FanoutTimelineName } from '@/core/FanoutTimelineService.js';
|
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { MiLocalUser } from '@/models/User.js';
|
import { MiLocalUser } from '@/models/User.js';
|
||||||
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
|
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
@@ -42,12 +42,6 @@ export const meta = {
|
|||||||
code: 'STL_DISABLED',
|
code: 'STL_DISABLED',
|
||||||
id: '620763f4-f621-4533-ab33-0577a1a3c342',
|
id: '620763f4-f621-4533-ab33-0577a1a3c342',
|
||||||
},
|
},
|
||||||
|
|
||||||
bothWithRepliesAndWithFiles: {
|
|
||||||
message: 'Specifying both withReplies and withFiles is not supported',
|
|
||||||
code: 'BOTH_WITH_REPLIES_AND_WITH_FILES',
|
|
||||||
id: 'dfaa3eb7-8002-4cb7-bcc4-1095df46656f'
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -59,7 +53,6 @@ export const paramDef = {
|
|||||||
untilId: { type: 'string', format: 'misskey:id' },
|
untilId: { type: 'string', format: 'misskey:id' },
|
||||||
sinceDate: { type: 'integer' },
|
sinceDate: { type: 'integer' },
|
||||||
untilDate: { type: 'integer' },
|
untilDate: { type: 'integer' },
|
||||||
allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default
|
|
||||||
includeMyRenotes: { type: 'boolean', default: true },
|
includeMyRenotes: { type: 'boolean', default: true },
|
||||||
includeRenotedMyNotes: { type: 'boolean', default: true },
|
includeRenotedMyNotes: { type: 'boolean', default: true },
|
||||||
includeLocalRenotes: { type: 'boolean', default: true },
|
includeLocalRenotes: { type: 'boolean', default: true },
|
||||||
@@ -84,10 +77,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
private activeUsersChart: ActiveUsersChart,
|
private activeUsersChart: ActiveUsersChart,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
|
private funoutTimelineService: FunoutTimelineService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private userFollowingService: UserFollowingService,
|
private userFollowingService: UserFollowingService,
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
|
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
||||||
@@ -98,12 +91,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
throw new ApiError(meta.errors.stlDisabled);
|
throw new ApiError(meta.errors.stlDisabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles);
|
|
||||||
|
|
||||||
const serverSettings = await this.metaService.fetch();
|
const serverSettings = await this.metaService.fetch();
|
||||||
|
|
||||||
if (!serverSettings.enableFanoutTimeline) {
|
if (!serverSettings.enableFanoutTimeline) {
|
||||||
const timeline = await this.getFromDb({
|
return await this.getFromDb({
|
||||||
untilId,
|
untilId,
|
||||||
sinceId,
|
sinceId,
|
||||||
limit: ps.limit,
|
limit: ps.limit,
|
||||||
@@ -113,61 +104,103 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
withFiles: ps.withFiles,
|
withFiles: ps.withFiles,
|
||||||
withReplies: ps.withReplies,
|
withReplies: ps.withReplies,
|
||||||
}, me);
|
}, me);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [
|
||||||
|
userIdsWhoMeMuting,
|
||||||
|
userIdsWhoMeMutingRenotes,
|
||||||
|
userIdsWhoBlockingMe,
|
||||||
|
] = await Promise.all([
|
||||||
|
this.cacheService.userMutingsCache.fetch(me.id),
|
||||||
|
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||||
|
this.cacheService.userBlockedCache.fetch(me.id),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let noteIds: string[];
|
||||||
|
let shouldFallbackToDb = false;
|
||||||
|
|
||||||
|
if (ps.withFiles) {
|
||||||
|
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([
|
||||||
|
`homeTimelineWithFiles:${me.id}`,
|
||||||
|
'localTimelineWithFiles',
|
||||||
|
], untilId, sinceId);
|
||||||
|
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
|
||||||
|
} else if (ps.withReplies) {
|
||||||
|
const [htlNoteIds, ltlNoteIds, ltlReplyNoteIds] = await this.funoutTimelineService.getMulti([
|
||||||
|
`homeTimeline:${me.id}`,
|
||||||
|
'localTimeline',
|
||||||
|
'localTimelineWithReplies',
|
||||||
|
], untilId, sinceId);
|
||||||
|
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds, ...ltlReplyNoteIds]));
|
||||||
|
} else {
|
||||||
|
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([
|
||||||
|
`homeTimeline:${me.id}`,
|
||||||
|
'localTimeline',
|
||||||
|
], untilId, sinceId);
|
||||||
|
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
|
||||||
|
shouldFallbackToDb = htlNoteIds.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
noteIds.sort((a, b) => a > b ? -1 : 1);
|
||||||
|
noteIds = noteIds.slice(0, ps.limit);
|
||||||
|
|
||||||
|
shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
|
||||||
|
|
||||||
|
let redisTimeline: MiNote[] = [];
|
||||||
|
|
||||||
|
if (!shouldFallbackToDb) {
|
||||||
|
const query = this.notesRepository.createQueryBuilder('note')
|
||||||
|
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||||
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
|
.leftJoinAndSelect('note.reply', 'reply')
|
||||||
|
.leftJoinAndSelect('note.renote', 'renote')
|
||||||
|
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||||
|
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||||
|
.leftJoinAndSelect('note.channel', 'channel');
|
||||||
|
|
||||||
|
redisTimeline = await query.getMany();
|
||||||
|
|
||||||
|
redisTimeline = redisTimeline.filter(note => {
|
||||||
|
if (note.userId === me.id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||||
|
if (isUserRelated(note, userIdsWhoMeMuting)) return false;
|
||||||
|
if (note.renoteId) {
|
||||||
|
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
||||||
|
if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
|
||||||
|
if (ps.withRenotes === false) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (redisTimeline.length > 0) {
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
this.activeUsersChart.read(me);
|
this.activeUsersChart.read(me);
|
||||||
});
|
});
|
||||||
|
|
||||||
return await this.noteEntityService.packMany(timeline, me);
|
return await this.noteEntityService.packMany(redisTimeline, me);
|
||||||
}
|
|
||||||
|
|
||||||
let timelineConfig: FanoutTimelineName[];
|
|
||||||
|
|
||||||
if (ps.withFiles) {
|
|
||||||
timelineConfig = [
|
|
||||||
`homeTimelineWithFiles:${me.id}`,
|
|
||||||
'localTimelineWithFiles',
|
|
||||||
];
|
|
||||||
} else if (ps.withReplies) {
|
|
||||||
timelineConfig = [
|
|
||||||
`homeTimeline:${me.id}`,
|
|
||||||
'localTimeline',
|
|
||||||
'localTimelineWithReplies',
|
|
||||||
];
|
|
||||||
} else {
|
} else {
|
||||||
timelineConfig = [
|
if (serverSettings.enableFanoutTimelineDbFallback) { // fallback to db
|
||||||
`homeTimeline:${me.id}`,
|
return await this.getFromDb({
|
||||||
'localTimeline',
|
untilId,
|
||||||
];
|
sinceId,
|
||||||
|
limit: ps.limit,
|
||||||
|
includeMyRenotes: ps.includeMyRenotes,
|
||||||
|
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
||||||
|
includeLocalRenotes: ps.includeLocalRenotes,
|
||||||
|
withFiles: ps.withFiles,
|
||||||
|
withReplies: ps.withReplies,
|
||||||
|
}, me);
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const redisTimeline = await this.fanoutTimelineEndpointService.timeline({
|
|
||||||
untilId,
|
|
||||||
sinceId,
|
|
||||||
limit: ps.limit,
|
|
||||||
allowPartial: ps.allowPartial,
|
|
||||||
me,
|
|
||||||
redisTimelines: timelineConfig,
|
|
||||||
useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
|
|
||||||
alwaysIncludeMyNotes: true,
|
|
||||||
excludePureRenotes: !ps.withRenotes,
|
|
||||||
dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({
|
|
||||||
untilId,
|
|
||||||
sinceId,
|
|
||||||
limit,
|
|
||||||
includeMyRenotes: ps.includeMyRenotes,
|
|
||||||
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
|
||||||
includeLocalRenotes: ps.includeLocalRenotes,
|
|
||||||
withFiles: ps.withFiles,
|
|
||||||
withReplies: ps.withReplies,
|
|
||||||
}, me),
|
|
||||||
});
|
|
||||||
|
|
||||||
process.nextTick(() => {
|
|
||||||
this.activeUsersChart.read(me);
|
|
||||||
});
|
|
||||||
|
|
||||||
return redisTimeline;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,6 +301,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
return await query.limit(ps.limit).getMany();
|
const timeline = await query.limit(ps.limit).getMany();
|
||||||
|
|
||||||
|
process.nextTick(() => {
|
||||||
|
this.activeUsersChart.read(me);
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.noteEntityService.packMany(timeline, me);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { Brackets } from 'typeorm';
|
import { Brackets } from 'typeorm';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { NotesRepository } from '@/models/_.js';
|
import type { MiNote, NotesRepository } from '@/models/_.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||||
@@ -13,10 +13,11 @@ import { DI } from '@/di-symbols.js';
|
|||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
|
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { MiLocalUser } from '@/models/User.js';
|
import { MiLocalUser } from '@/models/User.js';
|
||||||
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
|
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
@@ -38,12 +39,6 @@ export const meta = {
|
|||||||
code: 'LTL_DISABLED',
|
code: 'LTL_DISABLED',
|
||||||
id: '45a6eb02-7695-4393-b023-dd3be9aaaefd',
|
id: '45a6eb02-7695-4393-b023-dd3be9aaaefd',
|
||||||
},
|
},
|
||||||
|
|
||||||
bothWithRepliesAndWithFiles: {
|
|
||||||
message: 'Specifying both withReplies and withFiles is not supported',
|
|
||||||
code: 'BOTH_WITH_REPLIES_AND_WITH_FILES',
|
|
||||||
id: 'dd9c8400-1cb5-4eef-8a31-200c5f933793',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -53,10 +48,10 @@ export const paramDef = {
|
|||||||
withFiles: { type: 'boolean', default: false },
|
withFiles: { type: 'boolean', default: false },
|
||||||
withRenotes: { type: 'boolean', default: true },
|
withRenotes: { type: 'boolean', default: true },
|
||||||
withReplies: { type: 'boolean', default: false },
|
withReplies: { type: 'boolean', default: false },
|
||||||
|
excludeNsfw: { type: 'boolean', default: false },
|
||||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||||
sinceId: { type: 'string', format: 'misskey:id' },
|
sinceId: { type: 'string', format: 'misskey:id' },
|
||||||
untilId: { type: 'string', format: 'misskey:id' },
|
untilId: { type: 'string', format: 'misskey:id' },
|
||||||
allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default
|
|
||||||
sinceDate: { type: 'integer' },
|
sinceDate: { type: 'integer' },
|
||||||
untilDate: { type: 'integer' },
|
untilDate: { type: 'integer' },
|
||||||
},
|
},
|
||||||
@@ -74,7 +69,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
private activeUsersChart: ActiveUsersChart,
|
private activeUsersChart: ActiveUsersChart,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
|
private funoutTimelineService: FunoutTimelineService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
) {
|
) {
|
||||||
@@ -87,58 +82,98 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
throw new ApiError(meta.errors.ltlDisabled);
|
throw new ApiError(meta.errors.ltlDisabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles);
|
|
||||||
|
|
||||||
const serverSettings = await this.metaService.fetch();
|
const serverSettings = await this.metaService.fetch();
|
||||||
|
|
||||||
if (!serverSettings.enableFanoutTimeline) {
|
if (!serverSettings.enableFanoutTimeline) {
|
||||||
const timeline = await this.getFromDb({
|
return await this.getFromDb({
|
||||||
untilId,
|
untilId,
|
||||||
sinceId,
|
sinceId,
|
||||||
limit: ps.limit,
|
limit: ps.limit,
|
||||||
withFiles: ps.withFiles,
|
withFiles: ps.withFiles,
|
||||||
withReplies: ps.withReplies,
|
withReplies: ps.withReplies,
|
||||||
}, me);
|
}, me);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [
|
||||||
|
userIdsWhoMeMuting,
|
||||||
|
userIdsWhoMeMutingRenotes,
|
||||||
|
userIdsWhoBlockingMe,
|
||||||
|
] = me ? await Promise.all([
|
||||||
|
this.cacheService.userMutingsCache.fetch(me.id),
|
||||||
|
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||||
|
this.cacheService.userBlockedCache.fetch(me.id),
|
||||||
|
]) : [new Set<string>(), new Set<string>(), new Set<string>()];
|
||||||
|
|
||||||
|
let noteIds: string[];
|
||||||
|
|
||||||
|
if (ps.withFiles) {
|
||||||
|
noteIds = await this.funoutTimelineService.get('localTimelineWithFiles', untilId, sinceId);
|
||||||
|
} else {
|
||||||
|
const [nonReplyNoteIds, replyNoteIds] = await this.funoutTimelineService.getMulti([
|
||||||
|
'localTimeline',
|
||||||
|
'localTimelineWithReplies',
|
||||||
|
], untilId, sinceId);
|
||||||
|
noteIds = Array.from(new Set([...nonReplyNoteIds, ...replyNoteIds]));
|
||||||
|
noteIds.sort((a, b) => a > b ? -1 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
noteIds = noteIds.slice(0, ps.limit);
|
||||||
|
|
||||||
|
let redisTimeline: MiNote[] = [];
|
||||||
|
|
||||||
|
if (noteIds.length > 0) {
|
||||||
|
const query = this.notesRepository.createQueryBuilder('note')
|
||||||
|
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||||
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
|
.leftJoinAndSelect('note.reply', 'reply')
|
||||||
|
.leftJoinAndSelect('note.renote', 'renote')
|
||||||
|
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||||
|
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||||
|
.leftJoinAndSelect('note.channel', 'channel');
|
||||||
|
|
||||||
|
redisTimeline = await query.getMany();
|
||||||
|
|
||||||
|
redisTimeline = redisTimeline.filter(note => {
|
||||||
|
if (me && (note.userId === me.id)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!ps.withReplies && note.replyId && note.replyUserId !== note.userId && (me == null || note.replyUserId !== me.id)) return false;
|
||||||
|
if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||||
|
if (me && isUserRelated(note, userIdsWhoMeMuting)) return false;
|
||||||
|
if (note.renoteId) {
|
||||||
|
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
||||||
|
if (me && isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
|
||||||
|
if (ps.withRenotes === false) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (redisTimeline.length > 0) {
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
if (me) {
|
if (me) {
|
||||||
this.activeUsersChart.read(me);
|
this.activeUsersChart.read(me);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return await this.noteEntityService.packMany(timeline, me);
|
return await this.noteEntityService.packMany(redisTimeline, me);
|
||||||
}
|
} else {
|
||||||
|
if (serverSettings.enableFanoutTimelineDbFallback) { // fallback to db
|
||||||
const timeline = await this.fanoutTimelineEndpointService.timeline({
|
return await this.getFromDb({
|
||||||
untilId,
|
untilId,
|
||||||
sinceId,
|
sinceId,
|
||||||
limit: ps.limit,
|
limit: ps.limit,
|
||||||
allowPartial: ps.allowPartial,
|
withFiles: ps.withFiles,
|
||||||
me,
|
withReplies: ps.withReplies,
|
||||||
useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
|
}, me);
|
||||||
redisTimelines:
|
} else {
|
||||||
ps.withFiles ? ['localTimelineWithFiles']
|
return [];
|
||||||
: ps.withReplies ? ['localTimeline', 'localTimelineWithReplies']
|
|
||||||
: me ? ['localTimeline', `localTimelineWithReplyTo:${me.id}`]
|
|
||||||
: ['localTimeline'],
|
|
||||||
alwaysIncludeMyNotes: true,
|
|
||||||
excludePureRenotes: !ps.withRenotes,
|
|
||||||
dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({
|
|
||||||
untilId,
|
|
||||||
sinceId,
|
|
||||||
limit,
|
|
||||||
withFiles: ps.withFiles,
|
|
||||||
withReplies: ps.withReplies,
|
|
||||||
}, me),
|
|
||||||
});
|
|
||||||
|
|
||||||
process.nextTick(() => {
|
|
||||||
if (me) {
|
|
||||||
this.activeUsersChart.read(me);
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
return timeline;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,6 +214,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return await query.limit(ps.limit).getMany();
|
const timeline = await query.limit(ps.limit).getMany();
|
||||||
|
|
||||||
|
process.nextTick(() => {
|
||||||
|
if (me) {
|
||||||
|
this.activeUsersChart.read(me);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.noteEntityService.packMany(timeline, me);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { Brackets } from 'typeorm';
|
import { Brackets } from 'typeorm';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { NotesRepository, ChannelFollowingsRepository } from '@/models/_.js';
|
import type { MiNote, NotesRepository, ChannelFollowingsRepository } from '@/models/_.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||||
@@ -13,10 +13,11 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
|||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
|
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||||
import { MiLocalUser } from '@/models/User.js';
|
import { MiLocalUser } from '@/models/User.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
|
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['notes'],
|
tags: ['notes'],
|
||||||
@@ -42,7 +43,6 @@ export const paramDef = {
|
|||||||
untilId: { type: 'string', format: 'misskey:id' },
|
untilId: { type: 'string', format: 'misskey:id' },
|
||||||
sinceDate: { type: 'integer' },
|
sinceDate: { type: 'integer' },
|
||||||
untilDate: { type: 'integer' },
|
untilDate: { type: 'integer' },
|
||||||
allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default
|
|
||||||
includeMyRenotes: { type: 'boolean', default: true },
|
includeMyRenotes: { type: 'boolean', default: true },
|
||||||
includeRenotedMyNotes: { type: 'boolean', default: true },
|
includeRenotedMyNotes: { type: 'boolean', default: true },
|
||||||
includeLocalRenotes: { type: 'boolean', default: true },
|
includeLocalRenotes: { type: 'boolean', default: true },
|
||||||
@@ -65,7 +65,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
private activeUsersChart: ActiveUsersChart,
|
private activeUsersChart: ActiveUsersChart,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
|
private funoutTimelineService: FunoutTimelineService,
|
||||||
private userFollowingService: UserFollowingService,
|
private userFollowingService: UserFollowingService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
@@ -77,7 +77,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
const serverSettings = await this.metaService.fetch();
|
const serverSettings = await this.metaService.fetch();
|
||||||
|
|
||||||
if (!serverSettings.enableFanoutTimeline) {
|
if (!serverSettings.enableFanoutTimeline) {
|
||||||
const timeline = await this.getFromDb({
|
return await this.getFromDb({
|
||||||
untilId,
|
untilId,
|
||||||
sinceId,
|
sinceId,
|
||||||
limit: ps.limit,
|
limit: ps.limit,
|
||||||
@@ -87,54 +87,81 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
withFiles: ps.withFiles,
|
withFiles: ps.withFiles,
|
||||||
withRenotes: ps.withRenotes,
|
withRenotes: ps.withRenotes,
|
||||||
}, me);
|
}, me);
|
||||||
|
|
||||||
process.nextTick(() => {
|
|
||||||
this.activeUsersChart.read(me);
|
|
||||||
});
|
|
||||||
|
|
||||||
return await this.noteEntityService.packMany(timeline, me);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const [
|
const [
|
||||||
followings,
|
followings,
|
||||||
|
userIdsWhoMeMuting,
|
||||||
|
userIdsWhoMeMutingRenotes,
|
||||||
|
userIdsWhoBlockingMe,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.cacheService.userFollowingsCache.fetch(me.id),
|
this.cacheService.userFollowingsCache.fetch(me.id),
|
||||||
|
this.cacheService.userMutingsCache.fetch(me.id),
|
||||||
|
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||||
|
this.cacheService.userBlockedCache.fetch(me.id),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const timeline = this.fanoutTimelineEndpointService.timeline({
|
let noteIds = await this.funoutTimelineService.get(ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, untilId, sinceId);
|
||||||
untilId,
|
noteIds = noteIds.slice(0, ps.limit);
|
||||||
sinceId,
|
|
||||||
limit: ps.limit,
|
let redisTimeline: MiNote[] = [];
|
||||||
allowPartial: ps.allowPartial,
|
|
||||||
me,
|
if (noteIds.length > 0) {
|
||||||
useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
|
const query = this.notesRepository.createQueryBuilder('note')
|
||||||
redisTimelines: ps.withFiles ? [`homeTimelineWithFiles:${me.id}`] : [`homeTimeline:${me.id}`],
|
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||||
alwaysIncludeMyNotes: true,
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
excludePureRenotes: !ps.withRenotes,
|
.leftJoinAndSelect('note.reply', 'reply')
|
||||||
noteFilter: note => {
|
.leftJoinAndSelect('note.renote', 'renote')
|
||||||
|
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||||
|
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||||
|
.leftJoinAndSelect('note.channel', 'channel');
|
||||||
|
|
||||||
|
redisTimeline = await query.getMany();
|
||||||
|
|
||||||
|
redisTimeline = redisTimeline.filter(note => {
|
||||||
|
if (note.userId === me.id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||||
|
if (isUserRelated(note, userIdsWhoMeMuting)) return false;
|
||||||
|
if (note.renoteId) {
|
||||||
|
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
||||||
|
if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
|
||||||
|
if (ps.withRenotes === false) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (note.reply && note.reply.visibility === 'followers') {
|
if (note.reply && note.reply.visibility === 'followers') {
|
||||||
if (!Object.hasOwn(followings, note.reply.userId)) return false;
|
if (!Object.hasOwn(followings, note.reply.userId)) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
});
|
||||||
dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({
|
|
||||||
untilId,
|
|
||||||
sinceId,
|
|
||||||
limit,
|
|
||||||
includeMyRenotes: ps.includeMyRenotes,
|
|
||||||
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
|
||||||
includeLocalRenotes: ps.includeLocalRenotes,
|
|
||||||
withFiles: ps.withFiles,
|
|
||||||
withRenotes: ps.withRenotes,
|
|
||||||
}, me),
|
|
||||||
});
|
|
||||||
|
|
||||||
process.nextTick(() => {
|
redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||||
this.activeUsersChart.read(me);
|
}
|
||||||
});
|
|
||||||
|
|
||||||
return timeline;
|
if (redisTimeline.length > 0) {
|
||||||
|
process.nextTick(() => {
|
||||||
|
this.activeUsersChart.read(me);
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.noteEntityService.packMany(redisTimeline, me);
|
||||||
|
} else {
|
||||||
|
if (serverSettings.enableFanoutTimelineDbFallback) { // fallback to db
|
||||||
|
return await this.getFromDb({
|
||||||
|
untilId,
|
||||||
|
sinceId,
|
||||||
|
limit: ps.limit,
|
||||||
|
includeMyRenotes: ps.includeMyRenotes,
|
||||||
|
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
||||||
|
includeLocalRenotes: ps.includeLocalRenotes,
|
||||||
|
withFiles: ps.withFiles,
|
||||||
|
withRenotes: ps.withRenotes,
|
||||||
|
}, me);
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,6 +269,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
return await query.limit(ps.limit).getMany();
|
const timeline = await query.limit(ps.limit).getMany();
|
||||||
|
|
||||||
|
process.nextTick(() => {
|
||||||
|
this.activeUsersChart.read(me);
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.noteEntityService.packMany(timeline, me);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,17 +5,18 @@
|
|||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Brackets } from 'typeorm';
|
import { Brackets } from 'typeorm';
|
||||||
import type { MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
|
import type { MiNote, MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
|
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import { MiLocalUser } from '@/models/User.js';
|
import { MiLocalUser } from '@/models/User.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
|
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
@@ -51,7 +52,6 @@ export const paramDef = {
|
|||||||
untilId: { type: 'string', format: 'misskey:id' },
|
untilId: { type: 'string', format: 'misskey:id' },
|
||||||
sinceDate: { type: 'integer' },
|
sinceDate: { type: 'integer' },
|
||||||
untilDate: { type: 'integer' },
|
untilDate: { type: 'integer' },
|
||||||
allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default
|
|
||||||
includeMyRenotes: { type: 'boolean', default: true },
|
includeMyRenotes: { type: 'boolean', default: true },
|
||||||
includeRenotedMyNotes: { type: 'boolean', default: true },
|
includeRenotedMyNotes: { type: 'boolean', default: true },
|
||||||
includeLocalRenotes: { type: 'boolean', default: true },
|
includeLocalRenotes: { type: 'boolean', default: true },
|
||||||
@@ -81,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
private activeUsersChart: ActiveUsersChart,
|
private activeUsersChart: ActiveUsersChart,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
|
private funoutTimelineService: FunoutTimelineService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
) {
|
) {
|
||||||
@@ -101,7 +101,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
const serverSettings = await this.metaService.fetch();
|
const serverSettings = await this.metaService.fetch();
|
||||||
|
|
||||||
if (!serverSettings.enableFanoutTimeline) {
|
if (!serverSettings.enableFanoutTimeline) {
|
||||||
const timeline = await this.getFromDb(list, {
|
return await this.getFromDb(list, {
|
||||||
untilId,
|
untilId,
|
||||||
sinceId,
|
sinceId,
|
||||||
limit: ps.limit,
|
limit: ps.limit,
|
||||||
@@ -111,37 +111,73 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
withFiles: ps.withFiles,
|
withFiles: ps.withFiles,
|
||||||
withRenotes: ps.withRenotes,
|
withRenotes: ps.withRenotes,
|
||||||
}, me);
|
}, me);
|
||||||
|
|
||||||
this.activeUsersChart.read(me);
|
|
||||||
|
|
||||||
await this.noteEntityService.packMany(timeline, me);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeline = await this.fanoutTimelineEndpointService.timeline({
|
const [
|
||||||
untilId,
|
userIdsWhoMeMuting,
|
||||||
sinceId,
|
userIdsWhoMeMutingRenotes,
|
||||||
limit: ps.limit,
|
userIdsWhoBlockingMe,
|
||||||
allowPartial: ps.allowPartial,
|
] = await Promise.all([
|
||||||
me,
|
this.cacheService.userMutingsCache.fetch(me.id),
|
||||||
useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
|
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||||
redisTimelines: ps.withFiles ? [`userListTimelineWithFiles:${list.id}`] : [`userListTimeline:${list.id}`],
|
this.cacheService.userBlockedCache.fetch(me.id),
|
||||||
alwaysIncludeMyNotes: true,
|
]);
|
||||||
excludePureRenotes: !ps.withRenotes,
|
|
||||||
dbFallback: async (untilId, sinceId, limit) => await this.getFromDb(list, {
|
|
||||||
untilId,
|
|
||||||
sinceId,
|
|
||||||
limit,
|
|
||||||
includeMyRenotes: ps.includeMyRenotes,
|
|
||||||
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
|
||||||
includeLocalRenotes: ps.includeLocalRenotes,
|
|
||||||
withFiles: ps.withFiles,
|
|
||||||
withRenotes: ps.withRenotes,
|
|
||||||
}, me),
|
|
||||||
});
|
|
||||||
|
|
||||||
this.activeUsersChart.read(me);
|
let noteIds = await this.funoutTimelineService.get(ps.withFiles ? `userListTimelineWithFiles:${list.id}` : `userListTimeline:${list.id}`, untilId, sinceId);
|
||||||
|
noteIds = noteIds.slice(0, ps.limit);
|
||||||
|
|
||||||
return timeline;
|
let redisTimeline: MiNote[] = [];
|
||||||
|
|
||||||
|
if (noteIds.length > 0) {
|
||||||
|
const query = this.notesRepository.createQueryBuilder('note')
|
||||||
|
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||||
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
|
.leftJoinAndSelect('note.reply', 'reply')
|
||||||
|
.leftJoinAndSelect('note.renote', 'renote')
|
||||||
|
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||||
|
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||||
|
.leftJoinAndSelect('note.channel', 'channel');
|
||||||
|
|
||||||
|
redisTimeline = await query.getMany();
|
||||||
|
|
||||||
|
redisTimeline = redisTimeline.filter(note => {
|
||||||
|
if (note.userId === me.id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||||
|
if (isUserRelated(note, userIdsWhoMeMuting)) return false;
|
||||||
|
if (note.renoteId) {
|
||||||
|
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
||||||
|
if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
|
||||||
|
if (ps.withRenotes === false) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (redisTimeline.length > 0) {
|
||||||
|
this.activeUsersChart.read(me);
|
||||||
|
return await this.noteEntityService.packMany(redisTimeline, me);
|
||||||
|
} else {
|
||||||
|
if (serverSettings.enableFanoutTimelineDbFallback) { // fallback to db
|
||||||
|
return await this.getFromDb(list, {
|
||||||
|
untilId,
|
||||||
|
sinceId,
|
||||||
|
limit: ps.limit,
|
||||||
|
includeMyRenotes: ps.includeMyRenotes,
|
||||||
|
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
||||||
|
includeLocalRenotes: ps.includeLocalRenotes,
|
||||||
|
withFiles: ps.withFiles,
|
||||||
|
withRenotes: ps.withRenotes,
|
||||||
|
}, me);
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,6 +271,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
return await query.limit(ps.limit).getMany();
|
const timeline = await query.limit(ps.limit).getMany();
|
||||||
|
|
||||||
|
this.activeUsersChart.read(me);
|
||||||
|
|
||||||
|
return await this.noteEntityService.packMany(timeline, me);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,7 @@ import { QueryService } from '@/core/QueryService.js';
|
|||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
@@ -66,7 +66,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private fanoutTimelineService: FanoutTimelineService,
|
private funoutTimelineService: FunoutTimelineService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
||||||
@@ -84,7 +84,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
let noteIds = await this.fanoutTimelineService.get(`roleTimeline:${role.id}`, untilId, sinceId);
|
let noteIds = await this.funoutTimelineService.get(`roleTimeline:${role.id}`, untilId, sinceId);
|
||||||
noteIds = noteIds.slice(0, ps.limit);
|
noteIds = noteIds.slice(0, ps.limit);
|
||||||
|
|
||||||
if (noteIds.length === 0) {
|
if (noteIds.length === 0) {
|
||||||
|
@@ -42,12 +42,6 @@ export const meta = {
|
|||||||
code: 'FORBIDDEN',
|
code: 'FORBIDDEN',
|
||||||
id: 'f6cdb0df-c19f-ec5c-7dbb-0ba84a1f92ba',
|
id: 'f6cdb0df-c19f-ec5c-7dbb-0ba84a1f92ba',
|
||||||
},
|
},
|
||||||
|
|
||||||
birthdayInvalid: {
|
|
||||||
message: 'Birthday date format is invalid.',
|
|
||||||
code: 'BIRTHDAY_DATE_FORMAT_INVALID',
|
|
||||||
id: 'a2b007b9-4782-4eba-abd3-93b05ed4130d',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -65,8 +59,6 @@ export const paramDef = {
|
|||||||
nullable: true,
|
nullable: true,
|
||||||
description: 'The local host is represented with `null`.',
|
description: 'The local host is represented with `null`.',
|
||||||
},
|
},
|
||||||
|
|
||||||
birthday: { type: 'string', nullable: true },
|
|
||||||
},
|
},
|
||||||
anyOf: [
|
anyOf: [
|
||||||
{ required: ['userId'] },
|
{ required: ['userId'] },
|
||||||
@@ -125,21 +117,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
.andWhere('following.followerId = :userId', { userId: user.id })
|
.andWhere('following.followerId = :userId', { userId: user.id })
|
||||||
.innerJoinAndSelect('following.followee', 'followee');
|
.innerJoinAndSelect('following.followee', 'followee');
|
||||||
|
|
||||||
if (ps.birthday) {
|
|
||||||
try {
|
|
||||||
const d = new Date(ps.birthday);
|
|
||||||
d.setHours(0, 0, 0, 0);
|
|
||||||
const birthday = `${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`;
|
|
||||||
const birthdayUserQuery = this.userProfilesRepository.createQueryBuilder('user_profile');
|
|
||||||
birthdayUserQuery.select('user_profile.userId')
|
|
||||||
.where(`SUBSTR(user_profile.birthday, 6, 5) = '${birthday}'`);
|
|
||||||
|
|
||||||
query.andWhere(`following.followeeId IN (${ birthdayUserQuery.getQuery() })`);
|
|
||||||
} catch (err) {
|
|
||||||
throw new ApiError(meta.errors.birthdayInvalid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const followings = await query
|
const followings = await query
|
||||||
.limit(ps.limit)
|
.limit(ps.limit)
|
||||||
.getMany();
|
.getMany();
|
||||||
|
@@ -5,18 +5,17 @@
|
|||||||
|
|
||||||
import { Brackets } from 'typeorm';
|
import { Brackets } from 'typeorm';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { NotesRepository } from '@/models/_.js';
|
import * as Redis from 'ioredis';
|
||||||
|
import type { MiNote, NotesRepository } from '@/models/_.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||||
import { MiLocalUser } from '@/models/User.js';
|
import { ApiError } from '../../error.js';
|
||||||
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
|
|
||||||
import { FanoutTimelineName } from '@/core/FanoutTimelineService.js';
|
|
||||||
import { ApiError } from '@/server/api/error.js';
|
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['users', 'notes'],
|
tags: ['users', 'notes'],
|
||||||
@@ -37,12 +36,6 @@ export const meta = {
|
|||||||
code: 'NO_SUCH_USER',
|
code: 'NO_SUCH_USER',
|
||||||
id: '27e494ba-2ac2-48e8-893b-10d4d8c2387b',
|
id: '27e494ba-2ac2-48e8-893b-10d4d8c2387b',
|
||||||
},
|
},
|
||||||
|
|
||||||
bothWithRepliesAndWithFiles: {
|
|
||||||
message: 'Specifying both withReplies and withFiles is not supported',
|
|
||||||
code: 'BOTH_WITH_REPLIES_AND_WITH_FILES',
|
|
||||||
id: '91c8cb9f-36ed-46e7-9ca2-7df96ed6e222',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -58,8 +51,8 @@ export const paramDef = {
|
|||||||
untilId: { type: 'string', format: 'misskey:id' },
|
untilId: { type: 'string', format: 'misskey:id' },
|
||||||
sinceDate: { type: 'integer' },
|
sinceDate: { type: 'integer' },
|
||||||
untilDate: { type: 'integer' },
|
untilDate: { type: 'integer' },
|
||||||
allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default
|
|
||||||
withFiles: { type: 'boolean', default: false },
|
withFiles: { type: 'boolean', default: false },
|
||||||
|
excludeNsfw: { type: 'boolean', default: false },
|
||||||
},
|
},
|
||||||
required: ['userId'],
|
required: ['userId'],
|
||||||
} as const;
|
} as const;
|
||||||
@@ -67,6 +60,9 @@ export const paramDef = {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.redisForTimelines)
|
||||||
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
@@ -74,122 +70,118 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
|
private funoutTimelineService: FunoutTimelineService,
|
||||||
private metaService: MetaService,
|
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
||||||
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
|
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
|
||||||
|
const isRangeSpecified = untilId != null && sinceId != null;
|
||||||
const isSelf = me && (me.id === ps.userId);
|
const isSelf = me && (me.id === ps.userId);
|
||||||
|
|
||||||
const serverSettings = await this.metaService.fetch();
|
if (isRangeSpecified || sinceId == null) {
|
||||||
|
const [
|
||||||
|
userIdsWhoMeMuting,
|
||||||
|
] = me ? await Promise.all([
|
||||||
|
this.cacheService.userMutingsCache.fetch(me.id),
|
||||||
|
]) : [new Set<string>()];
|
||||||
|
|
||||||
if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles);
|
const [noteIdsRes, repliesNoteIdsRes, channelNoteIdsRes] = await Promise.all([
|
||||||
|
this.funoutTimelineService.get(ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`, untilId, sinceId),
|
||||||
|
ps.withReplies ? this.funoutTimelineService.get(`userTimelineWithReplies:${ps.userId}`, untilId, sinceId) : Promise.resolve([]),
|
||||||
|
ps.withChannelNotes ? this.funoutTimelineService.get(`userTimelineWithChannel:${ps.userId}`, untilId, sinceId) : Promise.resolve([]),
|
||||||
|
]);
|
||||||
|
|
||||||
if (!serverSettings.enableFanoutTimeline) {
|
let noteIds = Array.from(new Set([
|
||||||
const timeline = await this.getFromDb({
|
...noteIdsRes,
|
||||||
untilId,
|
...repliesNoteIdsRes,
|
||||||
sinceId,
|
...channelNoteIdsRes,
|
||||||
limit: ps.limit,
|
]));
|
||||||
userId: ps.userId,
|
noteIds.sort((a, b) => a > b ? -1 : 1);
|
||||||
withChannelNotes: ps.withChannelNotes,
|
noteIds = noteIds.slice(0, ps.limit);
|
||||||
withFiles: ps.withFiles,
|
|
||||||
withRenotes: ps.withRenotes,
|
|
||||||
}, me);
|
|
||||||
|
|
||||||
return await this.noteEntityService.packMany(timeline, me);
|
if (noteIds.length > 0) {
|
||||||
|
const isFollowing = me && Object.hasOwn(await this.cacheService.userFollowingsCache.fetch(me.id), ps.userId);
|
||||||
|
|
||||||
|
const query = this.notesRepository.createQueryBuilder('note')
|
||||||
|
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||||
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
|
.leftJoinAndSelect('note.reply', 'reply')
|
||||||
|
.leftJoinAndSelect('note.renote', 'renote')
|
||||||
|
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||||
|
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||||
|
.leftJoinAndSelect('note.channel', 'channel');
|
||||||
|
|
||||||
|
let timeline = await query.getMany();
|
||||||
|
|
||||||
|
timeline = timeline.filter(note => {
|
||||||
|
if (me && isUserRelated(note, userIdsWhoMeMuting, true)) return false;
|
||||||
|
|
||||||
|
if (note.renoteId) {
|
||||||
|
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
||||||
|
if (ps.withRenotes === false) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note.channel?.isSensitive && !isSelf) return false;
|
||||||
|
if (note.visibility === 'specified' && (!me || (me.id !== note.userId && !note.visibleUserIds.some(v => v === me.id)))) return false;
|
||||||
|
if (note.visibility === 'followers' && !isFollowing && !isSelf) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: フィルタで件数が減った場合の埋め合わせ処理
|
||||||
|
|
||||||
|
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||||
|
|
||||||
|
if (timeline.length > 0) {
|
||||||
|
return await this.noteEntityService.packMany(timeline, me);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const redisTimelines: FanoutTimelineName[] = [ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`];
|
//#region fallback to database
|
||||||
|
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||||
|
.andWhere('note.userId = :userId', { userId: ps.userId })
|
||||||
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
|
.leftJoinAndSelect('note.reply', 'reply')
|
||||||
|
.leftJoinAndSelect('note.renote', 'renote')
|
||||||
|
.leftJoinAndSelect('note.channel', 'channel')
|
||||||
|
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||||
|
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||||
|
|
||||||
if (ps.withReplies) redisTimelines.push(`userTimelineWithReplies:${ps.userId}`);
|
if (ps.withChannelNotes) {
|
||||||
if (ps.withChannelNotes) redisTimelines.push(`userTimelineWithChannel:${ps.userId}`);
|
if (!isSelf) query.andWhere(new Brackets(qb => {
|
||||||
|
qb.orWhere('note.channelId IS NULL');
|
||||||
|
qb.orWhere('channel.isSensitive = false');
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
query.andWhere('note.channelId IS NULL');
|
||||||
|
}
|
||||||
|
|
||||||
const isFollowing = me && Object.hasOwn(await this.cacheService.userFollowingsCache.fetch(me.id), ps.userId);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
|
if (me) {
|
||||||
|
this.queryService.generateMutedUserQuery(query, me, { id: ps.userId });
|
||||||
|
this.queryService.generateBlockedUserQuery(query, me);
|
||||||
|
}
|
||||||
|
|
||||||
const timeline = await this.fanoutTimelineEndpointService.timeline({
|
if (ps.withFiles) {
|
||||||
untilId,
|
query.andWhere('note.fileIds != \'{}\'');
|
||||||
sinceId,
|
}
|
||||||
limit: ps.limit,
|
|
||||||
allowPartial: ps.allowPartial,
|
|
||||||
me,
|
|
||||||
redisTimelines,
|
|
||||||
useDbFallback: true,
|
|
||||||
ignoreAuthorFromMute: true,
|
|
||||||
excludeReplies: ps.withChannelNotes && !ps.withReplies, // userTimelineWithChannel may include replies
|
|
||||||
excludeNoFiles: ps.withChannelNotes && ps.withFiles, // userTimelineWithChannel may include notes without files
|
|
||||||
excludePureRenotes: !ps.withRenotes,
|
|
||||||
noteFilter: note => {
|
|
||||||
if (note.channel?.isSensitive && !isSelf) return false;
|
|
||||||
if (note.visibility === 'specified' && (!me || (me.id !== note.userId && !note.visibleUserIds.some(v => v === me.id)))) return false;
|
|
||||||
if (note.visibility === 'followers' && !isFollowing && !isSelf) return false;
|
|
||||||
|
|
||||||
return true;
|
if (ps.withRenotes === false) {
|
||||||
},
|
query.andWhere(new Brackets(qb => {
|
||||||
dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({
|
qb.orWhere('note.userId != :userId', { userId: ps.userId });
|
||||||
untilId,
|
qb.orWhere('note.renoteId IS NULL');
|
||||||
sinceId,
|
qb.orWhere('note.text IS NOT NULL');
|
||||||
limit,
|
qb.orWhere('note.fileIds != \'{}\'');
|
||||||
userId: ps.userId,
|
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||||
withChannelNotes: ps.withChannelNotes,
|
}));
|
||||||
withFiles: ps.withFiles,
|
}
|
||||||
withRenotes: ps.withRenotes,
|
|
||||||
}, me),
|
|
||||||
});
|
|
||||||
|
|
||||||
return timeline;
|
const timeline = await query.limit(ps.limit).getMany();
|
||||||
|
|
||||||
|
return await this.noteEntityService.packMany(timeline, me);
|
||||||
|
//#endregion
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getFromDb(ps: {
|
|
||||||
untilId: string | null,
|
|
||||||
sinceId: string | null,
|
|
||||||
limit: number,
|
|
||||||
userId: string,
|
|
||||||
withChannelNotes: boolean,
|
|
||||||
withFiles: boolean,
|
|
||||||
withRenotes: boolean,
|
|
||||||
}, me: MiLocalUser | null) {
|
|
||||||
const isSelf = me && (me.id === ps.userId);
|
|
||||||
|
|
||||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
|
||||||
.andWhere('note.userId = :userId', { userId: ps.userId })
|
|
||||||
.innerJoinAndSelect('note.user', 'user')
|
|
||||||
.leftJoinAndSelect('note.reply', 'reply')
|
|
||||||
.leftJoinAndSelect('note.renote', 'renote')
|
|
||||||
.leftJoinAndSelect('note.channel', 'channel')
|
|
||||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
|
||||||
|
|
||||||
if (ps.withChannelNotes) {
|
|
||||||
if (!isSelf) query.andWhere(new Brackets(qb => {
|
|
||||||
qb.orWhere('note.channelId IS NULL');
|
|
||||||
qb.orWhere('channel.isSensitive = false');
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
query.andWhere('note.channelId IS NULL');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
|
||||||
if (me) {
|
|
||||||
this.queryService.generateMutedUserQuery(query, me, { id: ps.userId });
|
|
||||||
this.queryService.generateBlockedUserQuery(query, me);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.withFiles) {
|
|
||||||
query.andWhere('note.fileIds != \'{}\'');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.withRenotes === false) {
|
|
||||||
query.andWhere(new Brackets(qb => {
|
|
||||||
qb.orWhere('note.userId != :userId', { userId: ps.userId });
|
|
||||||
qb.orWhere('note.renoteId IS NULL');
|
|
||||||
qb.orWhere('note.text IS NOT NULL');
|
|
||||||
qb.orWhere('note.fileIds != \'{}\'');
|
|
||||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return await query.limit(ps.limit).getMany();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import endpoints, { IEndpoint } from '../endpoints.js';
|
import endpoints from '../endpoints.js';
|
||||||
import { errors as basicErrors } from './errors.js';
|
import { errors as basicErrors } from './errors.js';
|
||||||
import { schemas, convertSchemaToOpenApiSchema } from './schemas.js';
|
import { schemas, convertSchemaToOpenApiSchema } from './schemas.js';
|
||||||
|
|
||||||
@@ -33,17 +33,16 @@ export function genOpenapiSpec(config: Config) {
|
|||||||
schemas: schemas,
|
schemas: schemas,
|
||||||
|
|
||||||
securitySchemes: {
|
securitySchemes: {
|
||||||
bearerAuth: {
|
ApiKeyAuth: {
|
||||||
type: 'http',
|
type: 'apiKey',
|
||||||
scheme: 'bearer',
|
in: 'body',
|
||||||
|
name: 'i',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 書き換えたりするのでディープコピーしておく。そのまま編集するとメモリ上の値が汚れて次回以降の出力に影響する
|
for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) {
|
||||||
const copiedEndpoints = JSON.parse(JSON.stringify(endpoints)) as IEndpoint[];
|
|
||||||
for (const endpoint of copiedEndpoints.filter(ep => !ep.meta.secure)) {
|
|
||||||
const errors = {} as any;
|
const errors = {} as any;
|
||||||
|
|
||||||
if (endpoint.meta.errors) {
|
if (endpoint.meta.errors) {
|
||||||
@@ -80,13 +79,6 @@ export function genOpenapiSpec(config: Config) {
|
|||||||
schema.required = [...schema.required ?? [], 'file'];
|
schema.required = [...schema.required ?? [], 'file'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (schema.required && schema.required.length <= 0) {
|
|
||||||
// 空配列は許可されない
|
|
||||||
schema.required = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasBody = (schema.type === 'object' && schema.properties && Object.keys(schema.properties).length >= 1);
|
|
||||||
|
|
||||||
const info = {
|
const info = {
|
||||||
operationId: endpoint.name,
|
operationId: endpoint.name,
|
||||||
summary: endpoint.name,
|
summary: endpoint.name,
|
||||||
@@ -100,19 +92,17 @@ export function genOpenapiSpec(config: Config) {
|
|||||||
} : {}),
|
} : {}),
|
||||||
...(endpoint.meta.requireCredential ? {
|
...(endpoint.meta.requireCredential ? {
|
||||||
security: [{
|
security: [{
|
||||||
bearerAuth: [],
|
ApiKeyAuth: [],
|
||||||
}],
|
}],
|
||||||
} : {}),
|
} : {}),
|
||||||
...(hasBody ? {
|
requestBody: {
|
||||||
requestBody: {
|
required: true,
|
||||||
required: true,
|
content: {
|
||||||
content: {
|
[requestType]: {
|
||||||
[requestType]: {
|
schema,
|
||||||
schema,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} : {}),
|
},
|
||||||
responses: {
|
responses: {
|
||||||
...(endpoint.meta.res ? {
|
...(endpoint.meta.res ? {
|
||||||
'200': {
|
'200': {
|
||||||
@@ -128,11 +118,6 @@ export function genOpenapiSpec(config: Config) {
|
|||||||
description: 'OK (without any results)',
|
description: 'OK (without any results)',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
...(endpoint.meta.res?.optional === true || endpoint.meta.res?.nullable === true ? {
|
|
||||||
'204': {
|
|
||||||
description: 'OK (without any results)',
|
|
||||||
},
|
|
||||||
} : {}),
|
|
||||||
'400': {
|
'400': {
|
||||||
description: 'Client error',
|
description: 'Client error',
|
||||||
content: {
|
content: {
|
||||||
@@ -205,7 +190,6 @@ export function genOpenapiSpec(config: Config) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
spec.paths['/' + endpoint.name] = {
|
spec.paths['/' + endpoint.name] = {
|
||||||
...(endpoint.meta.allowGet ? { get: info } : {}),
|
|
||||||
post: info,
|
post: info,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -7,16 +7,10 @@ import type { Schema } from '@/misc/json-schema.js';
|
|||||||
import { refs } from '@/misc/json-schema.js';
|
import { refs } from '@/misc/json-schema.js';
|
||||||
|
|
||||||
export function convertSchemaToOpenApiSchema(schema: Schema) {
|
export function convertSchemaToOpenApiSchema(schema: Schema) {
|
||||||
// optional, refはスキーマ定義に含まれないので分離しておく
|
const res: any = schema;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const { optional, ref, ...res }: any = schema;
|
|
||||||
|
|
||||||
if (schema.type === 'object' && schema.properties) {
|
if (schema.type === 'object' && schema.properties) {
|
||||||
const required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k);
|
res.required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k);
|
||||||
if (required.length > 0) {
|
|
||||||
// 空配列は許可されない
|
|
||||||
res.required = required;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const k of Object.keys(schema.properties)) {
|
for (const k of Object.keys(schema.properties)) {
|
||||||
res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k]);
|
res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k]);
|
||||||
|
@@ -36,7 +36,6 @@ export default class Connection {
|
|||||||
public userIdsWhoMeMuting: Set<string> = new Set();
|
public userIdsWhoMeMuting: Set<string> = new Set();
|
||||||
public userIdsWhoBlockingMe: Set<string> = new Set();
|
public userIdsWhoBlockingMe: Set<string> = new Set();
|
||||||
public userIdsWhoMeMutingRenotes: Set<string> = new Set();
|
public userIdsWhoMeMutingRenotes: Set<string> = new Set();
|
||||||
public userMutedInstances: Set<string> = new Set();
|
|
||||||
private fetchIntervalId: NodeJS.Timeout | null = null;
|
private fetchIntervalId: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -70,7 +69,6 @@ export default class Connection {
|
|||||||
this.userIdsWhoMeMuting = userIdsWhoMeMuting;
|
this.userIdsWhoMeMuting = userIdsWhoMeMuting;
|
||||||
this.userIdsWhoBlockingMe = userIdsWhoBlockingMe;
|
this.userIdsWhoBlockingMe = userIdsWhoBlockingMe;
|
||||||
this.userIdsWhoMeMutingRenotes = userIdsWhoMeMutingRenotes;
|
this.userIdsWhoMeMutingRenotes = userIdsWhoMeMutingRenotes;
|
||||||
this.userMutedInstances = new Set(userProfile.mutedInstances);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user