mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-11 20:14:42 +02:00
Compare commits
98 Commits
room
...
improve-im
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aabd9e9cba | ||
|
|
7a31bfdbc2 | ||
|
|
3ba648b3d5 | ||
|
|
8f5949e1a2 | ||
|
|
37213d1c43 | ||
|
|
520ede4d63 | ||
|
|
ef034dc2ab | ||
|
|
491eba6772 | ||
|
|
9db840c17b | ||
|
|
5d92214ccc | ||
|
|
d55112ec7b | ||
|
|
9f37206f14 | ||
|
|
292005b27b | ||
|
|
43caa68594 | ||
|
|
1538df1c74 | ||
|
|
ea7f97c07a | ||
|
|
159d4e00dc | ||
|
|
955418a2e1 | ||
|
|
bebd318730 | ||
|
|
bf7986f2a4 | ||
|
|
10df70b3f5 | ||
|
|
a21a93190e | ||
|
|
afb72ac3b0 | ||
|
|
0ad2606ee5 | ||
|
|
734a19c06f | ||
|
|
7f3cbace05 | ||
|
|
4a941252f8 | ||
|
|
2335eb7b7f | ||
|
|
4e0aa269eb | ||
|
|
abae0b5eb1 | ||
|
|
4f6830fdd6 | ||
|
|
be09eb9d47 | ||
|
|
1398686cb7 | ||
|
|
bb315a8cbc | ||
|
|
b7be82bd41 | ||
|
|
965a7bcc41 | ||
|
|
fc59574f58 | ||
|
|
c8229fc5f2 | ||
|
|
7dd9574d32 | ||
|
|
c5dd17e7e2 | ||
|
|
a129b453b0 | ||
|
|
392fb2c40d | ||
|
|
3ecb2b4d13 | ||
|
|
f3dcca2d31 | ||
|
|
327c447b9a | ||
|
|
62afbc32fb | ||
|
|
5677646bd5 | ||
|
|
53b4d184ac | ||
|
|
1cabf88445 | ||
|
|
b47303f6d6 | ||
|
|
b48321154f | ||
|
|
f70048bd6c | ||
|
|
4b592f9c94 | ||
|
|
f5f5eab09e | ||
|
|
324aea004d | ||
|
|
dc07f0c6de | ||
|
|
2e54afd3a7 | ||
|
|
e41aedeb06 | ||
|
|
c3f8c0a871 | ||
|
|
95901e5220 | ||
|
|
d5609abf25 | ||
|
|
cf3b206ef4 | ||
|
|
951e22242d | ||
|
|
cc356cf542 | ||
|
|
f0b3a7241d | ||
|
|
e7bc3821d1 | ||
|
|
d2647e066e | ||
|
|
b7ca70bd1e | ||
|
|
564ed21b74 | ||
|
|
5c7f547342 | ||
|
|
75e97da41e | ||
|
|
059fbaa92b | ||
|
|
76ab562810 | ||
|
|
9472e72678 | ||
|
|
9440423a73 | ||
|
|
b2867eb0d7 | ||
|
|
5590f8bad9 | ||
|
|
c15f93e48d | ||
|
|
03d224acd0 | ||
|
|
c24ce9b55a | ||
|
|
7243bd34b9 | ||
|
|
d4dd02c744 | ||
|
|
3817f00803 | ||
|
|
f066c1c2ef | ||
|
|
1d41cb3d29 | ||
|
|
1858d56918 | ||
|
|
97082efd99 | ||
|
|
187610d516 | ||
|
|
00b2cb2076 | ||
|
|
426f6748e5 | ||
|
|
c423bf92a8 | ||
|
|
f59bf35615 | ||
|
|
0f056c4955 | ||
|
|
eb2c7ff6c6 | ||
|
|
9f614517c0 | ||
|
|
5432984af8 | ||
|
|
c29a3d902b | ||
|
|
721b1b06a0 |
@@ -35,7 +35,7 @@ Misskey リポジトリの `.claude/` 構成を 7 カテゴリで採点し、改
|
||||
| 2 | Context Efficiency | frontmatter description の冗長度、SKILL.md の長さ分布、重複情報、CLAUDE.md の肥大化 |
|
||||
| 3 | Quality Gates | Stop / PreToolUse / PostToolUse hook の整備、`/quality-gate` 等の完了前ゲートの有無、自動 lint/typecheck |
|
||||
| 4 | Memory Persistence | `.claude/skills/*/SKILL.md` と `references/` の同期状態を評価。プロジェクト側 `.claude/memory/` は未採用方針 (auto-memory はユーザーホーム側で自動運用) のため、ここを採点起点にせず既定 5/10 から開始する |
|
||||
| 5 | Eval Coverage | `working-on-backend` / `working-on-frontend` の testing リファレンス (backend-testing.md / frontend-testing.md) の網羅、Misskey 固有の e2e/fed/Storybook/Cypress 適用ガイド |
|
||||
| 5 | Eval Coverage | `working-on-backend` / `working-on-frontend` の testing リファレンス (backend-testing.md / frontend-testing.md) の網羅、Misskey 固有の e2e/fed/Storybook/Playwright 適用ガイド |
|
||||
| 6 | Security Guardrails | SPDX 規約適用、migration 不変性ルール、ja-JP.yml 限定編集ルール、secrets 検出 |
|
||||
| 7 | Cost Efficiency | enabledPlugins の重複・過剰、context-budget の整備、MCP 過剰登録なし |
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ note: 元 ECC 版は言語自動判定 + format/lint/type のジェネリック
|
||||
|
||||
`/quality-gate [scope]`
|
||||
|
||||
完了前の **軽量** 品質チェック。重い E2E / 連合テスト (test:e2e / test:fed / Cypress) は CI 側で実行されるため、本コマンドには含めない。
|
||||
完了前の **軽量** 品質チェック。重い E2E / 連合テスト (test:e2e / test:fed / Playwright) は CI 側で実行されるため、本コマンドには含めない。
|
||||
|
||||
## Scope
|
||||
|
||||
@@ -120,4 +120,4 @@ Frontend ut: PASS (87/87)
|
||||
|
||||
- ジェネリックな言語自動判定を排除し、Misskey 固定 pipeline に。
|
||||
- formatter フェーズなし (Misskey は ESLint --fix のみ採用)。
|
||||
- e2e / federation / Cypress は重いため除外し CI 側に委譲。
|
||||
- e2e / federation / Playwright は重いため除外し CI 側に委譲。
|
||||
|
||||
@@ -26,7 +26,7 @@ cp .github/misskey/test.yml .config/test.yml
|
||||
|
||||
補足:
|
||||
|
||||
- ルートの `pnpm start:test` (Cypress 用にテストサーバーを起動するコマンド) を使う経路では実行時に `ncp` で自動コピーされる ([package.json](../../../../../package.json))。それ以外で backend テストを直接走らせる時は上記の手動コピーが必要
|
||||
- ルートの `pnpm start:test` (Playwright 用にテストサーバーを起動するコマンド) を使う経路では実行時に `ncp` で自動コピーされる ([package.json](../../../../../package.json))。それ以外で backend テストを直接走らせる時は上記の手動コピーが必要
|
||||
- すでに `.config/test.yml` があれば各テストスクリプトの内部 `compile-config` で十分なので、追加で `pnpm --filter backend compile-config` を叩く必要はない
|
||||
- `pnpm start:test` は backend e2e テスト (`pnpm --filter backend test:e2e`) の前提ではない (ポート競合の元になるため使わないこと)
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ SKILL.md 本体は references への索引だけ。具体的な手順や規約
|
||||
- SCSS Modules / `--MI_THEME-*` `--MI-*` CSS 変数 / グローバル utility class (`_button` 等) → [references/knowledge/scss-modules.md](references/knowledge/scss-modules.md)
|
||||
- `os.alert` / `os.confirm` / `os.popup` 等 UI ヘルパー (ブラウザ標準 `alert()` 直呼びは禁止) → [references/knowledge/os-api.md](references/knowledge/os-api.md)
|
||||
- `*.stories.impl.ts` 併設規則 + 複数 story / argTypes / layout / action パターン → [references/knowledge/storybook.md](references/knowledge/storybook.md)
|
||||
- frontend Vitest / Cypress E2E の書き方と前提 → [references/knowledge/frontend-testing.md](references/knowledge/frontend-testing.md)
|
||||
- frontend Vitest / Playwright E2E の書き方と前提 → [references/knowledge/frontend-testing.md](references/knowledge/frontend-testing.md)
|
||||
|
||||
## 必ず最後に通る場所
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Frontend テスト (Vitest / Cypress)
|
||||
# Frontend テスト (Vitest / Playwright)
|
||||
|
||||
Misskey frontend のテスト構成。
|
||||
|
||||
@@ -13,11 +13,11 @@ pnpm --filter frontend test-and-coverage # カバレッジ付き
|
||||
|
||||
- 主な配置: `packages/frontend/test/*.test.ts` (例: `i18n.test.ts`, `theme.test.ts`, `is-birthday.test.ts`)
|
||||
- ビルドツール周りなど対象コードと隣接させた方が分かりやすいテストは、コードと同じディレクトリに `*.test.ts` として置く (例: [packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts](../../../../../packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts))
|
||||
- 共有コンポーネント (`MkX.vue`) のユニットテストは現状少なく、`*.spec.ts` / `__tests__/` 形式は採用していない (Storybook + Cypress でカバー)
|
||||
- 共有コンポーネント (`MkX.vue`) のユニットテストは現状少なく、`*.spec.ts` / `__tests__/` 形式は採用していない (Storybook + Playwright でカバー)
|
||||
|
||||
## Cypress E2E
|
||||
## Playwright E2E
|
||||
|
||||
Cypress は **起動済みのテストサーバー** に対して走るため、unit より前提が多い。[.github/workflows/test-frontend.yml](../../../../../.github/workflows/test-frontend.yml) の `e2e` ジョブと同じ手順をローカルで踏む:
|
||||
Playwright は **起動済みのテストサーバー** に対して走るため、unit より前提が多い。[.github/workflows/test-frontend.yml](../../../../../.github/workflows/test-frontend.yml) の `e2e` ジョブと同じ手順をローカルで踏む:
|
||||
|
||||
```bash
|
||||
# 1. テスト用 DB / Redis を起動 (テスト用ポート。開発用の compose.local-db.yml ではない)
|
||||
@@ -29,15 +29,14 @@ cp .github/misskey/test.yml .config/test.yml
|
||||
# 3. 全体ビルド
|
||||
pnpm build
|
||||
|
||||
# 4. テストサーバー起動 + Cypress 実行 (いずれもルートから)
|
||||
pnpm e2e # 内部で pnpm start:test を起動し http://localhost:61812 を待って Cypress run
|
||||
pnpm cy:open # 対話的に開く (サーバーは別途 pnpm start:test で起動しておく)
|
||||
# 4. テストサーバー起動 + Playwright 実行 (いずれもルートから)
|
||||
pnpm e2e # 内部で pnpm start:test を起動し http://localhost:61812 を待って Playwright を実行
|
||||
```
|
||||
|
||||
- 設定: ルート [cypress.config.ts](../../../../../cypress.config.ts)
|
||||
- テスト本体は [cypress/](../../../../../cypress/) 配下
|
||||
- 設定: ルート [packages/frontend/playwright.config.ts](../../../../../packages/frontend/playwright.config.ts) で、`packages/frontend/test/e2e/` 配下の `*.spec.ts` を対象にする
|
||||
- テスト本体は [packages/frontend/test/e2e/](../../../../../packages/frontend/test/e2e/) 配下に配置する。
|
||||
|
||||
新規 frontend 機能の E2E は Cypress に書くのが基本。ただし対象は主要 UI フロー (login / post / drive etc) に限定し、細かい単位テストは Vitest または Storybook で代替する慣習。
|
||||
新規 frontend 機能の E2E は Playwright に書くのが基本。ただし対象は主要 UI フロー (login / post / drive etc) に限定し、細かい単位テストは Vitest または Storybook で代替する慣習。
|
||||
|
||||
## Storybook (視覚確認 + Chromatic 視覚回帰)
|
||||
|
||||
@@ -55,6 +54,6 @@ pnpm --filter frontend build-storybook # 静的ビルド
|
||||
frontend のテスト種別で DB / Redis の要否が違う:
|
||||
|
||||
- **Vitest (unit)** — DB 不要。ロジック / コンポーネント単体のテストで backend に繋がない (CI の `vitest` ジョブにも `services:` は無い)
|
||||
- **Cypress (E2E)** — テストサーバー (`pnpm start:test`) 経由で backend に繋ぐため DB / Redis が必要。**テスト用ポートの [packages/backend/test/compose.yml](../../../../../packages/backend/test/compose.yml)** を使う (上記 Cypress E2E の手順を参照)
|
||||
- **Playwright (E2E)** — テストサーバー (`pnpm start:test`) 経由で backend に繋ぐため DB / Redis が必要。**テスト用ポートの [packages/backend/test/compose.yml](../../../../../packages/backend/test/compose.yml)** を使う (上記 Playwright E2E の手順を参照)
|
||||
|
||||
開発用の `compose.local-db.yml` (db `5432` / redis `6379`) は **テストには使わない**。テスト用の `packages/backend/test/compose.yml` (`54312` / `56312`) とはポートが異なり、混同すると接続できない。
|
||||
|
||||
@@ -303,8 +303,31 @@ id: 'aidx'
|
||||
|
||||
#sentryForBackend:
|
||||
# enableNodeProfiling: true
|
||||
# # Specify Sentry integration names to disable individual auto-instrumentation.
|
||||
# # The names are integration .name values, not factory function names.
|
||||
# # To check enabled names, set `options.debug: true` and see
|
||||
# # "Integration installed: <name>" in the Sentry logs.
|
||||
# #
|
||||
# # As of 2026-07-07 / @sentry/node 10.62.0, useful names for Misskey include:
|
||||
# # Postgres ... DB queries (when pg is externalized)
|
||||
# # Redis ... ioredis commands
|
||||
# # Fastify ... inbound HTTP routes
|
||||
# # Http ... inbound/outbound HTTP; disabling this can also affect request isolation
|
||||
# # NodeFetch ... fetch/undici requests
|
||||
# disabledIntegrations: ['Postgres']
|
||||
# options:
|
||||
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
|
||||
# # By default, Misskey prevents Sentry trace headers (`sentry-trace` and
|
||||
# # `baggage`) from being sent to remote ActivityPub/Webhook/etc. hosts.
|
||||
# # To intentionally propagate distributed traces to trusted internal services,
|
||||
# # list only those internal URL patterns here.
|
||||
# # Avoid broad patterns that match remote servers unless you intentionally
|
||||
# # want to restore Sentry's legacy behavior of propagating traces to all
|
||||
# # outbound HTTP requests.
|
||||
# #tracePropagationTargets: []
|
||||
# # Internal allowlist example:
|
||||
# #tracePropagationTargets:
|
||||
# # - 'internal-service.example'
|
||||
|
||||
#sentryForFrontend:
|
||||
# vueIntegration:
|
||||
|
||||
@@ -12,4 +12,4 @@ pnpm install --frozen-lockfile
|
||||
cp .devcontainer/devcontainer.yml .config/default.yml
|
||||
pnpm build
|
||||
pnpm migrate
|
||||
pnpm exec cypress install
|
||||
pnpm --filter frontend exec playwright install --with-deps chromium
|
||||
|
||||
2
.github/labeler.yml
vendored
2
.github/labeler.yml
vendored
@@ -16,7 +16,7 @@
|
||||
'packages/frontend:test':
|
||||
- any:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['cypress/**/*']
|
||||
- any-glob-to-any-file: ['packages/frontend/test/**/*']
|
||||
|
||||
'packages/sw':
|
||||
- any:
|
||||
|
||||
1
.github/workflows/check-spdx-license-id.yml
vendored
1
.github/workflows/check-spdx-license-id.yml
vendored
@@ -44,7 +44,6 @@ jobs:
|
||||
}
|
||||
|
||||
directories=(
|
||||
"cypress/e2e"
|
||||
"packages/backend/migration"
|
||||
"packages/backend/src"
|
||||
"packages/backend/test"
|
||||
|
||||
29
.github/workflows/lint.yml
vendored
29
.github/workflows/lint.yml
vendored
@@ -16,10 +16,10 @@ on:
|
||||
- packages/misskey-js/**
|
||||
- packages/misskey-bubble-game/**
|
||||
- packages/misskey-reversi/**
|
||||
- packages/misskey-world/**
|
||||
- packages/frontend-misskey-world-engine/**
|
||||
- packages/shared/eslint.config.js
|
||||
- scripts/check-dts*.mjs
|
||||
- .github/workflows/lint.yml
|
||||
- package.json
|
||||
pull_request:
|
||||
paths:
|
||||
- packages/backend/**
|
||||
@@ -32,10 +32,10 @@ on:
|
||||
- packages/misskey-js/**
|
||||
- packages/misskey-bubble-game/**
|
||||
- packages/misskey-reversi/**
|
||||
- packages/misskey-world/**
|
||||
- packages/frontend-misskey-world-engine/**
|
||||
- packages/shared/eslint.config.js
|
||||
- scripts/check-dts*.mjs
|
||||
- .github/workflows/lint.yml
|
||||
- package.json
|
||||
jobs:
|
||||
pnpm_install:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -69,8 +69,6 @@ jobs:
|
||||
- misskey-js
|
||||
- misskey-bubble-game
|
||||
- misskey-reversi
|
||||
- misskey-world
|
||||
- frontend-misskey-world-engine
|
||||
env:
|
||||
eslint-cache-version: v1
|
||||
eslint-cache-path: ${{ github.workspace }}/node_modules/.cache/eslint-${{ matrix.workspace }}
|
||||
@@ -119,3 +117,22 @@ jobs:
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- run: pnpm --filter "${{ matrix.workspace }}^..." run build
|
||||
- run: pnpm --filter ${{ matrix.workspace }} run typecheck
|
||||
|
||||
check-dts:
|
||||
needs: [pnpm_install]
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- run: node --test scripts/check-dts.test.mjs
|
||||
- run: pnpm check-dts
|
||||
|
||||
2
.github/workflows/report-api-diff.yml
vendored
2
.github/workflows/report-api-diff.yml
vendored
@@ -65,7 +65,7 @@ jobs:
|
||||
echo '```diff' >> ./output.md
|
||||
cat ./api.json.diff >> ./output.md
|
||||
echo '```' >> ./output.md
|
||||
echo '</details>' >> ./output.md
|
||||
echo '</details>' >> .output.md
|
||||
fi
|
||||
|
||||
echo "$FOOTER" >> ./output.md
|
||||
|
||||
40
.github/workflows/test-frontend.yml
vendored
40
.github/workflows/test-frontend.yml
vendored
@@ -57,11 +57,6 @@ jobs:
|
||||
name: E2E tests (frontend)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
browser: [chrome]
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18
|
||||
@@ -79,12 +74,6 @@ jobs:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
with:
|
||||
submodules: true
|
||||
# https://github.com/cypress-io/cypress-docker-images/issues/150
|
||||
#- name: Install mplayer for FireFox
|
||||
# run: sudo apt install mplayer -y
|
||||
# if: ${{ matrix.browser == 'firefox' }}
|
||||
#- uses: browser-actions/setup-firefox@latest
|
||||
# if: ${{ matrix.browser == 'firefox' }}
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- name: Use Node.js
|
||||
@@ -97,29 +86,14 @@ jobs:
|
||||
run: cp .github/misskey/test.yml .config
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
# https://github.com/cypress-io/cypress/issues/4351#issuecomment-559489091
|
||||
- name: ALSA Env
|
||||
run: echo -e 'pcm.!default {\n type hw\n card 0\n}\n\nctl.!default {\n type hw\n card 0\n}' > ~/.asoundrc
|
||||
# XXX: This tries reinstalling Cypress if the binary is not cached
|
||||
# Remove this when the cache issue is fixed
|
||||
- name: Cypress install
|
||||
run: pnpm exec cypress install
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@v7.4.0
|
||||
- name: Playwright install
|
||||
working-directory: packages/frontend
|
||||
run: pnpm exec playwright install --with-deps chromium
|
||||
- name: Test
|
||||
run: pnpm start-server-and-test start:test http://localhost:61812 "pnpm --filter frontend test:e2e"
|
||||
timeout-minutes: 15
|
||||
with:
|
||||
install: false
|
||||
start: pnpm start:test
|
||||
wait-on: 'http://localhost:61812'
|
||||
headed: true
|
||||
browser: ${{ matrix.browser }}
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: failure()
|
||||
with:
|
||||
name: ${{ matrix.browser }}-cypress-screenshots
|
||||
path: cypress/screenshots
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: ${{ matrix.browser }}-cypress-videos
|
||||
path: cypress/videos
|
||||
name: playwright-e2e-artifacts
|
||||
path: packages/frontend/test/e2e/artifacts
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -23,9 +23,8 @@ packages/sw/.yarn/cache
|
||||
# pnpm
|
||||
.pnpm-store
|
||||
|
||||
# Cypress
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
# Playwright
|
||||
packages/frontend/test/e2e/artifacts
|
||||
|
||||
# Coverage
|
||||
coverage
|
||||
@@ -35,7 +34,7 @@ coverage
|
||||
!/.config/example.yml
|
||||
!/.config/docker_example.yml
|
||||
!/.config/docker_example.env
|
||||
!/.config/cypress-devcontainer.yml
|
||||
!/.config/playwright-devcontainer.yml
|
||||
docker-compose.yml
|
||||
./compose.yml
|
||||
.devcontainer/compose.yml
|
||||
|
||||
20
CHANGELOG.md
20
CHANGELOG.md
@@ -1,4 +1,4 @@
|
||||
## 2026.6.1
|
||||
## 2026.7.0
|
||||
|
||||
### Note
|
||||
|
||||
@@ -15,19 +15,35 @@
|
||||
|
||||
### General
|
||||
- Feat: コントロールパネルから二要素認証を解除できるように
|
||||
- Feat: 条件に一致したURLプレビューのサムネイルを隠すことができるように
|
||||
(Based on https://github.com/MisskeyIO/misskey/pull/214)
|
||||
|
||||
### Client
|
||||
- Fix: チャットでIMEの変換を確定するEnterでメッセージが送信されてしまうことがある問題を修正
|
||||
- 2025.4.0 以前の設定情報の移行処理が削除されました
|
||||
- 2025.4.0 から直接 2026.6.0 以上にアップデートする場合は設定が移行されませんので注意してください。移行したい場合は一度 2026.5.1 を経由してください。
|
||||
- Enhance: 画像ビューワーを独自実装に変更・動画プレイヤーを統合
|
||||
- 操作性の改善
|
||||
- ビューワーとMisskeyの各種機能との統合を強化
|
||||
- パフォーマンスの向上
|
||||
- Enhance: マウスホイールで拡大縮小できるように
|
||||
- Fix: 幅が狭い画面で動画の再生が困難な問題を修正
|
||||
- Fix: 一部の画像のみセンシティブなとき、ビューワー内で画像を切り替えるとセンシティブな画像がそのまま表示される問題を修正
|
||||
- Fix: 一部の画像をビューワーで読み込んだ際に正しく表示されない問題を修正
|
||||
- Fix: 「画像を新しいタブで開く」が機能しなくなっていた問題を修正
|
||||
- Fix: デバイスタイプをスマートフォンに固定している状態で画面幅が広いとき、画面左上のアイコンが表示されない問題を修正
|
||||
- Fix: チャットでIMEの変換を確定するEnterでメッセージが送信されてしまうことがある問題を修正
|
||||
- Fix: 自分へのメンションに対する色分けで、判定が大文字/小文字を区別していた問題を修正
|
||||
- Fix: いくつかのイベントリスナーが正しく解除されない問題を修正(メモリ使用量の改善)
|
||||
|
||||
### Server
|
||||
- Enhance: センシティブメディアの判定を外部サービス ([sensitive-detector](https://github.com/misskey-dev/sensitive-detector)) に分離し、`nsfwjs` / `@tensorflow/tfjs(-node)` の同梱と NSFW 判定モデルを廃止 (#16804)
|
||||
- Enhance: Sentry バックエンドの自動計装を `sentryForBackend.disabledIntegrations` で個別に無効化できるように
|
||||
- Enhance: Node.js 22.23.0以降、24.17.0以降、26.4.0以降をサポートするように
|
||||
- Enhance: Docker Image の Node.js を 26.4.0 に、Debian を trixie (v13) に更新
|
||||
- Fix: `/stats` API のレスポンス型が正しくない問題を修正
|
||||
- Fix: ハッシュタグに関連するデータを更新する際のエラーハンドリングを修正
|
||||
- Fix: Sentry 使用環境下にて、Misskey が発行した SQL クエリが span に含まれない問題を修正
|
||||
- Fix: Sentry 使用環境下にて、外部送信リクエストへ `sentry-trace` / `baggage` ヘッダーが既定で付与されないように
|
||||
|
||||
## 2026.6.0
|
||||
|
||||
|
||||
10
Dockerfile
10
Dockerfile
@@ -30,8 +30,6 @@ COPY --link ["packages/sw/package.json", "./packages/sw/"]
|
||||
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
|
||||
COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"]
|
||||
COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bubble-game/"]
|
||||
COPY --link ["packages/misskey-world/package.json", "./packages/misskey-world/"]
|
||||
COPY --link ["packages/frontend-misskey-world-engine/package.json", "./packages/frontend-misskey-world-engine/"]
|
||||
|
||||
ARG NODE_ENV=production
|
||||
|
||||
@@ -63,8 +61,6 @@ COPY --link ["packages/backend/package.json", "./packages/backend/"]
|
||||
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
|
||||
COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"]
|
||||
COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bubble-game/"]
|
||||
COPY --link ["packages/misskey-world/package.json", "./packages/misskey-world/"]
|
||||
COPY --link ["packages/frontend-misskey-world-engine/package.json", "./packages/frontend-misskey-world-engine/"]
|
||||
|
||||
ARG NODE_ENV=production
|
||||
|
||||
@@ -103,18 +99,12 @@ COPY --chown=misskey:misskey --from=target-builder /misskey/packages/backend/nod
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-reversi/node_modules ./packages/misskey-reversi/node_modules
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-bubble-game/node_modules ./packages/misskey-bubble-game/node_modules
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-world/node_modules ./packages/misskey-world/node_modules
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/frontend-misskey-world-engine/node_modules ./packages/frontend-misskey-world-engine/node_modules
|
||||
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/built ./built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-js/built ./packages/misskey-js/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-reversi/built ./packages/misskey-reversi/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-bubble-game/built ./packages/misskey-bubble-game/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/built ./packages/backend/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/i18n/built ./packages/i18n/built
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-world/built ./packages/misskey-world/built
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/frontend-misskey-world-engine/built ./packages/frontend-misskey-world-engine/built
|
||||
|
||||
COPY --chown=misskey:misskey . ./
|
||||
|
||||
ENV LD_PRELOAD=/usr/local/lib/libjemalloc.so
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { defineConfig } from 'cypress'
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
baseUrl: 'http://localhost:61812',
|
||||
},
|
||||
})
|
||||
@@ -1,265 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
describe('Before setup instance', () => {
|
||||
beforeEach(() => {
|
||||
cy.resetState();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
|
||||
// waitを入れることでそれを防止できる
|
||||
cy.wait(1000);
|
||||
});
|
||||
|
||||
it('successfully loads', () => {
|
||||
cy.visitHome();
|
||||
});
|
||||
|
||||
it('setup instance', () => {
|
||||
cy.visitHome();
|
||||
|
||||
cy.intercept('POST', '/api/admin/accounts/create').as('signup');
|
||||
|
||||
cy.get('[data-cy-admin-initial-password] input').type('example_password_please_change_this_or_you_will_get_hacked');
|
||||
cy.get('[data-cy-admin-username] input').type('admin');
|
||||
cy.get('[data-cy-admin-password] input').type('admin1234');
|
||||
cy.get('[data-cy-admin-ok]').click();
|
||||
|
||||
// なぜか動かない
|
||||
//cy.wait('@signup').should('have.property', 'response.statusCode');
|
||||
cy.wait('@signup');
|
||||
|
||||
cy.intercept('POST', '/api/admin/update-meta').as('update-meta');
|
||||
|
||||
cy.get('[data-cy-next]').click();
|
||||
cy.get('[data-cy-server-name] input').type('Testskey');
|
||||
cy.get('[data-cy-server-setup-wizard-apply]').click();
|
||||
|
||||
cy.wait('@update-meta');
|
||||
});
|
||||
});
|
||||
|
||||
describe('After setup instance', () => {
|
||||
beforeEach(() => {
|
||||
cy.resetState();
|
||||
|
||||
// インスタンス初期セットアップ
|
||||
cy.registerUser('admin', 'pass', true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
|
||||
// waitを入れることでそれを防止できる
|
||||
cy.wait(1000);
|
||||
});
|
||||
|
||||
it('successfully loads', () => {
|
||||
cy.visitHome();
|
||||
});
|
||||
|
||||
it('signup', () => {
|
||||
cy.visitHome();
|
||||
|
||||
cy.intercept('POST', '/api/signup').as('signup');
|
||||
|
||||
cy.get('[data-cy-signup]').click();
|
||||
cy.get('[data-cy-signup-rules-continue]').should('be.disabled');
|
||||
cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click();
|
||||
cy.get('[data-cy-modal-dialog-ok]').click();
|
||||
cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled');
|
||||
cy.get('[data-cy-signup-rules-continue]').click();
|
||||
|
||||
cy.get('[data-cy-signup-submit]').should('be.disabled');
|
||||
cy.get('[data-cy-signup-username] input').type('alice');
|
||||
cy.get('[data-cy-signup-submit]').should('be.disabled');
|
||||
cy.get('[data-cy-signup-password] input').type('alice1234');
|
||||
cy.get('[data-cy-signup-submit]').should('be.disabled');
|
||||
cy.get('[data-cy-signup-password-retype] input').type('alice1234');
|
||||
cy.get('[data-cy-signup-submit]').should('be.disabled');
|
||||
cy.get('[data-cy-signup-invitation-code] input').type('test-invitation-code');
|
||||
cy.get('[data-cy-signup-submit]').should('not.be.disabled');
|
||||
cy.get('[data-cy-signup-submit]').click();
|
||||
|
||||
cy.wait('@signup');
|
||||
});
|
||||
|
||||
it('signup with duplicated username', () => {
|
||||
cy.registerUser('alice', 'alice1234');
|
||||
|
||||
cy.visitHome();
|
||||
|
||||
// ユーザー名が重複している場合の挙動確認
|
||||
cy.get('[data-cy-signup]').click();
|
||||
cy.get('[data-cy-signup-rules-continue]').should('be.disabled');
|
||||
cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click();
|
||||
cy.get('[data-cy-modal-dialog-ok]').click();
|
||||
cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled');
|
||||
cy.get('[data-cy-signup-rules-continue]').click();
|
||||
|
||||
cy.get('[data-cy-signup-username] input').type('alice');
|
||||
cy.get('[data-cy-signup-password] input').type('alice1234');
|
||||
cy.get('[data-cy-signup-password-retype] input').type('alice1234');
|
||||
cy.get('[data-cy-signup-submit]').should('be.disabled');
|
||||
});
|
||||
});
|
||||
|
||||
describe('After user signup', () => {
|
||||
beforeEach(() => {
|
||||
cy.resetState();
|
||||
|
||||
// インスタンス初期セットアップ
|
||||
cy.registerUser('admin', 'pass', true);
|
||||
|
||||
// ユーザー作成
|
||||
cy.registerUser('alice', 'alice1234');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
|
||||
// waitを入れることでそれを防止できる
|
||||
cy.wait(1000);
|
||||
});
|
||||
|
||||
it('successfully loads', () => {
|
||||
cy.visitHome();
|
||||
});
|
||||
|
||||
it('signin', () => {
|
||||
cy.visitHome();
|
||||
|
||||
cy.intercept('POST', '/api/signin-flow').as('signin');
|
||||
|
||||
cy.get('[data-cy-signin]').click();
|
||||
|
||||
cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
|
||||
// Enterキーで続行できるかの確認も兼ねる
|
||||
cy.get('[data-cy-signin-username] input').type('alice{enter}');
|
||||
|
||||
cy.get('[data-cy-signin-page-password]').should('be.visible', { timeout: 10000 });
|
||||
// Enterキーで続行できるかの確認も兼ねる
|
||||
cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
|
||||
|
||||
cy.wait('@signin');
|
||||
});
|
||||
|
||||
it('suspend', function() {
|
||||
cy.request('POST', '/api/admin/suspend-user', {
|
||||
i: this.admin.token,
|
||||
userId: this.alice.id,
|
||||
});
|
||||
|
||||
cy.visitHome();
|
||||
|
||||
cy.get('[data-cy-signin]').click();
|
||||
|
||||
cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
|
||||
cy.get('[data-cy-signin-username] input').type('alice{enter}');
|
||||
|
||||
// TODO: cypressにブラウザの言語指定できる機能が実装され次第英語のみテストするようにする
|
||||
cy.contains(/アカウントが凍結されています|This account has been suspended due to/gi);
|
||||
});
|
||||
});
|
||||
|
||||
describe('After user signed in', () => {
|
||||
beforeEach(() => {
|
||||
cy.resetState();
|
||||
|
||||
// インスタンス初期セットアップ
|
||||
cy.registerUser('admin', 'pass', true);
|
||||
|
||||
// ユーザー作成
|
||||
cy.registerUser('alice', 'alice1234');
|
||||
|
||||
cy.login('alice', 'alice1234');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
|
||||
// waitを入れることでそれを防止できる
|
||||
cy.wait(1000);
|
||||
});
|
||||
|
||||
it('successfully loads', () => {
|
||||
// 表示に時間がかかるのでデフォルト秒数だとタイムアウトする
|
||||
cy.get('[data-cy-user-setup-continue]', { timeout: 30000 }).should('be.visible');
|
||||
});
|
||||
|
||||
it('account setup wizard', () => {
|
||||
// 表示に時間がかかるのでデフォルト秒数だとタイムアウトする
|
||||
cy.get('[data-cy-user-setup-continue]', { timeout: 30000 }).click();
|
||||
|
||||
cy.get('[data-cy-user-setup-user-name] input').type('ありす');
|
||||
cy.get('[data-cy-user-setup-user-description] textarea').type('ほげ');
|
||||
// TODO: アイコン設定テスト
|
||||
|
||||
cy.get('[data-cy-user-setup-continue]').click();
|
||||
|
||||
// プライバシー設定
|
||||
|
||||
cy.get('[data-cy-user-setup-continue]').click();
|
||||
|
||||
// フォローはスキップ
|
||||
|
||||
cy.get('[data-cy-user-setup-continue]').click();
|
||||
|
||||
// プッシュ通知設定はスキップ
|
||||
|
||||
cy.get('[data-cy-user-setup-continue]').click();
|
||||
|
||||
cy.get('[data-cy-user-setup-continue]').click();
|
||||
});
|
||||
});
|
||||
|
||||
describe('After user setup', () => {
|
||||
beforeEach(() => {
|
||||
cy.resetState();
|
||||
|
||||
// インスタンス初期セットアップ
|
||||
cy.registerUser('admin', 'pass', true);
|
||||
|
||||
// ユーザー作成
|
||||
cy.registerUser('alice', 'alice1234');
|
||||
|
||||
cy.login('alice', 'alice1234');
|
||||
|
||||
// アカウント初期設定ウィザード
|
||||
// 表示に時間がかかるのでデフォルト秒数だとタイムアウトする
|
||||
cy.get('[data-cy-user-setup] [data-cy-modal-window-close]', { timeout: 30000 }).click();
|
||||
cy.get('[data-cy-modal-dialog-ok]').click();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
|
||||
// waitを入れることでそれを防止できる
|
||||
cy.wait(1000);
|
||||
});
|
||||
|
||||
it('note', () => {
|
||||
cy.get('[data-cy-open-post-form]').should('be.visible');
|
||||
cy.get('[data-cy-open-post-form]').click();
|
||||
cy.get('[data-cy-post-form-text]').type('Hello, Misskey!');
|
||||
cy.get('[data-cy-open-post-form-submit]').click();
|
||||
|
||||
cy.contains('Hello, Misskey!', { timeout: 15000 });
|
||||
});
|
||||
|
||||
it('open note form with hotkey', () => {
|
||||
// Wait until the page loads
|
||||
cy.get('[data-cy-open-post-form]').should('be.visible');
|
||||
// Use trigger() to give different `code` to test if hotkeys also work on non-QWERTY keyboards.
|
||||
cy.document().trigger("keydown", { eventConstructor: 'KeyboardEvent', key: "n", code: "KeyL" });
|
||||
// See if the form is opened
|
||||
cy.get('[data-cy-post-form-text]').should('be.visible');
|
||||
// Close it
|
||||
cy.focused().trigger("keydown", { eventConstructor: 'KeyboardEvent', key: "Escape", code: "Escape" });
|
||||
// See if the form is closed
|
||||
cy.get('[data-cy-post-form-text]').should('not.be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: 投稿フォームの公開範囲指定のテスト
|
||||
// TODO: 投稿フォームのファイル添付のテスト
|
||||
// TODO: 投稿フォームのハッシュタグ保持フィールドのテスト
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
describe('Router transition', () => {
|
||||
describe('Redirect', () => {
|
||||
// サーバの初期化。ルートのテストに関しては各describeごとに1度だけ実行で十分だと思う(使いまわした方が早い)
|
||||
before(() => {
|
||||
cy.resetState();
|
||||
|
||||
// インスタンス初期セットアップ
|
||||
cy.registerUser('admin', 'pass', true);
|
||||
|
||||
// ユーザー作成
|
||||
cy.registerUser('alice', 'alice1234');
|
||||
|
||||
cy.login('alice', 'alice1234');
|
||||
|
||||
// アカウント初期設定ウィザード
|
||||
// 表示に時間がかかるのでデフォルト秒数だとタイムアウトする
|
||||
cy.get('[data-cy-user-setup] [data-cy-modal-window-close]', { timeout: 30000 }).click();
|
||||
cy.wait(500);
|
||||
cy.get('[data-cy-modal-dialog-ok]').click();
|
||||
});
|
||||
|
||||
it('redirect to user profile', () => {
|
||||
// テストのためだけに用意されたリダイレクト用ルートに飛ぶ
|
||||
cy.visit('/redirect-test');
|
||||
|
||||
// プロフィールページのURLであることを確認する
|
||||
cy.url().should('include', '/@alice')
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,76 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/* flaky
|
||||
describe('After user signed in', () => {
|
||||
beforeEach(() => {
|
||||
cy.resetState();
|
||||
cy.viewport('macbook-16');
|
||||
|
||||
// インスタンス初期セットアップ
|
||||
cy.registerUser('admin', 'pass', true);
|
||||
|
||||
// ユーザー作成
|
||||
cy.registerUser('alice', 'alice1234');
|
||||
|
||||
cy.login('alice', 'alice1234');
|
||||
|
||||
// アカウント初期設定ウィザード
|
||||
cy.get('[data-cy-user-setup] [data-cy-modal-window-close]').click();
|
||||
cy.get('[data-cy-modal-dialog-ok]').click();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
|
||||
// waitを入れることでそれを防止できる
|
||||
cy.wait(1000);
|
||||
});
|
||||
|
||||
it('widget edit toggle is visible', () => {
|
||||
cy.get('[data-cy-widget-edit]').should('be.visible');
|
||||
});
|
||||
|
||||
it('widget select should be visible in edit mode', () => {
|
||||
cy.get('[data-cy-widget-edit]').click();
|
||||
cy.get('[data-cy-widget-select]').should('be.visible');
|
||||
});
|
||||
|
||||
it('first widget should be removed', () => {
|
||||
cy.get('[data-cy-widget-edit]').click();
|
||||
cy.get('[data-cy-customize-container]:first-child [data-cy-customize-container-remove]._button').click();
|
||||
cy.get('[data-cy-customize-container]').should('have.length', 2);
|
||||
});
|
||||
|
||||
function buildWidgetTest(widgetName) {
|
||||
it(`${widgetName} widget should get added`, () => {
|
||||
cy.get('[data-cy-widget-edit]').click();
|
||||
cy.get('[data-cy-widget-select] select').select(widgetName, { force: true });
|
||||
cy.get('[data-cy-bg]._modalBg[data-cy-transparent]').click({ multiple: true, force: true });
|
||||
cy.get('[data-cy-widget-add]').click({ force: true });
|
||||
cy.get(`[data-cy-mkw-${widgetName}]`).should('exist');
|
||||
});
|
||||
}
|
||||
|
||||
buildWidgetTest('memo');
|
||||
buildWidgetTest('notifications');
|
||||
buildWidgetTest('timeline');
|
||||
buildWidgetTest('calendar');
|
||||
buildWidgetTest('rss');
|
||||
buildWidgetTest('trends');
|
||||
buildWidgetTest('clock');
|
||||
buildWidgetTest('activity');
|
||||
buildWidgetTest('photos');
|
||||
buildWidgetTest('digitalClock');
|
||||
buildWidgetTest('federation');
|
||||
buildWidgetTest('postForm');
|
||||
buildWidgetTest('slideshow');
|
||||
buildWidgetTest('serverMetric');
|
||||
buildWidgetTest('onlineUsers');
|
||||
buildWidgetTest('jobQueue');
|
||||
buildWidgetTest('button');
|
||||
buildWidgetTest('aiscript');
|
||||
buildWidgetTest('aichan');
|
||||
});
|
||||
*/
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
|
||||
Cypress.Commands.add('visitHome', () => {
|
||||
cy.visit('/');
|
||||
cy.get('button', { timeout: 30000 }).should('be.visible');
|
||||
})
|
||||
|
||||
Cypress.Commands.add('resetState', () => {
|
||||
// iframe.contentWindow.indexedDB.deleteDatabase() がchromeのバグで使用できないため、indexedDBを無効化している。
|
||||
// see https://github.com/misskey-dev/misskey/issues/13605#issuecomment-2053652123
|
||||
/*
|
||||
cy.window().then(win => {
|
||||
win.indexedDB.deleteDatabase('keyval-store');
|
||||
});
|
||||
*/
|
||||
cy.request('POST', '/api/reset-db', {}).as('reset');
|
||||
cy.get('@reset').its('status').should('equal', 204);
|
||||
cy.reload(true);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('registerUser', (username, password, isAdmin = false) => {
|
||||
const route = isAdmin ? '/api/admin/accounts/create' : '/api/signup';
|
||||
|
||||
cy.request('POST', route, {
|
||||
username: username,
|
||||
password: password,
|
||||
...(isAdmin ? { setupPassword: 'example_password_please_change_this_or_you_will_get_hacked' } : {}),
|
||||
}).its('body').as(username);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('login', (username, password) => {
|
||||
cy.visitHome();
|
||||
|
||||
cy.intercept('POST', '/api/signin-flow').as('signin');
|
||||
|
||||
cy.get('[data-cy-signin]').click();
|
||||
cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
|
||||
cy.get('[data-cy-signin-username] input').type(`${username}{enter}`);
|
||||
cy.get('[data-cy-signin-page-password]').should('be.visible', { timeout: 10000 });
|
||||
cy.get('[data-cy-signin-password] input').type(`${password}{enter}`);
|
||||
|
||||
cy.wait('@signin').as('signedIn');
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
|
||||
Cypress.on('uncaught:exception', (err, runnable) => {
|
||||
if ([
|
||||
'The source image cannot be decoded',
|
||||
|
||||
// Chrome
|
||||
'ResizeObserver loop limit exceeded',
|
||||
|
||||
// Firefox
|
||||
'ResizeObserver loop completed with undelivered notifications',
|
||||
].some(msg => err.message.includes(msg))) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
login(username: string, password: string): Chainable<void>;
|
||||
|
||||
registerUser(
|
||||
username: string,
|
||||
password: string,
|
||||
isAdmin?: boolean
|
||||
): Chainable<void>;
|
||||
|
||||
resetState(): Chainable<void>;
|
||||
|
||||
visitHome(): Chainable<void>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom"],
|
||||
"target": "esnext",
|
||||
"types": ["cypress", "node"]
|
||||
},
|
||||
"include": ["./**/*.ts"]
|
||||
}
|
||||
@@ -1414,13 +1414,13 @@ viewRenotedChannel: "リノート先のチャンネルを見る"
|
||||
previewingTheme: "テーマのプレビュー中"
|
||||
previewingThemeRestore: "元に戻す"
|
||||
accessToken: "アクセストークン"
|
||||
choose: "選択"
|
||||
rotate: "回転"
|
||||
chooseEmojiPalette: "絵文字パレットを選択"
|
||||
addToEmojiPalette: "絵文字パレットに追加"
|
||||
emojiPaletteAlreadyAddedConfirm: "この絵文字はすでにこの絵文字パレットに含まれています。追加しなおしますか?"
|
||||
append: "末尾に追加"
|
||||
prepend: "先頭に追加"
|
||||
urlPreviewSensitiveList: "サムネイルの表示を制限するURL"
|
||||
urlPreviewSensitiveListDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。スラッシュで囲むと正規表現になります。一致した場合、サムネイルが表示されなくなります。"
|
||||
|
||||
_imageEditing:
|
||||
_vars:
|
||||
@@ -3580,476 +3580,3 @@ _qr:
|
||||
scanFile: "端末の画像をスキャン"
|
||||
raw: "テキスト"
|
||||
mfm: "MFM"
|
||||
|
||||
worldAvatar: "Worldアバター"
|
||||
worldAvatar_description: "MisskeyWorld / MisskeyRoomsで使用可能な3Dアバターを作成できます。"
|
||||
|
||||
_miWorld:
|
||||
separateRenderingThread: "描画を別スレッドに分離"
|
||||
separateRenderingThread_description: "有効にするとパフォーマンスが向上します。不安定になる場合は無効すると改善する可能性があります。"
|
||||
graphicsQuality: "グラフィックの品質"
|
||||
graphicsSettings: "グラフィック設定"
|
||||
frameRateLimitation: "フレームレート制限"
|
||||
higherValuePerformanceNote: "高くすると体験が向上しますが、消費電力が増加するなどパフォーマンスに影響を与えます。"
|
||||
resolution: "解像度"
|
||||
fov: "視野角"
|
||||
failedToInitialize: "初期化に失敗しました"
|
||||
crushed_description: "描画が継続できなくなりました。デバイスのリソース不足の可能性が考えられます。"
|
||||
antialiasing: "アンチエイリアス"
|
||||
avatar: "アバター"
|
||||
advancedCustomize: "高度なアレンジ"
|
||||
attachAccessory: "アクセサリーをつける"
|
||||
takeScreenShot: "スクリーンショット"
|
||||
onlineMenu: "オンラインメニュー"
|
||||
connectToOnline: "オンラインに接続"
|
||||
disconnectToOnline: "オンラインから切断"
|
||||
character: "キャラクター"
|
||||
sit: "座る"
|
||||
lyingDown: "寝そべる"
|
||||
standUp: "立ち上がる"
|
||||
showUsernameOnAvatar: "アバターにユーザー名を表示"
|
||||
show2dAvatarOnAvatar: "アバターにユーザーアイコンを表示"
|
||||
|
||||
_avatars:
|
||||
_default:
|
||||
body: "ボディ"
|
||||
eyes: "目"
|
||||
mouth: "口"
|
||||
|
||||
_avatarAccessories:
|
||||
mug: "マグカップ"
|
||||
_mug:
|
||||
bodyMat: "コップの素材"
|
||||
liquidMat: "液体の素材"
|
||||
mikan: "みかん"
|
||||
bolt: "ボルト"
|
||||
_bolt:
|
||||
mat: "素材"
|
||||
|
||||
_miRoom:
|
||||
snapToGrid: "グリッドにスナップ"
|
||||
gridScale: "グリッドサイズ"
|
||||
thereAreUnsavedChanges: "未保存の変更があります"
|
||||
revertAllChangesConfirmation: "全ての変更を取り消し、部屋を最後に保存した状態まで戻しますか?"
|
||||
yourDeviceNotSupported_title: "MisskeyRoomを起動できません"
|
||||
yourDeviceNotSupported_description: "お使いのデバイスがMisskeyRoomをサポートしていないか、デバイスのリソース不足などにより一時的に利用できなくなっています。\nMisskeyRoomを動作させるには、WebGPUをサポートするデバイス・ブラウザが必要です。"
|
||||
imageFit: "画像のはめ込み"
|
||||
imageFit_cover: "覆う"
|
||||
imageFit_contain: "収める"
|
||||
imageFit_stretch: "伸縮"
|
||||
material_metallic: "光沢"
|
||||
material_roughness: "粗さ"
|
||||
light_brightness: "明るさ"
|
||||
advancedCustomize: "高度なアレンジ"
|
||||
enterEditMode: "エディットモードを始める"
|
||||
exitEditMode: "エディットモードを終了"
|
||||
installFurniture: "家具を設置"
|
||||
roomCustomize: "部屋のカスタマイズ"
|
||||
changeRoomName: "ルーム名を編集"
|
||||
roomInfo: "ルーム情報"
|
||||
duplicate: "複製"
|
||||
grab: "掴む"
|
||||
furnitureCustomize: "家具のアレンジ"
|
||||
uninstallFurniture: "しまう"
|
||||
furnituresCount: "家具の数"
|
||||
attachedFilesCount: "添付ファイル数"
|
||||
_furniturePlacement:
|
||||
top: "上面設置"
|
||||
bottom: "下面設置"
|
||||
side: "側面設置"
|
||||
|
||||
_furnitures:
|
||||
haniwa: "はにわ"
|
||||
_haniwa:
|
||||
bodyMat: "本体の素材"
|
||||
insideColor: "中身の色"
|
||||
woodRingFloorLamp: "リングシェードフロアランプ"
|
||||
_woodRingFloorLamp:
|
||||
shadeMat: "シェードの素材"
|
||||
bodyMat: "本体の素材"
|
||||
light: "照明"
|
||||
a4Case: "A4ケース"
|
||||
_a4Case:
|
||||
mat: "素材"
|
||||
aircon: "エアコン"
|
||||
allInOnePc: "一体型PC"
|
||||
_allInOnePc:
|
||||
bezelMat: "ベゼルの素材"
|
||||
bodyMat: "本体の素材"
|
||||
image: "画面の画像"
|
||||
image_desktop: "デスクトップ"
|
||||
screenBrightness: "画面の明るさ"
|
||||
aquarium: "水槽"
|
||||
aromaReedDiffuser: "アロマリードディフューザー"
|
||||
_aromaReedDiffuser:
|
||||
bottleMat: "ボトルの素材"
|
||||
oilMat: "オイルの素材"
|
||||
banknote: "紙幣"
|
||||
beamLamp: "ビームランプ"
|
||||
bed: "ベッド"
|
||||
_bed:
|
||||
frameMat: "フレームの素材"
|
||||
blind: "ブラインド"
|
||||
_blind:
|
||||
angle: "羽根の回転角度"
|
||||
blades: "羽根の枚数"
|
||||
open: "開閉状態"
|
||||
book: "本"
|
||||
_book:
|
||||
height: "高さ"
|
||||
thickness: "厚み"
|
||||
variation: "バリエーション"
|
||||
width: "幅"
|
||||
books: "本の束"
|
||||
_books:
|
||||
variation: "バリエーション"
|
||||
boxWallShelf: "ボックス型ウォールシェルフ"
|
||||
_boxWallShelf:
|
||||
bodyMat: "本体の素材"
|
||||
height: "高さ"
|
||||
width: "幅"
|
||||
withBack: "背板"
|
||||
cactusS: "サボテン S"
|
||||
_cactusS:
|
||||
potMat: "鉢の素材"
|
||||
cardboardBox: "段ボール箱"
|
||||
_cardboardBox:
|
||||
variation: "種類"
|
||||
variation_aizon: "Aizon"
|
||||
variation_default: "デフォルト"
|
||||
variation_mikan: "みかん"
|
||||
ceilingFanLight: "シーリングファンライト"
|
||||
_ceilingFanLight:
|
||||
shadeMat: "シェードの素材"
|
||||
bodyMat: "本体の素材"
|
||||
ceilingFan: "シーリングファン"
|
||||
_ceilingFan:
|
||||
shadeMat: "シェードの素材"
|
||||
bodyMat: "本体の素材"
|
||||
chair: "椅子"
|
||||
_chair:
|
||||
primaryMat: "メインの素材"
|
||||
secondaryMat: "サブの素材"
|
||||
frameMat: "フレームの素材"
|
||||
clippedPicture: "留められた写真"
|
||||
_clippedPicture:
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
width: "幅"
|
||||
coffeeCup: "コーヒーカップ"
|
||||
colorBox: "カラーボックス"
|
||||
_colorBox:
|
||||
mat: "素材"
|
||||
cuboid: "直方体"
|
||||
_cuboid:
|
||||
mat: "素材"
|
||||
x: "X"
|
||||
y: "Y"
|
||||
z: "Z"
|
||||
cupNoodle: "インスタントラーメン"
|
||||
curtain: "カーテン"
|
||||
custardPudding: "プリン"
|
||||
descriptionPlate: "説明が書かれたプレート"
|
||||
desk: "デスク"
|
||||
_desk:
|
||||
boardMat: "天板の素材"
|
||||
depth: "奥行き"
|
||||
frameMat: "フレームの素材"
|
||||
width: "幅"
|
||||
desktopPc: "デスクトップPC"
|
||||
_desktopPc:
|
||||
bodyMat: "本体の素材"
|
||||
coverMat: "カバーの素材"
|
||||
inner1Mat: "内部素材1"
|
||||
inner2Mat: "内部素材2"
|
||||
inner3Mat: "内部素材3"
|
||||
ledColor: "LEDの色"
|
||||
djMixer: "DJミキサー"
|
||||
djPlayer: "DJプレーヤー"
|
||||
_djPlayer:
|
||||
image: "画像"
|
||||
"image:waveform": "波形"
|
||||
screenBrightness: "画面の明るさ"
|
||||
ductRailSpotLights: "スポットライト付きダクトレール"
|
||||
_ductRailSpotLights:
|
||||
angleH: "水平角度"
|
||||
angleV: "垂直角度"
|
||||
bodyMat: "本体の素材"
|
||||
light: "照明"
|
||||
ductTape: "ガムテープ"
|
||||
herbarium: "ハーバリウム"
|
||||
electronicDisplayBoard: "電光掲示板"
|
||||
_electronicDisplayBoard:
|
||||
frameMat: "フレームの素材"
|
||||
ledBrightness: "LEDの明るさ"
|
||||
ledColor: "LEDの色"
|
||||
text: "テキスト"
|
||||
emptyBento: "空の弁当容器"
|
||||
energyDrink: "エナジードリンク"
|
||||
envelope: "封筒"
|
||||
facialTissue: "ティッシュ"
|
||||
glassCylinderPotPlant: "ガラスシリンダーの鉢植えと植物"
|
||||
handheldGameConsole: "携帯ゲーム機"
|
||||
_handheldGameConsole:
|
||||
bodyMat: "本体の素材"
|
||||
image: "画像"
|
||||
screenBrightness: "画面の明るさ"
|
||||
hangingDuctRail: "吊り下げダクトレール"
|
||||
_hangingDuctRail:
|
||||
bodyMat: "本体の素材"
|
||||
height: "高さ"
|
||||
width: "幅"
|
||||
hangingTShirt: "吊り下げTシャツ"
|
||||
icosahedron: "正二十面体のオブジェ"
|
||||
_icosahedron:
|
||||
mat: "素材"
|
||||
ironFrameTable: "アイアンフレームテーブル"
|
||||
_ironFrameTable:
|
||||
boardMat: "天板の素材"
|
||||
depth: "奥行き"
|
||||
frameMat: "フレームの素材"
|
||||
height: "高さ"
|
||||
width: "幅"
|
||||
issyoubin: "一升瓶"
|
||||
_issyoubin:
|
||||
variation: "種類"
|
||||
keyboard: "キーボード"
|
||||
_keyboard:
|
||||
bodyMat: "本体の素材"
|
||||
keyMat: "キーの素材"
|
||||
laptopPc: "ノートPC"
|
||||
_laptopPc:
|
||||
bezelMat: "ベゼルの素材"
|
||||
bodyMat: "本体の素材"
|
||||
image: "画像"
|
||||
openAngle: "開き具合"
|
||||
screenBrightness: "画面の明るさ"
|
||||
largeMousepad: "大きいマウスパッド"
|
||||
_largeMousepad:
|
||||
image: "画像"
|
||||
lavaLamp: "ラバランプ"
|
||||
_lavaLamp:
|
||||
bodyMat: "本体の素材"
|
||||
glassMat: "ガラスの素材"
|
||||
lavaColor: "オイルの色"
|
||||
lightColor: "ランプの色"
|
||||
letterCase: "レターケース"
|
||||
lowPartitionBar: "低いパーティションバー"
|
||||
_lowPartitionBar:
|
||||
bodyMat: "本体の素材"
|
||||
width: "幅"
|
||||
miObjet: "Miオブジェ"
|
||||
miPlate: "Miプレート"
|
||||
miPlateDisplayed: "飾られたMiプレート"
|
||||
milk: "牛乳"
|
||||
mixer: "ミキサー"
|
||||
monitor: "モニター"
|
||||
_monitor:
|
||||
bodyMat: "本体の素材"
|
||||
image: "画像"
|
||||
screenBrightness: "画面の明るさ"
|
||||
monitorSpeaker: "モニタースピーカー"
|
||||
_monitorSpeaker:
|
||||
mat: "素材"
|
||||
monstera: "モンステラ"
|
||||
_monstera:
|
||||
potMat: "鉢の素材"
|
||||
mug: "マグカップ"
|
||||
_mug:
|
||||
bodyMat: "コップの素材"
|
||||
liquidMat: "液体の素材"
|
||||
newtonsCradle: "ニュートンクレードル"
|
||||
_newtonsCradle:
|
||||
frameMat: "フレームの素材"
|
||||
openedCardboardBox: "開いた段ボール箱"
|
||||
pachira: "パキラ"
|
||||
_pachira:
|
||||
potMat: "鉢の素材"
|
||||
petBottle: "ペットボトル"
|
||||
_petBottle:
|
||||
empty: "空"
|
||||
variation: "種類"
|
||||
variation_greenTea: "緑茶"
|
||||
variation_mineralWater: "ミネラルウォーター"
|
||||
withCap: "キャップ"
|
||||
withLabel: "ラベル"
|
||||
piano: "ピアノ"
|
||||
_piano:
|
||||
bodyMat: "本体の素材"
|
||||
pictureFrame: "シンプルな額縁"
|
||||
_pictureFrame:
|
||||
depth: "厚さ"
|
||||
frameMat: "フレームの素材"
|
||||
frameThickness: "フレームの幅"
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
matHThickness: "マットの横幅"
|
||||
matVThickness: "マットの縦幅"
|
||||
width: "幅"
|
||||
withCover: "カバーあり"
|
||||
pizza: "ピザ"
|
||||
plant: "植物"
|
||||
plant2: "植物 2"
|
||||
poster: "ポスター"
|
||||
_poster:
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
width: "幅"
|
||||
powerStrip: "電源タップ"
|
||||
radiometer: "ラジオメーター"
|
||||
randomBooks: "雑多な本"
|
||||
_randomBooks:
|
||||
count: "数"
|
||||
seed: "シード"
|
||||
stackVertically: "平積み"
|
||||
variation: "バリエーション"
|
||||
variation_mix: "いろいろ"
|
||||
variation_mixPlain: "いろいろ(無地)"
|
||||
recordPlayer: "レコードプレーヤー"
|
||||
rolledUpPoster: "丸めたポスター"
|
||||
roundRug: "円形のラグ"
|
||||
router: "ルーター"
|
||||
siphon: "サイフォン"
|
||||
snakeplant: "サンセベリア"
|
||||
_snakeplant:
|
||||
potMat: "鉢の素材"
|
||||
sofa: "ソファ"
|
||||
_sofa:
|
||||
bodyMat: "本体の素材"
|
||||
speaker: "スピーカー"
|
||||
_speaker:
|
||||
innerMat: "内側の素材"
|
||||
outerMat: "外側の素材"
|
||||
speakerStand: "スピーカースタンド"
|
||||
_speakerStand:
|
||||
bodyMat: "本体の素材"
|
||||
height: "高さ"
|
||||
spotLight: "スポットライト"
|
||||
_spotLight:
|
||||
angleH: "横方向の角度"
|
||||
angleV: "縦方向の角度"
|
||||
bodyMat: "本体の素材"
|
||||
light: "照明"
|
||||
downlight: "ダウンライト"
|
||||
_downlight:
|
||||
bodyMat: "本体の素材"
|
||||
light: "照明"
|
||||
sprayer: "霧吹き"
|
||||
stanchionPole: "スタンションポール"
|
||||
_stanchionPole:
|
||||
bodyMat: "本体の素材"
|
||||
ropeMat: "ロープの素材"
|
||||
steelRack: "スチールラック"
|
||||
_steelRack:
|
||||
height: "高さ"
|
||||
numberOfShelfs: "シェルフの数"
|
||||
poleMat: "ポールの素材"
|
||||
shelfPositionOf: "シェルフの位置"
|
||||
shelfMat: "シェルフの素材"
|
||||
widthAndDepthVariation: "W x D"
|
||||
stormGlass: "ストームグラス"
|
||||
tableSalt: "食卓塩"
|
||||
tabletopCalendar: "卓上カレンダー"
|
||||
tabletopDigitalClock: "卓上デジタル時計"
|
||||
_tabletopDigitalClock:
|
||||
bodyMat: "本体の素材"
|
||||
lcdColor: "LCDの色"
|
||||
tabletopFlag: "卓上旗"
|
||||
_tabletopFlag:
|
||||
image: "画像"
|
||||
tabletopGlassPictureFrame: "卓上ガラス製フォトフレーム"
|
||||
_tabletopGlassPictureFrame:
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
width: "幅"
|
||||
tabletopIronFrameStand: "卓上アイアンフレームスタンド"
|
||||
_tabletopIronFrameStand:
|
||||
boardMat: "板の素材"
|
||||
depth: "奥行き"
|
||||
frameMat: "フレームの素材"
|
||||
height: "高さ"
|
||||
width: "幅"
|
||||
tabletopLcdButtonsController: "LCDボタン付き卓上コントローラー"
|
||||
_tabletopLcdButtonsController:
|
||||
bodyMat: "本体の素材"
|
||||
image: "画像"
|
||||
screenBrightness: "画面の明るさ"
|
||||
tabletopPictureFrame: "卓上フォトフレーム"
|
||||
_tabletopPictureFrame:
|
||||
depth: "厚さ"
|
||||
frameMat: "フレームの素材"
|
||||
frameThickness: "フレームの幅"
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
matHThickness: "マットの横幅"
|
||||
matVThickness: "マットの縦幅"
|
||||
width: "幅"
|
||||
tapestry: "タペストリー"
|
||||
_tapestry:
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
width: "幅"
|
||||
tetrapod: "波消ブロックの置物"
|
||||
tv: "テレビ"
|
||||
_tv:
|
||||
bodyMat: "本体の素材"
|
||||
screenBrightness: "画面の明るさ"
|
||||
twistedCubeObjet: "ねじれた立方体のオブジェ"
|
||||
usedTissue: "使用済みティッシュ"
|
||||
wallCanvas: "壁掛けキャンバス"
|
||||
_wallCanvas:
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
width: "幅"
|
||||
wallClock: "壁掛け時計"
|
||||
_wallClock:
|
||||
frameMat: "フレームの素材"
|
||||
faceMat: "文字盤の素材"
|
||||
handsMat: "針の素材"
|
||||
wallGlassPictureFrame: "ガラスの壁掛けフォトフレーム"
|
||||
_wallGlassPictureFrame:
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
width: "幅"
|
||||
wallMirror: "壁掛けミラー"
|
||||
_wallMirror:
|
||||
frameMat: "フレームの素材"
|
||||
frameThickness: "フレームの厚み"
|
||||
height: "高さ"
|
||||
width: "幅"
|
||||
wallMountSpotLight: "ウォールマウントスポットライト"
|
||||
_wallMountSpotLight:
|
||||
angleH: "横方向の角度"
|
||||
angleV: "縦方向の角度"
|
||||
bodyMat: "本体の素材"
|
||||
light: "照明"
|
||||
wallShelf: "ウォールシェルフ"
|
||||
_wallShelf:
|
||||
boardMat: "板の素材"
|
||||
boardStyle: "板のスタイル"
|
||||
"boardStyle:color": "単色"
|
||||
"boardStyle:wood": "木目"
|
||||
style: "スタイル"
|
||||
wireBasket: "ワイヤーバスケット"
|
||||
_wireBasket:
|
||||
bodyMat: "本体の素材"
|
||||
wireNet: "ワイヤーネット"
|
||||
_wireNet:
|
||||
bodyMat: "本体の素材"
|
||||
woodRingsPendantLight: "リングペンダントライト"
|
||||
_woodRingsPendantLight:
|
||||
bodyMat: "本体の素材"
|
||||
length: "長さ"
|
||||
light: "照明"
|
||||
shadeMat: "シェードの素材"
|
||||
woodSoundAbsorbingPanel: "木製吸音パネル"
|
||||
ironFrameShelf: "アイアンフレームシェルフ"
|
||||
_ironFrameShelf:
|
||||
boardMat: "板の素材"
|
||||
frameMat: "フレームの素材"
|
||||
height: "段数"
|
||||
width: "幅"
|
||||
kakejiku: "掛軸"
|
||||
_kakejiku:
|
||||
image: "画像"
|
||||
"image:kitsuaishinsei": "喫藍心整"
|
||||
|
||||
31
package.json
31
package.json
@@ -1,20 +1,18 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "2026.6.1-alpha.1",
|
||||
"version": "2026.7.0-alpha.2",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/misskey-dev/misskey.git"
|
||||
},
|
||||
"packageManager": "pnpm@11.8.0",
|
||||
"packageManager": "pnpm@11.9.0",
|
||||
"workspaces": [
|
||||
"packages/misskey-js",
|
||||
"packages/i18n",
|
||||
"packages/misskey-reversi",
|
||||
"packages/misskey-bubble-game",
|
||||
"packages/misskey-world",
|
||||
"packages/icons-subsetter",
|
||||
"packages/frontend-misskey-world-engine",
|
||||
"packages/frontend-shared",
|
||||
"packages/frontend-builder",
|
||||
"packages/sw",
|
||||
@@ -41,11 +39,10 @@
|
||||
"migrateandstart": "pnpm migrate && pnpm start",
|
||||
"watch": "pnpm dev",
|
||||
"dev": "node scripts/dev.mjs",
|
||||
"lint": "pnpm --no-bail -r lint",
|
||||
"cy:open": "pnpm cypress open --config-file=cypress.config.ts",
|
||||
"cy:run": "pnpm cypress run",
|
||||
"e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run",
|
||||
"e2e-dev-container": "ncp ./.config/cypress-devcontainer.yml ./.config/test.yml && pnpm start-server-and-test start:test http://localhost:61812 cy:run",
|
||||
"check-dts": "node scripts/check-dts.mjs",
|
||||
"lint": "pnpm --no-bail -r lint && pnpm check-dts",
|
||||
"e2e": "pnpm start-server-and-test start:test http://localhost:61812 \"pnpm --filter frontend test:e2e\"",
|
||||
"e2e-dev-container": "ncp ./.config/playwright-devcontainer.yml ./.config/test.yml && pnpm start-server-and-test start:test http://localhost:61812 \"pnpm --filter frontend test:e2e\"",
|
||||
"backend-unit-test": "cd packages/backend && pnpm test",
|
||||
"backend-unit-test-and-coverage": "cd packages/backend && pnpm test-and-coverage",
|
||||
"test": "pnpm -r test",
|
||||
@@ -58,23 +55,21 @@
|
||||
"esbuild": "0.28.1",
|
||||
"execa": "9.6.1",
|
||||
"ignore-walk": "9.0.0",
|
||||
"js-yaml": "4.2.0",
|
||||
"tar": "7.5.16"
|
||||
"js-yaml": "5.2.0",
|
||||
"tar": "7.5.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.39.4",
|
||||
"@misskey-dev/eslint-plugin": "2.1.0",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/node": "26.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.61.1",
|
||||
"@typescript-eslint/parser": "8.61.1",
|
||||
"@types/node": "26.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.62.0",
|
||||
"@typescript-eslint/parser": "8.62.0",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260426.1",
|
||||
"cross-env": "10.1.0",
|
||||
"cypress": "15.17.0",
|
||||
"eslint": "9.39.4",
|
||||
"globals": "17.6.0",
|
||||
"globals": "17.7.0",
|
||||
"ncp": "2.0.0",
|
||||
"pnpm": "11.8.0",
|
||||
"pnpm": "11.9.0",
|
||||
"start-server-and-test": "3.0.11",
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class WorldRoom1778744540138 {
|
||||
name = 'WorldRoom1778744540138'
|
||||
|
||||
/**
|
||||
* @param {QueryRunner} queryRunner
|
||||
*/
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TABLE "world_room" ("id" character varying(32) NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(256) NOT NULL, "description" character varying(1024) NOT NULL, "userId" character varying(32) NOT NULL, "likedCount" integer NOT NULL DEFAULT '0', "visibility" character varying(128) NOT NULL DEFAULT 'public', "def" jsonb NOT NULL DEFAULT '{}', CONSTRAINT "PK_40cfacaf35b0b54bb2281c89767" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_88289375952050da4a7752a366" ON "world_room" ("updatedAt") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_f803a5efb4125c5fd8a414285e" ON "world_room" ("userId") `);
|
||||
await queryRunner.query(`ALTER TABLE "world_room" ADD CONSTRAINT "FK_f803a5efb4125c5fd8a414285ed" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {QueryRunner} queryRunner
|
||||
*/
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "world_room" DROP CONSTRAINT "FK_f803a5efb4125c5fd8a414285ed"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_f803a5efb4125c5fd8a414285e"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_88289375952050da4a7752a366"`);
|
||||
await queryRunner.query(`DROP TABLE "world_room"`);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class WorldAvatar1779921322355 {
|
||||
name = 'WorldAvatar1779921322355'
|
||||
|
||||
/**
|
||||
* @param {QueryRunner} queryRunner
|
||||
*/
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TABLE "world_avatar" ("id" character varying(32) NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(256) NOT NULL, "userId" character varying(32) NOT NULL, "def" jsonb NOT NULL DEFAULT '{}', "active" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_e7a27262285cc2c27114871f866" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_0f1d0bdfaca455cc2f13defabe" ON "world_avatar" ("updatedAt") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_4eba43c8e2540a92e99dd7f5a9" ON "world_avatar" ("userId") `);
|
||||
await queryRunner.query(`ALTER TABLE "world_room" ADD "accessCount" integer NOT NULL DEFAULT '0'`);
|
||||
await queryRunner.query(`ALTER TABLE "world_avatar" ADD CONSTRAINT "FK_4eba43c8e2540a92e99dd7f5a9a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {QueryRunner} queryRunner
|
||||
*/
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "world_avatar" DROP CONSTRAINT "FK_4eba43c8e2540a92e99dd7f5a9a"`);
|
||||
await queryRunner.query(`ALTER TABLE "world_room" DROP COLUMN "accessCount"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_4eba43c8e2540a92e99dd7f5a9"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_0f1d0bdfaca455cc2f13defabe"`);
|
||||
await queryRunner.query(`DROP TABLE "world_avatar"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class UrlPreviewSensitiveList1782581064131 {
|
||||
name = 'UrlPreviewSensitiveList1782581064131'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "urlPreviewSensitiveList" character varying(3072) array NOT NULL DEFAULT '{}'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "urlPreviewSensitiveList"`);
|
||||
}
|
||||
}
|
||||
@@ -51,8 +51,8 @@
|
||||
"utf-8-validate": "6.0.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.1073.0",
|
||||
"@aws-sdk/lib-storage": "3.1073.0",
|
||||
"@aws-sdk/client-s3": "3.1075.0",
|
||||
"@aws-sdk/lib-storage": "3.1075.0",
|
||||
"@fastify/accepts": "5.0.4",
|
||||
"@fastify/cors": "11.2.0",
|
||||
"@fastify/http-proxy": "11.5.0",
|
||||
@@ -63,23 +63,22 @@
|
||||
"@misskey-dev/emoji-data": "17.0.3",
|
||||
"@misskey-dev/sharp-read-bmp": "1.3.1",
|
||||
"@misskey-dev/summaly": "5.5.1",
|
||||
"@napi-rs/canvas": "1.0.0",
|
||||
"@napi-rs/canvas": "1.0.1",
|
||||
"@nestjs/common": "11.1.27",
|
||||
"@nestjs/core": "11.1.27",
|
||||
"@nestjs/testing": "11.1.27",
|
||||
"@oxc-project/runtime": "0.137.0",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@sentry/node": "10.59.0",
|
||||
"@sentry/profiling-node": "10.59.0",
|
||||
"@simplewebauthn/server": "13.3.1",
|
||||
"@sinonjs/fake-timers": "15.4.0",
|
||||
"@smithy/node-http-handler": "4.8.1",
|
||||
"@sentry/node": "10.62.0",
|
||||
"@sentry/profiling-node": "10.62.0",
|
||||
"@simplewebauthn/server": "13.3.2",
|
||||
"@smithy/node-http-handler": "4.8.2",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.20.0",
|
||||
"archiver": "8.0.0",
|
||||
"bcryptjs": "3.0.3",
|
||||
"blurhash": "2.0.5",
|
||||
"bullmq": "5.79.0",
|
||||
"bullmq": "5.79.2",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"chalk": "5.6.2",
|
||||
"chalk-template": "1.1.2",
|
||||
@@ -88,12 +87,12 @@
|
||||
"content-disposition": "2.0.1",
|
||||
"date-fns": "4.4.0",
|
||||
"deep-email-validator": "0.1.27",
|
||||
"fastify": "5.8.5",
|
||||
"fastify": "5.9.0",
|
||||
"fastify-raw-body": "5.0.0",
|
||||
"feed": "5.2.1",
|
||||
"file-type": "22.0.1",
|
||||
"fluent-ffmpeg": "2.1.3",
|
||||
"got": "15.0.5",
|
||||
"got": "15.0.7",
|
||||
"hpagent": "1.2.0",
|
||||
"http-link-header": "1.1.3",
|
||||
"i18n": "workspace:*",
|
||||
@@ -110,10 +109,10 @@
|
||||
"misskey-js": "workspace:*",
|
||||
"misskey-reversi": "workspace:*",
|
||||
"ms": "3.0.0-canary.202508261828",
|
||||
"nanoid": "5.1.14",
|
||||
"nanoid": "5.1.16",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.3.2",
|
||||
"node-html-parser": "7.1.0",
|
||||
"node-html-parser": "8.0.3",
|
||||
"nodemailer": "9.0.1",
|
||||
"os-utils": "0.0.14",
|
||||
"otpauth": "9.5.1",
|
||||
@@ -135,7 +134,7 @@
|
||||
"slacc": "0.1.5",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"systeminformation": "5.31.7",
|
||||
"systeminformation": "5.31.11",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.7",
|
||||
"tsc-alias": "1.8.17",
|
||||
@@ -150,16 +149,16 @@
|
||||
"@kitajs/ts-html-plugin": "4.1.4",
|
||||
"@nestjs/platform-express": "11.1.27",
|
||||
"@rollup/plugin-esm-shim": "0.1.8",
|
||||
"@sentry/vue": "10.59.0",
|
||||
"@sentry/vue": "10.62.0",
|
||||
"@sinonjs/fake-timers": "15.4.0",
|
||||
"@types/accepts": "1.3.7",
|
||||
"@types/archiver": "8.0.0",
|
||||
"@types/fluent-ffmpeg": "2.1.28",
|
||||
"@types/http-link-header": "1.0.7",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/jsonld": "1.5.15",
|
||||
"@types/mime-types": "3.0.1",
|
||||
"@types/ms": "2.1.0",
|
||||
"@types/node": "26.0.0",
|
||||
"@types/node": "26.0.1",
|
||||
"@types/nodemailer": "8.0.1",
|
||||
"@types/pg": "8.20.0",
|
||||
"@types/qrcode": "1.5.6",
|
||||
@@ -170,14 +169,13 @@
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/simple-oauth2": "5.0.8",
|
||||
"@types/sinonjs__fake-timers": "15.0.1",
|
||||
"@types/supertest": "7.2.0",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/tmp": "0.2.6",
|
||||
"@types/vary": "1.1.3",
|
||||
"@types/web-push": "3.6.4",
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.61.1",
|
||||
"@typescript-eslint/parser": "8.61.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.62.0",
|
||||
"@typescript-eslint/parser": "8.62.0",
|
||||
"@vitest/coverage-v8": "4.1.9",
|
||||
"aws-sdk-client-mock": "4.1.0",
|
||||
"cbor2": "2.3.0",
|
||||
@@ -185,11 +183,10 @@
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"execa": "9.6.1",
|
||||
"fkill": "10.0.3",
|
||||
"js-yaml": "4.2.0",
|
||||
"js-yaml": "5.2.0",
|
||||
"pid-port": "2.1.1",
|
||||
"rolldown": "1.1.2",
|
||||
"rolldown": "1.1.3",
|
||||
"simple-oauth2": "5.1.0",
|
||||
"supertest": "7.2.2",
|
||||
"vite": "8.1.0",
|
||||
"vitest": "4.1.9",
|
||||
"vitest-mock-extended": "4.0.0"
|
||||
|
||||
@@ -3,6 +3,7 @@ import { version as summalyVersion } from '@misskey-dev/summaly';
|
||||
import type { Plugin, ExternalOption } from 'rolldown';
|
||||
import { execa, execaNode } from 'execa';
|
||||
import type { ResultPromise } from 'execa';
|
||||
import fkill from 'fkill';
|
||||
import esmShim from '@rollup/plugin-esm-shim';
|
||||
|
||||
/**
|
||||
@@ -10,6 +11,7 @@ import esmShim from '@rollup/plugin-esm-shim';
|
||||
*/
|
||||
function backendDevServerPlugin(): Plugin {
|
||||
let backendProcess: ResultPromise | null = null;
|
||||
let backendShutdownPromise: Promise<void> | null = null;
|
||||
|
||||
async function runBuildAssets() {
|
||||
await execa('pnpm', ['run', 'build-assets'], {
|
||||
@@ -20,12 +22,31 @@ function backendDevServerPlugin(): Plugin {
|
||||
}
|
||||
|
||||
async function killBackendProcess() {
|
||||
if (backendProcess) {
|
||||
backendProcess.catch(() => {}); // backendProcess.kill()によって発生する例外を無視するためにcatch()を呼び出す
|
||||
backendProcess.kill();
|
||||
await new Promise(resolve => backendProcess!.on('exit', resolve));
|
||||
backendProcess = null;
|
||||
}
|
||||
if (backendShutdownPromise) return backendShutdownPromise;
|
||||
if (!backendProcess) return;
|
||||
|
||||
const processToKill = backendProcess;
|
||||
backendProcess = null;
|
||||
processToKill.catch(() => {}); // プロセスの終了によって発生する例外を無視するためにcatch()を呼び出す
|
||||
|
||||
backendShutdownPromise = (async () => {
|
||||
if (process.platform === 'win32' && processToKill.pid != null) {
|
||||
await fkill(processToKill.pid, {
|
||||
force: true,
|
||||
tree: true,
|
||||
silent: true,
|
||||
waitForExit: 5000,
|
||||
});
|
||||
} else {
|
||||
processToKill.kill();
|
||||
}
|
||||
|
||||
await processToKill.catch(() => {});
|
||||
})().finally(() => {
|
||||
backendShutdownPromise = null;
|
||||
});
|
||||
|
||||
return backendShutdownPromise;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -49,6 +70,9 @@ function backendDevServerPlugin(): Plugin {
|
||||
await runBuildAssets();
|
||||
}
|
||||
},
|
||||
async closeWatcher() {
|
||||
await killBackendProcess();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -75,6 +99,8 @@ export default defineConfig((args) => {
|
||||
're2',
|
||||
'ipaddr.js',
|
||||
'file-type',
|
||||
// バンドルするとSentryの自動計装が正しく行われなくなるため外しておく
|
||||
'pg',
|
||||
];
|
||||
|
||||
const define: Record<string, string> = {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import fs from 'node:fs';
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import yaml from 'js-yaml';
|
||||
import { load as loadYaml } from 'js-yaml';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
@@ -34,7 +34,7 @@ function yamlToJson(ymlPath) {
|
||||
console.log(`${ymlPath} → ${OUTPUT_PATH}`);
|
||||
|
||||
const yamlContent = fs.readFileSync(ymlPath, 'utf-8');
|
||||
const jsonContent = yaml.load(yamlContent);
|
||||
const jsonContent = loadYaml(yamlContent);
|
||||
if (!fs.existsSync(dirname(OUTPUT_PATH))) {
|
||||
fs.mkdirSync(dirname(OUTPUT_PATH), { recursive: true });
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ declare module 'probe-image-size' {
|
||||
function probeImageSize(src: string | ReadStream, callback: (err: Error | null, result?: ProbeResult) => void): void;
|
||||
function probeImageSize(src: string | ReadStream, options: ProbeOptions, callback: (err: Error | null, result?: ProbeResult) => void): void;
|
||||
|
||||
namespace probeImageSize {} // Hack
|
||||
|
||||
export = probeImageSize;
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { probeImageSize as default };
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { loadConfig } from '@/config.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { showMachineInfo } from '@/misc/show-machine-info.js';
|
||||
import { envOption } from '@/env.js';
|
||||
import { initTelemetry } from '@/core/telemetry/telemetry-registry.js';
|
||||
import { initExtraThreadPool, jobQueue, server } from './common.js';
|
||||
|
||||
const logger = new Logger('core', 'cyan');
|
||||
@@ -66,26 +67,7 @@ export async function masterMain() {
|
||||
|
||||
initExtraThreadPool(config);
|
||||
|
||||
if (config.sentryForBackend) {
|
||||
const Sentry = await import('@sentry/node');
|
||||
const { nodeProfilingIntegration } = await import('@sentry/profiling-node');
|
||||
|
||||
Sentry.init({
|
||||
integrations: [
|
||||
...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []),
|
||||
],
|
||||
|
||||
// Performance Monitoring
|
||||
tracesSampleRate: 1.0, // Capture 100% of the transactions
|
||||
|
||||
// Set sampling rate for profiling - this is relative to tracesSampleRate
|
||||
profilesSampleRate: 1.0,
|
||||
|
||||
maxBreadcrumbs: 0,
|
||||
|
||||
...config.sentryForBackend.options,
|
||||
});
|
||||
}
|
||||
await initTelemetry(config);
|
||||
|
||||
bootLogger.info(
|
||||
`mode: [disableClustering: ${envOption.disableClustering}, onlyServer: ${envOption.onlyServer}, onlyQueue: ${envOption.onlyQueue}]`,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import cluster from 'node:cluster';
|
||||
import { envOption } from '@/env.js';
|
||||
import { loadConfig } from '@/config.js';
|
||||
import { initTelemetry } from '@/core/telemetry/telemetry-registry.js';
|
||||
import { initExtraThreadPool, jobQueue, server } from './common.js';
|
||||
|
||||
/**
|
||||
@@ -16,26 +17,7 @@ export async function workerMain() {
|
||||
|
||||
initExtraThreadPool(config);
|
||||
|
||||
if (config.sentryForBackend) {
|
||||
const Sentry = await import('@sentry/node');
|
||||
const { nodeProfilingIntegration } = await import('@sentry/profiling-node');
|
||||
|
||||
Sentry.init({
|
||||
integrations: [
|
||||
...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []),
|
||||
],
|
||||
|
||||
// Performance Monitoring
|
||||
tracesSampleRate: 1.0, // Capture 100% of the transactions
|
||||
|
||||
// Set sampling rate for profiling - this is relative to tracesSampleRate
|
||||
profilesSampleRate: 1.0,
|
||||
|
||||
maxBreadcrumbs: 0,
|
||||
|
||||
...config.sentryForBackend.options,
|
||||
});
|
||||
}
|
||||
await initTelemetry(config);
|
||||
|
||||
if (envOption.onlyServer) {
|
||||
await server();
|
||||
|
||||
@@ -20,6 +20,12 @@ type RedisOptionsSource = Partial<RedisOptions> & {
|
||||
prefix?: string;
|
||||
};
|
||||
|
||||
type SentryBackendConfig = {
|
||||
options: Partial<Sentry.NodeOptions>;
|
||||
enableNodeProfiling: boolean;
|
||||
disabledIntegrations?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 設定ファイルの型
|
||||
*/
|
||||
@@ -64,7 +70,7 @@ type Source = {
|
||||
index: string;
|
||||
scope?: 'local' | 'global' | string[];
|
||||
};
|
||||
sentryForBackend?: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; };
|
||||
sentryForBackend?: SentryBackendConfig;
|
||||
sentryForFrontend?: {
|
||||
options: Partial<SentryVue.BrowserOptions> & { dsn: string };
|
||||
vueIntegration?: SentryVue.VueIntegrationOptions | null;
|
||||
@@ -201,7 +207,7 @@ export type Config = {
|
||||
redisForJobQueue: RedisOptions & RedisOptionsSource;
|
||||
redisForTimelines: RedisOptions & RedisOptionsSource;
|
||||
redisForReactions: RedisOptions & RedisOptionsSource;
|
||||
sentryForBackend: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; } | undefined;
|
||||
sentryForBackend: SentryBackendConfig | undefined;
|
||||
sentryForFrontend: {
|
||||
options: Partial<SentryVue.BrowserOptions> & { dsn: string };
|
||||
vueIntegration?: SentryVue.VueIntegrationOptions | null;
|
||||
|
||||
@@ -154,15 +154,12 @@ import { ApQuestionService } from './activitypub/models/ApQuestionService.js';
|
||||
import { QueueModule } from './QueueModule.js';
|
||||
import { QueueService } from './QueueService.js';
|
||||
import { LoggerService } from './LoggerService.js';
|
||||
import { WorldRoomService } from './WorldRoomService.js';
|
||||
import { WorldRoomEntityService } from './entities/WorldRoomEntityService.js';
|
||||
import { WorldRoomMultiplayService } from './WorldRoomMultiplayService.js';
|
||||
import { WorldAvatarService } from './WorldAvatarService.js';
|
||||
import { WorldAvatarEntityService } from './entities/WorldAvatarEntityService.js';
|
||||
import { TelemetryService } from './telemetry/TelemetryService.js';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
|
||||
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
||||
const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService };
|
||||
const $TelemetryService: Provider = { provide: 'TelemetryService', useExisting: TelemetryService };
|
||||
const $AbuseReportService: Provider = { provide: 'AbuseReportService', useExisting: AbuseReportService };
|
||||
const $AbuseReportNotificationService: Provider = { provide: 'AbuseReportNotificationService', useExisting: AbuseReportNotificationService };
|
||||
const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService };
|
||||
@@ -234,9 +231,6 @@ const $ChatService: Provider = { provide: 'ChatService', useExisting: ChatServic
|
||||
const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService };
|
||||
const $ReversiService: Provider = { provide: 'ReversiService', useExisting: ReversiService };
|
||||
const $PageService: Provider = { provide: 'PageService', useExisting: PageService };
|
||||
const $WorldRoomService: Provider = { provide: 'WorldRoomService', useExisting: WorldRoomService };
|
||||
const $WorldRoomMultiplayService: Provider = { provide: 'WorldRoomMultiplayService', useExisting: WorldRoomMultiplayService };
|
||||
const $WorldAvatarService: Provider = { provide: 'WorldAvatarService', useExisting: WorldAvatarService };
|
||||
|
||||
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
|
||||
const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
|
||||
@@ -292,8 +286,6 @@ const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting
|
||||
const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService };
|
||||
const $MetaEntityService: Provider = { provide: 'MetaEntityService', useExisting: MetaEntityService };
|
||||
const $SystemWebhookEntityService: Provider = { provide: 'SystemWebhookEntityService', useExisting: SystemWebhookEntityService };
|
||||
const $WorldRoomEntityService: Provider = { provide: 'WorldRoomEntityService', useExisting: WorldRoomEntityService };
|
||||
const $WorldAvatarEntityService: Provider = { provide: 'WorldAvatarEntityService', useExisting: WorldAvatarEntityService };
|
||||
|
||||
const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService };
|
||||
const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService };
|
||||
@@ -392,9 +384,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
RegistryApiService,
|
||||
ReversiService,
|
||||
PageService,
|
||||
WorldRoomService,
|
||||
WorldRoomMultiplayService,
|
||||
WorldAvatarService,
|
||||
|
||||
ChartLoggerService,
|
||||
FederationChart,
|
||||
@@ -450,8 +439,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
ReversiGameEntityService,
|
||||
MetaEntityService,
|
||||
SystemWebhookEntityService,
|
||||
WorldRoomEntityService,
|
||||
WorldAvatarEntityService,
|
||||
|
||||
ApAudienceService,
|
||||
ApDbResolverService,
|
||||
@@ -473,6 +460,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
ApPersonService,
|
||||
ApQuestionService,
|
||||
QueueService,
|
||||
TelemetryService,
|
||||
|
||||
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
||||
$LoggerService,
|
||||
@@ -547,9 +535,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$RegistryApiService,
|
||||
$ReversiService,
|
||||
$PageService,
|
||||
$WorldRoomService,
|
||||
$WorldRoomMultiplayService,
|
||||
$WorldAvatarService,
|
||||
|
||||
$ChartLoggerService,
|
||||
$FederationChart,
|
||||
@@ -605,8 +590,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$ReversiGameEntityService,
|
||||
$MetaEntityService,
|
||||
$SystemWebhookEntityService,
|
||||
$WorldRoomEntityService,
|
||||
$WorldAvatarEntityService,
|
||||
|
||||
$ApAudienceService,
|
||||
$ApDbResolverService,
|
||||
@@ -626,6 +609,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$ApNoteService,
|
||||
$ApPersonService,
|
||||
$ApQuestionService,
|
||||
$TelemetryService,
|
||||
//#endregion
|
||||
],
|
||||
exports: [
|
||||
@@ -702,9 +686,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
RegistryApiService,
|
||||
ReversiService,
|
||||
PageService,
|
||||
WorldRoomService,
|
||||
WorldRoomMultiplayService,
|
||||
WorldAvatarService,
|
||||
|
||||
FederationChart,
|
||||
NotesChart,
|
||||
@@ -759,8 +740,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
ReversiGameEntityService,
|
||||
MetaEntityService,
|
||||
SystemWebhookEntityService,
|
||||
WorldRoomEntityService,
|
||||
WorldAvatarEntityService,
|
||||
|
||||
ApAudienceService,
|
||||
ApDbResolverService,
|
||||
@@ -782,6 +761,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
ApPersonService,
|
||||
ApQuestionService,
|
||||
QueueService,
|
||||
TelemetryService,
|
||||
|
||||
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
||||
$LoggerService,
|
||||
@@ -855,9 +835,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$RegistryApiService,
|
||||
$ReversiService,
|
||||
$PageService,
|
||||
$WorldRoomService,
|
||||
$WorldRoomMultiplayService,
|
||||
$WorldAvatarService,
|
||||
|
||||
$FederationChart,
|
||||
$NotesChart,
|
||||
@@ -912,8 +889,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$ReversiGameEntityService,
|
||||
$MetaEntityService,
|
||||
$SystemWebhookEntityService,
|
||||
$WorldRoomEntityService,
|
||||
$WorldAvatarEntityService,
|
||||
|
||||
$ApAudienceService,
|
||||
$ApDbResolverService,
|
||||
@@ -933,6 +908,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$ApNoteService,
|
||||
$ApPersonService,
|
||||
$ApQuestionService,
|
||||
$TelemetryService,
|
||||
//#endregion
|
||||
],
|
||||
})
|
||||
|
||||
@@ -3,11 +3,9 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { URLSearchParams } from 'node:url';
|
||||
import * as nodemailer from 'nodemailer';
|
||||
import juice from 'juice';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { validate as validateEmail } from 'deep-email-validator';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
@@ -197,6 +195,7 @@ export class EmailService {
|
||||
} else if (this.meta.enableTruemailApi && this.meta.truemailInstance && this.meta.truemailAuthKey != null) {
|
||||
validated = await this.trueMail(this.meta.truemailInstance, emailAddress, this.meta.truemailAuthKey);
|
||||
} else {
|
||||
const { validate: validateEmail } = await import('deep-email-validator');
|
||||
validated = await validateEmail({
|
||||
email: emailAddress,
|
||||
validateRegex: true,
|
||||
|
||||
@@ -173,16 +173,6 @@ export interface ChatEventTypes {
|
||||
};
|
||||
}
|
||||
|
||||
export interface WorldRoomEventTypes {
|
||||
enter: {
|
||||
user: Packed<'UserLite'>;
|
||||
avatar: Packed<'WorldAvatarLite'>['def'] | null;
|
||||
};
|
||||
left: {
|
||||
userId: MiUser['id'];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ReversiEventTypes {
|
||||
matched: {
|
||||
game: Packed<'ReversiGameDetailed'>;
|
||||
@@ -325,10 +315,6 @@ export type GlobalEvents = {
|
||||
name: `chatRoomStream:${MiChatRoom['id']}`;
|
||||
payload: EventTypesToEventPayload<ChatEventTypes>;
|
||||
};
|
||||
worldRoom: {
|
||||
name: `worldRoomStream:${string}`;
|
||||
payload: EventTypesToEventPayload<WorldRoomEventTypes>;
|
||||
};
|
||||
reversi: {
|
||||
name: `reversiStream:${MiUser['id']}`;
|
||||
payload: EventTypesToEventPayload<ReversiEventTypes>;
|
||||
@@ -449,9 +435,4 @@ export class GlobalEventService {
|
||||
public publishReversiGameStream<K extends keyof ReversiGameEventTypes>(gameId: MiReversiGame['id'], type: K, value?: ReversiGameEventTypes[K]): void {
|
||||
this.publish(`reversiGameStream:${gameId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public publishWorldRoomStream<K extends keyof WorldRoomEventTypes>(roomId: string, type: K, value?: WorldRoomEventTypes[K]): void {
|
||||
this.publish(`worldRoomStream:${roomId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DataSource, In, Not } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import {
|
||||
MiDriveFile,
|
||||
MiWorldAvatar,
|
||||
} from '@/models/_.js';
|
||||
import type { DriveFilesRepository, WorldAvatarsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
|
||||
@Injectable()
|
||||
export class WorldAvatarService {
|
||||
constructor(
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.worldAvatarsRepository)
|
||||
private worldAvatarsRepository: WorldAvatarsRepository,
|
||||
|
||||
private roleService: RoleService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
private queryService: QueryService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
||||
public defaultAvatar = {
|
||||
type: 'default',
|
||||
body: {
|
||||
color: [0.8, 0.8, 0.8],
|
||||
roughness: 1,
|
||||
metallic: 0,
|
||||
},
|
||||
eyes: {
|
||||
type: 'a',
|
||||
color: [0, 0, 0],
|
||||
},
|
||||
mouth: {
|
||||
type: 'a',
|
||||
color: [0, 0, 0],
|
||||
},
|
||||
accessories: [],
|
||||
} satisfies MiWorldAvatar['def'];
|
||||
|
||||
@bindThis
|
||||
public async validateDef(
|
||||
me: MiUser,
|
||||
def: MiWorldAvatar['def'],
|
||||
): Promise<boolean> {
|
||||
// TODO
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async findMyAvatarById(userId: MiUser['id'], avatarId: MiWorldAvatar['id']) {
|
||||
return this.worldAvatarsRepository.findOneBy({ id: avatarId, userId: userId });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async findAvatarById(avatarId: MiWorldAvatar['id']) {
|
||||
return this.worldAvatarsRepository.findOne({ where: { id: avatarId }, relations: { user: true } });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getMyAvatarsWithPagination(userId: MiUser['id'], limit: number, sinceId?: MiWorldAvatar['id'] | null, untilId?: MiWorldAvatar['id'] | null) {
|
||||
const query = this.queryService.makePaginationQuery(this.worldAvatarsRepository.createQueryBuilder('avatar'), sinceId, untilId)
|
||||
.andWhere('avatar.userId = :userId', { userId });
|
||||
|
||||
const avatars = await query.take(limit).getMany();
|
||||
|
||||
return avatars;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async create(
|
||||
me: MiUser,
|
||||
body: Partial<MiWorldAvatar>,
|
||||
): Promise<MiWorldAvatar> {
|
||||
const currentAvatarsCount = await this.worldAvatarsRepository.countBy({ userId: me.id });
|
||||
|
||||
// TODO: limit by role policy
|
||||
|
||||
const avatar = await this.worldAvatarsRepository.insertOne(new MiWorldAvatar({
|
||||
id: this.idService.gen(),
|
||||
updatedAt: new Date(),
|
||||
name: body.name,
|
||||
def: body.def,
|
||||
userId: me.id,
|
||||
active: currentAvatarsCount === 0,
|
||||
}));
|
||||
|
||||
return avatar;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async update(
|
||||
avatar: MiWorldAvatar,
|
||||
body: Partial<MiWorldAvatar>,
|
||||
): Promise<void> {
|
||||
body.updatedAt = new Date();
|
||||
const updated = await this.worldAvatarsRepository.createQueryBuilder().update()
|
||||
.set(body)
|
||||
.where('id = :id', { id: avatar.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => {
|
||||
return response.raw[0];
|
||||
});
|
||||
|
||||
if (body.active) {
|
||||
await this.worldAvatarsRepository.createQueryBuilder().update()
|
||||
.set({ active: false })
|
||||
.where('userId = :userId', { userId: avatar.userId })
|
||||
.andWhere('id != :id', { id: avatar.id })
|
||||
.execute();
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async delete(avatar: MiWorldAvatar, deleter?: MiUser): Promise<void> {
|
||||
await this.worldAvatarsRepository.delete(avatar.id);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getActiveAvatarOfUser(userId: MiUser['id']) {
|
||||
return this.worldAvatarsRepository.findOneBy({ userId, active: true });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getActiveAvatarOfUsers(userIds: MiUser['id'][]): Promise<MiWorldAvatar[]> {
|
||||
if (userIds.length === 0) return [];
|
||||
return this.worldAvatarsRepository.findBy({ userId: In(userIds), active: true });
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DataSource, In, Not } from 'typeorm';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import {
|
||||
MiWorldRoom,
|
||||
} from '@/models/_.js';
|
||||
import type { MiWorldAvatar, WorldRoomsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { WorldAvatarEntityService } from '@/core/entities/WorldAvatarEntityService.js';
|
||||
|
||||
type PlayerState = {
|
||||
position: [number, number, number],
|
||||
rotation: [number, number, number],
|
||||
sit?: string; // id
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class WorldRoomMultiplayService {
|
||||
constructor(
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.worldRoomsRepository)
|
||||
private worldRoomsRepository: WorldRoomsRepository,
|
||||
|
||||
private roleService: RoleService,
|
||||
private queryService: QueryService,
|
||||
private idService: IdService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private userEntityService: UserEntityService,
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
private worldAvatarEntityService: WorldAvatarEntityService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async enter(userId: MiUser['id'], roomId: MiWorldRoom['id']) {
|
||||
console.log('enter', { userId, roomId });
|
||||
|
||||
// TODO: atomicにやる
|
||||
const currentPlayers = await this.redisClient.hlen(`worldRoom:${roomId}:players`);
|
||||
if (currentPlayers < 10) {
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
redisPipeline.hset(`worldRoom:${roomId}:players`, userId, 1);
|
||||
redisPipeline.hexpire(`worldRoom:${roomId}:players`, 30, 'FIELDS', 1, userId);
|
||||
await redisPipeline.exec();
|
||||
} else {
|
||||
throw new Error('Room is full.');
|
||||
}
|
||||
|
||||
// TODO: 既に入っていたらスキップ
|
||||
const avatar = await this.worldAvatarService.getActiveAvatarOfUser(userId);
|
||||
this.globalEventService.publishWorldRoomStream(roomId, 'enter', {
|
||||
user: await this.userEntityService.pack(userId),
|
||||
avatar: avatar?.def,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async heartbeat(userId: MiUser['id'], roomId: MiWorldRoom['id']) {
|
||||
const exists = await this.redisClient.hexists(`worldRoom:${roomId}:players`, userId);
|
||||
if (exists) {
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
redisPipeline.hexpire(`worldRoom:${roomId}:players`, 30, 'FIELDS', 1, userId);
|
||||
redisPipeline.hexpire(`worldRoom:${roomId}:playerStates`, 30, 'FIELDS', 1, userId);
|
||||
await redisPipeline.exec();
|
||||
} else {
|
||||
throw new Error('Not in room.');
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async left(userId: MiUser['id'], roomId: MiWorldRoom['id']) {
|
||||
console.log('left', { userId, roomId });
|
||||
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
redisPipeline.hdel(`worldRoom:${roomId}:players`, userId);
|
||||
redisPipeline.hdel(`worldRoom:${roomId}:playerStates`, userId);
|
||||
await redisPipeline.exec();
|
||||
|
||||
this.globalEventService.publishWorldRoomStream(roomId, 'left', {
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async updatePlayerState(userId: MiUser['id'], roomId: MiWorldRoom['id'], state: PlayerState) {
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
redisPipeline.hset(`worldRoom:${roomId}:playerStates`, userId, JSON.stringify(state));
|
||||
redisPipeline.hexpire(`worldRoom:${roomId}:playerStates`, 30, 'FIELDS', 1, userId);
|
||||
await redisPipeline.exec();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getPlayerStates(roomId: MiWorldRoom['id']): Promise<Record<string, PlayerState>> {
|
||||
const entries = await this.redisClient.hgetall(`worldRoom:${roomId}:playerStates`);
|
||||
return Object.fromEntries(Object.entries(entries).map(([userId, state]) => [userId, JSON.parse(state) as PlayerState]));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getPlayerStatesAndHeatbeat(userId: MiUser['id'], roomId: MiWorldRoom['id']): Promise<Record<string, PlayerState>> {
|
||||
// TODO: atomicにやる
|
||||
this.heartbeat(userId, roomId);
|
||||
return this.getPlayerStates(roomId);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public packPlayerProfile(user: Packed<'UserLite'>, avatar: Packed<'WorldAvatarLite'>['def'] | null) {
|
||||
return {
|
||||
user: {
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
avatarUrl: user.avatarUrl,
|
||||
},
|
||||
avatar: avatar ?? this.worldAvatarService.defaultAvatar,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getPlayerProfiles(roomId: MiWorldRoom['id'], userId?: MiUser['id']): Promise<Record<string, any>> {
|
||||
let playerIds = await this.redisClient.hkeys(`worldRoom:${roomId}:players`);
|
||||
playerIds = playerIds.filter(id => id !== userId);
|
||||
|
||||
const packedUsers = await this.userEntityService.packMany(playerIds);
|
||||
const avatars = await this.worldAvatarService.getActiveAvatarOfUsers(playerIds);
|
||||
|
||||
const profiles: Record<string, any> = {};
|
||||
for (const playerId of playerIds) {
|
||||
const packedUser = packedUsers.find(u => u.id === playerId);
|
||||
if (packedUser == null) continue;
|
||||
profiles[playerId] = this.packPlayerProfile(packedUser, avatars.find(a => a.userId === playerId)?.def ?? null);
|
||||
}
|
||||
return profiles;
|
||||
}
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DataSource, In, Not } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import {
|
||||
MiDriveFile,
|
||||
MiWorldRoom,
|
||||
} from '@/models/_.js';
|
||||
import type { DriveFilesRepository, WorldRoomsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
|
||||
const driveFileReferencingOptions = {
|
||||
clippedPicture: ['image'],
|
||||
tapestry: ['image'],
|
||||
poster: ['image'],
|
||||
pictureFrame: ['image'],
|
||||
tabletopPictureFrame: ['image'],
|
||||
tabletopGlassPictureFrame: ['image'],
|
||||
wallCanvas: ['image'],
|
||||
wallGlassPictureFrame: ['image'],
|
||||
tabletopFlag: ['image'],
|
||||
tabletopLcdButtonsController: ['image'],
|
||||
djPlayer: ['image'],
|
||||
monitor: ['image'],
|
||||
allInOnePc: ['image'],
|
||||
laptopPc: ['image'],
|
||||
handheldGameConsole: ['image'],
|
||||
largeMousepad: ['image'],
|
||||
kakejiku: ['image'],
|
||||
} as Record<string, string[]>;
|
||||
|
||||
@Injectable()
|
||||
export class WorldRoomService {
|
||||
constructor(
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.worldRoomsRepository)
|
||||
private worldRoomsRepository: WorldRoomsRepository,
|
||||
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private roleService: RoleService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
private queryService: QueryService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async validateDef(
|
||||
me: MiUser,
|
||||
def: MiWorldRoom['def'],
|
||||
): Promise<boolean> {
|
||||
// TODO: スキーマ検証(関係ないプロパティを入れたり不正な値を入れたりできないように)
|
||||
// そのためにはJSON SchemaでRoomState/各objectのoptionsを定義する必要がある
|
||||
|
||||
const objectsLimit = 100; // TODO: ref role policy
|
||||
if (def.installedFurnitures.length > objectsLimit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const attachedFilesLimit = 30; // TODO: ref role policy
|
||||
|
||||
const attachedFileIds = this.collectReferencedDriveFileIds(def);
|
||||
if (attachedFileIds.size > attachedFilesLimit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const attachedFiles = attachedFileIds.size === 0 ? [] : await this.driveFilesRepository.findBy({ id: In([...attachedFileIds]), userId: me.id });
|
||||
for (const file of attachedFiles) {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
return false;
|
||||
}
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
return false;
|
||||
}
|
||||
if (Math.max(file.properties.width ?? 0, file.properties.height ?? 0) > 2048) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async findMyRoomById(userId: MiUser['id'], roomId: MiWorldRoom['id']) {
|
||||
return this.worldRoomsRepository.findOneBy({ id: roomId, userId: userId });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async findRoomById(roomId: MiWorldRoom['id']) {
|
||||
return this.worldRoomsRepository.findOne({ where: { id: roomId }, relations: { user: true } });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getRoomsOfUserWithPagination(userId: MiUser['id'], self: boolean, limit: number, sinceId?: MiWorldRoom['id'] | null, untilId?: MiWorldRoom['id'] | null) {
|
||||
const query = this.queryService.makePaginationQuery(this.worldRoomsRepository.createQueryBuilder('room'), sinceId, untilId)
|
||||
.andWhere('room.userId = :userId', { userId });
|
||||
|
||||
if (!self) {
|
||||
query.andWhere('room.visibility = :visibility', { visibility: 'public' });
|
||||
}
|
||||
|
||||
const rooms = await query.take(limit).getMany();
|
||||
|
||||
return rooms;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async create(
|
||||
me: MiUser,
|
||||
body: Partial<MiWorldRoom>,
|
||||
): Promise<MiWorldRoom> {
|
||||
const room = await this.worldRoomsRepository.insertOne(new MiWorldRoom({
|
||||
id: this.idService.gen(),
|
||||
updatedAt: new Date(),
|
||||
name: body.name,
|
||||
description: body.description,
|
||||
def: body.def,
|
||||
userId: me.id,
|
||||
visibility: body.visibility,
|
||||
}));
|
||||
|
||||
return room;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async update(
|
||||
room: MiWorldRoom,
|
||||
body: Partial<MiWorldRoom>,
|
||||
): Promise<void> {
|
||||
body.updatedAt = new Date();
|
||||
return this.worldRoomsRepository.createQueryBuilder().update()
|
||||
.set(body)
|
||||
.where('id = :id', { id: room.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => {
|
||||
return response.raw[0];
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async delete(room: MiWorldRoom, deleter?: MiUser): Promise<void> {
|
||||
await this.worldRoomsRepository.delete(room.id);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public collectReferencedDriveFileIds(roomState: MiWorldRoom['def']): Set<MiDriveFile['id']> {
|
||||
const fileIds = new Set<MiDriveFile['id']>();
|
||||
const installedFurnitures = roomState.installedFurnitures ?? roomState.installedObjects; // 後方互換性のため
|
||||
for (const o of installedFurnitures) {
|
||||
const def = driveFileReferencingOptions[o.type];
|
||||
if (def == null) continue;
|
||||
for (const key of def) {
|
||||
const optionValue = o.options[key];
|
||||
if (optionValue != null && optionValue.driveFileId != null && optionValue.driveFileId !== '') {
|
||||
fileIds.add(optionValue.driveFileId);
|
||||
}
|
||||
}
|
||||
}
|
||||
return fileIds;
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { DriveFilesRepository, MiWorldAvatar, WorldAvatarsRepository } from '@/models/_.js';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { } from '@/models/Blocking.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
import { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||
import { In } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class WorldAvatarEntityService {
|
||||
constructor(
|
||||
@Inject(DI.worldAvatarsRepository)
|
||||
private worldAvatarsRepository: WorldAvatarsRepository,
|
||||
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
private userEntityService: UserEntityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packLite(
|
||||
src: MiWorldAvatar['id'] | MiWorldAvatar,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
hint?: {
|
||||
packedUser?: Packed<'UserLite'>
|
||||
},
|
||||
): Promise<Packed<'WorldAvatarLite'>> {
|
||||
const meId = me ? me.id : null;
|
||||
const avatar = typeof src === 'object' ? src : await this.worldAvatarsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: avatar.id,
|
||||
def: avatar.def,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packDetailed(
|
||||
src: MiWorldAvatar['id'] | MiWorldAvatar,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
hint?: {
|
||||
packedUser?: Packed<'UserLite'>
|
||||
},
|
||||
): Promise<Packed<'WorldAvatarDetailed'>> {
|
||||
const meId = me ? me.id : null;
|
||||
const avatar = typeof src === 'object' ? src : await this.worldAvatarsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: avatar.id,
|
||||
createdAt: this.idService.parse(avatar.id).date.toISOString(),
|
||||
updatedAt: avatar.updatedAt.toISOString(),
|
||||
name: avatar.name,
|
||||
def: avatar.def,
|
||||
active: avatar.active,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packLiteMany(
|
||||
avatars: MiWorldAvatar[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
) {
|
||||
const _users = avatars.map(({ user, userId }) => user ?? userId);
|
||||
const _userMap = await this.userEntityService.packMany(_users, me)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(avatars.map(avatar => this.packLite(avatar, me, { packedUser: _userMap.get(avatar.userId) })));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packDetailedMany(
|
||||
avatars: MiWorldAvatar[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
) {
|
||||
const _users = avatars.map(({ user, userId }) => user ?? userId);
|
||||
const _userMap = await this.userEntityService.packMany(_users, me)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(avatars.map(avatar => this.packDetailed(avatar, me, { packedUser: _userMap.get(avatar.userId) })));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { DriveFilesRepository, MiWorldRoom, WorldRoomsRepository } from '@/models/_.js';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { } from '@/models/Blocking.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { WorldRoomService } from '@/core/WorldRoomService.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
import { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||
import { In } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class WorldRoomEntityService {
|
||||
constructor(
|
||||
@Inject(DI.worldRoomsRepository)
|
||||
private worldRoomsRepository: WorldRoomsRepository,
|
||||
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private worldRoomService: WorldRoomService,
|
||||
private userEntityService: UserEntityService,
|
||||
private driveFileEntityService: DriveFileEntityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packLite(
|
||||
src: MiWorldRoom['id'] | MiWorldRoom,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
hint?: {
|
||||
packedUser?: Packed<'UserLite'>
|
||||
},
|
||||
): Promise<Packed<'WorldRoomLite'>> {
|
||||
const meId = me ? me.id : null;
|
||||
const room = typeof src === 'object' ? src : await this.worldRoomsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: room.id,
|
||||
createdAt: this.idService.parse(room.id).date.toISOString(),
|
||||
updatedAt: room.updatedAt.toISOString(),
|
||||
userId: room.userId,
|
||||
user: hint?.packedUser ?? this.userEntityService.pack(room.user ?? room.userId, me),
|
||||
name: room.name,
|
||||
description: room.description,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packDetailed(
|
||||
src: MiWorldRoom['id'] | MiWorldRoom,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
hint?: {
|
||||
packedUser?: Packed<'UserLite'>
|
||||
},
|
||||
): Promise<Packed<'WorldRoomDetailed'>> {
|
||||
const meId = me ? me.id : null;
|
||||
const room = typeof src === 'object' ? src : await this.worldRoomsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
const attachedFileIds = this.worldRoomService.collectReferencedDriveFileIds(room.def);
|
||||
const attachedFiles = attachedFileIds.size === 0 ? [] : await this.driveFilesRepository.findBy({ id: In([...attachedFileIds]), userId: room.userId });
|
||||
|
||||
return await awaitAll({
|
||||
id: room.id,
|
||||
createdAt: this.idService.parse(room.id).date.toISOString(),
|
||||
updatedAt: room.updatedAt.toISOString(),
|
||||
userId: room.userId,
|
||||
user: hint?.packedUser ?? this.userEntityService.pack(room.user ?? room.userId, me),
|
||||
name: room.name,
|
||||
description: room.description,
|
||||
def: room.def,
|
||||
attachedFiles: this.driveFileEntityService.packMany(attachedFiles),
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packLiteMany(
|
||||
rooms: MiWorldRoom[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
) {
|
||||
const _users = rooms.map(({ user, userId }) => user ?? userId);
|
||||
const _userMap = await this.userEntityService.packMany(_users, me)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(rooms.map(room => this.packLite(room, me, { packedUser: _userMap.get(room.userId) })));
|
||||
}
|
||||
}
|
||||
|
||||
28
packages/backend/src/core/telemetry/TelemetryService.ts
Normal file
28
packages/backend/src/core/telemetry/TelemetryService.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { captureMessage, shutdownTelemetry, startSpan } from './telemetry-registry.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
import type { TelemetryCaptureMessageOptions } from './adapters/TelemetryAdapter.js';
|
||||
|
||||
@Injectable()
|
||||
export class TelemetryService implements OnApplicationShutdown {
|
||||
@bindThis
|
||||
public captureMessage(message: string, opts: TelemetryCaptureMessageOptions): void {
|
||||
captureMessage(message, opts);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public startSpan<T>(name: string, fn: () => T): T {
|
||||
return startSpan(name, fn);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async onApplicationShutdown(_signal?: string): Promise<void> {
|
||||
await shutdownTelemetry();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Config } from '@/config.js';
|
||||
import type { TelemetryAdapter, TelemetryCaptureMessageOptions } from './TelemetryAdapter.js';
|
||||
|
||||
type SentryIntegrationsOption = NonNullable<import('@sentry/node').NodeOptions['integrations']>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type SentryIntegrationFactory = Extract<SentryIntegrationsOption, (integrations: any[]) => any[]>;
|
||||
type SentryIntegration = Parameters<SentryIntegrationFactory>[0][number];
|
||||
type SentryNodeOptions = import('@sentry/node').NodeOptions;
|
||||
|
||||
type BuildSentryIntegrationsOptions = {
|
||||
disabledIntegrations?: string[];
|
||||
enableNodeProfiling: boolean;
|
||||
nodeProfilingIntegration?: () => SentryIntegration;
|
||||
warn?: (message: string) => void;
|
||||
};
|
||||
|
||||
export function buildSentryIntegrations(options: BuildSentryIntegrationsOptions): SentryIntegrationFactory {
|
||||
return (defaults) => {
|
||||
const disabledIntegrations = new Set(options.disabledIntegrations ?? []);
|
||||
const defaultIntegrationNames = new Set(defaults.map((integration) => integration.name));
|
||||
const unknownIntegrations = [...disabledIntegrations].filter((name) => !defaultIntegrationNames.has(name));
|
||||
|
||||
if (unknownIntegrations.length > 0) {
|
||||
(options.warn ?? console.warn)(`Unknown Sentry integration configured in sentryForBackend.disabledIntegrations: ${unknownIntegrations.join(', ')}`);
|
||||
}
|
||||
|
||||
return [
|
||||
...defaults.filter((integration) => !disabledIntegrations.has(integration.name)),
|
||||
...(options.enableNodeProfiling && options.nodeProfilingIntegration != null ? [options.nodeProfilingIntegration()] : []),
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
export function buildSentryNodeOptions(
|
||||
config: NonNullable<Config['sentryForBackend']>,
|
||||
nodeProfilingIntegration?: () => SentryIntegration,
|
||||
): SentryNodeOptions {
|
||||
return {
|
||||
// Do not send Sentry trace headers to remote ActivityPub/Webhook/etc. hosts by default.
|
||||
// Admins can opt in for trusted internal services via sentryForBackend.options.
|
||||
tracePropagationTargets: [],
|
||||
|
||||
// Performance Monitoring
|
||||
tracesSampleRate: 1.0, // Capture 100% of the transactions
|
||||
|
||||
// Set sampling rate for profiling - this is relative to tracesSampleRate
|
||||
profilesSampleRate: 1.0,
|
||||
|
||||
maxBreadcrumbs: 0,
|
||||
|
||||
...config.options,
|
||||
|
||||
integrations: buildSentryIntegrations({
|
||||
disabledIntegrations: config.disabledIntegrations,
|
||||
enableNodeProfiling: config.enableNodeProfiling,
|
||||
nodeProfilingIntegration,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export class SentryTelemetryAdapter implements TelemetryAdapter {
|
||||
private constructor(
|
||||
private readonly Sentry: typeof import('@sentry/node'),
|
||||
) {
|
||||
}
|
||||
|
||||
public static async create(config: NonNullable<Config['sentryForBackend']>): Promise<SentryTelemetryAdapter> {
|
||||
const Sentry = await import('@sentry/node');
|
||||
const { nodeProfilingIntegration } = await import('@sentry/profiling-node');
|
||||
|
||||
Sentry.init(buildSentryNodeOptions(config, nodeProfilingIntegration));
|
||||
|
||||
return new SentryTelemetryAdapter(Sentry);
|
||||
}
|
||||
|
||||
public captureMessage(message: string, opts: TelemetryCaptureMessageOptions): void {
|
||||
this.Sentry.captureMessage(message, {
|
||||
level: opts.level,
|
||||
...(opts.userId != null ? { user: { id: opts.userId } } : {}),
|
||||
extra: opts.extra,
|
||||
});
|
||||
}
|
||||
|
||||
public startSpan<T>(name: string, fn: () => T): T {
|
||||
return this.Sentry.startSpan({ name }, fn);
|
||||
}
|
||||
|
||||
public async shutdown(): Promise<void> {
|
||||
await this.Sentry.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export interface TelemetryCaptureMessageOptions {
|
||||
level: 'error';
|
||||
userId?: string;
|
||||
extra?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sentry・OpenTelemetryなど、エラートラッキング/APMサービスごとの実装差異を隠蔽するための抽象。
|
||||
* 新しいサービスを追加する場合はこのインターフェースを実装するアダプタをこのディレクトリに追加し、
|
||||
* telemetry-registry.tsのinitTelemetry内で登録する。
|
||||
*/
|
||||
export interface TelemetryAdapter {
|
||||
captureMessage(message: string, opts: TelemetryCaptureMessageOptions): void;
|
||||
startSpan<T>(name: string, fn: () => T): T;
|
||||
shutdown(): Promise<void>;
|
||||
}
|
||||
39
packages/backend/src/core/telemetry/telemetry-registry.ts
Normal file
39
packages/backend/src/core/telemetry/telemetry-registry.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Config } from '@/config.js';
|
||||
import { SentryTelemetryAdapter } from './adapters/SentryTelemetryAdapter.js';
|
||||
import type { TelemetryAdapter, TelemetryCaptureMessageOptions } from './adapters/TelemetryAdapter.js';
|
||||
|
||||
/**
|
||||
* NestのDIコンテナが構築される前(boot処理内)で初期化する必要があるため、
|
||||
* DIを介さないモジュールレベルの状態として有効なアダプタを保持する。
|
||||
* TelemetryServiceはこの状態への薄いラッパーとして振る舞う。
|
||||
*/
|
||||
const adapters: TelemetryAdapter[] = [];
|
||||
|
||||
export async function initTelemetry(config: Config): Promise<void> {
|
||||
if (config.sentryForBackend) {
|
||||
adapters.push(await SentryTelemetryAdapter.create(config.sentryForBackend));
|
||||
}
|
||||
}
|
||||
|
||||
export function captureMessage(message: string, opts: TelemetryCaptureMessageOptions): void {
|
||||
for (const adapter of adapters) {
|
||||
adapter.captureMessage(message, opts);
|
||||
}
|
||||
}
|
||||
|
||||
export function startSpan<T>(name: string, fn: () => T): T {
|
||||
const wrapped = adapters.reduceRight<() => T>(
|
||||
(inner, adapter) => () => adapter.startSpan(name, inner),
|
||||
fn,
|
||||
);
|
||||
return wrapped();
|
||||
}
|
||||
|
||||
export async function shutdownTelemetry(): Promise<void> {
|
||||
await Promise.all(adapters.map(adapter => adapter.shutdown()));
|
||||
}
|
||||
@@ -91,7 +91,5 @@ export const DI = {
|
||||
bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'),
|
||||
reversiGamesRepository: Symbol('reversiGamesRepository'),
|
||||
noteDraftsRepository: Symbol('noteDraftsRepository'),
|
||||
worldRoomsRepository: Symbol('worldRoomsRepository'),
|
||||
worldAvatarsRepository: Symbol('worldAvatarsRepository'),
|
||||
//#endregion
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ const envOption = {
|
||||
verbose: false,
|
||||
withLogTime: false,
|
||||
quiet: false,
|
||||
enableCrossOriginIsolation: false,
|
||||
};
|
||||
|
||||
for (const key of Object.keys(envOption) as (keyof typeof envOption)[]) {
|
||||
|
||||
@@ -75,8 +75,6 @@ import { packedChatRoomInvitationSchema } from '@/models/json-schema/chat-room-i
|
||||
import { packedChatRoomMembershipSchema } from '@/models/json-schema/chat-room-membership.js';
|
||||
import { packedAchievementNameSchema, packedAchievementSchema } from '@/models/json-schema/achievement.js';
|
||||
import { packedNoteDraftSchema } from '@/models/json-schema/note-draft.js';
|
||||
import { packedWorldRoomDetailedSchema, packedWorldRoomLiteSchema } from '@/models/json-schema/world-room.js';
|
||||
import { packedWorldAvatarDetailedSchema, packedWorldAvatarLiteSchema } from '@/models/json-schema/world-avatar.js';
|
||||
|
||||
export const refs = {
|
||||
UserLite: packedUserLiteSchema,
|
||||
@@ -149,10 +147,6 @@ export const refs = {
|
||||
ChatRoom: packedChatRoomSchema,
|
||||
ChatRoomInvitation: packedChatRoomInvitationSchema,
|
||||
ChatRoomMembership: packedChatRoomMembershipSchema,
|
||||
WorldRoomLite: packedWorldRoomLiteSchema,
|
||||
WorldRoomDetailed: packedWorldRoomDetailedSchema,
|
||||
WorldAvatarLite: packedWorldAvatarLiteSchema,
|
||||
WorldAvatarDetailed: packedWorldAvatarDetailedSchema,
|
||||
};
|
||||
|
||||
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
|
||||
|
||||
@@ -672,6 +672,11 @@ export class MiMeta {
|
||||
})
|
||||
public urlPreviewUserAgent: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 3072, array: true, default: '{}',
|
||||
})
|
||||
public urlPreviewSensitiveList: string[];
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128,
|
||||
default: 'none',
|
||||
|
||||
@@ -84,8 +84,6 @@ import {
|
||||
MiChatRoomMembership,
|
||||
MiChatRoomInvitation,
|
||||
MiChatApproval,
|
||||
MiWorldRoom,
|
||||
MiWorldAvatar,
|
||||
} from './_.js';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
import type { DataSource } from 'typeorm';
|
||||
@@ -546,18 +544,6 @@ const $reversiGamesRepository: Provider = {
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $worldRoomsRepository: Provider = {
|
||||
provide: DI.worldRoomsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiWorldRoom).extend(miRepository as MiRepository<MiWorldRoom>),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $worldAvatarsRepository: Provider = {
|
||||
provide: DI.worldAvatarsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiWorldAvatar).extend(miRepository as MiRepository<MiWorldAvatar>),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
providers: [
|
||||
@@ -637,8 +623,6 @@ const $worldAvatarsRepository: Provider = {
|
||||
$chatApprovalsRepository,
|
||||
$bubbleGameRecordsRepository,
|
||||
$reversiGamesRepository,
|
||||
$worldRoomsRepository,
|
||||
$worldAvatarsRepository,
|
||||
],
|
||||
exports: [
|
||||
$usersRepository,
|
||||
@@ -717,8 +701,6 @@ const $worldAvatarsRepository: Provider = {
|
||||
$chatApprovalsRepository,
|
||||
$bubbleGameRecordsRepository,
|
||||
$reversiGamesRepository,
|
||||
$worldRoomsRepository,
|
||||
$worldAvatarsRepository,
|
||||
],
|
||||
})
|
||||
export class RepositoryModule {
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
|
||||
@Entity('world_avatar')
|
||||
export class MiWorldAvatar {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone', {
|
||||
})
|
||||
public updatedAt: Date;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 256,
|
||||
})
|
||||
public name: string;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
})
|
||||
public userId: MiUser['id'];
|
||||
|
||||
@ManyToOne(() => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: MiUser | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public active: boolean;
|
||||
|
||||
@Column('jsonb', {
|
||||
default: {},
|
||||
})
|
||||
public def: Record<string, any>;
|
||||
|
||||
constructor(data: Partial<MiWorldAvatar>) {
|
||||
if (data == null) return;
|
||||
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
(this as any)[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
|
||||
export const worldRoomVisibility = ['public', 'private'] as const;
|
||||
export type WorldRoomVisibility = typeof worldRoomVisibility[number];
|
||||
|
||||
@Entity('world_room')
|
||||
export class MiWorldRoom {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone', {
|
||||
})
|
||||
public updatedAt: Date;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 256,
|
||||
})
|
||||
public name: string;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
})
|
||||
public description: string;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
})
|
||||
public userId: MiUser['id'];
|
||||
|
||||
@ManyToOne(() => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: MiUser | null;
|
||||
|
||||
@Column('integer', {
|
||||
default: 0,
|
||||
})
|
||||
public likedCount: number;
|
||||
|
||||
@Column('integer', {
|
||||
default: 0,
|
||||
})
|
||||
public accessCount: number;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, default: 'public',
|
||||
})
|
||||
public visibility: WorldRoomVisibility;
|
||||
|
||||
@Column('jsonb', {
|
||||
default: {},
|
||||
})
|
||||
public def: Record<string, any>;
|
||||
|
||||
constructor(data: Partial<MiWorldRoom>) {
|
||||
if (data == null) return;
|
||||
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
(this as any)[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
|
||||
import { MiChannel } from '@/models/Channel.js';
|
||||
import { MiChannelFavorite } from '@/models/ChannelFavorite.js';
|
||||
import { MiChannelFollowing } from '@/models/ChannelFollowing.js';
|
||||
import { MiChannelMuting } from '@/models/ChannelMuting.js';
|
||||
import { MiChannelMuting } from "@/models/ChannelMuting.js";
|
||||
import { MiChatApproval } from '@/models/ChatApproval.js';
|
||||
import { MiChatMessage } from '@/models/ChatMessage.js';
|
||||
import { MiChatRoom } from '@/models/ChatRoom.js';
|
||||
@@ -84,8 +84,6 @@ import { MiUserProfile } from '@/models/UserProfile.js';
|
||||
import { MiUserPublickey } from '@/models/UserPublickey.js';
|
||||
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
|
||||
import { MiWebhook } from '@/models/Webhook.js';
|
||||
import { MiWorldRoom } from '@/models/WorldRoom.js';
|
||||
import { MiWorldAvatar } from '@/models/WorldAvatar.js';
|
||||
import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
|
||||
|
||||
export interface MiRepository<T extends ObjectLiteral> {
|
||||
@@ -175,8 +173,6 @@ export {
|
||||
MiChatApproval,
|
||||
MiBubbleGameRecord,
|
||||
MiReversiGame,
|
||||
MiWorldRoom,
|
||||
MiWorldAvatar,
|
||||
};
|
||||
|
||||
export type AbuseUserReportsRepository = Repository<MiAbuseUserReport> & MiRepository<MiAbuseUserReport>;
|
||||
@@ -257,5 +253,3 @@ export type ChatRoomInvitationsRepository = Repository<MiChatRoomInvitation> & M
|
||||
export type ChatApprovalsRepository = Repository<MiChatApproval> & MiRepository<MiChatApproval>;
|
||||
export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord> & MiRepository<MiBubbleGameRecord>;
|
||||
export type ReversiGamesRepository = Repository<MiReversiGame> & MiRepository<MiReversiGame>;
|
||||
export type WorldRoomsRepository = Repository<MiWorldRoom> & MiRepository<MiWorldRoom>;
|
||||
export type WorldAvatarsRepository = Repository<MiWorldAvatar> & MiRepository<MiWorldAvatar>;
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export const packedWorldAvatarLiteSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
def: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const packedWorldAvatarDetailedSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
def: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
active: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export const packedWorldRoomLiteSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
user: {
|
||||
type: 'object',
|
||||
ref: 'UserLite',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const packedWorldRoomDetailedSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
user: {
|
||||
type: 'object',
|
||||
ref: 'UserLite',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
def: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
attachedFiles: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'DriveFile',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -87,8 +87,6 @@ import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
|
||||
import { MiReversiGame } from '@/models/ReversiGame.js';
|
||||
import { MiChatApproval } from '@/models/ChatApproval.js';
|
||||
import { MiSystemAccount } from '@/models/SystemAccount.js';
|
||||
import { MiWorldRoom } from '@/models/WorldRoom.js';
|
||||
import { MiWorldAvatar } from '@/models/WorldAvatar.js';
|
||||
|
||||
pg.types.setTypeParser(20, Number);
|
||||
|
||||
@@ -256,8 +254,6 @@ export const entities = [
|
||||
MiChatApproval,
|
||||
MiBubbleGameRecord,
|
||||
MiReversiGame,
|
||||
MiWorldRoom,
|
||||
MiWorldAvatar,
|
||||
...charts,
|
||||
];
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { Config } from '@/config.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { TelemetryService } from '@/core/telemetry/TelemetryService.js';
|
||||
import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
|
||||
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
|
||||
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
|
||||
@@ -92,6 +93,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
private config: Config,
|
||||
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
private telemetryService: TelemetryService,
|
||||
private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService,
|
||||
private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService,
|
||||
private endedPollNotificationProcessorService: EndedPollNotificationProcessorService,
|
||||
@@ -156,13 +158,6 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
};
|
||||
}
|
||||
|
||||
let Sentry: typeof import('@sentry/node') | undefined;
|
||||
if (this.config.sentryForBackend) {
|
||||
import('@sentry/node').then((mod) => {
|
||||
Sentry = mod;
|
||||
});
|
||||
}
|
||||
|
||||
//#region system
|
||||
{
|
||||
const processer = (job: Bull.Job) => {
|
||||
@@ -181,11 +176,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
};
|
||||
|
||||
this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: System: ' + job.name }, () => processer(job));
|
||||
} else {
|
||||
return processer(job);
|
||||
}
|
||||
return this.telemetryService.startSpan('Queue: System: ' + job.name, () => processer(job));
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.SYSTEM),
|
||||
autorun: false,
|
||||
@@ -198,12 +189,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
||||
.on('failed', (job, err: Error) => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
|
||||
if (Sentry != null) {
|
||||
Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
}
|
||||
this.telemetryService.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
})
|
||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||
@@ -238,11 +227,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
};
|
||||
|
||||
this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: DB: ' + job.name }, () => processer(job));
|
||||
} else {
|
||||
return processer(job);
|
||||
}
|
||||
return this.telemetryService.startSpan('Queue: DB: ' + job.name, () => processer(job));
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.DB),
|
||||
autorun: false,
|
||||
@@ -255,12 +240,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
|
||||
if (Sentry != null) {
|
||||
Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
}
|
||||
this.telemetryService.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
})
|
||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||
@@ -270,11 +253,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
//#region deliver
|
||||
{
|
||||
this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: Deliver' }, () => this.deliverProcessorService.process(job));
|
||||
} else {
|
||||
return this.deliverProcessorService.process(job);
|
||||
}
|
||||
return this.telemetryService.startSpan('Queue: Deliver', () => this.deliverProcessorService.process(job));
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.DELIVER),
|
||||
autorun: false,
|
||||
@@ -295,12 +274,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
|
||||
if (Sentry != null) {
|
||||
Sentry.captureMessage(`Queue: Deliver: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
}
|
||||
this.telemetryService.captureMessage(`Queue: Deliver: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
})
|
||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||
@@ -310,11 +287,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
//#region inbox
|
||||
{
|
||||
this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: Inbox' }, () => this.inboxProcessorService.process(job));
|
||||
} else {
|
||||
return this.inboxProcessorService.process(job);
|
||||
}
|
||||
return this.telemetryService.startSpan('Queue: Inbox', () => this.inboxProcessorService.process(job));
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.INBOX),
|
||||
autorun: false,
|
||||
@@ -335,12 +308,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job: renderJob(job), e: renderError(err) });
|
||||
if (Sentry != null) {
|
||||
Sentry.captureMessage(`Queue: Inbox: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
}
|
||||
this.telemetryService.captureMessage(`Queue: Inbox: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
})
|
||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||
@@ -350,11 +321,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
//#region user-webhook deliver
|
||||
{
|
||||
this.userWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.USER_WEBHOOK_DELIVER, (job) => {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: UserWebhookDeliver' }, () => this.userWebhookDeliverProcessorService.process(job));
|
||||
} else {
|
||||
return this.userWebhookDeliverProcessorService.process(job);
|
||||
}
|
||||
return this.telemetryService.startSpan('Queue: UserWebhookDeliver', () => this.userWebhookDeliverProcessorService.process(job));
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.USER_WEBHOOK_DELIVER),
|
||||
autorun: false,
|
||||
@@ -375,12 +342,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
|
||||
if (Sentry != null) {
|
||||
Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
}
|
||||
this.telemetryService.captureMessage(`Queue: UserWebhookDeliver: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
})
|
||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||
@@ -390,11 +355,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
//#region system-webhook deliver
|
||||
{
|
||||
this.systemWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.SYSTEM_WEBHOOK_DELIVER, (job) => {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: SystemWebhookDeliver' }, () => this.systemWebhookDeliverProcessorService.process(job));
|
||||
} else {
|
||||
return this.systemWebhookDeliverProcessorService.process(job);
|
||||
}
|
||||
return this.telemetryService.startSpan('Queue: SystemWebhookDeliver', () => this.systemWebhookDeliverProcessorService.process(job));
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.SYSTEM_WEBHOOK_DELIVER),
|
||||
autorun: false,
|
||||
@@ -415,12 +376,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
|
||||
if (Sentry != null) {
|
||||
Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
}
|
||||
this.telemetryService.captureMessage(`Queue: SystemWebhookDeliver: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
})
|
||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||
@@ -440,11 +399,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
};
|
||||
|
||||
this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: Relationship: ' + job.name }, () => processer(job));
|
||||
} else {
|
||||
return processer(job);
|
||||
}
|
||||
return this.telemetryService.startSpan('Queue: Relationship: ' + job.name, () => processer(job));
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.RELATIONSHIP),
|
||||
autorun: false,
|
||||
@@ -462,12 +417,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
|
||||
if (Sentry != null) {
|
||||
Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
}
|
||||
this.telemetryService.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
})
|
||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||
@@ -485,11 +438,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
};
|
||||
|
||||
this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: ObjectStorage: ' + job.name }, () => processer(job));
|
||||
} else {
|
||||
return processer(job);
|
||||
}
|
||||
return this.telemetryService.startSpan('Queue: ObjectStorage: ' + job.name, () => processer(job));
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.OBJECT_STORAGE),
|
||||
autorun: false,
|
||||
@@ -503,12 +452,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
|
||||
if (Sentry != null) {
|
||||
Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
}
|
||||
this.telemetryService.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
})
|
||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||
@@ -518,11 +465,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
//#region ended poll notification
|
||||
{
|
||||
this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: EndedPollNotification' }, () => this.endedPollNotificationProcessorService.process(job));
|
||||
} else {
|
||||
return this.endedPollNotificationProcessorService.process(job);
|
||||
}
|
||||
return this.telemetryService.startSpan('Queue: EndedPollNotification', () => this.endedPollNotificationProcessorService.process(job));
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION),
|
||||
autorun: false,
|
||||
@@ -533,11 +476,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
//#region post scheduled note
|
||||
{
|
||||
this.postScheduledNoteQueueWorker = new Bull.Worker(QUEUE.POST_SCHEDULED_NOTE, async (job) => {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: PostScheduledNote' }, () => this.postScheduledNoteProcessorService.process(job));
|
||||
} else {
|
||||
return this.postScheduledNoteProcessorService.process(job);
|
||||
}
|
||||
return this.telemetryService.startSpan('Queue: PostScheduledNote', () => this.postScheduledNoteProcessorService.process(job));
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.POST_SCHEDULED_NOTE),
|
||||
autorun: false,
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { EndpointsModule } from '@/server/api/EndpointsModule.js';
|
||||
import { CoreModule } from '@/core/CoreModule.js';
|
||||
import MainStreamConnection from '@/server/api/stream/Connection.js';
|
||||
import { ApiCallService } from './api/ApiCallService.js';
|
||||
import { FileServerService } from './FileServerService.js';
|
||||
import { HealthServerService } from './HealthServerService.js';
|
||||
@@ -31,6 +30,7 @@ import { UrlPreviewService } from './web/UrlPreviewService.js';
|
||||
import { ClientLoggerService } from './web/ClientLoggerService.js';
|
||||
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
||||
|
||||
import MainStreamConnection from '@/server/api/stream/Connection.js';
|
||||
import { MainChannel } from './api/stream/channels/main.js';
|
||||
import { AdminChannel } from './api/stream/channels/admin.js';
|
||||
import { AntennaChannel } from './api/stream/channels/antenna.js';
|
||||
@@ -49,7 +49,6 @@ import { ChatUserChannel } from './api/stream/channels/chat-user.js';
|
||||
import { ChatRoomChannel } from './api/stream/channels/chat-room.js';
|
||||
import { ReversiChannel } from './api/stream/channels/reversi.js';
|
||||
import { ReversiGameChannel } from './api/stream/channels/reversi-game.js';
|
||||
import { WorldRoomChannel } from './api/stream/channels/world-room.js';
|
||||
import { NoteStreamingHidingService } from './api/stream/NoteStreamingHidingService.js';
|
||||
import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
|
||||
|
||||
@@ -100,7 +99,6 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
|
||||
QueueStatsChannel,
|
||||
ServerStatsChannel,
|
||||
UserListChannel,
|
||||
WorldRoomChannel,
|
||||
NoteStreamingHidingService,
|
||||
OpenApiServerService,
|
||||
OAuth2ProviderService,
|
||||
|
||||
@@ -21,6 +21,7 @@ import { genIdenticon } from '@/misc/gen-identicon.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { envOption } from '@/env.js';
|
||||
import { ActivityPubServerService } from './ActivityPubServerService.js';
|
||||
import { NodeinfoServerService } from './NodeinfoServerService.js';
|
||||
import { ApiServerService } from './api/ApiServerService.js';
|
||||
@@ -89,6 +90,15 @@ export class ServerService implements OnApplicationShutdown {
|
||||
});
|
||||
}
|
||||
|
||||
// for test
|
||||
if (envOption.enableCrossOriginIsolation) {
|
||||
fastify.addHook('onRequest', (request, reply, done) => {
|
||||
reply.header('Cross-Origin-Opener-Policy', 'same-origin');
|
||||
reply.header('Cross-Origin-Embedder-Policy', 'credentialless');
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
// Register raw-body parser for ActivityPub HTTP signature validation.
|
||||
await fastify.register(fastifyRawBody, {
|
||||
global: false,
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { MiMeta, UserIpsRepository } from '@/models/_.js';
|
||||
import { createTemp } from '@/misc/create-temp.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { TelemetryService } from '@/core/telemetry/TelemetryService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { ApiError } from './error.js';
|
||||
import { RateLimiterService } from './RateLimiterService.js';
|
||||
@@ -36,7 +37,6 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
private logger: Logger;
|
||||
private userIpHistories: Map<MiUser['id'], Set<string>>;
|
||||
private userIpHistoriesClearIntervalId: NodeJS.Timeout;
|
||||
private Sentry: typeof import('@sentry/node') | null = null;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.meta)
|
||||
@@ -52,6 +52,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
private rateLimiterService: RateLimiterService,
|
||||
private roleService: RoleService,
|
||||
private apiLoggerService: ApiLoggerService,
|
||||
private telemetryService: TelemetryService,
|
||||
) {
|
||||
this.logger = this.apiLoggerService.logger;
|
||||
this.userIpHistories = new Map<MiUser['id'], Set<string>>();
|
||||
@@ -59,12 +60,6 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
this.userIpHistoriesClearIntervalId = setInterval(() => {
|
||||
this.userIpHistories.clear();
|
||||
}, 1000 * 60 * 60);
|
||||
|
||||
if (this.config.sentryForBackend) {
|
||||
import('@sentry/node').then((Sentry) => {
|
||||
this.Sentry = Sentry;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#sendApiError(reply: FastifyReply, err: ApiError): void {
|
||||
@@ -126,24 +121,20 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
},
|
||||
});
|
||||
|
||||
if (this.Sentry != null) {
|
||||
this.Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
user: {
|
||||
id: userId,
|
||||
this.telemetryService.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
userId,
|
||||
extra: {
|
||||
ep: ep.name,
|
||||
ps: data,
|
||||
e: {
|
||||
message: err.message,
|
||||
code: err.name,
|
||||
stack: err.stack,
|
||||
id: errId,
|
||||
},
|
||||
extra: {
|
||||
ep: ep.name,
|
||||
ps: data,
|
||||
e: {
|
||||
message: err.message,
|
||||
code: err.name,
|
||||
stack: err.stack,
|
||||
id: errId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
throw new ApiError(null, {
|
||||
e: {
|
||||
@@ -441,15 +432,8 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
// API invoking
|
||||
if (this.Sentry != null) {
|
||||
return await this.Sentry.startSpan({
|
||||
name: 'API: ' + ep.name,
|
||||
}, () => ep.exec(data, user, token, file, request.ip, request.headers)
|
||||
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id)));
|
||||
} else {
|
||||
return await ep.exec(data, user, token, file, request.ip, request.headers)
|
||||
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id));
|
||||
}
|
||||
return await this.telemetryService.startSpan('API: ' + ep.name, () => ep.exec(data, user, token, file, request.ip, request.headers)
|
||||
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id)));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
||||
@@ -447,14 +447,4 @@ export * as 'chat/rooms/invitations/inbox' from './endpoints/chat/rooms/invitati
|
||||
export * as 'chat/rooms/invitations/outbox' from './endpoints/chat/rooms/invitations/outbox.js';
|
||||
export * as 'chat/history' from './endpoints/chat/history.js';
|
||||
export * as 'chat/read-all' from './endpoints/chat/read-all.js';
|
||||
export * as 'world/rooms/create' from './endpoints/world/rooms/create.js';
|
||||
export * as 'world/rooms/update' from './endpoints/world/rooms/update.js';
|
||||
export * as 'world/rooms/delete' from './endpoints/world/rooms/delete.js';
|
||||
export * as 'world/rooms/list-by-user' from './endpoints/world/rooms/list-by-user.js';
|
||||
export * as 'world/rooms/show' from './endpoints/world/rooms/show.js';
|
||||
export * as 'world/avatars/create' from './endpoints/world/avatars/create.js';
|
||||
export * as 'world/avatars/update' from './endpoints/world/avatars/update.js';
|
||||
export * as 'world/avatars/delete' from './endpoints/world/avatars/delete.js';
|
||||
export * as 'world/avatars/list' from './endpoints/world/avatars/list.js';
|
||||
export * as 'world/avatars/show' from './endpoints/world/avatars/show.js';
|
||||
export * as 'v2/admin/emoji/list' from './endpoints/v2/admin/emoji/list.js';
|
||||
|
||||
@@ -544,6 +544,14 @@ export const meta = {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
urlPreviewSensitiveList: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
federation: {
|
||||
type: 'string',
|
||||
enum: ['all', 'specified', 'none'],
|
||||
@@ -760,6 +768,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength,
|
||||
urlPreviewUserAgent: instance.urlPreviewUserAgent,
|
||||
urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl,
|
||||
urlPreviewSensitiveList: instance.urlPreviewSensitiveList,
|
||||
federation: instance.federation,
|
||||
federationHosts: instance.federationHosts,
|
||||
deliverSuspendedSoftware: instance.deliverSuspendedSoftware,
|
||||
|
||||
@@ -189,6 +189,12 @@ export const paramDef = {
|
||||
urlPreviewRequireContentLength: { type: 'boolean' },
|
||||
urlPreviewUserAgent: { type: 'string', nullable: true },
|
||||
urlPreviewSummaryProxyUrl: { type: 'string', nullable: true },
|
||||
urlPreviewSensitiveList: {
|
||||
type: 'array', nullable: true,
|
||||
items: {
|
||||
type: 'string',
|
||||
}
|
||||
},
|
||||
federation: {
|
||||
type: 'string',
|
||||
enum: ['all', 'none', 'specified'],
|
||||
@@ -734,6 +740,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
set.urlPreviewSummaryProxyUrl = value === '' ? null : value;
|
||||
}
|
||||
|
||||
if (Array.isArray(ps.urlPreviewSensitiveList)) {
|
||||
set.urlPreviewSensitiveList = ps.urlPreviewSensitiveList.filter(Boolean);
|
||||
}
|
||||
|
||||
if (ps.federation !== undefined) {
|
||||
set.federation = ps.federation;
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { WorldAvatarEntityService } from '@/core/entities/WorldAvatarEntityService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldAvatar'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:worldAvatar',
|
||||
|
||||
limit: {
|
||||
duration: ms('1day'),
|
||||
max: 10,
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'WorldAvatarDetailed',
|
||||
},
|
||||
|
||||
errors: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', maxLength: 256 },
|
||||
def: { type: 'object', additionalProperties: true },
|
||||
},
|
||||
required: ['name', 'def'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
private worldAvatarEntityService: WorldAvatarEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
// TODO: validate avatar
|
||||
|
||||
const avatar = await this.worldAvatarService.create(me, {
|
||||
name: ps.name,
|
||||
def: ps.def,
|
||||
});
|
||||
return await this.worldAvatarEntityService.packDetailed(avatar);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldAvatar'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:worldAvatar',
|
||||
|
||||
errors: {
|
||||
noSuchAvatar: {
|
||||
message: 'No such avatar.',
|
||||
code: 'NO_SUCH_ROOM',
|
||||
id: 'd4e3753d-97bf-4a19-ab8e-21080fbc0f4c',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
avatarId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['avatarId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const avatar = await this.worldAvatarService.findMyAvatarById(me.id, ps.avatarId);
|
||||
if (avatar == null) {
|
||||
throw new ApiError(meta.errors.noSuchAvatar);
|
||||
}
|
||||
|
||||
await this.worldAvatarService.delete(avatar, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { WorldAvatarEntityService } from '@/core/entities/WorldAvatarEntityService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldAvatar'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:worldAvatar',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'WorldAvatarDetailed',
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
sinceDate: { type: 'integer' },
|
||||
untilDate: { type: 'integer' },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldAvatarEntityService: WorldAvatarEntityService,
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
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 avatars = await this.worldAvatarService.getMyAvatarsWithPagination(me.id, ps.limit, sinceId, untilId);
|
||||
return this.worldAvatarEntityService.packDetailedMany(avatars, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { WorldAvatarEntityService } from '@/core/entities/WorldAvatarEntityService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldAvatar'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:worldAvatar',
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'WorldAvatarDetailed',
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchAvatar: {
|
||||
message: 'No such avatar.',
|
||||
code: 'NO_SUCH_ROOM',
|
||||
id: '857ae02f-8759-4d20-9adb-6e95fffe4fd8',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
avatarId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['avatarId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
private worldAvatarEntityService: WorldAvatarEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const avatar = await this.worldAvatarService.findAvatarById(ps.avatarId);
|
||||
if (avatar == null) {
|
||||
throw new ApiError(meta.errors.noSuchAvatar);
|
||||
}
|
||||
|
||||
if (avatar.userId !== me.id) {
|
||||
throw new ApiError(meta.errors.noSuchAvatar);
|
||||
}
|
||||
|
||||
return this.worldAvatarEntityService.packDetailed(avatar, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldAvatar'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:worldAvatar',
|
||||
|
||||
res: {
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchAvatar: {
|
||||
message: 'No such avatar.',
|
||||
code: 'NO_SUCH_ROOM',
|
||||
id: 'fcdb0f92-bda6-47f9-bd05-343e0e020933',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
avatarId: { type: 'string', format: 'misskey:id' },
|
||||
name: { type: 'string', maxLength: 256 },
|
||||
def: { type: 'object', additionalProperties: true },
|
||||
active: { type: 'boolean' },
|
||||
},
|
||||
required: ['avatarId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const avatar = await this.worldAvatarService.findMyAvatarById(me.id, ps.avatarId);
|
||||
if (avatar == null) {
|
||||
throw new ApiError(meta.errors.noSuchAvatar);
|
||||
}
|
||||
|
||||
// TODO: validate avatar
|
||||
|
||||
await this.worldAvatarService.update(avatar, {
|
||||
name: ps.name,
|
||||
def: ps.def,
|
||||
active: ps.active,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { WorldRoomService } from '@/core/WorldRoomService.js';
|
||||
import { WorldRoomEntityService } from '@/core/entities/WorldRoomEntityService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldRoom'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:worldRoom',
|
||||
|
||||
limit: {
|
||||
duration: ms('1day'),
|
||||
max: 10,
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'WorldRoomDetailed',
|
||||
},
|
||||
|
||||
errors: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', maxLength: 256 },
|
||||
description: { type: 'string', maxLength: 1024 },
|
||||
visibility: { type: 'string', enum: ['public', 'private'] },
|
||||
def: { type: 'object', additionalProperties: true },
|
||||
},
|
||||
required: ['name', 'visibility', 'def'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldRoomService: WorldRoomService,
|
||||
private worldRoomEntityService: WorldRoomEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
// TODO: validate room
|
||||
|
||||
const room = await this.worldRoomService.create(me, {
|
||||
name: ps.name,
|
||||
description: ps.description ?? '',
|
||||
visibility: ps.visibility,
|
||||
def: ps.def,
|
||||
});
|
||||
return await this.worldRoomEntityService.packDetailed(room);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldRoomService } from '@/core/WorldRoomService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldRoom'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:worldRoom',
|
||||
|
||||
errors: {
|
||||
noSuchRoom: {
|
||||
message: 'No such room.',
|
||||
code: 'NO_SUCH_ROOM',
|
||||
id: 'd4e3753d-97bf-4a19-ab8e-21080fbc0f4c',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
roomId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['roomId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldRoomService: WorldRoomService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const room = await this.worldRoomService.findMyRoomById(me.id, ps.roomId);
|
||||
if (room == null) {
|
||||
throw new ApiError(meta.errors.noSuchRoom);
|
||||
}
|
||||
|
||||
await this.worldRoomService.delete(room, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldRoomService } from '@/core/WorldRoomService.js';
|
||||
import { WorldRoomEntityService } from '@/core/entities/WorldRoomEntityService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldRoom'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:worldRoom',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'WorldRoomLite',
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
sinceDate: { type: 'integer' },
|
||||
untilDate: { type: 'integer' },
|
||||
},
|
||||
required: ['userId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldRoomEntityService: WorldRoomEntityService,
|
||||
private worldRoomService: WorldRoomService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
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 rooms = await this.worldRoomService.getRoomsOfUserWithPagination(ps.userId, ps.userId === me.id, ps.limit, sinceId, untilId);
|
||||
return this.worldRoomEntityService.packLiteMany(rooms, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldRoomService } from '@/core/WorldRoomService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { WorldRoomEntityService } from '@/core/entities/WorldRoomEntityService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldRoom'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:worldRoom',
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'WorldRoomDetailed',
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchRoom: {
|
||||
message: 'No such room.',
|
||||
code: 'NO_SUCH_ROOM',
|
||||
id: '857ae02f-8759-4d20-9adb-6e95fffe4fd8',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
roomId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['roomId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldRoomService: WorldRoomService,
|
||||
private worldRoomEntityService: WorldRoomEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const room = await this.worldRoomService.findRoomById(ps.roomId);
|
||||
if (room == null) {
|
||||
throw new ApiError(meta.errors.noSuchRoom);
|
||||
}
|
||||
|
||||
if (room.userId !== me.id && room.visibility === 'private') {
|
||||
throw new ApiError(meta.errors.noSuchRoom);
|
||||
}
|
||||
|
||||
return this.worldRoomEntityService.packDetailed(room, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldRoomService } from '@/core/WorldRoomService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldRoom'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:worldRoom',
|
||||
|
||||
res: {
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchRoom: {
|
||||
message: 'No such room.',
|
||||
code: 'NO_SUCH_ROOM',
|
||||
id: 'fcdb0f92-bda6-47f9-bd05-343e0e020933',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
roomId: { type: 'string', format: 'misskey:id' },
|
||||
name: { type: 'string', maxLength: 256 },
|
||||
description: { type: 'string', maxLength: 1024 },
|
||||
visibility: { type: 'string', enum: ['public', 'private'] },
|
||||
def: { type: 'object', additionalProperties: true },
|
||||
},
|
||||
required: ['roomId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldRoomService: WorldRoomService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const room = await this.worldRoomService.findMyRoomById(me.id, ps.roomId);
|
||||
if (room == null) {
|
||||
throw new ApiError(meta.errors.noSuchRoom);
|
||||
}
|
||||
|
||||
// TODO: validate room
|
||||
|
||||
await this.worldRoomService.update(room, {
|
||||
name: ps.name,
|
||||
description: ps.description,
|
||||
visibility: ps.visibility,
|
||||
def: ps.def,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,6 @@ import { ChatUserChannel } from '@/server/api/stream/channels/chat-user.js';
|
||||
import { ChatRoomChannel } from '@/server/api/stream/channels/chat-room.js';
|
||||
import { ReversiChannel } from '@/server/api/stream/channels/reversi.js';
|
||||
import { ReversiGameChannel } from '@/server/api/stream/channels/reversi-game.js';
|
||||
import { WorldRoomChannel } from '@/server/api/stream/channels/world-room.js';
|
||||
import type { ChannelRequest } from './channel.js';
|
||||
import type { ChannelConstructor } from './channel.js';
|
||||
import type Channel from './channel.js';
|
||||
@@ -339,7 +338,6 @@ export default class Connection {
|
||||
case 'chatRoom': return ChatRoomChannel;
|
||||
case 'reversi': return ReversiChannel;
|
||||
case 'reversiGame': return ReversiGameChannel;
|
||||
case 'worldRoom': return WorldRoomChannel;
|
||||
|
||||
default:
|
||||
throw new Error(`no such channel: ${name}`);
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import { WorldRoomService } from '@/core/WorldRoomService.js';
|
||||
import { WorldRoomMultiplayService } from '@/core/WorldRoomMultiplayService.js';
|
||||
import Channel, { type ChannelRequest } from '../channel.js';
|
||||
|
||||
@Injectable({ scope: Scope.TRANSIENT })
|
||||
export class WorldRoomChannel extends Channel {
|
||||
public readonly chName = 'worldRoom';
|
||||
public static shouldShare = false;
|
||||
public static requireCredential = true as const;
|
||||
public static kind = 'read:worldRoom';
|
||||
private roomId: string;
|
||||
private intervalId: NodeJS.Timeout;
|
||||
private isEntered = false;
|
||||
|
||||
constructor(
|
||||
@Inject(REQUEST)
|
||||
request: ChannelRequest,
|
||||
|
||||
private worldRoomService: WorldRoomService,
|
||||
private worldRoomMultiplayService: WorldRoomMultiplayService,
|
||||
) {
|
||||
super(request);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: JsonObject): Promise<boolean> {
|
||||
if (typeof params.roomId !== 'string') return false;
|
||||
if (!this.user) return false;
|
||||
|
||||
this.roomId = params.roomId;
|
||||
|
||||
const room = await this.worldRoomService.findRoomById(this.roomId);
|
||||
if (room == null) return false;
|
||||
|
||||
try {
|
||||
await this.enter();
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.subscriber.on(`worldRoomStream:${this.roomId}`, this.onEvent);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async enter() {
|
||||
if (this.isEntered) return;
|
||||
|
||||
await this.worldRoomMultiplayService.enter(this.user!.id, this.roomId);
|
||||
|
||||
this.isEntered = true;
|
||||
|
||||
this.send('entered', {
|
||||
playerProfiles: await this.worldRoomMultiplayService.getPlayerProfiles(this.roomId, this.user!.id),
|
||||
});
|
||||
|
||||
this.intervalId = setInterval(async () => {
|
||||
const states = await this.worldRoomMultiplayService.getPlayerStatesAndHeatbeat(this.user!.id, this.roomId);
|
||||
delete states[this.user!.id];
|
||||
this.send('sync', states);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onEvent(data: GlobalEvents['worldRoom']['payload']) {
|
||||
switch (data.type) {
|
||||
case 'enter': {
|
||||
if (data.body.user.id === this.user!.id) return; // 自分の入室は無視
|
||||
this.send('playerEntered', {
|
||||
id: data.body.user.id,
|
||||
profile: this.worldRoomMultiplayService.packPlayerProfile(data.body.user, data.body.avatar),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'left': {
|
||||
if (data.body.userId === this.user!.id) return; // 自分の退室は無視
|
||||
this.send('playerLeft', {
|
||||
id: data.body.userId,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onMessage(type: string, body: any) {
|
||||
switch (type) {
|
||||
case 'update':
|
||||
if (this.roomId && this.isEntered) {
|
||||
this.worldRoomMultiplayService.updatePlayerState(this.user!.id, this.roomId, body);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose() {
|
||||
this.subscriber.off(`worldRoomStream:${this.roomId}`, this.onEvent);
|
||||
|
||||
clearInterval(this.intervalId);
|
||||
this.worldRoomMultiplayService.left(this.user!.id, this.roomId);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { query } from '@/misc/prelude/url.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { MiMeta } from '@/models/Meta.js';
|
||||
@@ -29,6 +30,7 @@ export class UrlPreviewService {
|
||||
private meta: MiMeta,
|
||||
|
||||
private httpRequestService: HttpRequestService,
|
||||
private utilityService: UtilityService,
|
||||
private loggerService: LoggerService,
|
||||
) {
|
||||
this.logger = this.loggerService.getLogger('url-preview');
|
||||
@@ -95,6 +97,10 @@ export class UrlPreviewService {
|
||||
summary.icon = this.wrap(summary.icon);
|
||||
summary.thumbnail = this.wrap(summary.thumbnail);
|
||||
|
||||
if (summary.sensitive !== true) {
|
||||
summary.sensitive = this.utilityService.isKeyWordIncluded(summary.url, this.meta.urlPreviewSensitiveList);
|
||||
}
|
||||
|
||||
// Cache 1day
|
||||
reply.header('Cache-Control', 'max-age=86400, immutable');
|
||||
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { describe, beforeAll, beforeEach, test } from 'vitest';
|
||||
import { describe, beforeAll, beforeEach, test, vi } from 'vitest';
|
||||
import { UserToken, api, post, signup } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
const waitForPushToTlOptions = { timeout: 3000, interval: 25 };
|
||||
|
||||
describe('API visibility', () => {
|
||||
describe('Note visibility', () => {
|
||||
//#region vars
|
||||
@@ -409,10 +411,12 @@ describe('API visibility', () => {
|
||||
|
||||
//#region HTL
|
||||
test('[HTL] public-post が 自分が見れる', async () => {
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter(n => n.id === pub.id);
|
||||
assert.strictEqual(notes[0].text, 'x');
|
||||
await vi.waitFor(async () => {
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter(n => n.id === pub.id);
|
||||
assert.strictEqual(notes[0].text, 'x');
|
||||
}, waitForPushToTlOptions);
|
||||
});
|
||||
|
||||
test('[HTL] public-post が 非フォロワーから見れない', async () => {
|
||||
@@ -423,10 +427,12 @@ describe('API visibility', () => {
|
||||
});
|
||||
|
||||
test('[HTL] followers-post が フォロワーから見れる', async () => {
|
||||
const res = await api('notes/timeline', { limit: 100 }, follower);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter(n => n.id === fol.id);
|
||||
assert.strictEqual(notes[0].text, 'x');
|
||||
await vi.waitFor(async () => {
|
||||
const res = await api('notes/timeline', { limit: 100 }, follower);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter(n => n.id === fol.id);
|
||||
assert.strictEqual(notes[0].text, 'x');
|
||||
}, waitForPushToTlOptions);
|
||||
});
|
||||
//#endregion
|
||||
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { describe, beforeAll, test } from 'vitest';
|
||||
import { describe, beforeAll, test, vi } from 'vitest';
|
||||
import { api, castAsError, post, signup } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
const waitForPushToTlOptions = { timeout: 3000, interval: 25 };
|
||||
|
||||
describe('Block', () => {
|
||||
// alice blocks bob
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
@@ -75,13 +77,15 @@ describe('Block', () => {
|
||||
const bobNote = await post(bob, { text: 'hi' });
|
||||
const carolNote = await post(carol, { text: 'hi' });
|
||||
|
||||
const res = await api('notes/local-timeline', {}, bob);
|
||||
const body = res.body as misskey.entities.Note[];
|
||||
await vi.waitFor(async () => {
|
||||
const res = await api('notes/local-timeline', {}, bob);
|
||||
const body = res.body as misskey.entities.Note[];
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(body.some(note => note.id === aliceNote.id), false);
|
||||
assert.strictEqual(body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(body.some(note => note.id === carolNote.id), true);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(body.some(note => note.id === aliceNote.id), false);
|
||||
assert.strictEqual(body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(body.some(note => note.id === carolNote.id), true);
|
||||
}, waitForPushToTlOptions);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { describe, beforeAll, test, expect } from 'vitest';
|
||||
import { describe, beforeAll, test, expect, vi } from 'vitest';
|
||||
// node-fetch only supports it's own Blob yet
|
||||
// https://github.com/node-fetch/node-fetch/pull/1664
|
||||
import { Blob } from 'node-fetch';
|
||||
@@ -14,6 +14,8 @@ import { api, castAsError, initTestDb, post, role, signup, simpleGet, uploadFile
|
||||
import type * as misskey from 'misskey-js';
|
||||
import { MiUser } from '@/models/_.js';
|
||||
|
||||
const waitForPushToTlOptions = { timeout: 3000, interval: 25 };
|
||||
|
||||
describe('Endpoints', () => {
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let bob: misskey.entities.SignupResponse;
|
||||
@@ -1149,12 +1151,14 @@ describe('Endpoints', () => {
|
||||
visibility: 'followers',
|
||||
});
|
||||
|
||||
const res = await api('notes/timeline', {}, dave);
|
||||
await vi.waitFor(async () => {
|
||||
const res = await api('notes/timeline', {}, dave);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.length, 1);
|
||||
assert.strictEqual(res.body[0].id, carolPost.id);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.length, 1);
|
||||
assert.strictEqual(res.body[0].id, carolPost.id);
|
||||
}, waitForPushToTlOptions);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { beforeAll, describe, test } from 'vitest';
|
||||
import { beforeAll, describe, test, vi } from 'vitest';
|
||||
import { api, post, react, signup, waitFire } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
const waitForPushToTlOptions = { timeout: 3000, interval: 25 };
|
||||
|
||||
describe('Mute', () => {
|
||||
// alice mutes carol
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
@@ -67,13 +69,15 @@ describe('Mute', () => {
|
||||
const bobNote = await post(bob, { text: 'hi' });
|
||||
const carolNote = await post(carol, { text: 'hi' });
|
||||
|
||||
const res = await api('notes/local-timeline', {}, alice);
|
||||
await vi.waitFor(async () => {
|
||||
const res = await api('notes/local-timeline', {}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||
}, waitForPushToTlOptions);
|
||||
});
|
||||
|
||||
test('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async () => {
|
||||
@@ -83,13 +87,15 @@ describe('Mute', () => {
|
||||
renoteId: carolNote.id,
|
||||
});
|
||||
|
||||
const res = await api('notes/local-timeline', {}, alice);
|
||||
await vi.waitFor(async () => {
|
||||
const res = await api('notes/local-timeline', {}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||
}, waitForPushToTlOptions);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { beforeAll, describe, test } from 'vitest';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { beforeAll, describe, test, vi } from 'vitest';
|
||||
import { api, post, signup, waitFire } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
const waitForPushToTlOptions = { timeout: 3000, interval: 25 };
|
||||
|
||||
describe('Renote Mute', () => {
|
||||
// alice mutes carol
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
@@ -36,16 +37,15 @@ describe('Renote Mute', () => {
|
||||
const carolRenote = await post(carol, { renoteId: bobNote.id });
|
||||
const carolNote = await post(carol, { text: 'hi' });
|
||||
|
||||
// redisに追加されるのを待つ
|
||||
await setTimeout(100);
|
||||
await vi.waitFor(async () => {
|
||||
const res = await api('notes/local-timeline', {}, alice);
|
||||
|
||||
const res = await api('notes/local-timeline', {}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolRenote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolRenote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
|
||||
}, waitForPushToTlOptions);
|
||||
});
|
||||
|
||||
test('タイムラインにリノートミュートしているユーザーの引用が含まれる', async () => {
|
||||
@@ -53,16 +53,15 @@ describe('Renote Mute', () => {
|
||||
const carolRenote = await post(carol, { renoteId: bobNote.id, text: 'kore' });
|
||||
const carolNote = await post(carol, { text: 'hi' });
|
||||
|
||||
// redisに追加されるのを待つ
|
||||
await setTimeout(100);
|
||||
await vi.waitFor(async () => {
|
||||
const res = await api('notes/local-timeline', {}, alice);
|
||||
|
||||
const res = await api('notes/local-timeline', {}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolRenote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolRenote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
|
||||
}, waitForPushToTlOptions);
|
||||
});
|
||||
|
||||
// #12956
|
||||
@@ -70,15 +69,14 @@ describe('Renote Mute', () => {
|
||||
const carolNote = await post(carol, { text: 'hi' });
|
||||
const bobRenote = await post(bob, { renoteId: carolNote.id });
|
||||
|
||||
// redisに追加されるのを待つ
|
||||
await setTimeout(100);
|
||||
await vi.waitFor(async () => {
|
||||
const res = await api('notes/local-timeline', {}, alice);
|
||||
|
||||
const res = await api('notes/local-timeline', {}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobRenote.id), true);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobRenote.id), true);
|
||||
}, waitForPushToTlOptions);
|
||||
});
|
||||
|
||||
test('ストリームにリノートミュートしているユーザーのリノートが流れない', async () => {
|
||||
|
||||
@@ -510,7 +510,7 @@ describe('Streaming', () => {
|
||||
test('withReplies: true のとき自分のfollowers投稿に対するリプライが流れる', async () => {
|
||||
const erinNote = await post(erin, { text: 'hi', visibility: 'followers' });
|
||||
const fired = await waitFire(
|
||||
erin, 'homeTimeline', // erin:home
|
||||
erin, 'hybridTimeline', // erin:Hybrid
|
||||
() => api('notes/create', { text: 'hello', replyId: erinNote.id }, ayano), // ayano reply to erin's followers post
|
||||
msg => msg.type === 'note' && msg.body.userId === ayano.id, // wait ayano
|
||||
);
|
||||
@@ -521,7 +521,7 @@ describe('Streaming', () => {
|
||||
test('withReplies: false でも自分の投稿に対するリプライが流れる', async () => {
|
||||
const ayanoNote = await post(ayano, { text: 'hi', visibility: 'followers' });
|
||||
const fired = await waitFire(
|
||||
ayano, 'homeTimeline', // ayano:home
|
||||
ayano, 'hybridTimeline', // ayano:Hybrid
|
||||
() => api('notes/create', { text: 'hello', replyId: ayanoNote.id }, erin), // erin reply to ayano's followers post
|
||||
msg => msg.type === 'note' && msg.body.userId === erin.id, // wait erin
|
||||
);
|
||||
@@ -530,9 +530,12 @@ describe('Streaming', () => {
|
||||
});
|
||||
|
||||
test('withReplies: true のフォローしていない人のfollowersノートに対するリプライが流れない', async () => {
|
||||
// ayano は kyoko をフォローしているため kyoko の followers 投稿にリプライできるが、
|
||||
// erin は kyoko をフォローしていないため、そのリプライは erin の Hybrid Timeline には流れないはず
|
||||
const kyokoFollowersNote = await post(kyoko, { text: 'hi', visibility: 'followers' });
|
||||
const fired = await waitFire(
|
||||
erin, 'homeTimeline', // erin:home
|
||||
() => api('notes/create', { text: 'hello', replyId: chitose.id }, ayano), // ayano reply to chitose's post
|
||||
erin, 'hybridTimeline', // erin:Hybrid
|
||||
() => api('notes/create', { text: 'hello', replyId: kyokoFollowersNote.id }, ayano), // ayano reply to kyoko's followers post
|
||||
msg => msg.type === 'note' && msg.body.userId === ayano.id, // wait ayano
|
||||
);
|
||||
|
||||
@@ -680,7 +683,7 @@ describe('Streaming', () => {
|
||||
const fired = await waitFire(
|
||||
chitose, 'userList',
|
||||
() => api('notes/create', { text: 'foo' }, takumi),
|
||||
msg => msg.type === 'note' && msg.body.userId === kyoko.id,
|
||||
msg => msg.type === 'note' && msg.body.userId === takumi.id,
|
||||
{ listId: list.id },
|
||||
);
|
||||
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { beforeAll, describe, test } from 'vitest';
|
||||
import { beforeAll, describe, test, vi } from 'vitest';
|
||||
import { api, post, signup, uploadUrl } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
const waitForPushToTlOptions = { timeout: 3000, interval: 25 };
|
||||
|
||||
describe('users/notes', () => {
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let jpgNote: misskey.entities.Note;
|
||||
@@ -32,16 +34,18 @@ describe('users/notes', () => {
|
||||
}, 1000 * 60 * 2);
|
||||
|
||||
test('withFiles', async () => {
|
||||
const res = await api('users/notes', {
|
||||
userId: alice.id,
|
||||
withFiles: true,
|
||||
}, alice);
|
||||
await vi.waitFor(async () => {
|
||||
const res = await api('users/notes', {
|
||||
userId: alice.id,
|
||||
withFiles: true,
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.length, 3);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === jpgNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === pngNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === jpgPngNote.id), true);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.length, 3);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === jpgNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === pngNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === jpgPngNote.id), true);
|
||||
}, waitForPushToTlOptions);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,9 @@ import { beforeAll } from 'vitest';
|
||||
import { initTestDb, sendEnvResetRequest } from './utils.js';
|
||||
|
||||
beforeAll(async () => {
|
||||
await initTestDb(false);
|
||||
// 前ファイルのNestJSアプリをdispose(env-reset)した後にスキーマをdrop & 再作成する。
|
||||
// 逆順だと、前ファイルの最後のテストが投げっぱなしにした非同期処理(cacheServiceのrefresh等)が
|
||||
// dispose前のdrop中に発火し、Unhandled Rejection (relation does not exist) でクラッシュしうる。
|
||||
await sendEnvResetRequest();
|
||||
await initTestDb(false);
|
||||
});
|
||||
|
||||
60
packages/backend/test/unit/SentryTelemetryAdapter.ts
Normal file
60
packages/backend/test/unit/SentryTelemetryAdapter.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
import { buildSentryIntegrations } from '@/core/telemetry/adapters/SentryTelemetryAdapter.js';
|
||||
|
||||
type TestIntegration = Parameters<ReturnType<typeof buildSentryIntegrations>>[0][number];
|
||||
|
||||
function testIntegration(name: string): TestIntegration {
|
||||
return { name };
|
||||
}
|
||||
|
||||
describe('SentryTelemetryAdapter', () => {
|
||||
test('removes disabled integrations from Sentry defaults', () => {
|
||||
const integrations = buildSentryIntegrations({
|
||||
disabledIntegrations: ['Postgres'],
|
||||
enableNodeProfiling: false,
|
||||
});
|
||||
|
||||
const result = integrations([
|
||||
testIntegration('Http'),
|
||||
testIntegration('Postgres'),
|
||||
testIntegration('Redis'),
|
||||
]);
|
||||
|
||||
expect(result.map((integration: TestIntegration) => integration.name)).toEqual(['Http', 'Redis']);
|
||||
});
|
||||
|
||||
test('keeps profiling integration when enabled', () => {
|
||||
const integrations = buildSentryIntegrations({
|
||||
disabledIntegrations: [],
|
||||
enableNodeProfiling: true,
|
||||
nodeProfilingIntegration: () => testIntegration('ProfilingIntegration'),
|
||||
});
|
||||
|
||||
const result = integrations([
|
||||
testIntegration('Http'),
|
||||
]);
|
||||
|
||||
expect(result.map((integration: TestIntegration) => integration.name)).toEqual(['Http', 'ProfilingIntegration']);
|
||||
});
|
||||
|
||||
test('warns about unknown disabled integration names without removing defaults', () => {
|
||||
const warn = vi.fn();
|
||||
const integrations = buildSentryIntegrations({
|
||||
disabledIntegrations: ['Unknown'],
|
||||
enableNodeProfiling: false,
|
||||
warn,
|
||||
});
|
||||
|
||||
const result = integrations([
|
||||
testIntegration('Http'),
|
||||
]);
|
||||
|
||||
expect(result.map((integration: TestIntegration) => integration.name)).toEqual(['Http']);
|
||||
expect(warn).toHaveBeenCalledWith('Unknown Sentry integration configured in sentryForBackend.disabledIntegrations: Unknown');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { buildSentryNodeOptions } from '../../../../../src/core/telemetry/adapters/SentryTelemetryAdapter.js';
|
||||
|
||||
describe('buildSentryNodeOptions', () => {
|
||||
test('disables outbound trace propagation by default', () => {
|
||||
const options = buildSentryNodeOptions({
|
||||
enableNodeProfiling: false,
|
||||
options: {},
|
||||
});
|
||||
|
||||
expect(options.tracePropagationTargets).toEqual([]);
|
||||
});
|
||||
|
||||
test('allows explicit tracePropagationTargets to override the default', () => {
|
||||
const options = buildSentryNodeOptions({
|
||||
enableNodeProfiling: false,
|
||||
options: {
|
||||
tracePropagationTargets: ['^https://internal\\.example/'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(options.tracePropagationTargets).toEqual(['^https://internal\\.example/']);
|
||||
});
|
||||
});
|
||||
@@ -11,16 +11,16 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/estree": "1.0.9",
|
||||
"@types/node": "26.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.61.1",
|
||||
"@typescript-eslint/parser": "8.61.1",
|
||||
"@types/node": "26.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.62.0",
|
||||
"@typescript-eslint/parser": "8.62.0",
|
||||
"rollup": "4.62.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"i18n": "workspace:*",
|
||||
"magic-string": "0.30.21",
|
||||
"oxc-walker": "1.0.0",
|
||||
"rolldown": "1.1.2",
|
||||
"rolldown": "1.1.3",
|
||||
"vite": "8.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"sourceMap": false,
|
||||
"noEmit": true,
|
||||
"removeComments": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"strictFunctionTypes": true,
|
||||
|
||||
@@ -8,5 +8,5 @@ export function assertNever(x: never): never {
|
||||
throw new Error(`Unexpected type: ${(x as any)?.type ?? x}`);
|
||||
}
|
||||
|
||||
export function assertType<T>(_node: unknown): asserts node is T {
|
||||
export function assertType<T>(_node: unknown): asserts _node is T {
|
||||
}
|
||||
|
||||
2
packages/frontend-embed/@types/theme.d.ts
vendored
2
packages/frontend-embed/@types/theme.d.ts
vendored
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
declare module '@@/themes/*.json5' {
|
||||
import { Theme } from '@/theme.js';
|
||||
import { Theme } from '@@/js/theme.js';
|
||||
|
||||
const theme: Theme;
|
||||
|
||||
|
||||
@@ -10,9 +10,6 @@
|
||||
"lint": "pnpm typecheck && pnpm eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/pluginutils": "5.4.0",
|
||||
"@vitejs/plugin-vue": "6.0.7",
|
||||
"buraha": "0.0.1",
|
||||
"frontend-shared": "workspace:*",
|
||||
"i18n": "workspace:*",
|
||||
@@ -21,42 +18,34 @@
|
||||
"mfm-js": "0.26.0",
|
||||
"misskey-js": "workspace:*",
|
||||
"punycode.js": "2.3.1",
|
||||
"rollup": "4.62.2",
|
||||
"shiki": "4.2.0",
|
||||
"shiki": "4.3.0",
|
||||
"tinycolor2": "1.6.0",
|
||||
"uuid": "14.0.0",
|
||||
"vue": "3.5.38"
|
||||
"uuid": "14.0.1",
|
||||
"vue": "3.5.39"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/emoji-assets": "17.0.3",
|
||||
"@misskey-dev/summaly": "5.5.1",
|
||||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/pluginutils": "5.4.0",
|
||||
"@tabler/icons-webfont": "3.35.0",
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/estree": "1.0.9",
|
||||
"@types/micromatch": "4.0.10",
|
||||
"@types/node": "26.0.0",
|
||||
"@types/node": "26.0.1",
|
||||
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.61.1",
|
||||
"@typescript-eslint/parser": "8.61.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.62.0",
|
||||
"@typescript-eslint/parser": "8.62.0",
|
||||
"@vitest/coverage-v8": "4.1.9",
|
||||
"@vue/runtime-core": "3.5.38",
|
||||
"acorn": "8.17.0",
|
||||
"cross-env": "10.1.0",
|
||||
"@vitejs/plugin-vue": "6.0.7",
|
||||
"@vue/runtime-core": "3.5.39",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-vue": "10.9.2",
|
||||
"happy-dom": "20.10.6",
|
||||
"intersection-observer": "0.12.2",
|
||||
"lightningcss": "1.32.0",
|
||||
"micromatch": "4.0.8",
|
||||
"msw": "2.14.6",
|
||||
"prettier": "3.8.4",
|
||||
"sass-embedded": "1.100.0",
|
||||
"start-server-and-test": "3.0.11",
|
||||
"tsx": "4.22.4",
|
||||
"vite": "8.1.0",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vue-component-type-helpers": "3.3.5",
|
||||
"vue-eslint-parser": "10.4.1",
|
||||
"vue-tsc": "3.3.5"
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "Bundler",
|
||||
"removeComments": false,
|
||||
"skipLibCheck": true,
|
||||
"noLib": false,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import path from 'path';
|
||||
import pluginVue from '@vitejs/plugin-vue';
|
||||
import { defineConfig, type UserConfig } from 'vite';
|
||||
import * as yaml from 'js-yaml';
|
||||
import { load as loadYaml } from 'js-yaml';
|
||||
import { promises as fsp } from 'fs';
|
||||
|
||||
import locales from 'i18n';
|
||||
@@ -11,7 +11,7 @@ import pluginJson5 from './lib/vite-plugin-json5.js';
|
||||
import { pluginRemoveUnrefI18n } from '../frontend-builder/rollup-plugin-remove-unref-i18n';
|
||||
import { Features } from 'lightningcss';
|
||||
|
||||
const url = process.env.NODE_ENV === 'development' ? (yaml.load(await fsp.readFile('../../.config/default.yml', 'utf-8')) as any).url : null;
|
||||
const url = process.env.NODE_ENV === 'development' ? (loadYaml(await fsp.readFile('../../.config/default.yml', 'utf-8')) as any).url : null;
|
||||
const host = url ? (new URL(url)).hostname : undefined;
|
||||
|
||||
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue'];
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# frontend用Misskey Worldエンジン
|
||||
|
||||
エンジンはWeb Worker内で動作し、ほぼすべてのMisskey Webの機能は使えないため、意図しないそれらへの参照/依存が原理的に発生しないように別パッケージとする
|
||||
|
||||
ただしヘッドレス動作することは(今のところ)意図していない
|
||||
@@ -1,28 +0,0 @@
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
import sharedConfig from '../shared/eslint.config.js';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default [
|
||||
...sharedConfig,
|
||||
{
|
||||
ignores: [
|
||||
'**/node_modules',
|
||||
'built',
|
||||
'coverage',
|
||||
'jest.config.ts',
|
||||
'test',
|
||||
'test-d',
|
||||
],
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: tsParser,
|
||||
project: ['./tsconfig.json'],
|
||||
sourceType: 'module',
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "frontend-misskey-world-engine",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"eslint": "eslint './**/*.{js,jsx,ts,tsx}'",
|
||||
"typecheck": "tsgo --noEmit",
|
||||
"lint": "pnpm typecheck && pnpm eslint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/seedrandom": "3.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "8.59.2",
|
||||
"@typescript-eslint/parser": "8.59.2",
|
||||
"esbuild": "0.28.0",
|
||||
"execa": "9.6.1",
|
||||
"nodemon": "3.1.14",
|
||||
"throttle-debounce": "5.0.2",
|
||||
"@types/tinycolor2": "1.4.6"
|
||||
},
|
||||
"files": [
|
||||
"built"
|
||||
],
|
||||
"dependencies": {
|
||||
"@babylonjs/core": "9.15.0",
|
||||
"@babylonjs/inspector": "9.15.0",
|
||||
"@babylonjs/loaders": "9.15.0",
|
||||
"@babylonjs/materials": "9.15.0",
|
||||
"@types/throttle-debounce": "5.0.2",
|
||||
"eventemitter3": "5.0.4",
|
||||
"seedrandom": "3.0.5",
|
||||
"tinycolor2": "1.6.0",
|
||||
"hls.js": "1.6.16"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user