Compare commits

..

36 Commits

Author SHA1 Message Date
syuilo
9784d10c62 Merge pull request #12330 from misskey-dev/develop
Release: 2023.11.1
2023-11-17 18:32:42 +09:00
syuilo
0c2dd33593 Merge pull request #12177 from misskey-dev/develop
Release: 2023.11.0
2023-11-05 18:18:35 +09:00
syuilo
3043b5256d Merge pull request #12060 from misskey-dev/develop
Release: 2023.10.2
2023-10-21 14:18:53 +09:00
syuilo
7e7138c0eb Merge pull request #12011 from misskey-dev/develop
Release: 2023.10.1
2023-10-12 09:21:04 +09:00
syuilo
f964ef163b Merge pull request #11963 from misskey-dev/develop
Release: 2023.10.0
2023-10-10 20:40:13 +09:00
syuilo
0e6cd577cc Merge pull request #11926 from misskey-dev/develop
* fix(backend): Redisに古いMisskeyバージョンのキャッシュが残っている場合の問題を修正

* Update CHANGELOG.md

* enhance(front)end: improve moderation log

* enhance: ノートの翻訳機能の利用可否をロールで設定可能に

Resolve #11923

* 2023.9.3

* 後方互換性の強化

* Update CHANGELOG.md

* fix test

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

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (Korean)

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

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Thai)

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

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

* feat: AiScriptでホストのアドレスを参照できる定数 (#11924)

* add HOST_URL

* Update CHANGELOG.md

* tweak

---------

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

---------

Co-authored-by: FineArchs <133759614+FineArchs@users.noreply.github.com>
2023-09-30 09:40:00 +09:00
syuilo
7adc8fcaf5 Merge pull request #11920 from misskey-dev/develop
Release: 2023.9.2
2023-09-29 18:11:30 +09:00
syuilo
e57b536767 Merge pull request #11898 from misskey-dev/develop
2023.9.1
2023-09-25 17:12:28 +09:00
syuilo
f32915b515 Merge pull request #11874 from misskey-dev/develop
Release: 2023.9.0
2023-09-24 18:21:31 +09:00
syuilo
a8d45d4b0d Merge pull request #11384 from misskey-dev/develop
Release: 13.14.2
2023-07-27 13:00:14 +09:00
syuilo
4e24aff408 Merge pull request #11338 from misskey-dev/develop
Release: 13.14.1
2023-07-21 20:40:03 +09:00
syuilo
e64a81aa1d Merge pull request #11301 from misskey-dev/develop
Release: 13.14.0
2023-07-21 20:36:07 +09:00
syuilo
7093662ce5 Merge pull request #10990 from misskey-dev/develop
Release: 13.13.2
2023-06-13 16:46:01 +09:00
syuilo
32c741154d Merge pull request #10961 from misskey-dev/develop
Release: 13.13.1
2023-06-06 11:34:36 +09:00
syuilo
407a965c1d Merge pull request #10932 from misskey-dev/develop
Release: 13.13.0
2023-06-05 19:47:08 +09:00
syuilo
de6348e8a0 Merge pull request #10833 from misskey-dev/develop
* refactor(frontend): use css modules

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

Resolve #10819

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

* 念のためnoimageaiもつける

* add X-Robots-Tag: noai

* Update ja-JP.yml

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

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

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

* fix test

* New Crowdin updates (#10815)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Korean)

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

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

* refactor

* bump

* refactor(frontend): use css module

* refactor(frontend): use css module

* delete unused component

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

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

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

* 比較演算子間違えた

* とりあえずチェック

* 正規表現対応

* /test/giにも対応

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

* レビュー修正

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

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

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

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

* 修正

* wipかも

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

* なんか動いたかも

* test作成

* 文言の修正

* 修正

* note参照

---------

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

* Update CHANGELOG.md

* New Crowdin updates (#10823)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* ci: fix typo

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

* fix typo

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

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

Fix #10793

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

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

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

* fix(frontend): fix retention chart rendering

* Update about-misskey.vue

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

* New Crowdin updates (#10824)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

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

* New translations ja-JP.yml (English)

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

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

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

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

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

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

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Norwegian)

* New translations ja-JP.yml (Russian)

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

* New translations ja-JP.yml (Indonesian)

* New translations ja-JP.yml (Thai)

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

* Update CHANGELOG.md

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

* fix MkUserSetupDialog.Privacy.vue

* ci: skip non-Japanese locale on TurboSnap

* ci: notify on changes for push events

* ci: fix missing branch

* Update basic.cy.js

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (German)

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

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

* New translations ja-JP.yml (Arabic)

* 🎨

* 🎨

* enhance(frontend): add retention line chart

* update deps

* refactor

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

Fix #10837

---------

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

* update PULL_REQUEST_TEMPLATE

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

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

* check:connectをstart内に移動

---------

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

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

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

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

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

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

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

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

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

* fix(client): MkTimeを改良

* numberを許容

* falsyな値もとる

* 不明

* ありません

* fix

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

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

* NO_SUCH_FILE

* Update codecov.yml

* Update apple-touch-icon.png

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

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

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

* enhance(client): improve clip menu ux

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

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

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

* 順番関係ある?

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

* Update @swc/core to v1.3.36

* Update CHANGELOG.md

* Update CHANGELOG.md

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

* 🎨

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

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

* fix: エラーの修正

* add: changelog

---------

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

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

Resolve #10019

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

Resolve #10078

* Update CHANGELOG.md

* enhance(client): tweak contextmenu position calculation

* 🎨

* 🎨

* feat: in-channel featured note

Resolve #9938

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

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

* Simplify search.vue

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

* Remove unused imports

* Update about-misskey.vue

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

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

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

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

* ✌️

* wip

* wip

* ✌️

* RequiredProp

* Revert "RequiredProp"

This reverts commit 7469390011.

* add api:notes/create

* fix lint

* text

* ✌️

* improve readability

---------

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

* New Crowdin updates (#10059)

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

* New translations ja-JP.yml (Romanian)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Slovak)

* New translations ja-JP.yml (Ukrainian)

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

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Vietnamese)

* New translations ja-JP.yml (Indonesian)

* New translations ja-JP.yml (Bengali)

* New translations ja-JP.yml (Thai)

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

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

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

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Ukrainian)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

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

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

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* enhance(client): improve user menu ux

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

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

* add: changelog

---------

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

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

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

* Update codecov.yml

* Update CHANGELOG.md

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

Fix #10064

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

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

* Applied editorconfig for pnpm-workspace.yaml

---------

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

* update deps

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

* refactor

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

* Update CHANGELOG.md

* 13.8.0

---------

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

View File

@@ -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],

View File

@@ -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

View File

@@ -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

View File

@@ -13,7 +13,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [20.10.0] node-version: [20.5.1]
services: services:
postgres: postgres:

View File

@@ -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:

View File

@@ -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:

View File

@@ -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

View File

@@ -1 +1 @@
20.10.0 20.5.1

View File

@@ -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: タイムライン取得時のパフォーマンスを向上

View File

@@ -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

View File

@@ -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"

View File

@@ -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
View File

@@ -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;

View File

@@ -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();

View File

@@ -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などでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。"

View File

@@ -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"

View File

@@ -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');

View File

@@ -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"`);
}
}

View File

@@ -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"`);
}
}

View File

@@ -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"`);
}
}

View File

@@ -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",

View File

@@ -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);
} }

View File

@@ -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,

View File

@@ -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,
};
}
} }

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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);
} }
} }

View File

@@ -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 日時

View File

@@ -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);
}
} }
}, },

View File

@@ -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);
} }
} }
} }

View File

@@ -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);
} }
} }

View File

@@ -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);
} }

View File

@@ -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)) {

View File

@@ -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,

View File

@@ -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;
} }

View File

@@ -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,

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
} }

View File

@@ -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;
}

View File

@@ -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]>;

View File

@@ -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,
}) })

View File

@@ -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: [],

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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: {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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;

View File

@@ -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',

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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)));

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -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;
} }

View File

@@ -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],

View File

@@ -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();

View File

@@ -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,
};
}); });
} }
} }

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,

View File

@@ -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,
});
});
}
}

View File

@@ -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,
});
});
}
}

View File

@@ -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;
} }

View File

@@ -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 [];

View File

@@ -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();
}
} }

View File

@@ -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,
}); });
} }

View File

@@ -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;

View File

@@ -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);
}); });

View File

@@ -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);
}); });
} }

View File

@@ -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);
}); });
} }

View File

@@ -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 = {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);
}); });
} }

View File

@@ -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);
} }
} }

View File

@@ -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);
} }
} }

View File

@@ -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);
} }
} }

View File

@@ -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);
} }
} }

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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();
}
} }

View File

@@ -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,
}; };
} }

View File

@@ -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]);

View File

@@ -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