mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-05-08 13:55:30 +02:00
Compare commits
146 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dc2f1f49d | ||
|
|
353098f576 | ||
|
|
4c43ee4b53 | ||
|
|
037a1daa79 | ||
|
|
43401210c3 | ||
|
|
1259fabd7f | ||
|
|
58f4d5d790 | ||
|
|
c1019a006b | ||
|
|
7ca93893ca | ||
|
|
fbad40bb9b | ||
|
|
7794e7ae2f | ||
|
|
a78013d0e5 | ||
|
|
768d0bdc00 | ||
|
|
36450d7fac | ||
|
|
4e1fb618b8 | ||
|
|
1a25401452 | ||
|
|
67a41c09ae | ||
|
|
fc7cd636a3 | ||
|
|
8ef70283fa | ||
|
|
22bb79b3fd | ||
|
|
38e7054f06 | ||
|
|
1e237a7df5 | ||
|
|
1dcf25c24f | ||
|
|
9a40b366a3 | ||
|
|
d6c8e520f8 | ||
|
|
1b5ad63672 | ||
|
|
945d6a2b09 | ||
|
|
0283142d0e | ||
|
|
bcb4560f0d | ||
|
|
5bcdd6e849 | ||
|
|
8db800ed98 | ||
|
|
acab9ccb81 | ||
|
|
bc5aebe956 | ||
|
|
c971edd2dd | ||
|
|
8b0fdfcd69 | ||
|
|
3d83c1aaba | ||
|
|
d92aaf81c4 | ||
|
|
0ea8e0c25c | ||
|
|
ec4e57bb67 | ||
|
|
12142a221a | ||
|
|
79a9defa0c | ||
|
|
fc4c868269 | ||
|
|
27dc0d3530 | ||
|
|
57017f2747 | ||
|
|
bc8a741e14 | ||
|
|
4846ab077b | ||
|
|
920888ed2a | ||
|
|
c26c01c7a0 | ||
|
|
d792f4f348 | ||
|
|
79370aaee2 | ||
|
|
18a5e377f4 | ||
|
|
19fe32bd7e | ||
|
|
d246f6c360 | ||
|
|
5503ad9d1a | ||
|
|
7b0f5b50fc | ||
|
|
6177fcb2f5 | ||
|
|
c33f56e3ed | ||
|
|
b13410df02 | ||
|
|
87ffc672dd | ||
|
|
5672ead957 | ||
|
|
0aefebf02a | ||
|
|
271407312e | ||
|
|
c2a9a7b69e | ||
|
|
b920435068 | ||
|
|
1aeede97f5 | ||
|
|
a5ea7c976b | ||
|
|
d2063df78d | ||
|
|
be57ff4985 | ||
|
|
cf54c2ba47 | ||
|
|
762fa6a8d8 | ||
|
|
36fd7d17cf | ||
|
|
5c786cace8 | ||
|
|
c1c363bf08 | ||
|
|
4bd9f664d7 | ||
|
|
3d9e42efca | ||
|
|
138a248a6c | ||
|
|
6bae440f39 | ||
|
|
f5b864df7b | ||
|
|
7e52ea4818 | ||
|
|
358dc6289b | ||
|
|
1063d39de8 | ||
|
|
14aedc17ae | ||
|
|
0d7f9308cc | ||
|
|
34088ecd27 | ||
|
|
0f9e3bccef | ||
|
|
64de87438e | ||
|
|
5dcd8c827b | ||
|
|
35ec41fc1e | ||
|
|
618e2ba1d2 | ||
|
|
04f9147db6 | ||
|
|
0ed2a220f4 | ||
|
|
e9c3fe1228 | ||
|
|
0c2118e963 | ||
|
|
145d28a8e4 | ||
|
|
6a02dfdd3b | ||
|
|
831131864f | ||
|
|
1bd7693416 | ||
|
|
5251cd3aad | ||
|
|
0e536bdd86 | ||
|
|
fd519f5def | ||
|
|
0d830d720a | ||
|
|
0d49e94982 | ||
|
|
e6022c0d51 | ||
|
|
5e71418d5c | ||
|
|
c6a4caa8be | ||
|
|
1d1780081e | ||
|
|
622a09f8ed | ||
|
|
00e195f50b | ||
|
|
8bf6d31334 | ||
|
|
2a9db983fc | ||
|
|
4ea030d669 | ||
|
|
f2dee7b25e | ||
|
|
a0976772b3 | ||
|
|
0815a5235d | ||
|
|
9eae82de1d | ||
|
|
746367004e | ||
|
|
072f67d6e7 | ||
|
|
b55a6a80e1 | ||
|
|
24645e3d3d | ||
|
|
d415fd29a3 | ||
|
|
7768385be2 | ||
|
|
2177792a3c | ||
|
|
9e20065496 | ||
|
|
2cd32b2248 | ||
|
|
fa9c4a19b9 | ||
|
|
ea41cc6ec0 | ||
|
|
9716ea0324 | ||
|
|
02978d0247 | ||
|
|
6598d320d6 | ||
|
|
f8d5a46dbf | ||
|
|
da154c8209 | ||
|
|
b46f431a2e | ||
|
|
30c3f6a222 | ||
|
|
30311aca18 | ||
|
|
a9127e3ecd | ||
|
|
58469c0a69 | ||
|
|
9c5559a570 | ||
|
|
3187c6b28d | ||
|
|
09aba4cf16 | ||
|
|
5498ec57d0 | ||
|
|
4893cce43c | ||
|
|
a40ededf6b | ||
|
|
379079ee42 | ||
|
|
1d5a0d0777 | ||
|
|
2a33981811 | ||
|
|
c0466d1585 |
@@ -2,3 +2,4 @@
|
||||
POSTGRES_PASSWORD=example-misskey-pass
|
||||
POSTGRES_USER=example-misskey-user
|
||||
POSTGRES_DB=misskey
|
||||
DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}"
|
||||
|
||||
30
.github/dependabot.yml
vendored
30
.github/dependabot.yml
vendored
@@ -17,16 +17,32 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
# PNPM has an issue with dependabot. See:
|
||||
# https://github.com/dependabot/dependabot-core/issues/7258
|
||||
# https://github.com/pnpm/pnpm/issues/6530
|
||||
# TODO: Restore this when the issue is solved
|
||||
open-pull-requests-limit: 0
|
||||
open-pull-requests-limit: 10
|
||||
# List dependencies required to be updated together, sharing the same version numbers.
|
||||
# Those who simply have the common owner (e.g. @fastify) don't need to be listed.
|
||||
groups:
|
||||
swc:
|
||||
aws-sdk:
|
||||
patterns:
|
||||
- "@swc/*"
|
||||
- "@aws-sdk/*"
|
||||
bull-board:
|
||||
patterns:
|
||||
- "@bull-board/*"
|
||||
nestjs:
|
||||
patterns:
|
||||
- "@nestjs/*"
|
||||
slacc:
|
||||
patterns:
|
||||
- "slacc-*"
|
||||
storybook:
|
||||
patterns:
|
||||
- "storybook*"
|
||||
- "@storybook/*"
|
||||
swc-core:
|
||||
patterns:
|
||||
- "@swc/core*"
|
||||
typescript-eslint:
|
||||
patterns:
|
||||
- "@typescript-eslint/*"
|
||||
tensorflow:
|
||||
patterns:
|
||||
- "@tensorflow/*"
|
||||
|
||||
8
.github/workflows/api-misskey-js.yml
vendored
8
.github/workflows/api-misskey-js.yml
vendored
@@ -1,6 +1,12 @@
|
||||
name: API report (misskey.js)
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- packages/misskey-js/**
|
||||
pull_request:
|
||||
paths:
|
||||
- packages/misskey-js/**
|
||||
|
||||
jobs:
|
||||
report:
|
||||
|
||||
43
.github/workflows/changelog-check.yml
vendored
Normal file
43
.github/workflows/changelog-check.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Check the description in CHANGELOG.md
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
check-changelog:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout head
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4.0.1
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
|
||||
- name: Checkout base
|
||||
run: |
|
||||
mkdir _base
|
||||
cp -r .git _base/.git
|
||||
cd _base
|
||||
git fetch --depth 1 origin ${{ github.base_ref }}
|
||||
git checkout origin/${{ github.base_ref }} CHANGELOG.md
|
||||
|
||||
- name: Copy to Checker directory for CHANGELOG-base.md
|
||||
run: cp _base/CHANGELOG.md scripts/changelog-checker/CHANGELOG-base.md
|
||||
- name: Copy to Checker directory for CHANGELOG-head.md
|
||||
run: cp CHANGELOG.md scripts/changelog-checker/CHANGELOG-head.md
|
||||
- name: diff
|
||||
continue-on-error: true
|
||||
run: diff -u CHANGELOG-base.md CHANGELOG-head.md
|
||||
working-directory: scripts/changelog-checker
|
||||
|
||||
- name: Setup Checker
|
||||
run: npm install
|
||||
working-directory: scripts/changelog-checker
|
||||
- name: Run Checker
|
||||
run: npm run run
|
||||
working-directory: scripts/changelog-checker
|
||||
127
.github/workflows/check-misskey-js-autogen.yml
vendored
Normal file
127
.github/workflows/check-misskey-js-autogen.yml
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
name: Check Misskey JS autogen
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
paths:
|
||||
- packages/backend/**
|
||||
|
||||
jobs:
|
||||
check-misskey-js-autogen:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
api_json_names: "api-base.json api-head.json"
|
||||
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- name: setup node
|
||||
id: setup-node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: pnpm
|
||||
|
||||
- name: install dependencies
|
||||
run: pnpm i --frozen-lockfile
|
||||
|
||||
- name: wait get-api-diff
|
||||
uses: lewagon/wait-on-check-action@v1.3.3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
check-regexp: get-from-misskey .+
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
wait-interval: 30
|
||||
|
||||
- name: Download artifact
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
const workflows = await github.rest.actions.listWorkflowRunsForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
head_sha: `${{ github.event.pull_request.head.sha }}`
|
||||
}).then(x => x.data.workflow_runs);
|
||||
|
||||
console.log(workflows.map(x => ({name: x.name, title: x.display_title})));
|
||||
|
||||
const run_id = workflows.find(x => x.name.includes("Get api.json from Misskey")).id;
|
||||
|
||||
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: run_id,
|
||||
});
|
||||
|
||||
let matchArtifacts = allArtifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name.startsWith("api-artifact-") || artifact.name == "api-artifact"
|
||||
});
|
||||
|
||||
await Promise.all(matchArtifacts.map(async (artifact) => {
|
||||
let download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: artifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
await fs.promises.writeFile(`${process.env.GITHUB_WORKSPACE}/${artifact.name}.zip`, Buffer.from(download.data));
|
||||
}));
|
||||
|
||||
- name: unzip artifacts
|
||||
run: |-
|
||||
find . -mindepth 1 -maxdepth 1 -type f -name '*.zip' -exec unzip {} -d . ';'
|
||||
ls -la
|
||||
|
||||
- name: build autogen
|
||||
run: |-
|
||||
for name in $(echo $api_json_names)
|
||||
do
|
||||
checksum=$(mktemp)
|
||||
mv $name packages/misskey-js/generator/api.json
|
||||
|
||||
cd packages/misskey-js/generator
|
||||
pnpm run generate
|
||||
find built -type f -exec sh -c 'echo $(sed -E "s/^\s+\*\s+generatedAt:.+$//" {} | sha256sum | cut -d" " -f 1) {}' \; > $checksum
|
||||
cd ../../..
|
||||
cp $checksum ${name}_checksum
|
||||
done
|
||||
|
||||
- name: check update for type definitions
|
||||
run: diff $(echo -n ${api_json_names} | awk -v RS=" " '{ printf "%s_checksum ", $0 }')
|
||||
|
||||
- name: send message
|
||||
if: failure()
|
||||
uses: thollander/actions-comment-pull-request@v2
|
||||
with:
|
||||
comment_tag: check-misskey-js-autogen
|
||||
message: |-
|
||||
Thank you for sending us a great Pull Request! 👍
|
||||
Please regenerate misskey-js type definitions! 🙏
|
||||
|
||||
example:
|
||||
```sh
|
||||
pnpm run build-misskey-js-with-types
|
||||
```
|
||||
|
||||
- name: send message
|
||||
if: success()
|
||||
uses: thollander/actions-comment-pull-request@v2
|
||||
with:
|
||||
comment_tag: check-misskey-js-autogen
|
||||
mode: delete
|
||||
message: "Thank you!"
|
||||
12
.github/workflows/lint.yml
vendored
12
.github/workflows/lint.yml
vendored
@@ -5,7 +5,19 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
paths:
|
||||
- packages/backend/**
|
||||
- packages/frontend/**
|
||||
- packages/sw/**
|
||||
- packages/misskey-js/**
|
||||
- packages/shared/.eslintrc.js
|
||||
pull_request:
|
||||
paths:
|
||||
- packages/backend/**
|
||||
- packages/frontend/**
|
||||
- packages/sw/**
|
||||
- packages/misskey-js/**
|
||||
- packages/shared/.eslintrc.js
|
||||
|
||||
jobs:
|
||||
pnpm_install:
|
||||
|
||||
64
.github/workflows/test-backend.yml
vendored
64
.github/workflows/test-backend.yml
vendored
@@ -5,10 +5,18 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
paths:
|
||||
- packages/backend/**
|
||||
# for permissions
|
||||
- packages/misskey-js/**
|
||||
pull_request:
|
||||
paths:
|
||||
- packages/backend/**
|
||||
# for permissions
|
||||
- packages/misskey-js/**
|
||||
|
||||
jobs:
|
||||
jest:
|
||||
unit:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
@@ -51,9 +59,59 @@ jobs:
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
- name: Test
|
||||
run: pnpm jest-and-coverage
|
||||
- name: Upload Coverage
|
||||
run: pnpm --filter backend test-and-coverage
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/backend/coverage/coverage-final.json
|
||||
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.10.0]
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
ports:
|
||||
- 54312:5432
|
||||
env:
|
||||
POSTGRES_DB: test-misskey
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
redis:
|
||||
image: redis:7
|
||||
ports:
|
||||
- 56312:6379
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
submodules: true
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.0.1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
- run: corepack enable
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- name: Check pnpm-lock.yaml
|
||||
run: git diff --exit-code pnpm-lock.yaml
|
||||
- name: Copy Configure
|
||||
run: cp .github/misskey/test.yml .config
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
- name: Test
|
||||
run: pnpm --filter backend test-and-coverage:e2e
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/backend/coverage/coverage-final.json
|
||||
|
||||
13
.github/workflows/test-frontend.yml
vendored
13
.github/workflows/test-frontend.yml
vendored
@@ -5,7 +5,20 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
paths:
|
||||
- packages/frontend/**
|
||||
# for permissions
|
||||
- packages/misskey-js/**
|
||||
# for e2e
|
||||
- packages/backend/**
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- packages/frontend/**
|
||||
# for permissions
|
||||
- packages/misskey-js/**
|
||||
# for e2e
|
||||
- packages/backend/**
|
||||
|
||||
jobs:
|
||||
vitest:
|
||||
|
||||
4
.github/workflows/test-misskey-js.yml
vendored
4
.github/workflows/test-misskey-js.yml
vendored
@@ -6,8 +6,12 @@ name: Test (misskey.js)
|
||||
on:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
paths:
|
||||
- packages/misskey-js/**
|
||||
pull_request:
|
||||
branches: [ develop ]
|
||||
paths:
|
||||
- packages/misskey-js/**
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
||||
47
.github/workflows/validate-api-json.yml
vendored
Normal file
47
.github/workflows/validate-api-json.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: Test (backend)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
paths:
|
||||
- packages/backend/**
|
||||
pull_request:
|
||||
paths:
|
||||
- packages/backend/**
|
||||
|
||||
jobs:
|
||||
validate-api-json:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.10.0]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
submodules: true
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.0.1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
- name: Install Redocly CLI
|
||||
run: npm i -g @redocly/cli
|
||||
- run: corepack enable
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- name: Check pnpm-lock.yaml
|
||||
run: git diff --exit-code pnpm-lock.yaml
|
||||
- name: Copy Configure
|
||||
run: cp .config/example.yml .config/default.yml
|
||||
- name: Build and generate
|
||||
run: pnpm build && pnpm --filter backend generate-api-json
|
||||
- name: Validation
|
||||
run: npx @redocly/cli lint --extends=minimal ./packages/backend/built/api.json
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -41,6 +41,7 @@ docker-compose.yml
|
||||
# misskey
|
||||
/build
|
||||
built
|
||||
built-test
|
||||
/data
|
||||
/.cache-loader
|
||||
/db
|
||||
|
||||
36
CHANGELOG.md
36
CHANGELOG.md
@@ -14,9 +14,43 @@
|
||||
|
||||
## 202x.x.x (Unreleased)
|
||||
|
||||
### General
|
||||
- Feat: [mCaptcha](https://github.com/mCaptcha/mCaptcha)のサポートを追加
|
||||
- Fix: リストライムラインの「リノートを表示」が正しく機能しない問題を修正
|
||||
|
||||
### Client
|
||||
- Feat: 新しいゲームを追加
|
||||
- Feat: 音声・映像プレイヤーを追加
|
||||
- Feat: 絵文字の詳細ダイアログを追加
|
||||
- Feat: 枠線をつけるMFM`$[border.width=1,style=solid,color=fff,radius=0 ...]`を追加
|
||||
- デフォルトで枠線からはみ出る部分が隠されるようにしました。初期と同じ挙動にするには`$[border.noclip`が必要です
|
||||
- Feat: スワイプでタブを切り替えられるように
|
||||
- Enhance: MFM等のコードブロックに全文コピー用のボタンを追加
|
||||
- Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように
|
||||
- Enhance: ActivityPubをサポートしているウェブリンクを展開できるように
|
||||
- Enhance: チャンネルノートのピン留めをノートのメニューからできるように
|
||||
- Enhance: 管理者の場合はAPI tokenの発行画面で管理機能に関する権限を付与できるように
|
||||
- Enhance: AiScriptを0.17.0に更新 [CHANGELOG](https://github.com/aiscript-dev/aiscript/blob/bb89d132b633a622d3cb0eff0d0cc7e476c0cfdd/CHANGELOG.md)
|
||||
- 配列の範囲外・非整数のインデックスへの代入が完全禁止になるので注意
|
||||
- Enhance: 絵文字ピッカー・オートコンプリートで、完全一致した絵文字を優先的に表示するように
|
||||
- Enhance: Playの説明欄にMFMを使えるように
|
||||
- Enhance: チャンネルノートの場合は詳細ページからその前後のノートを見れるように
|
||||
- Fix: ネイティブモードの絵文字がモノクロにならないように
|
||||
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
|
||||
- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
|
||||
- Fix: v2023.12.1で追加された`$[clickable ...]`および`onClickEv`が正しく機能していないのを修正
|
||||
- Enhance: ページ遷移時にPlayerを閉じるように
|
||||
|
||||
### Server
|
||||
- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました
|
||||
- Enhance: ActivityPub Deliver queueでBodyを事前処理するように (#12916)
|
||||
- Enhance: クリップをエクスポートできるように
|
||||
- Enhance: `/files`のファイルに対してHTTP Rangeリクエストを行えるように
|
||||
- Enhance: `api.json`のOpenAPI Specificationを3.1.0に更新
|
||||
- Fix: `drive/files/update`でファイル名のバリデーションが機能していない問題を修正
|
||||
- Fix: `notes/create`で、`text`が空白文字のみで構成されているか`null`であって、かつ`text`だけであるリクエストに対するレスポンスが400になるように変更
|
||||
- Fix: `notes/create`で、`text`が空白文字のみで構成されていてかつリノート、ファイルまたは投票を含んでいるリクエストに対するレスポンスの`text`が`""`から`null`になるように変更
|
||||
- Fix: ipv4とipv6の両方が利用可能な環境でallowedPrivateNetworksが設定されていた場合プライベートipの検証ができていなかった問題を修正
|
||||
- Fix: properly handle cc followers
|
||||
|
||||
## 2023.12.2
|
||||
|
||||
|
||||
2
COPYING
2
COPYING
@@ -1,5 +1,5 @@
|
||||
Unless otherwise stated this repository is
|
||||
Copyright © 2014-2023 syuilo and contributors
|
||||
Copyright © 2014-2024 syuilo and contributors
|
||||
|
||||
And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE.
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ Also, the later tasks are more indefinite and are subject to change as developme
|
||||
This is the phase we are at now. We need to make a high-maintenance environment that can withstand future development.
|
||||
|
||||
- ~~Make the number of type errors zero (backend)~~ → Done ✔️
|
||||
- Make the number of type errors zero (frontend)
|
||||
- Improve CI
|
||||
- ~~Fix tests~~ → Done ✔️
|
||||
- Fix random test failures - https://github.com/misskey-dev/misskey/issues/7985 and https://github.com/misskey-dev/misskey/issues/7986
|
||||
|
||||
@@ -7,6 +7,7 @@ services:
|
||||
links:
|
||||
- db
|
||||
- redis
|
||||
# - mcaptcha
|
||||
# - meilisearch
|
||||
depends_on:
|
||||
db:
|
||||
@@ -48,6 +49,36 @@ services:
|
||||
interval: 5s
|
||||
retries: 20
|
||||
|
||||
# mcaptcha:
|
||||
# restart: always
|
||||
# image: mcaptcha/mcaptcha:latest
|
||||
# networks:
|
||||
# internal_network:
|
||||
# external_network:
|
||||
# aliases:
|
||||
# - localhost
|
||||
# ports:
|
||||
# - 7493:7493
|
||||
# env_file:
|
||||
# - .config/docker.env
|
||||
# environment:
|
||||
# PORT: 7493
|
||||
# MCAPTCHA_redis_URL: "redis://mcaptcha_redis/"
|
||||
# depends_on:
|
||||
# db:
|
||||
# condition: service_healthy
|
||||
# mcaptcha_redis:
|
||||
# condition: service_healthy
|
||||
#
|
||||
# mcaptcha_redis:
|
||||
# image: mcaptcha/cache:latest
|
||||
# networks:
|
||||
# - internal_network
|
||||
# healthcheck:
|
||||
# test: "redis-cli ping"
|
||||
# interval: 5s
|
||||
# retries: 20
|
||||
|
||||
# meilisearch:
|
||||
# restart: always
|
||||
# image: getmeili/meilisearch:v1.3.4
|
||||
|
||||
@@ -6,54 +6,171 @@ import ts from 'typescript';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const parameterRegExp = /\{(\w+)\}/g;
|
||||
|
||||
function createMemberType(item) {
|
||||
if (typeof item !== 'string') {
|
||||
return ts.factory.createTypeLiteralNode(createMembers(item));
|
||||
}
|
||||
const parameters = Array.from(
|
||||
item.matchAll(parameterRegExp),
|
||||
([, parameter]) => parameter,
|
||||
);
|
||||
if (!parameters.length) {
|
||||
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
|
||||
}
|
||||
return ts.factory.createTypeReferenceNode(
|
||||
ts.factory.createIdentifier('ParameterizedString'),
|
||||
[
|
||||
ts.factory.createUnionTypeNode(
|
||||
parameters.map((parameter) =>
|
||||
ts.factory.createLiteralTypeNode(
|
||||
ts.factory.createStringLiteral(parameter),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
function createMembers(record) {
|
||||
return Object.entries(record)
|
||||
.map(([k, v]) => ts.factory.createPropertySignature(
|
||||
return Object.entries(record).map(([k, v]) =>
|
||||
ts.factory.createPropertySignature(
|
||||
undefined,
|
||||
ts.factory.createStringLiteral(k),
|
||||
undefined,
|
||||
typeof v === 'string'
|
||||
? ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)
|
||||
: ts.factory.createTypeLiteralNode(createMembers(v)),
|
||||
));
|
||||
createMemberType(v),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export default function generateDTS() {
|
||||
const locale = yaml.load(fs.readFileSync(`${__dirname}/ja-JP.yml`, 'utf-8'));
|
||||
const members = createMembers(locale);
|
||||
const elements = [
|
||||
ts.factory.createVariableStatement(
|
||||
[ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)],
|
||||
ts.factory.createVariableDeclarationList(
|
||||
[
|
||||
ts.factory.createVariableDeclaration(
|
||||
ts.factory.createIdentifier('kParameters'),
|
||||
undefined,
|
||||
ts.factory.createTypeOperatorNode(
|
||||
ts.SyntaxKind.UniqueKeyword,
|
||||
ts.factory.createKeywordTypeNode(ts.SyntaxKind.SymbolKeyword),
|
||||
),
|
||||
undefined,
|
||||
),
|
||||
],
|
||||
ts.NodeFlags.Const,
|
||||
),
|
||||
),
|
||||
ts.factory.createInterfaceDeclaration(
|
||||
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
|
||||
ts.factory.createIdentifier('ParameterizedString'),
|
||||
[
|
||||
ts.factory.createTypeParameterDeclaration(
|
||||
undefined,
|
||||
ts.factory.createIdentifier('T'),
|
||||
ts.factory.createTypeReferenceNode(
|
||||
ts.factory.createIdentifier('string'),
|
||||
undefined,
|
||||
),
|
||||
),
|
||||
],
|
||||
undefined,
|
||||
[
|
||||
ts.factory.createPropertySignature(
|
||||
undefined,
|
||||
ts.factory.createComputedPropertyName(
|
||||
ts.factory.createIdentifier('kParameters'),
|
||||
),
|
||||
undefined,
|
||||
ts.factory.createTypeReferenceNode(
|
||||
ts.factory.createIdentifier('T'),
|
||||
undefined,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
ts.factory.createInterfaceDeclaration(
|
||||
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
|
||||
ts.factory.createIdentifier('ILocale'),
|
||||
undefined,
|
||||
undefined,
|
||||
[
|
||||
ts.factory.createIndexSignature(
|
||||
undefined,
|
||||
[
|
||||
ts.factory.createParameterDeclaration(
|
||||
undefined,
|
||||
undefined,
|
||||
ts.factory.createIdentifier('_'),
|
||||
undefined,
|
||||
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
||||
undefined,
|
||||
),
|
||||
],
|
||||
ts.factory.createUnionTypeNode([
|
||||
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
||||
ts.factory.createTypeReferenceNode(
|
||||
ts.factory.createIdentifier('ParameterizedString'),
|
||||
[ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)],
|
||||
),
|
||||
ts.factory.createTypeReferenceNode(
|
||||
ts.factory.createIdentifier('ILocale'),
|
||||
undefined,
|
||||
),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
ts.factory.createInterfaceDeclaration(
|
||||
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
|
||||
ts.factory.createIdentifier('Locale'),
|
||||
undefined,
|
||||
undefined,
|
||||
[
|
||||
ts.factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [
|
||||
ts.factory.createExpressionWithTypeArguments(
|
||||
ts.factory.createIdentifier('ILocale'),
|
||||
undefined,
|
||||
),
|
||||
]),
|
||||
],
|
||||
members,
|
||||
),
|
||||
ts.factory.createVariableStatement(
|
||||
[ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)],
|
||||
ts.factory.createVariableDeclarationList(
|
||||
[ts.factory.createVariableDeclaration(
|
||||
ts.factory.createIdentifier('locales'),
|
||||
undefined,
|
||||
ts.factory.createTypeLiteralNode([ts.factory.createIndexSignature(
|
||||
[
|
||||
ts.factory.createVariableDeclaration(
|
||||
ts.factory.createIdentifier('locales'),
|
||||
undefined,
|
||||
[ts.factory.createParameterDeclaration(
|
||||
undefined,
|
||||
undefined,
|
||||
ts.factory.createIdentifier('lang'),
|
||||
undefined,
|
||||
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
||||
undefined,
|
||||
)],
|
||||
ts.factory.createTypeReferenceNode(
|
||||
ts.factory.createIdentifier('Locale'),
|
||||
undefined,
|
||||
),
|
||||
)]),
|
||||
undefined,
|
||||
)],
|
||||
ts.NodeFlags.Const | ts.NodeFlags.Ambient | ts.NodeFlags.ContextFlags,
|
||||
ts.factory.createTypeLiteralNode([
|
||||
ts.factory.createIndexSignature(
|
||||
undefined,
|
||||
[
|
||||
ts.factory.createParameterDeclaration(
|
||||
undefined,
|
||||
undefined,
|
||||
ts.factory.createIdentifier('lang'),
|
||||
undefined,
|
||||
ts.factory.createKeywordTypeNode(
|
||||
ts.SyntaxKind.StringKeyword,
|
||||
),
|
||||
undefined,
|
||||
),
|
||||
],
|
||||
ts.factory.createTypeReferenceNode(
|
||||
ts.factory.createIdentifier('Locale'),
|
||||
undefined,
|
||||
),
|
||||
),
|
||||
]),
|
||||
undefined,
|
||||
),
|
||||
],
|
||||
ts.NodeFlags.Const,
|
||||
),
|
||||
),
|
||||
ts.factory.createFunctionDeclaration(
|
||||
@@ -70,16 +187,28 @@ export default function generateDTS() {
|
||||
),
|
||||
ts.factory.createExportDefault(ts.factory.createIdentifier('locales')),
|
||||
];
|
||||
const printed = ts.createPrinter({
|
||||
newLine: ts.NewLineKind.LineFeed,
|
||||
}).printList(
|
||||
ts.ListFormat.MultiLine,
|
||||
ts.factory.createNodeArray(elements),
|
||||
ts.createSourceFile('index.d.ts', '', ts.ScriptTarget.ESNext, true, ts.ScriptKind.TS),
|
||||
);
|
||||
const printed = ts
|
||||
.createPrinter({
|
||||
newLine: ts.NewLineKind.LineFeed,
|
||||
})
|
||||
.printList(
|
||||
ts.ListFormat.MultiLine,
|
||||
ts.factory.createNodeArray(elements),
|
||||
ts.createSourceFile(
|
||||
'index.d.ts',
|
||||
'',
|
||||
ts.ScriptTarget.ESNext,
|
||||
true,
|
||||
ts.ScriptKind.TS,
|
||||
),
|
||||
);
|
||||
|
||||
fs.writeFileSync(`${__dirname}/index.d.ts`, `/* eslint-disable */
|
||||
fs.writeFileSync(
|
||||
`${__dirname}/index.d.ts`,
|
||||
`/* eslint-disable */
|
||||
// This file is generated by locales/generateDTS.js
|
||||
// Do not edit this file directly.
|
||||
${printed}`, 'utf-8');
|
||||
${printed}`,
|
||||
'utf-8',
|
||||
);
|
||||
}
|
||||
|
||||
300
locales/index.d.ts
vendored
300
locales/index.d.ts
vendored
@@ -1,12 +1,19 @@
|
||||
/* eslint-disable */
|
||||
// This file is generated by locales/generateDTS.js
|
||||
// Do not edit this file directly.
|
||||
export interface Locale {
|
||||
declare const kParameters: unique symbol;
|
||||
export interface ParameterizedString<T extends string> {
|
||||
[kParameters]: T;
|
||||
}
|
||||
export interface ILocale {
|
||||
[_: string]: string | ParameterizedString<string> | ILocale;
|
||||
}
|
||||
export interface Locale extends ILocale {
|
||||
"_lang_": string;
|
||||
"headlineMisskey": string;
|
||||
"introMisskey": string;
|
||||
"poweredByMisskeyDescription": string;
|
||||
"monthAndDay": string;
|
||||
"poweredByMisskeyDescription": ParameterizedString<"name">;
|
||||
"monthAndDay": ParameterizedString<"month" | "day">;
|
||||
"search": string;
|
||||
"notifications": string;
|
||||
"username": string;
|
||||
@@ -18,7 +25,7 @@ export interface Locale {
|
||||
"cancel": string;
|
||||
"noThankYou": string;
|
||||
"enterUsername": string;
|
||||
"renotedBy": string;
|
||||
"renotedBy": ParameterizedString<"user">;
|
||||
"noNotes": string;
|
||||
"noNotifications": string;
|
||||
"instance": string;
|
||||
@@ -78,8 +85,8 @@ export interface Locale {
|
||||
"export": string;
|
||||
"files": string;
|
||||
"download": string;
|
||||
"driveFileDeleteConfirm": string;
|
||||
"unfollowConfirm": string;
|
||||
"driveFileDeleteConfirm": ParameterizedString<"name">;
|
||||
"unfollowConfirm": ParameterizedString<"name">;
|
||||
"exportRequested": string;
|
||||
"importRequested": string;
|
||||
"lists": string;
|
||||
@@ -183,9 +190,9 @@ export interface Locale {
|
||||
"wallpaper": string;
|
||||
"setWallpaper": string;
|
||||
"removeWallpaper": string;
|
||||
"searchWith": string;
|
||||
"searchWith": ParameterizedString<"q">;
|
||||
"youHaveNoLists": string;
|
||||
"followConfirm": string;
|
||||
"followConfirm": ParameterizedString<"name">;
|
||||
"proxyAccount": string;
|
||||
"proxyAccountDescription": string;
|
||||
"host": string;
|
||||
@@ -208,7 +215,7 @@ export interface Locale {
|
||||
"software": string;
|
||||
"version": string;
|
||||
"metadata": string;
|
||||
"withNFiles": string;
|
||||
"withNFiles": ParameterizedString<"n">;
|
||||
"monitor": string;
|
||||
"jobQueue": string;
|
||||
"cpuAndMemory": string;
|
||||
@@ -237,7 +244,7 @@ export interface Locale {
|
||||
"processing": string;
|
||||
"preview": string;
|
||||
"default": string;
|
||||
"defaultValueIs": string;
|
||||
"defaultValueIs": ParameterizedString<"value">;
|
||||
"noCustomEmojis": string;
|
||||
"noJobs": string;
|
||||
"federating": string;
|
||||
@@ -266,8 +273,8 @@ export interface Locale {
|
||||
"imageUrl": string;
|
||||
"remove": string;
|
||||
"removed": string;
|
||||
"removeAreYouSure": string;
|
||||
"deleteAreYouSure": string;
|
||||
"removeAreYouSure": ParameterizedString<"x">;
|
||||
"deleteAreYouSure": ParameterizedString<"x">;
|
||||
"resetAreYouSure": string;
|
||||
"areYouSure": string;
|
||||
"saved": string;
|
||||
@@ -285,8 +292,8 @@ export interface Locale {
|
||||
"messageRead": string;
|
||||
"noMoreHistory": string;
|
||||
"startMessaging": string;
|
||||
"nUsersRead": string;
|
||||
"agreeTo": string;
|
||||
"nUsersRead": ParameterizedString<"n">;
|
||||
"agreeTo": ParameterizedString<"0">;
|
||||
"agree": string;
|
||||
"agreeBelow": string;
|
||||
"basicNotesBeforeCreateAccount": string;
|
||||
@@ -298,7 +305,7 @@ export interface Locale {
|
||||
"images": string;
|
||||
"image": string;
|
||||
"birthday": string;
|
||||
"yearsOld": string;
|
||||
"yearsOld": ParameterizedString<"age">;
|
||||
"registeredDate": string;
|
||||
"location": string;
|
||||
"theme": string;
|
||||
@@ -353,9 +360,9 @@ export interface Locale {
|
||||
"thisYear": string;
|
||||
"thisMonth": string;
|
||||
"today": string;
|
||||
"dayX": string;
|
||||
"monthX": string;
|
||||
"yearX": string;
|
||||
"dayX": ParameterizedString<"day">;
|
||||
"monthX": ParameterizedString<"month">;
|
||||
"yearX": ParameterizedString<"year">;
|
||||
"pages": string;
|
||||
"integration": string;
|
||||
"connectService": string;
|
||||
@@ -382,6 +389,11 @@ export interface Locale {
|
||||
"enableHcaptcha": string;
|
||||
"hcaptchaSiteKey": string;
|
||||
"hcaptchaSecretKey": string;
|
||||
"mcaptcha": string;
|
||||
"enableMcaptcha": string;
|
||||
"mcaptchaSiteKey": string;
|
||||
"mcaptchaSecretKey": string;
|
||||
"mcaptchaInstanceUrl": string;
|
||||
"recaptcha": string;
|
||||
"enableRecaptcha": string;
|
||||
"recaptchaSiteKey": string;
|
||||
@@ -415,7 +427,7 @@ export interface Locale {
|
||||
"recentlyUpdatedUsers": string;
|
||||
"recentlyRegisteredUsers": string;
|
||||
"recentlyDiscoveredUsers": string;
|
||||
"exploreUsersCount": string;
|
||||
"exploreUsersCount": ParameterizedString<"count">;
|
||||
"exploreFediverse": string;
|
||||
"popularTags": string;
|
||||
"userList": string;
|
||||
@@ -432,16 +444,16 @@ export interface Locale {
|
||||
"moderationNote": string;
|
||||
"addModerationNote": string;
|
||||
"moderationLogs": string;
|
||||
"nUsersMentioned": string;
|
||||
"nUsersMentioned": ParameterizedString<"n">;
|
||||
"securityKeyAndPasskey": string;
|
||||
"securityKey": string;
|
||||
"lastUsed": string;
|
||||
"lastUsedAt": string;
|
||||
"lastUsedAt": ParameterizedString<"t">;
|
||||
"unregister": string;
|
||||
"passwordLessLogin": string;
|
||||
"passwordLessLoginDescription": string;
|
||||
"resetPassword": string;
|
||||
"newPasswordIs": string;
|
||||
"newPasswordIs": ParameterizedString<"password">;
|
||||
"reduceUiAnimation": string;
|
||||
"share": string;
|
||||
"notFound": string;
|
||||
@@ -461,7 +473,7 @@ export interface Locale {
|
||||
"enable": string;
|
||||
"next": string;
|
||||
"retype": string;
|
||||
"noteOf": string;
|
||||
"noteOf": ParameterizedString<"user">;
|
||||
"quoteAttached": string;
|
||||
"quoteQuestion": string;
|
||||
"noMessagesYet": string;
|
||||
@@ -481,12 +493,12 @@ export interface Locale {
|
||||
"strongPassword": string;
|
||||
"passwordMatched": string;
|
||||
"passwordNotMatched": string;
|
||||
"signinWith": string;
|
||||
"signinWith": ParameterizedString<"x">;
|
||||
"signinFailed": string;
|
||||
"or": string;
|
||||
"language": string;
|
||||
"uiLanguage": string;
|
||||
"aboutX": string;
|
||||
"aboutX": ParameterizedString<"x">;
|
||||
"emojiStyle": string;
|
||||
"native": string;
|
||||
"disableDrawer": string;
|
||||
@@ -504,7 +516,7 @@ export interface Locale {
|
||||
"regenerate": string;
|
||||
"fontSize": string;
|
||||
"mediaListWithOneImageAppearance": string;
|
||||
"limitTo": string;
|
||||
"limitTo": ParameterizedString<"x">;
|
||||
"noFollowRequests": string;
|
||||
"openImageInNewTab": string;
|
||||
"dashboard": string;
|
||||
@@ -582,7 +594,7 @@ export interface Locale {
|
||||
"deleteAllFiles": string;
|
||||
"deleteAllFilesConfirm": string;
|
||||
"removeAllFollowing": string;
|
||||
"removeAllFollowingDescription": string;
|
||||
"removeAllFollowingDescription": ParameterizedString<"host">;
|
||||
"userSuspended": string;
|
||||
"userSilenced": string;
|
||||
"yourAccountSuspendedTitle": string;
|
||||
@@ -609,7 +621,6 @@ export interface Locale {
|
||||
"enablePlayer": string;
|
||||
"disablePlayer": string;
|
||||
"expandTweet": string;
|
||||
"expandNote": string;
|
||||
"themeEditor": string;
|
||||
"description": string;
|
||||
"describeFile": string;
|
||||
@@ -630,6 +641,7 @@ export interface Locale {
|
||||
"small": string;
|
||||
"generateAccessToken": string;
|
||||
"permission": string;
|
||||
"adminPermission": string;
|
||||
"enableAll": string;
|
||||
"disableAll": string;
|
||||
"tokenRequested": string;
|
||||
@@ -653,9 +665,9 @@ export interface Locale {
|
||||
"wordMute": string;
|
||||
"hardWordMute": string;
|
||||
"regexpError": string;
|
||||
"regexpErrorDescription": string;
|
||||
"regexpErrorDescription": ParameterizedString<"tab" | "line">;
|
||||
"instanceMute": string;
|
||||
"userSaysSomething": string;
|
||||
"userSaysSomething": ParameterizedString<"name">;
|
||||
"makeActive": string;
|
||||
"display": string;
|
||||
"copy": string;
|
||||
@@ -673,6 +685,7 @@ export interface Locale {
|
||||
"other": string;
|
||||
"regenerateLoginToken": string;
|
||||
"regenerateLoginTokenDescription": string;
|
||||
"theKeywordWhenSearchingForCustomEmoji": string;
|
||||
"setMultipleBySeparatingWithSpace": string;
|
||||
"fileIdOrUrl": string;
|
||||
"behavior": string;
|
||||
@@ -680,7 +693,7 @@ export interface Locale {
|
||||
"abuseReports": string;
|
||||
"reportAbuse": string;
|
||||
"reportAbuseRenote": string;
|
||||
"reportAbuseOf": string;
|
||||
"reportAbuseOf": ParameterizedString<"name">;
|
||||
"fillAbuseReportDescription": string;
|
||||
"abuseReported": string;
|
||||
"reporter": string;
|
||||
@@ -695,7 +708,7 @@ export interface Locale {
|
||||
"defaultNavigationBehaviour": string;
|
||||
"editTheseSettingsMayBreakAccount": string;
|
||||
"instanceTicker": string;
|
||||
"waitingFor": string;
|
||||
"waitingFor": ParameterizedString<"x">;
|
||||
"random": string;
|
||||
"system": string;
|
||||
"switchUi": string;
|
||||
@@ -705,10 +718,10 @@ export interface Locale {
|
||||
"optional": string;
|
||||
"createNewClip": string;
|
||||
"unclip": string;
|
||||
"confirmToUnclipAlreadyClippedNote": string;
|
||||
"confirmToUnclipAlreadyClippedNote": ParameterizedString<"name">;
|
||||
"public": string;
|
||||
"private": string;
|
||||
"i18nInfo": string;
|
||||
"i18nInfo": ParameterizedString<"link">;
|
||||
"manageAccessTokens": string;
|
||||
"accountInfo": string;
|
||||
"notesCount": string;
|
||||
@@ -758,9 +771,9 @@ export interface Locale {
|
||||
"needReloadToApply": string;
|
||||
"showTitlebar": string;
|
||||
"clearCache": string;
|
||||
"onlineUsersCount": string;
|
||||
"nUsers": string;
|
||||
"nNotes": string;
|
||||
"onlineUsersCount": ParameterizedString<"n">;
|
||||
"nUsers": ParameterizedString<"n">;
|
||||
"nNotes": ParameterizedString<"n">;
|
||||
"sendErrorReports": string;
|
||||
"sendErrorReportsDescription": string;
|
||||
"myTheme": string;
|
||||
@@ -792,7 +805,7 @@ export interface Locale {
|
||||
"publish": string;
|
||||
"inChannelSearch": string;
|
||||
"useReactionPickerForContextMenu": string;
|
||||
"typingUsers": string;
|
||||
"typingUsers": ParameterizedString<"users">;
|
||||
"jumpToSpecifiedDate": string;
|
||||
"showingPastTimeline": string;
|
||||
"clear": string;
|
||||
@@ -859,7 +872,7 @@ export interface Locale {
|
||||
"misskeyUpdated": string;
|
||||
"whatIsNew": string;
|
||||
"translate": string;
|
||||
"translatedFrom": string;
|
||||
"translatedFrom": ParameterizedString<"x">;
|
||||
"accountDeletionInProgress": string;
|
||||
"usernameInfo": string;
|
||||
"aiChanMode": string;
|
||||
@@ -890,11 +903,11 @@ export interface Locale {
|
||||
"continueThread": string;
|
||||
"deleteAccountConfirm": string;
|
||||
"incorrectPassword": string;
|
||||
"voteConfirm": string;
|
||||
"voteConfirm": ParameterizedString<"choice">;
|
||||
"hide": string;
|
||||
"useDrawerReactionPickerForMobile": string;
|
||||
"welcomeBackWithName": string;
|
||||
"clickToFinishEmailVerification": string;
|
||||
"welcomeBackWithName": ParameterizedString<"name">;
|
||||
"clickToFinishEmailVerification": ParameterizedString<"ok">;
|
||||
"overridedDeviceKind": string;
|
||||
"smartphone": string;
|
||||
"tablet": string;
|
||||
@@ -922,8 +935,8 @@ export interface Locale {
|
||||
"cropYes": string;
|
||||
"cropNo": string;
|
||||
"file": string;
|
||||
"recentNHours": string;
|
||||
"recentNDays": string;
|
||||
"recentNHours": ParameterizedString<"n">;
|
||||
"recentNDays": ParameterizedString<"n">;
|
||||
"noEmailServerWarning": string;
|
||||
"thereIsUnresolvedAbuseReportWarning": string;
|
||||
"recommended": string;
|
||||
@@ -932,7 +945,7 @@ export interface Locale {
|
||||
"driveCapOverrideCaption": string;
|
||||
"requireAdminForView": string;
|
||||
"isSystemAccount": string;
|
||||
"typeToConfirm": string;
|
||||
"typeToConfirm": ParameterizedString<"x">;
|
||||
"deleteAccount": string;
|
||||
"document": string;
|
||||
"numberOfPageCache": string;
|
||||
@@ -986,7 +999,7 @@ export interface Locale {
|
||||
"neverShow": string;
|
||||
"remindMeLater": string;
|
||||
"didYouLikeMisskey": string;
|
||||
"pleaseDonate": string;
|
||||
"pleaseDonate": ParameterizedString<"host">;
|
||||
"roles": string;
|
||||
"role": string;
|
||||
"noRole": string;
|
||||
@@ -1055,6 +1068,8 @@ export interface Locale {
|
||||
"noteIdOrUrl": string;
|
||||
"video": string;
|
||||
"videos": string;
|
||||
"audio": string;
|
||||
"audioFiles": string;
|
||||
"dataSaver": string;
|
||||
"accountMigration": string;
|
||||
"accountMoved": string;
|
||||
@@ -1082,7 +1097,7 @@ export interface Locale {
|
||||
"preservedUsernamesDescription": string;
|
||||
"createNoteFromTheFile": string;
|
||||
"archive": string;
|
||||
"channelArchiveConfirmTitle": string;
|
||||
"channelArchiveConfirmTitle": ParameterizedString<"name">;
|
||||
"channelArchiveConfirmDescription": string;
|
||||
"thisChannelArchived": string;
|
||||
"displayOfNote": string;
|
||||
@@ -1112,8 +1127,8 @@ export interface Locale {
|
||||
"createCount": string;
|
||||
"inviteCodeCreated": string;
|
||||
"inviteLimitExceeded": string;
|
||||
"createLimitRemaining": string;
|
||||
"inviteLimitResetCycle": string;
|
||||
"createLimitRemaining": ParameterizedString<"limit">;
|
||||
"inviteLimitResetCycle": ParameterizedString<"time" | "limit">;
|
||||
"expirationDate": string;
|
||||
"noExpirationDate": string;
|
||||
"inviteCodeUsedAt": string;
|
||||
@@ -1126,7 +1141,7 @@ export interface Locale {
|
||||
"expired": string;
|
||||
"doYouAgree": string;
|
||||
"beSureToReadThisAsItIsImportant": string;
|
||||
"iHaveReadXCarefullyAndAgree": string;
|
||||
"iHaveReadXCarefullyAndAgree": ParameterizedString<"x">;
|
||||
"dialog": string;
|
||||
"icon": string;
|
||||
"forYou": string;
|
||||
@@ -1181,12 +1196,30 @@ export interface Locale {
|
||||
"doReaction": string;
|
||||
"code": string;
|
||||
"reloadRequiredToApplySettings": string;
|
||||
"remainingN": string;
|
||||
"remainingN": ParameterizedString<"n">;
|
||||
"overwriteContentConfirm": string;
|
||||
"seasonalScreenEffect": string;
|
||||
"decorate": string;
|
||||
"addMfmFunction": string;
|
||||
"enableQuickAddMfmFunction": string;
|
||||
"bubbleGame": string;
|
||||
"sfx": string;
|
||||
"soundWillBePlayed": string;
|
||||
"showReplay": string;
|
||||
"replay": string;
|
||||
"replaying": string;
|
||||
"ranking": string;
|
||||
"lastNDays": ParameterizedString<"n">;
|
||||
"backToTitle": string;
|
||||
"enableHorizontalSwipe": string;
|
||||
"_bubbleGame": {
|
||||
"howToPlay": string;
|
||||
"_howToPlay": {
|
||||
"section1": string;
|
||||
"section2": string;
|
||||
"section3": string;
|
||||
};
|
||||
};
|
||||
"_announcement": {
|
||||
"forExistingUsers": string;
|
||||
"forExistingUsersDescription": string;
|
||||
@@ -1195,7 +1228,7 @@ export interface Locale {
|
||||
"end": string;
|
||||
"tooManyActiveAnnouncementDescription": string;
|
||||
"readConfirmTitle": string;
|
||||
"readConfirmText": string;
|
||||
"readConfirmText": ParameterizedString<"title">;
|
||||
"shouldNotBeUsedToPresentPermanentInfo": string;
|
||||
"dialogAnnouncementUxWarn": string;
|
||||
"silence": string;
|
||||
@@ -1210,10 +1243,10 @@ export interface Locale {
|
||||
"theseSettingsCanEditLater": string;
|
||||
"youCanEditMoreSettingsInSettingsPageLater": string;
|
||||
"followUsers": string;
|
||||
"pushNotificationDescription": string;
|
||||
"pushNotificationDescription": ParameterizedString<"name">;
|
||||
"initialAccountSettingCompleted": string;
|
||||
"haveFun": string;
|
||||
"youCanContinueTutorial": string;
|
||||
"haveFun": ParameterizedString<"name">;
|
||||
"youCanContinueTutorial": ParameterizedString<"name">;
|
||||
"startTutorial": string;
|
||||
"skipAreYouSure": string;
|
||||
"laterAreYouSure": string;
|
||||
@@ -1251,7 +1284,7 @@ export interface Locale {
|
||||
"social": string;
|
||||
"global": string;
|
||||
"description2": string;
|
||||
"description3": string;
|
||||
"description3": ParameterizedString<"link">;
|
||||
};
|
||||
"_postNote": {
|
||||
"title": string;
|
||||
@@ -1289,7 +1322,7 @@ export interface Locale {
|
||||
};
|
||||
"_done": {
|
||||
"title": string;
|
||||
"description": string;
|
||||
"description": ParameterizedString<"link">;
|
||||
};
|
||||
};
|
||||
"_timelineDescription": {
|
||||
@@ -1303,10 +1336,10 @@ export interface Locale {
|
||||
};
|
||||
"_serverSettings": {
|
||||
"iconUrl": string;
|
||||
"appIconDescription": string;
|
||||
"appIconDescription": ParameterizedString<"host">;
|
||||
"appIconUsageExample": string;
|
||||
"appIconStyleRecommendation": string;
|
||||
"appIconResolutionMustBe": string;
|
||||
"appIconResolutionMustBe": ParameterizedString<"resolution">;
|
||||
"manifestJsonOverride": string;
|
||||
"shortName": string;
|
||||
"shortNameDescription": string;
|
||||
@@ -1317,7 +1350,7 @@ export interface Locale {
|
||||
"_accountMigration": {
|
||||
"moveFrom": string;
|
||||
"moveFromSub": string;
|
||||
"moveFromLabel": string;
|
||||
"moveFromLabel": ParameterizedString<"n">;
|
||||
"moveFromDescription": string;
|
||||
"moveTo": string;
|
||||
"moveToLabel": string;
|
||||
@@ -1325,7 +1358,7 @@ export interface Locale {
|
||||
"moveAccountDescription": string;
|
||||
"moveAccountHowTo": string;
|
||||
"startMigration": string;
|
||||
"migrationConfirm": string;
|
||||
"migrationConfirm": ParameterizedString<"account">;
|
||||
"movedAndCannotBeUndone": string;
|
||||
"postMigrationNote": string;
|
||||
"movedTo": string;
|
||||
@@ -1651,6 +1684,15 @@ export interface Locale {
|
||||
"title": string;
|
||||
"description": string;
|
||||
};
|
||||
"_bubbleGameExplodingHead": {
|
||||
"title": string;
|
||||
"description": string;
|
||||
};
|
||||
"_bubbleGameDoubleExplodingHead": {
|
||||
"title": string;
|
||||
"description": string;
|
||||
"flavor": string;
|
||||
};
|
||||
};
|
||||
};
|
||||
"_role": {
|
||||
@@ -1758,7 +1800,7 @@ export interface Locale {
|
||||
"_signup": {
|
||||
"almostThere": string;
|
||||
"emailAddressInfo": string;
|
||||
"emailSent": string;
|
||||
"emailSent": ParameterizedString<"email">;
|
||||
};
|
||||
"_accountDelete": {
|
||||
"accountDelete": string;
|
||||
@@ -1811,14 +1853,14 @@ export interface Locale {
|
||||
"save": string;
|
||||
"inputName": string;
|
||||
"cannotSave": string;
|
||||
"nameAlreadyExists": string;
|
||||
"applyConfirm": string;
|
||||
"saveConfirm": string;
|
||||
"deleteConfirm": string;
|
||||
"renameConfirm": string;
|
||||
"nameAlreadyExists": ParameterizedString<"name">;
|
||||
"applyConfirm": ParameterizedString<"name">;
|
||||
"saveConfirm": ParameterizedString<"name">;
|
||||
"deleteConfirm": ParameterizedString<"name">;
|
||||
"renameConfirm": ParameterizedString<"old" | "new">;
|
||||
"noBackups": string;
|
||||
"createdAt": string;
|
||||
"updatedAt": string;
|
||||
"createdAt": ParameterizedString<"date" | "time">;
|
||||
"updatedAt": ParameterizedString<"date" | "time">;
|
||||
"cannotLoad": string;
|
||||
"invalidFile": string;
|
||||
};
|
||||
@@ -1863,8 +1905,8 @@ export interface Locale {
|
||||
"featured": string;
|
||||
"owned": string;
|
||||
"following": string;
|
||||
"usersCount": string;
|
||||
"notesCount": string;
|
||||
"usersCount": ParameterizedString<"n">;
|
||||
"notesCount": ParameterizedString<"n">;
|
||||
"nameAndDescription": string;
|
||||
"nameOnly": string;
|
||||
"allowRenoteToExternal": string;
|
||||
@@ -1892,7 +1934,7 @@ export interface Locale {
|
||||
"manage": string;
|
||||
"code": string;
|
||||
"description": string;
|
||||
"installed": string;
|
||||
"installed": ParameterizedString<"name">;
|
||||
"installedThemes": string;
|
||||
"builtinThemes": string;
|
||||
"alreadyInstalled": string;
|
||||
@@ -1915,7 +1957,7 @@ export interface Locale {
|
||||
"lighten": string;
|
||||
"inputConstantName": string;
|
||||
"importInfo": string;
|
||||
"deleteConstantConfirm": string;
|
||||
"deleteConstantConfirm": ParameterizedString<"const">;
|
||||
"keys": {
|
||||
"accent": string;
|
||||
"bg": string;
|
||||
@@ -1978,23 +2020,23 @@ export interface Locale {
|
||||
"_ago": {
|
||||
"future": string;
|
||||
"justNow": string;
|
||||
"secondsAgo": string;
|
||||
"minutesAgo": string;
|
||||
"hoursAgo": string;
|
||||
"daysAgo": string;
|
||||
"weeksAgo": string;
|
||||
"monthsAgo": string;
|
||||
"yearsAgo": string;
|
||||
"secondsAgo": ParameterizedString<"n">;
|
||||
"minutesAgo": ParameterizedString<"n">;
|
||||
"hoursAgo": ParameterizedString<"n">;
|
||||
"daysAgo": ParameterizedString<"n">;
|
||||
"weeksAgo": ParameterizedString<"n">;
|
||||
"monthsAgo": ParameterizedString<"n">;
|
||||
"yearsAgo": ParameterizedString<"n">;
|
||||
"invalid": string;
|
||||
};
|
||||
"_timeIn": {
|
||||
"seconds": string;
|
||||
"minutes": string;
|
||||
"hours": string;
|
||||
"days": string;
|
||||
"weeks": string;
|
||||
"months": string;
|
||||
"years": string;
|
||||
"seconds": ParameterizedString<"n">;
|
||||
"minutes": ParameterizedString<"n">;
|
||||
"hours": ParameterizedString<"n">;
|
||||
"days": ParameterizedString<"n">;
|
||||
"weeks": ParameterizedString<"n">;
|
||||
"months": ParameterizedString<"n">;
|
||||
"years": ParameterizedString<"n">;
|
||||
};
|
||||
"_time": {
|
||||
"second": string;
|
||||
@@ -2005,7 +2047,7 @@ export interface Locale {
|
||||
"_2fa": {
|
||||
"alreadyRegistered": string;
|
||||
"registerTOTP": string;
|
||||
"step1": string;
|
||||
"step1": ParameterizedString<"a" | "b">;
|
||||
"step2": string;
|
||||
"step2Click": string;
|
||||
"step2Uri": string;
|
||||
@@ -2020,7 +2062,7 @@ export interface Locale {
|
||||
"securityKeyName": string;
|
||||
"tapSecurityKey": string;
|
||||
"removeKey": string;
|
||||
"removeKeyConfirm": string;
|
||||
"removeKeyConfirm": ParameterizedString<"name">;
|
||||
"whyTOTPOnlyRenew": string;
|
||||
"renewTOTP": string;
|
||||
"renewTOTPConfirm": string;
|
||||
@@ -2121,9 +2163,9 @@ export interface Locale {
|
||||
};
|
||||
"_auth": {
|
||||
"shareAccessTitle": string;
|
||||
"shareAccess": string;
|
||||
"shareAccess": ParameterizedString<"name">;
|
||||
"shareAccessAsk": string;
|
||||
"permission": string;
|
||||
"permission": ParameterizedString<"name">;
|
||||
"permissionAsk": string;
|
||||
"pleaseGoBack": string;
|
||||
"callback": string;
|
||||
@@ -2182,12 +2224,12 @@ export interface Locale {
|
||||
"_cw": {
|
||||
"hide": string;
|
||||
"show": string;
|
||||
"chars": string;
|
||||
"files": string;
|
||||
"chars": ParameterizedString<"count">;
|
||||
"files": ParameterizedString<"count">;
|
||||
};
|
||||
"_poll": {
|
||||
"noOnlyOneChoice": string;
|
||||
"choiceN": string;
|
||||
"choiceN": ParameterizedString<"n">;
|
||||
"noMore": string;
|
||||
"canMultipleVote": string;
|
||||
"expiration": string;
|
||||
@@ -2197,16 +2239,16 @@ export interface Locale {
|
||||
"deadlineDate": string;
|
||||
"deadlineTime": string;
|
||||
"duration": string;
|
||||
"votesCount": string;
|
||||
"totalVotes": string;
|
||||
"votesCount": ParameterizedString<"n">;
|
||||
"totalVotes": ParameterizedString<"n">;
|
||||
"vote": string;
|
||||
"showResult": string;
|
||||
"voted": string;
|
||||
"closed": string;
|
||||
"remainingDays": string;
|
||||
"remainingHours": string;
|
||||
"remainingMinutes": string;
|
||||
"remainingSeconds": string;
|
||||
"remainingDays": ParameterizedString<"d" | "h">;
|
||||
"remainingHours": ParameterizedString<"h" | "m">;
|
||||
"remainingMinutes": ParameterizedString<"m" | "s">;
|
||||
"remainingSeconds": ParameterizedString<"s">;
|
||||
};
|
||||
"_visibility": {
|
||||
"public": string;
|
||||
@@ -2246,11 +2288,12 @@ export interface Locale {
|
||||
"changeAvatar": string;
|
||||
"changeBanner": string;
|
||||
"verifiedLinkDescription": string;
|
||||
"avatarDecorationMax": string;
|
||||
"avatarDecorationMax": ParameterizedString<"max">;
|
||||
};
|
||||
"_exportOrImport": {
|
||||
"allNotes": string;
|
||||
"favoritedNotes": string;
|
||||
"clips": string;
|
||||
"followingList": string;
|
||||
"muteList": string;
|
||||
"blockingList": string;
|
||||
@@ -2368,16 +2411,16 @@ export interface Locale {
|
||||
};
|
||||
"_notification": {
|
||||
"fileUploaded": string;
|
||||
"youGotMention": string;
|
||||
"youGotReply": string;
|
||||
"youGotQuote": string;
|
||||
"youRenoted": string;
|
||||
"youGotMention": ParameterizedString<"name">;
|
||||
"youGotReply": ParameterizedString<"name">;
|
||||
"youGotQuote": ParameterizedString<"name">;
|
||||
"youRenoted": ParameterizedString<"name">;
|
||||
"youWereFollowed": string;
|
||||
"youReceivedFollowRequest": string;
|
||||
"yourFollowRequestAccepted": string;
|
||||
"pollEnded": string;
|
||||
"newNote": string;
|
||||
"unreadAntennaNote": string;
|
||||
"unreadAntennaNote": ParameterizedString<"name">;
|
||||
"roleAssigned": string;
|
||||
"emptyPushNotificationMessage": string;
|
||||
"achievementEarned": string;
|
||||
@@ -2385,9 +2428,9 @@ export interface Locale {
|
||||
"checkNotificationBehavior": string;
|
||||
"sendTestNotification": string;
|
||||
"notificationWillBeDisplayedLikeThis": string;
|
||||
"reactedBySomeUsers": string;
|
||||
"renotedBySomeUsers": string;
|
||||
"followedBySomeUsers": string;
|
||||
"reactedBySomeUsers": ParameterizedString<"n">;
|
||||
"renotedBySomeUsers": ParameterizedString<"n">;
|
||||
"followedBySomeUsers": ParameterizedString<"n">;
|
||||
"_types": {
|
||||
"all": string;
|
||||
"note": string;
|
||||
@@ -2444,8 +2487,8 @@ export interface Locale {
|
||||
};
|
||||
};
|
||||
"_dialog": {
|
||||
"charactersExceeded": string;
|
||||
"charactersBelow": string;
|
||||
"charactersExceeded": ParameterizedString<"current" | "max">;
|
||||
"charactersBelow": ParameterizedString<"current" | "min">;
|
||||
};
|
||||
"_disabledTimeline": {
|
||||
"title": string;
|
||||
@@ -2590,6 +2633,41 @@ export interface Locale {
|
||||
"description": string;
|
||||
};
|
||||
};
|
||||
"_reversi": {
|
||||
"reversi": string;
|
||||
"gameSettings": string;
|
||||
"chooseBoard": string;
|
||||
"blackOrWhite": string;
|
||||
"blackIs": ParameterizedString<"name">;
|
||||
"rules": string;
|
||||
"thisGameIsStartedSoon": string;
|
||||
"waitingForOther": string;
|
||||
"waitingForMe": string;
|
||||
"waitingBoth": string;
|
||||
"ready": string;
|
||||
"cancelReady": string;
|
||||
"opponentTurn": string;
|
||||
"myTurn": string;
|
||||
"turnOf": ParameterizedString<"name">;
|
||||
"pastTurnOf": ParameterizedString<"name">;
|
||||
"surrender": string;
|
||||
"surrendered": string;
|
||||
"drawn": string;
|
||||
"won": ParameterizedString<"name">;
|
||||
"black": string;
|
||||
"white": string;
|
||||
"total": string;
|
||||
"turnCount": ParameterizedString<"count">;
|
||||
"myGames": string;
|
||||
"allGames": string;
|
||||
"ended": string;
|
||||
"playing": string;
|
||||
"isLlotheo": string;
|
||||
"loopedMap": string;
|
||||
"canPutEverywhere": string;
|
||||
"freeMatch": string;
|
||||
"lookingForPlayer": string;
|
||||
};
|
||||
}
|
||||
declare const locales: {
|
||||
[lang: string]: Locale;
|
||||
|
||||
@@ -379,6 +379,11 @@ hcaptcha: "hCaptcha"
|
||||
enableHcaptcha: "hCaptchaを有効にする"
|
||||
hcaptchaSiteKey: "サイトキー"
|
||||
hcaptchaSecretKey: "シークレットキー"
|
||||
mcaptcha: "mCaptcha"
|
||||
enableMcaptcha: "mCaptchaを有効にする"
|
||||
mcaptchaSiteKey: "サイトキー"
|
||||
mcaptchaSecretKey: "シークレットキー"
|
||||
mcaptchaInstanceUrl: "mCaptchaのインスタンスのURL"
|
||||
recaptcha: "reCAPTCHA"
|
||||
enableRecaptcha: "reCAPTCHAを有効にする"
|
||||
recaptchaSiteKey: "サイトキー"
|
||||
@@ -606,7 +611,6 @@ useCw: "内容を隠す"
|
||||
enablePlayer: "プレイヤーを開く"
|
||||
disablePlayer: "プレイヤーを閉じる"
|
||||
expandTweet: "ポストを展開する"
|
||||
expandNote: "ノートを展開する"
|
||||
themeEditor: "テーマエディター"
|
||||
description: "説明"
|
||||
describeFile: "キャプションを付ける"
|
||||
@@ -627,6 +631,7 @@ medium: "中"
|
||||
small: "小"
|
||||
generateAccessToken: "アクセストークンの発行"
|
||||
permission: "権限"
|
||||
adminPermission: "管理者権限"
|
||||
enableAll: "全て有効にする"
|
||||
disableAll: "全て無効にする"
|
||||
tokenRequested: "アカウントへのアクセス許可"
|
||||
@@ -670,6 +675,7 @@ useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使
|
||||
other: "その他"
|
||||
regenerateLoginToken: "ログイントークンを再生成"
|
||||
regenerateLoginTokenDescription: "ログインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスでログアウトされます。"
|
||||
theKeywordWhenSearchingForCustomEmoji: "カスタム絵文字を検索する時のキーワードになります。"
|
||||
setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。"
|
||||
fileIdOrUrl: "ファイルIDまたはURL"
|
||||
behavior: "動作"
|
||||
@@ -1052,6 +1058,8 @@ limitWidthOfReaction: "リアクションの最大横幅を制限し、縮小し
|
||||
noteIdOrUrl: "ノートIDまたはURL"
|
||||
video: "動画"
|
||||
videos: "動画"
|
||||
audio: "音声"
|
||||
audioFiles: "音声"
|
||||
dataSaver: "データセーバー"
|
||||
accountMigration: "アカウントの移行"
|
||||
accountMoved: "このユーザーは新しいアカウントに移行しました:"
|
||||
@@ -1184,6 +1192,23 @@ seasonalScreenEffect: "季節に応じた画面の演出"
|
||||
decorate: "デコる"
|
||||
addMfmFunction: "装飾を追加"
|
||||
enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する"
|
||||
bubbleGame: "バブルゲーム"
|
||||
sfx: "効果音"
|
||||
soundWillBePlayed: "サウンドが再生されます"
|
||||
showReplay: "リプレイを見る"
|
||||
replay: "リプレイ"
|
||||
replaying: "リプレイ中"
|
||||
ranking: "ランキング"
|
||||
lastNDays: "直近{n}日"
|
||||
backToTitle: "タイトルへ"
|
||||
enableHorizontalSwipe: "スワイプしてタブを切り替える"
|
||||
|
||||
_bubbleGame:
|
||||
howToPlay: "遊び方"
|
||||
_howToPlay:
|
||||
section1: "位置を調整してハコにモノを落とします。"
|
||||
section2: "同じ種類のモノがくっつくと別のモノに変化して、スコアが得られます。"
|
||||
section3: "モノがハコからあふれるとゲームオーバーです。ハコからあふれないようにしつつモノを融合させてハイスコアを目指そう!"
|
||||
|
||||
_announcement:
|
||||
forExistingUsers: "既存ユーザーのみ"
|
||||
@@ -1562,6 +1587,13 @@ _achievements:
|
||||
_tutorialCompleted:
|
||||
title: "Misskey初心者講座 修了証"
|
||||
description: "チュートリアルを完了した"
|
||||
_bubbleGameExplodingHead:
|
||||
title: "🤯"
|
||||
description: "バブルゲームで最も大きいモノを出した"
|
||||
_bubbleGameDoubleExplodingHead:
|
||||
title: "ダブル🤯"
|
||||
description: "バブルゲームで最も大きいモノを2つ同時に出した"
|
||||
flavor: "これくらいの おべんとばこに 🤯 🤯 ちょっとつめて"
|
||||
|
||||
_role:
|
||||
new: "ロールの作成"
|
||||
@@ -2154,6 +2186,7 @@ _profile:
|
||||
_exportOrImport:
|
||||
allNotes: "全てのノート"
|
||||
favoritedNotes: "お気に入りにしたノート"
|
||||
clips: "クリップ"
|
||||
followingList: "フォロー"
|
||||
muteList: "ミュート"
|
||||
blockingList: "ブロック"
|
||||
@@ -2473,3 +2506,38 @@ _dataSaver:
|
||||
_code:
|
||||
title: "コードハイライト"
|
||||
description: "MFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。"
|
||||
|
||||
_reversi:
|
||||
reversi: "リバーシ"
|
||||
gameSettings: "対局の設定"
|
||||
chooseBoard: "ボードを選択"
|
||||
blackOrWhite: "先行/後攻"
|
||||
blackIs: "{name}が黒(先行)"
|
||||
rules: "ルール"
|
||||
thisGameIsStartedSoon: "対局はまもなく開始されます"
|
||||
waitingForOther: "相手の準備が完了するのを待っています"
|
||||
waitingForMe: "あなたの準備が完了するのを待っています"
|
||||
waitingBoth: "準備してください"
|
||||
ready: "準備完了"
|
||||
cancelReady: "準備を再開"
|
||||
opponentTurn: "相手のターンです"
|
||||
myTurn: "あなたのターンです"
|
||||
turnOf: "{name}のターンです"
|
||||
pastTurnOf: "{name}のターン"
|
||||
surrender: "投了"
|
||||
surrendered: "投了により"
|
||||
drawn: "引き分け"
|
||||
won: "{name}の勝ち"
|
||||
black: "黒"
|
||||
white: "白"
|
||||
total: "合計"
|
||||
turnCount: "{count}ターン目"
|
||||
myGames: "自分の対局"
|
||||
allGames: "みんなの対局"
|
||||
ended: "終了"
|
||||
playing: "対局中"
|
||||
isLlotheo: "石の少ない方が勝ち(ロセオ)"
|
||||
loopedMap: "ループマップ"
|
||||
canPutEverywhere: "どこでも置けるモード"
|
||||
freeMatch: "フリーマッチ"
|
||||
lookingForPlayer: "対戦相手を探しています"
|
||||
|
||||
@@ -160,7 +160,6 @@ module.exports = {
|
||||
testMatch: [
|
||||
"<rootDir>/test/unit/**/*.ts",
|
||||
"<rootDir>/src/**/*.test.ts",
|
||||
"<rootDir>/test/e2e/**/*.ts",
|
||||
],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
|
||||
15
packages/backend/jest.config.e2e.cjs
Normal file
15
packages/backend/jest.config.e2e.cjs
Normal file
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* For a detailed explanation regarding each configuration property and type check, visit:
|
||||
* https://jestjs.io/docs/en/configuration.html
|
||||
*/
|
||||
|
||||
const base = require('./jest.config.cjs')
|
||||
|
||||
module.exports = {
|
||||
...base,
|
||||
globalSetup: "<rootDir>/built-test/entry.js",
|
||||
setupFilesAfterEnv: ["<rootDir>/test/jest.setup.ts"],
|
||||
testMatch: [
|
||||
"<rootDir>/test/e2e/**/*.ts",
|
||||
],
|
||||
};
|
||||
14
packages/backend/jest.config.unit.cjs
Normal file
14
packages/backend/jest.config.unit.cjs
Normal file
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* For a detailed explanation regarding each configuration property and type check, visit:
|
||||
* https://jestjs.io/docs/en/configuration.html
|
||||
*/
|
||||
|
||||
const base = require('./jest.config.cjs')
|
||||
|
||||
module.exports = {
|
||||
...base,
|
||||
testMatch: [
|
||||
"<rootDir>/test/unit/**/*.ts",
|
||||
"<rootDir>/src/**/*.test.ts",
|
||||
],
|
||||
};
|
||||
22
packages/backend/migration/1704373210054-support-mcaptcha.js
Normal file
22
packages/backend/migration/1704373210054-support-mcaptcha.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class SupportMcaptcha1704373210054 {
|
||||
name = 'SupportMcaptcha1704373210054'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "enableMcaptcha" boolean NOT NULL DEFAULT false`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "mcaptchaSitekey" character varying(1024)`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "mcaptchaSecretKey" character varying(1024)`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "mcaptchaInstanceUrl" character varying(1024)`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mcaptchaInstanceUrl"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mcaptchaSecretKey"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mcaptchaSitekey"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableMcaptcha"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class BubbleGameRecord1704959805077 {
|
||||
name = 'BubbleGameRecord1704959805077'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TABLE "bubble_game_record" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "seededAt" TIMESTAMP WITH TIME ZONE NOT NULL, "seed" character varying(1024) NOT NULL, "gameVersion" integer NOT NULL, "gameMode" character varying(128) NOT NULL, "score" integer NOT NULL, "logs" jsonb NOT NULL DEFAULT '[]', "isVerified" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_a75395fe404b392e2893b50d7ea" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_75276757070d21fdfaf4c05290" ON "bubble_game_record" ("userId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_4ae7053179014915d1432d3f40" ON "bubble_game_record" ("seededAt") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_26d4ee490b5a487142d35466ee" ON "bubble_game_record" ("score") `);
|
||||
await queryRunner.query(`ALTER TABLE "bubble_game_record" ADD CONSTRAINT "FK_75276757070d21fdfaf4c052909" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "bubble_game_record" DROP CONSTRAINT "FK_75276757070d21fdfaf4c052909"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_26d4ee490b5a487142d35466ee"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_4ae7053179014915d1432d3f40"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_75276757070d21fdfaf4c05290"`);
|
||||
await queryRunner.query(`DROP TABLE "bubble_game_record"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class OptimizeNoteIndexForArrayColumns1705222772858 {
|
||||
name = 'OptimizeNoteIndexForArrayColumns1705222772858'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_796a8c03959361f97dc2be1d5c"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_54ebcb6d27222913b908d56fd8"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_88937d94d7443d9a99a76fa5c0"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_51c063b6a133a9cb87145450f5"`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_NOTE_FILE_IDS" ON "note" using gin ("fileIds")`)
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "IDX_NOTE_FILE_IDS"`)
|
||||
await queryRunner.query(`CREATE INDEX "IDX_51c063b6a133a9cb87145450f5" ON "note" ("fileIds") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_88937d94d7443d9a99a76fa5c0" ON "note" ("tags") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_54ebcb6d27222913b908d56fd8" ON "note" ("mentions") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_796a8c03959361f97dc2be1d5c" ON "note" ("visibleUserIds") `);
|
||||
}
|
||||
}
|
||||
22
packages/backend/migration/1705475608437-reversi.js
Normal file
22
packages/backend/migration/1705475608437-reversi.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class Reversi1705475608437 {
|
||||
name = 'Reversi1705475608437'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_b46ec40746efceac604142be1c"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_b604d92d6c7aec38627f6eaf16"`);
|
||||
await queryRunner.query(`ALTER TABLE "reversi_game" DROP COLUMN "createdAt"`);
|
||||
await queryRunner.query(`ALTER TABLE "reversi_matching" DROP COLUMN "createdAt"`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "reversi_matching" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "reversi_game" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_b604d92d6c7aec38627f6eaf16" ON "reversi_matching" ("createdAt") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_b46ec40746efceac604142be1c" ON "reversi_game" ("createdAt") `);
|
||||
}
|
||||
}
|
||||
18
packages/backend/migration/1705654039457-reversi-2.js
Normal file
18
packages/backend/migration/1705654039457-reversi-2.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class Reversi21705654039457 {
|
||||
name = 'Reversi21705654039457'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "user1Accepted" TO "user1Ready"`);
|
||||
await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "user2Accepted" TO "user2Ready"`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "user1Ready" TO "user1Accepted"`);
|
||||
await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "user2Ready" TO "user2Accepted"`);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
"revert": "pnpm typeorm migration:revert -d ormconfig.js",
|
||||
"check:connect": "node ./check_connect.js",
|
||||
"build": "swc src -d built -D",
|
||||
"build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc",
|
||||
"watch:swc": "swc src -d built -D -w",
|
||||
"build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
|
||||
"watch": "node watch.mjs",
|
||||
@@ -21,11 +22,15 @@
|
||||
"typecheck": "tsc --noEmit",
|
||||
"eslint": "eslint --quiet \"src/**/*.ts\"",
|
||||
"lint": "pnpm typecheck && pnpm eslint",
|
||||
"jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit",
|
||||
"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit",
|
||||
"jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.unit.cjs",
|
||||
"jest:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.e2e.cjs",
|
||||
"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.unit.cjs",
|
||||
"jest-and-coverage:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.e2e.cjs",
|
||||
"jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache",
|
||||
"test": "pnpm jest",
|
||||
"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
|
||||
"test-and-coverage": "pnpm jest-and-coverage",
|
||||
"test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e",
|
||||
"generate-api-json": "node ./generate_api_json.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
@@ -74,6 +79,8 @@
|
||||
"@fastify/multipart": "8.0.0",
|
||||
"@fastify/static": "6.12.0",
|
||||
"@fastify/view": "8.2.0",
|
||||
"@misskey-dev/sharp-read-bmp": "^1.1.1",
|
||||
"@misskey-dev/summaly": "^5.0.3",
|
||||
"@nestjs/common": "10.2.10",
|
||||
"@nestjs/core": "10.2.10",
|
||||
"@nestjs/testing": "10.2.10",
|
||||
@@ -100,6 +107,7 @@
|
||||
"cli-highlight": "2.1.11",
|
||||
"color-convert": "2.0.1",
|
||||
"content-disposition": "0.5.4",
|
||||
"crc-32": "^1.2.2",
|
||||
"date-fns": "2.30.0",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"fastify": "4.24.3",
|
||||
@@ -126,6 +134,7 @@
|
||||
"microformats-parser": "2.0.2",
|
||||
"mime-types": "2.1.35",
|
||||
"misskey-js": "workspace:*",
|
||||
"misskey-reversi": "workspace:*",
|
||||
"ms": "3.0.0-canary.1",
|
||||
"nanoid": "5.0.4",
|
||||
"nested-property": "4.0.0",
|
||||
@@ -157,11 +166,9 @@
|
||||
"sanitize-html": "2.11.0",
|
||||
"secure-json-parse": "2.7.0",
|
||||
"sharp": "0.32.6",
|
||||
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
|
||||
"slacc": "0.0.10",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"summaly": "github:misskey-dev/summaly",
|
||||
"systeminformation": "5.21.20",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.1",
|
||||
@@ -177,6 +184,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.7.0",
|
||||
"@misskey-dev/eslint-plugin": "^1.0.0",
|
||||
"@nestjs/platform-express": "^10.3.0",
|
||||
"@simplewebauthn/typescript-types": "8.3.4",
|
||||
"@swc/jest": "0.2.29",
|
||||
"@types/accepts": "1.3.7",
|
||||
@@ -225,9 +234,11 @@
|
||||
"eslint": "8.56.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"execa": "8.0.1",
|
||||
"fkill": "^9.0.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-mock": "29.7.0",
|
||||
"nodemon": "3.0.2",
|
||||
"pid-port": "^1.0.0",
|
||||
"simple-oauth2": "5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { Global, Inject, Module } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DataSource } from 'typeorm';
|
||||
@@ -12,6 +11,7 @@ import { DI } from './di-symbols.js';
|
||||
import { Config, loadConfig } from './config.js';
|
||||
import { createPostgresDataSource } from './postgres.js';
|
||||
import { RepositoryModule } from './models/RepositoryModule.js';
|
||||
import { allSettled } from './misc/promise-tracker.js';
|
||||
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
const $config: Provider = {
|
||||
@@ -33,7 +33,7 @@ const $meilisearch: Provider = {
|
||||
useFactory: (config: Config) => {
|
||||
if (config.meilisearch) {
|
||||
return new MeiliSearch({
|
||||
host: `${config.meilisearch.ssl ? 'https' : 'http' }://${config.meilisearch.host}:${config.meilisearch.port}`,
|
||||
host: `${config.meilisearch.ssl ? 'https' : 'http'}://${config.meilisearch.host}:${config.meilisearch.port}`,
|
||||
apiKey: config.meilisearch.apiKey,
|
||||
});
|
||||
} else {
|
||||
@@ -91,17 +91,12 @@ export class GlobalModule implements OnApplicationShutdown {
|
||||
@Inject(DI.redisForPub) private redisForPub: Redis.Redis,
|
||||
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
|
||||
@Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
// XXX:
|
||||
// Shutting down the existing connections causes errors on Jest as
|
||||
// Misskey has asynchronous postgres/redis connections that are not
|
||||
// awaited.
|
||||
// Let's wait for some random time for them to finish.
|
||||
await setTimeout(5000);
|
||||
}
|
||||
// Wait for all potential DB queries
|
||||
await allSettled();
|
||||
// And then disconnect from DB
|
||||
await Promise.all([
|
||||
this.db.destroy(),
|
||||
this.redisClient.disconnect(),
|
||||
|
||||
@@ -87,6 +87,8 @@ export const ACHIEVEMENT_TYPES = [
|
||||
'brainDiver',
|
||||
'smashTestNotificationButton',
|
||||
'tutorialCompleted',
|
||||
'bubbleGameExplodingHead',
|
||||
'bubbleGameDoubleExplodingHead',
|
||||
] as const;
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -73,6 +73,37 @@ export class CaptchaService {
|
||||
}
|
||||
}
|
||||
|
||||
// https://codeberg.org/Gusted/mCaptcha/src/branch/main/mcaptcha.go
|
||||
@bindThis
|
||||
public async verifyMcaptcha(secret: string, siteKey: string, instanceHost: string, response: string | null | undefined): Promise<void> {
|
||||
if (response == null) {
|
||||
throw new Error('mcaptcha-failed: no response provided');
|
||||
}
|
||||
|
||||
const endpointUrl = new URL('/api/v1/pow/siteverify', instanceHost);
|
||||
const result = await this.httpRequestService.send(endpointUrl.toString(), {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
key: siteKey,
|
||||
secret: secret,
|
||||
token: response,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (result.status !== 200) {
|
||||
throw new Error('mcaptcha-failed: mcaptcha didn\'t return 200 OK');
|
||||
}
|
||||
|
||||
const resp = (await result.json()) as { valid: boolean };
|
||||
|
||||
if (!resp.valid) {
|
||||
throw new Error('mcaptcha-request-failed');
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async verifyTurnstile(secret: string, response: string | null | undefined): Promise<void> {
|
||||
if (response == null) {
|
||||
|
||||
@@ -66,6 +66,8 @@ import { FeaturedService } from './FeaturedService.js';
|
||||
import { FanoutTimelineService } from './FanoutTimelineService.js';
|
||||
import { ChannelFollowingService } from './ChannelFollowingService.js';
|
||||
import { RegistryApiService } from './RegistryApiService.js';
|
||||
import { ReversiService } from './ReversiService.js';
|
||||
|
||||
import { ChartLoggerService } from './chart/ChartLoggerService.js';
|
||||
import FederationChart from './chart/charts/federation.js';
|
||||
import NotesChart from './chart/charts/notes.js';
|
||||
@@ -80,6 +82,7 @@ import PerUserFollowingChart from './chart/charts/per-user-following.js';
|
||||
import PerUserDriveChart from './chart/charts/per-user-drive.js';
|
||||
import ApRequestChart from './chart/charts/ap-request.js';
|
||||
import { ChartManagementService } from './chart/ChartManagementService.js';
|
||||
|
||||
import { AbuseUserReportEntityService } from './entities/AbuseUserReportEntityService.js';
|
||||
import { AntennaEntityService } from './entities/AntennaEntityService.js';
|
||||
import { AppEntityService } from './entities/AppEntityService.js';
|
||||
@@ -112,6 +115,8 @@ import { UserListEntityService } from './entities/UserListEntityService.js';
|
||||
import { FlashEntityService } from './entities/FlashEntityService.js';
|
||||
import { FlashLikeEntityService } from './entities/FlashLikeEntityService.js';
|
||||
import { RoleEntityService } from './entities/RoleEntityService.js';
|
||||
import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js';
|
||||
|
||||
import { ApAudienceService } from './activitypub/ApAudienceService.js';
|
||||
import { ApDbResolverService } from './activitypub/ApDbResolverService.js';
|
||||
import { ApDeliverManagerService } from './activitypub/ApDeliverManagerService.js';
|
||||
@@ -199,6 +204,7 @@ const $FanoutTimelineService: Provider = { provide: 'FanoutTimelineService', use
|
||||
const $FanoutTimelineEndpointService: Provider = { provide: 'FanoutTimelineEndpointService', useExisting: FanoutTimelineEndpointService };
|
||||
const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService };
|
||||
const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService };
|
||||
const $ReversiService: Provider = { provide: 'ReversiService', useExisting: ReversiService };
|
||||
|
||||
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
|
||||
const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
|
||||
@@ -247,6 +253,7 @@ const $UserListEntityService: Provider = { provide: 'UserListEntityService', use
|
||||
const $FlashEntityService: Provider = { provide: 'FlashEntityService', useExisting: FlashEntityService };
|
||||
const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService };
|
||||
const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService };
|
||||
const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService };
|
||||
|
||||
const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService };
|
||||
const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService };
|
||||
@@ -336,6 +343,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
FanoutTimelineEndpointService,
|
||||
ChannelFollowingService,
|
||||
RegistryApiService,
|
||||
ReversiService,
|
||||
|
||||
ChartLoggerService,
|
||||
FederationChart,
|
||||
NotesChart,
|
||||
@@ -350,6 +359,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
PerUserDriveChart,
|
||||
ApRequestChart,
|
||||
ChartManagementService,
|
||||
|
||||
AbuseUserReportEntityService,
|
||||
AntennaEntityService,
|
||||
AppEntityService,
|
||||
@@ -382,6 +392,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
FlashEntityService,
|
||||
FlashLikeEntityService,
|
||||
RoleEntityService,
|
||||
ReversiGameEntityService,
|
||||
|
||||
ApAudienceService,
|
||||
ApDbResolverService,
|
||||
ApDeliverManagerService,
|
||||
@@ -466,6 +478,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$FanoutTimelineEndpointService,
|
||||
$ChannelFollowingService,
|
||||
$RegistryApiService,
|
||||
$ReversiService,
|
||||
|
||||
$ChartLoggerService,
|
||||
$FederationChart,
|
||||
$NotesChart,
|
||||
@@ -480,6 +494,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$PerUserDriveChart,
|
||||
$ApRequestChart,
|
||||
$ChartManagementService,
|
||||
|
||||
$AbuseUserReportEntityService,
|
||||
$AntennaEntityService,
|
||||
$AppEntityService,
|
||||
@@ -512,6 +527,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$FlashEntityService,
|
||||
$FlashLikeEntityService,
|
||||
$RoleEntityService,
|
||||
$ReversiGameEntityService,
|
||||
|
||||
$ApAudienceService,
|
||||
$ApDbResolverService,
|
||||
$ApDeliverManagerService,
|
||||
@@ -597,6 +614,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
FanoutTimelineEndpointService,
|
||||
ChannelFollowingService,
|
||||
RegistryApiService,
|
||||
ReversiService,
|
||||
|
||||
FederationChart,
|
||||
NotesChart,
|
||||
UsersChart,
|
||||
@@ -610,6 +629,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
PerUserDriveChart,
|
||||
ApRequestChart,
|
||||
ChartManagementService,
|
||||
|
||||
AbuseUserReportEntityService,
|
||||
AntennaEntityService,
|
||||
AppEntityService,
|
||||
@@ -642,6 +662,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
FlashEntityService,
|
||||
FlashLikeEntityService,
|
||||
RoleEntityService,
|
||||
ReversiGameEntityService,
|
||||
|
||||
ApAudienceService,
|
||||
ApDbResolverService,
|
||||
ApDeliverManagerService,
|
||||
@@ -726,6 +748,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$FanoutTimelineEndpointService,
|
||||
$ChannelFollowingService,
|
||||
$RegistryApiService,
|
||||
$ReversiService,
|
||||
|
||||
$FederationChart,
|
||||
$NotesChart,
|
||||
$UsersChart,
|
||||
@@ -739,6 +763,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$PerUserDriveChart,
|
||||
$ApRequestChart,
|
||||
$ChartManagementService,
|
||||
|
||||
$AbuseUserReportEntityService,
|
||||
$AntennaEntityService,
|
||||
$AppEntityService,
|
||||
@@ -771,6 +796,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$FlashEntityService,
|
||||
$FlashLikeEntityService,
|
||||
$RoleEntityService,
|
||||
$ReversiGameEntityService,
|
||||
|
||||
$ApAudienceService,
|
||||
$ApDbResolverService,
|
||||
$ApDeliverManagerService,
|
||||
|
||||
@@ -145,7 +145,8 @@ export class DownloadService {
|
||||
const parsedIp = ipaddr.parse(ip);
|
||||
|
||||
for (const net of this.config.allowedPrivateNetworks ?? []) {
|
||||
if (parsedIp.match(ipaddr.parseCIDR(net))) {
|
||||
const cidr = ipaddr.parseCIDR(net);
|
||||
if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(ipaddr.parseCIDR(net))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { randomUUID } from 'node:crypto';
|
||||
import * as fs from 'node:fs';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import sharp from 'sharp';
|
||||
import { sharpBmp } from 'sharp-read-bmp';
|
||||
import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
@@ -655,7 +655,7 @@ export class DriveService {
|
||||
public async updateFile(file: MiDriveFile, values: Partial<MiDriveFile>, updater: MiUser) {
|
||||
const alwaysMarkNsfw = (await this.roleService.getUserPolicies(file.userId)).alwaysMarkNsfw;
|
||||
|
||||
if (values.name && !this.driveFileEntityService.validateFileName(file.name)) {
|
||||
if (values.name != null && !this.driveFileEntityService.validateFileName(values.name)) {
|
||||
throw new DriveService.InvalidFileNameError();
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import type { MiSignin } from '@/models/Signin.js';
|
||||
import type { MiPage } from '@/models/Page.js';
|
||||
import type { MiWebhook } from '@/models/Webhook.js';
|
||||
import type { MiMeta } from '@/models/Meta.js';
|
||||
import { MiAvatarDecoration, MiRole, MiRoleAssignment } from '@/models/_.js';
|
||||
import { MiAvatarDecoration, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
@@ -159,6 +159,43 @@ export interface AdminEventTypes {
|
||||
comment: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ReversiEventTypes {
|
||||
matched: {
|
||||
game: Packed<'ReversiGameDetailed'>;
|
||||
};
|
||||
invited: {
|
||||
user: Packed<'User'>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ReversiGameEventTypes {
|
||||
changeReadyStates: {
|
||||
user1: boolean;
|
||||
user2: boolean;
|
||||
};
|
||||
updateSettings: {
|
||||
userId: MiUser['id'];
|
||||
key: string;
|
||||
value: any;
|
||||
};
|
||||
putStone: {
|
||||
at: number;
|
||||
color: boolean;
|
||||
pos: number;
|
||||
next: boolean;
|
||||
};
|
||||
syncState: {
|
||||
crc32: string;
|
||||
};
|
||||
started: {
|
||||
game: Packed<'ReversiGameDetailed'>;
|
||||
};
|
||||
ended: {
|
||||
winnerId: MiUser['id'] | null;
|
||||
game: Packed<'ReversiGameDetailed'>;
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
|
||||
// 辞書(interface or type)から{ type, body }ユニオンを定義
|
||||
@@ -249,6 +286,14 @@ export type GlobalEvents = {
|
||||
name: 'notesStream';
|
||||
payload: Serialized<Packed<'Note'>>;
|
||||
};
|
||||
reversi: {
|
||||
name: `reversiStream:${MiUser['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<ReversiEventTypes>>;
|
||||
};
|
||||
reversiGame: {
|
||||
name: `reversiGameStream:${MiReversiGame['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<ReversiGameEventTypes>>;
|
||||
};
|
||||
};
|
||||
|
||||
// API event definitions
|
||||
@@ -338,4 +383,14 @@ export class GlobalEventService {
|
||||
public publishAdminStream<K extends keyof AdminEventTypes>(userId: MiUser['id'], type: K, value?: AdminEventTypes[K]): void {
|
||||
this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public publishReversiStream<K extends keyof ReversiEventTypes>(userId: MiUser['id'], type: K, value?: ReversiEventTypes[K]): void {
|
||||
this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { isReply } from '@/misc/is-reply.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||
|
||||
@@ -324,6 +325,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH);
|
||||
}
|
||||
data.text = data.text.trim();
|
||||
if (data.text === '') {
|
||||
data.text = null;
|
||||
}
|
||||
} else {
|
||||
data.text = null;
|
||||
}
|
||||
@@ -676,7 +680,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
this.relayService.deliverToRelays(user, noteActivity);
|
||||
}
|
||||
|
||||
dm.execute();
|
||||
trackPromise(dm.execute());
|
||||
})();
|
||||
}
|
||||
//#endregion
|
||||
|
||||
@@ -14,6 +14,7 @@ import { IdService } from '@/core/IdService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
|
||||
@Injectable()
|
||||
export class NoteReadService implements OnApplicationShutdown {
|
||||
@@ -107,7 +108,7 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||
|
||||
// TODO: ↓まとめてクエリしたい
|
||||
|
||||
this.noteUnreadsRepository.countBy({
|
||||
trackPromise(this.noteUnreadsRepository.countBy({
|
||||
userId: userId,
|
||||
isMentioned: true,
|
||||
}).then(mentionsCount => {
|
||||
@@ -115,9 +116,9 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||
// 全て既読になったイベントを発行
|
||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
this.noteUnreadsRepository.countBy({
|
||||
trackPromise(this.noteUnreadsRepository.countBy({
|
||||
userId: userId,
|
||||
isSpecified: true,
|
||||
}).then(specifiedCount => {
|
||||
@@ -125,7 +126,7 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||
// 全て既読になったイベントを発行
|
||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import { CacheService } from '@/core/CacheService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { UserListService } from '@/core/UserListService.js';
|
||||
import type { FilterUnionByProperty } from '@/types.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
|
||||
@Injectable()
|
||||
export class NotificationService implements OnApplicationShutdown {
|
||||
@@ -74,7 +75,18 @@ export class NotificationService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async createNotification<T extends MiNotification['type']>(
|
||||
public createNotification<T extends MiNotification['type']>(
|
||||
notifieeId: MiUser['id'],
|
||||
type: T,
|
||||
data: Omit<FilterUnionByProperty<MiNotification, 'type', T>, 'type' | 'id' | 'createdAt' | 'notifierId'>,
|
||||
notifierId?: MiUser['id'] | null,
|
||||
) {
|
||||
trackPromise(
|
||||
this.#createNotificationInternal(notifieeId, type, data, notifierId),
|
||||
);
|
||||
}
|
||||
|
||||
async #createNotificationInternal<T extends MiNotification['type']>(
|
||||
notifieeId: MiUser['id'],
|
||||
type: T,
|
||||
data: Omit<FilterUnionByProperty<MiNotification, 'type', T>, 'type' | 'id' | 'createdAt' | 'notifierId'>,
|
||||
|
||||
@@ -212,8 +212,8 @@ export class QueryService {
|
||||
// または 自分自身
|
||||
.orWhere('note.userId = :meId')
|
||||
// または 自分宛て
|
||||
.orWhere(':meId = ANY(note.visibleUserIds)')
|
||||
.orWhere(':meId = ANY(note.mentions)')
|
||||
.orWhere(':meIdAsList <@ note.visibleUserIds')
|
||||
.orWhere(':meIdAsList <@ note.mentions')
|
||||
.orWhere(new Brackets(qb => {
|
||||
qb
|
||||
// または フォロワー宛ての投稿であり、
|
||||
@@ -228,7 +228,7 @@ export class QueryService {
|
||||
}));
|
||||
}));
|
||||
|
||||
q.setParameters({ meId: me.id });
|
||||
q.setParameters({ meId: me.id, meIdAsList: [me.id] });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
|
||||
import * as Bull from 'bullmq';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { QUEUE, baseQueueOptions } from '@/queue/const.js';
|
||||
import { allSettled } from '@/misc/promise-tracker.js';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js';
|
||||
|
||||
@@ -106,14 +106,9 @@ export class QueueModule implements OnApplicationShutdown {
|
||||
) {}
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
// XXX:
|
||||
// Shutting down the existing connections causes errors on Jest as
|
||||
// Misskey has asynchronous postgres/redis connections that are not
|
||||
// awaited.
|
||||
// Let's wait for some random time for them to finish.
|
||||
await setTimeout(5000);
|
||||
}
|
||||
// Wait for all potential queue jobs
|
||||
await allSettled();
|
||||
// And then close all queues
|
||||
await Promise.all([
|
||||
this.systemQueue.close(),
|
||||
this.endedPollNotificationQueue.close(),
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, Obj
|
||||
import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
|
||||
import type httpSignature from '@peertube/http-signature';
|
||||
import type * as Bull from 'bullmq';
|
||||
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
|
||||
|
||||
@Injectable()
|
||||
export class QueueService {
|
||||
@@ -74,11 +75,15 @@ export class QueueService {
|
||||
if (content == null) return null;
|
||||
if (to == null) return null;
|
||||
|
||||
const contentBody = JSON.stringify(content);
|
||||
const digest = ApRequestCreator.createDigest(contentBody);
|
||||
|
||||
const data: DeliverJobData = {
|
||||
user: {
|
||||
id: user.id,
|
||||
},
|
||||
content,
|
||||
content: contentBody,
|
||||
digest,
|
||||
to,
|
||||
isSharedInbox,
|
||||
};
|
||||
@@ -103,6 +108,8 @@ export class QueueService {
|
||||
@bindThis
|
||||
public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map<string, boolean>) {
|
||||
if (content == null) return null;
|
||||
const contentBody = JSON.stringify(content);
|
||||
const digest = ApRequestCreator.createDigest(contentBody);
|
||||
|
||||
const opts = {
|
||||
attempts: this.config.deliverJobMaxAttempts ?? 12,
|
||||
@@ -117,7 +124,8 @@ export class QueueService {
|
||||
name: d[0],
|
||||
data: {
|
||||
user,
|
||||
content,
|
||||
content: contentBody,
|
||||
digest,
|
||||
to: d[0],
|
||||
isSharedInbox: d[1],
|
||||
} as DeliverJobData,
|
||||
@@ -174,6 +182,16 @@ export class QueueService {
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createExportClipsJob(user: ThinUser) {
|
||||
return this.dbQueue.add('exportClips', {
|
||||
user: { id: user.id },
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createExportFavoritesJob(user: ThinUser) {
|
||||
return this.dbQueue.add('exportFavorites', {
|
||||
|
||||
@@ -28,6 +28,7 @@ import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
|
||||
const FALLBACK = '❤';
|
||||
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
|
||||
@@ -268,7 +269,7 @@ export class ReactionService {
|
||||
}
|
||||
}
|
||||
|
||||
dm.execute();
|
||||
trackPromise(dm.execute());
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
@@ -316,7 +317,7 @@ export class ReactionService {
|
||||
dm.addDirectRecipe(reactee as MiRemoteUser);
|
||||
}
|
||||
dm.addFollowersRecipe();
|
||||
dm.execute();
|
||||
trackPromise(dm.execute());
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
||||
411
packages/backend/src/core/ReversiService.ts
Normal file
411
packages/backend/src/core/ReversiService.ts
Normal file
@@ -0,0 +1,411 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import CRC32 from 'crc-32';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import * as Reversi from 'misskey-reversi';
|
||||
import { IsNull } from 'typeorm';
|
||||
import type {
|
||||
MiReversiGame,
|
||||
ReversiGamesRepository,
|
||||
UsersRepository,
|
||||
} from '@/models/_.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js';
|
||||
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
|
||||
|
||||
const MATCHING_TIMEOUT_MS = 1000 * 15; // 15sec
|
||||
|
||||
@Injectable()
|
||||
export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||
private notificationService: NotificationService;
|
||||
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.reversiGamesRepository)
|
||||
private reversiGamesRepository: ReversiGamesRepository,
|
||||
|
||||
private cacheService: CacheService,
|
||||
private userEntityService: UserEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private reversiGameEntityService: ReversiGameEntityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
this.notificationService = this.moduleRef.get(NotificationService.name);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async matchSpecificUser(me: MiUser, targetUser: MiUser): Promise<MiReversiGame | null> {
|
||||
if (targetUser.id === me.id) {
|
||||
throw new Error('You cannot match yourself.');
|
||||
}
|
||||
|
||||
const invitations = await this.redisClient.zrange(
|
||||
`reversi:matchSpecific:${me.id}`,
|
||||
Date.now() - MATCHING_TIMEOUT_MS,
|
||||
'+inf',
|
||||
'BYSCORE');
|
||||
|
||||
if (invitations.includes(targetUser.id)) {
|
||||
await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, targetUser.id);
|
||||
|
||||
const game = await this.reversiGamesRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
user1Id: targetUser.id,
|
||||
user2Id: me.id,
|
||||
user1Ready: false,
|
||||
user2Ready: false,
|
||||
isStarted: false,
|
||||
isEnded: false,
|
||||
logs: [],
|
||||
map: Reversi.maps.eighteight.data,
|
||||
bw: 'random',
|
||||
isLlotheo: false,
|
||||
}).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
const packed = await this.reversiGameEntityService.packDetail(game, { id: targetUser.id });
|
||||
this.globalEventService.publishReversiStream(targetUser.id, 'matched', { game: packed });
|
||||
|
||||
return game;
|
||||
} else {
|
||||
this.redisClient.zadd(`reversi:matchSpecific:${targetUser.id}`, Date.now(), me.id);
|
||||
|
||||
this.globalEventService.publishReversiStream(targetUser.id, 'invited', {
|
||||
user: await this.userEntityService.pack(me, targetUser),
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async matchAnyUser(me: MiUser): Promise<MiReversiGame | null> {
|
||||
//#region まず自分宛ての招待を探す
|
||||
const invitations = await this.redisClient.zrange(
|
||||
`reversi:matchSpecific:${me.id}`,
|
||||
Date.now() - MATCHING_TIMEOUT_MS,
|
||||
'+inf',
|
||||
'BYSCORE');
|
||||
|
||||
if (invitations.length > 0) {
|
||||
const invitorId = invitations[Math.floor(Math.random() * invitations.length)];
|
||||
await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, invitorId);
|
||||
|
||||
const game = await this.reversiGamesRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
user1Id: invitorId,
|
||||
user2Id: me.id,
|
||||
user1Ready: false,
|
||||
user2Ready: false,
|
||||
isStarted: false,
|
||||
isEnded: false,
|
||||
logs: [],
|
||||
map: Reversi.maps.eighteight.data,
|
||||
bw: 'random',
|
||||
isLlotheo: false,
|
||||
}).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
const packed = await this.reversiGameEntityService.packDetail(game, { id: invitorId });
|
||||
this.globalEventService.publishReversiStream(invitorId, 'matched', { game: packed });
|
||||
|
||||
return game;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const matchings = await this.redisClient.zrange(
|
||||
'reversi:matchAny',
|
||||
Date.now() - MATCHING_TIMEOUT_MS,
|
||||
'+inf',
|
||||
'BYSCORE');
|
||||
|
||||
const userIds = matchings.filter(id => id !== me.id);
|
||||
|
||||
if (userIds.length > 0) {
|
||||
// pick random
|
||||
const matchedUserId = userIds[Math.floor(Math.random() * userIds.length)];
|
||||
|
||||
await this.redisClient.zrem('reversi:matchAny', me.id, matchedUserId);
|
||||
|
||||
const game = await this.reversiGamesRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
user1Id: matchedUserId,
|
||||
user2Id: me.id,
|
||||
user1Ready: false,
|
||||
user2Ready: false,
|
||||
isStarted: false,
|
||||
isEnded: false,
|
||||
logs: [],
|
||||
map: Reversi.maps.eighteight.data,
|
||||
bw: 'random',
|
||||
isLlotheo: false,
|
||||
}).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
const packed = await this.reversiGameEntityService.packDetail(game, { id: matchedUserId });
|
||||
this.globalEventService.publishReversiStream(matchedUserId, 'matched', { game: packed });
|
||||
|
||||
return game;
|
||||
} else {
|
||||
await this.redisClient.zadd('reversi:matchAny', Date.now(), me.id);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async matchSpecificUserCancel(user: MiUser, targetUserId: MiUser['id']) {
|
||||
await this.redisClient.zrem(`reversi:matchSpecific:${targetUserId}`, user.id);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async matchAnyUserCancel(user: MiUser) {
|
||||
await this.redisClient.zrem('reversi:matchAny', user.id);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async gameReady(game: MiReversiGame, user: MiUser, ready: boolean) {
|
||||
if (game.isStarted) return;
|
||||
|
||||
let isBothReady = false;
|
||||
|
||||
if (game.user1Id === user.id) {
|
||||
await this.reversiGamesRepository.update(game.id, {
|
||||
user1Ready: ready,
|
||||
});
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', {
|
||||
user1: ready,
|
||||
user2: game.user2Ready,
|
||||
});
|
||||
|
||||
if (ready && game.user2Ready) isBothReady = true;
|
||||
} else if (game.user2Id === user.id) {
|
||||
await this.reversiGamesRepository.update(game.id, {
|
||||
user2Ready: ready,
|
||||
});
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', {
|
||||
user1: game.user1Ready,
|
||||
user2: ready,
|
||||
});
|
||||
|
||||
if (ready && game.user1Ready) isBothReady = true;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isBothReady) {
|
||||
// 3秒後、両者readyならゲーム開始
|
||||
setTimeout(async () => {
|
||||
const freshGame = await this.reversiGamesRepository.findOneBy({ id: game.id });
|
||||
if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return;
|
||||
if (!freshGame.user1Ready || !freshGame.user2Ready) return;
|
||||
|
||||
let bw: number;
|
||||
if (freshGame.bw === 'random') {
|
||||
bw = Math.random() > 0.5 ? 1 : 2;
|
||||
} else {
|
||||
bw = parseInt(freshGame.bw, 10);
|
||||
}
|
||||
|
||||
function getRandomMap() {
|
||||
const mapCount = Object.entries(Reversi.maps).length;
|
||||
const rnd = Math.floor(Math.random() * mapCount);
|
||||
return Object.values(Reversi.maps)[rnd].data;
|
||||
}
|
||||
|
||||
const map = freshGame.map != null ? freshGame.map : getRandomMap();
|
||||
|
||||
await this.reversiGamesRepository.update(game.id, {
|
||||
startedAt: new Date(),
|
||||
isStarted: true,
|
||||
black: bw,
|
||||
map: map,
|
||||
});
|
||||
|
||||
//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
|
||||
const o = new Reversi.Game(map, {
|
||||
isLlotheo: freshGame.isLlotheo,
|
||||
canPutEverywhere: freshGame.canPutEverywhere,
|
||||
loopedBoard: freshGame.loopedBoard,
|
||||
});
|
||||
|
||||
if (o.isEnded) {
|
||||
let winner;
|
||||
if (o.winner === true) {
|
||||
winner = freshGame.black === 1 ? freshGame.user1Id : freshGame.user2Id;
|
||||
} else if (o.winner === false) {
|
||||
winner = freshGame.black === 1 ? freshGame.user2Id : freshGame.user1Id;
|
||||
} else {
|
||||
winner = null;
|
||||
}
|
||||
|
||||
await this.reversiGamesRepository.update(game.id, {
|
||||
isEnded: true,
|
||||
winnerId: winner,
|
||||
});
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
||||
winnerId: winner,
|
||||
game: await this.reversiGameEntityService.packDetail(game.id, user),
|
||||
});
|
||||
}
|
||||
//#endregion
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'started', {
|
||||
game: await this.reversiGameEntityService.packDetail(game.id, user),
|
||||
});
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getInvitations(user: MiUser): Promise<MiUser['id'][]> {
|
||||
const invitations = await this.redisClient.zrange(
|
||||
`reversi:matchSpecific:${user.id}`,
|
||||
Date.now() - MATCHING_TIMEOUT_MS,
|
||||
'+inf',
|
||||
'BYSCORE');
|
||||
return invitations;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async updateSettings(game: MiReversiGame, user: MiUser, key: string, value: any) {
|
||||
if (game.isStarted) return;
|
||||
if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) return;
|
||||
if ((game.user1Id === user.id) && game.user1Ready) return;
|
||||
if ((game.user2Id === user.id) && game.user2Ready) return;
|
||||
|
||||
if (!['map', 'bw', 'isLlotheo', 'canPutEverywhere', 'loopedBoard'].includes(key)) return;
|
||||
|
||||
await this.reversiGamesRepository.update(game.id, {
|
||||
[key]: value,
|
||||
});
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'updateSettings', {
|
||||
userId: user.id,
|
||||
key: key,
|
||||
value: value,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async putStoneToGame(game: MiReversiGame, user: MiUser, pos: number) {
|
||||
if (!game.isStarted) return;
|
||||
if (game.isEnded) return;
|
||||
if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) return;
|
||||
|
||||
const myColor =
|
||||
((game.user1Id === user.id) && game.black === 1) || ((game.user2Id === user.id) && game.black === 2)
|
||||
? true
|
||||
: false;
|
||||
|
||||
const o = new Reversi.Game(game.map, {
|
||||
isLlotheo: game.isLlotheo,
|
||||
canPutEverywhere: game.canPutEverywhere,
|
||||
loopedBoard: game.loopedBoard,
|
||||
});
|
||||
|
||||
// 盤面の状態を再生
|
||||
for (const log of game.logs) {
|
||||
o.put(log.color, log.pos);
|
||||
}
|
||||
|
||||
if (o.turn !== myColor) return;
|
||||
|
||||
if (!o.canPut(myColor, pos)) return;
|
||||
o.put(myColor, pos);
|
||||
|
||||
let winner;
|
||||
if (o.isEnded) {
|
||||
if (o.winner === true) {
|
||||
winner = game.black === 1 ? game.user1Id : game.user2Id;
|
||||
} else if (o.winner === false) {
|
||||
winner = game.black === 1 ? game.user2Id : game.user1Id;
|
||||
} else {
|
||||
winner = null;
|
||||
}
|
||||
}
|
||||
|
||||
const log = {
|
||||
at: Date.now(),
|
||||
color: myColor,
|
||||
pos,
|
||||
};
|
||||
|
||||
const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()).toString();
|
||||
|
||||
game.logs.push(log);
|
||||
|
||||
await this.reversiGamesRepository.update(game.id, {
|
||||
crc32,
|
||||
isEnded: o.isEnded,
|
||||
winnerId: winner,
|
||||
logs: game.logs,
|
||||
});
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'putStone', {
|
||||
...log,
|
||||
next: o.turn,
|
||||
});
|
||||
|
||||
if (o.isEnded) {
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
||||
winnerId: winner,
|
||||
game: await this.reversiGameEntityService.packDetail(game.id, user),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async surrender(game: MiReversiGame, user: MiUser) {
|
||||
if (game.isEnded) return;
|
||||
if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) return;
|
||||
|
||||
const winnerId = game.user1Id === user.id ? game.user2Id : game.user1Id;
|
||||
|
||||
await this.reversiGamesRepository.update(game.id, {
|
||||
surrendered: user.id,
|
||||
isEnded: true,
|
||||
winnerId: winnerId,
|
||||
});
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
||||
winnerId: winnerId,
|
||||
game: await this.reversiGameEntityService.packDetail(game.id, user),
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async get(id: MiReversiGame['id']) {
|
||||
return this.reversiGamesRepository.findOneBy({ id });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ export class ApAudienceService {
|
||||
};
|
||||
}
|
||||
|
||||
if (toGroups.followers.length > 0) {
|
||||
if (toGroups.followers.length > 0 || ccGroups.followers.length > 0) {
|
||||
return {
|
||||
visibility: 'followers',
|
||||
mentionedUsers,
|
||||
|
||||
@@ -144,7 +144,7 @@ class DeliverManager {
|
||||
}
|
||||
|
||||
// deliver
|
||||
this.queueService.deliverMany(this.actor, this.activity, inboxes);
|
||||
await this.queueService.deliverMany(this.actor, this.activity, inboxes);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -97,6 +97,8 @@ export class ApInboxService {
|
||||
} catch (err) {
|
||||
if (err instanceof Error || typeof err === 'string') {
|
||||
this.logger.error(err);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -256,7 +258,7 @@ export class ApInboxService {
|
||||
|
||||
const targetUri = getApId(activity.object);
|
||||
|
||||
this.announceNote(actor, activity, targetUri);
|
||||
await this.announceNote(actor, activity, targetUri);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -288,7 +290,7 @@ export class ApInboxService {
|
||||
} catch (err) {
|
||||
// 対象が4xxならスキップ
|
||||
if (err instanceof StatusError) {
|
||||
if (err.isClientError) {
|
||||
if (!err.isRetryable) {
|
||||
this.logger.warn(`Ignored announce target ${targetUri} - ${err.statusCode}`);
|
||||
return;
|
||||
}
|
||||
@@ -373,7 +375,7 @@ export class ApInboxService {
|
||||
});
|
||||
|
||||
if (isPost(object)) {
|
||||
this.createNote(resolver, actor, object, false, activity);
|
||||
await this.createNote(resolver, actor, object, false, activity);
|
||||
} else {
|
||||
this.logger.warn(`Unknown type: ${getApType(object)}`);
|
||||
}
|
||||
@@ -404,7 +406,7 @@ export class ApInboxService {
|
||||
await this.apNoteService.createNote(note, resolver, silent);
|
||||
return 'ok';
|
||||
} catch (err) {
|
||||
if (err instanceof StatusError && err.isClientError) {
|
||||
if (err instanceof StatusError && !err.isRetryable) {
|
||||
return `skip ${err.statusCode}`;
|
||||
} else {
|
||||
throw err;
|
||||
|
||||
@@ -34,9 +34,9 @@ type PrivateKey = {
|
||||
};
|
||||
|
||||
export class ApRequestCreator {
|
||||
static createSignedPost(args: { key: PrivateKey, url: string, body: string, additionalHeaders: Record<string, string> }): Signed {
|
||||
static createSignedPost(args: { key: PrivateKey, url: string, body: string, digest?: string, additionalHeaders: Record<string, string> }): Signed {
|
||||
const u = new URL(args.url);
|
||||
const digestHeader = `SHA-256=${crypto.createHash('sha256').update(args.body).digest('base64')}`;
|
||||
const digestHeader = args.digest ?? this.createDigest(args.body);
|
||||
|
||||
const request: Request = {
|
||||
url: u.href,
|
||||
@@ -59,6 +59,10 @@ export class ApRequestCreator {
|
||||
};
|
||||
}
|
||||
|
||||
static createDigest(body: string) {
|
||||
return `SHA-256=${crypto.createHash('sha256').update(body).digest('base64')}`;
|
||||
}
|
||||
|
||||
static createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record<string, string> }): Signed {
|
||||
const u = new URL(args.url);
|
||||
|
||||
@@ -145,8 +149,8 @@ export class ApRequestService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown): Promise<void> {
|
||||
const body = JSON.stringify(object);
|
||||
public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, digest?: string): Promise<void> {
|
||||
const body = typeof object === 'string' ? object : JSON.stringify(object);
|
||||
|
||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||
|
||||
@@ -157,6 +161,7 @@ export class ApRequestService {
|
||||
},
|
||||
url,
|
||||
body,
|
||||
digest,
|
||||
additionalHeaders: {
|
||||
},
|
||||
});
|
||||
|
||||
@@ -216,7 +216,7 @@ export class ApNoteService {
|
||||
return { status: 'ok', res };
|
||||
} catch (e) {
|
||||
return {
|
||||
status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror',
|
||||
status: (e instanceof StatusError && !e.isRetryable) ? 'permerror' : 'temperror',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -351,6 +351,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||
color: channel.color,
|
||||
isSensitive: channel.isSensitive,
|
||||
allowRenoteToExternal: channel.allowRenoteToExternal,
|
||||
userId: channel.userId,
|
||||
} : undefined,
|
||||
mentions: note.mentions.length > 0 ? note.mentions : undefined,
|
||||
uri: note.uri ?? undefined,
|
||||
|
||||
115
packages/backend/src/core/entities/ReversiGameEntityService.ts
Normal file
115
packages/backend/src/core/entities/ReversiGameEntityService.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { ReversiGamesRepository } 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 { MiReversiGame } from '@/models/ReversiGame.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
|
||||
@Injectable()
|
||||
export class ReversiGameEntityService {
|
||||
constructor(
|
||||
@Inject(DI.reversiGamesRepository)
|
||||
private reversiGamesRepository: ReversiGamesRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packDetail(
|
||||
src: MiReversiGame['id'] | MiReversiGame,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
): Promise<Packed<'ReversiGameDetailed'>> {
|
||||
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: game.id,
|
||||
createdAt: this.idService.parse(game.id).date.toISOString(),
|
||||
startedAt: game.startedAt && game.startedAt.toISOString(),
|
||||
isStarted: game.isStarted,
|
||||
isEnded: game.isEnded,
|
||||
form1: game.form1,
|
||||
form2: game.form2,
|
||||
user1Ready: game.user1Ready,
|
||||
user2Ready: game.user2Ready,
|
||||
user1Id: game.user1Id,
|
||||
user2Id: game.user2Id,
|
||||
user1: this.userEntityService.pack(game.user1Id, me),
|
||||
user2: this.userEntityService.pack(game.user2Id, me),
|
||||
winnerId: game.winnerId,
|
||||
winner: game.winnerId ? this.userEntityService.pack(game.winnerId, me) : null,
|
||||
surrendered: game.surrendered,
|
||||
black: game.black,
|
||||
bw: game.bw,
|
||||
isLlotheo: game.isLlotheo,
|
||||
canPutEverywhere: game.canPutEverywhere,
|
||||
loopedBoard: game.loopedBoard,
|
||||
logs: game.logs.map(log => ({
|
||||
at: log.at,
|
||||
color: log.color,
|
||||
pos: log.pos,
|
||||
})),
|
||||
map: game.map,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public packDetailMany(
|
||||
xs: MiReversiGame[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
) {
|
||||
return Promise.all(xs.map(x => this.packDetail(x, me)));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packLite(
|
||||
src: MiReversiGame['id'] | MiReversiGame,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
): Promise<Packed<'ReversiGameLite'>> {
|
||||
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: game.id,
|
||||
createdAt: this.idService.parse(game.id).date.toISOString(),
|
||||
startedAt: game.startedAt && game.startedAt.toISOString(),
|
||||
isStarted: game.isStarted,
|
||||
isEnded: game.isEnded,
|
||||
form1: game.form1,
|
||||
form2: game.form2,
|
||||
user1Ready: game.user1Ready,
|
||||
user2Ready: game.user2Ready,
|
||||
user1Id: game.user1Id,
|
||||
user2Id: game.user2Id,
|
||||
user1: this.userEntityService.pack(game.user1Id, me),
|
||||
user2: this.userEntityService.pack(game.user2Id, me),
|
||||
winnerId: game.winnerId,
|
||||
winner: game.winnerId ? this.userEntityService.pack(game.winnerId, me) : null,
|
||||
surrendered: game.surrendered,
|
||||
black: game.black,
|
||||
bw: game.bw,
|
||||
isLlotheo: game.isLlotheo,
|
||||
canPutEverywhere: game.canPutEverywhere,
|
||||
loopedBoard: game.loopedBoard,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public packLiteMany(
|
||||
xs: MiReversiGame[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
) {
|
||||
return Promise.all(xs.map(x => this.packLite(x, me)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ export class ServerStatsService implements OnApplicationShutdown {
|
||||
const log = [] as any[];
|
||||
|
||||
ev.on('requestServerStatsLog', x => {
|
||||
ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length ?? 50));
|
||||
ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length));
|
||||
});
|
||||
|
||||
const tick = async () => {
|
||||
|
||||
@@ -78,5 +78,7 @@ export const DI = {
|
||||
flashsRepository: Symbol('flashsRepository'),
|
||||
flashLikesRepository: Symbol('flashLikesRepository'),
|
||||
userMemosRepository: Symbol('userMemosRepository'),
|
||||
bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'),
|
||||
reversiGamesRepository: Symbol('reversiGamesRepository'),
|
||||
//#endregion
|
||||
};
|
||||
|
||||
@@ -71,8 +71,11 @@ export default class Logger {
|
||||
let log = `${l} ${worker}\t[${contexts.join(' ')}]\t${m}`;
|
||||
if (envOption.withLogTime) log = chalk.gray(time) + ' ' + log;
|
||||
|
||||
console.log(important ? chalk.bold(log) : log);
|
||||
if (level === 'error' && data) console.log(data);
|
||||
const args: unknown[] = [important ? chalk.bold(log) : log];
|
||||
if (data != null) {
|
||||
args.push(data);
|
||||
}
|
||||
console.log(...args);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
||||
@@ -39,6 +39,7 @@ import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
|
||||
import { packedSigninSchema } from '@/models/json-schema/signin.js';
|
||||
import { packedRoleLiteSchema, packedRoleSchema } from '@/models/json-schema/role.js';
|
||||
import { packedAdSchema } from '@/models/json-schema/ad.js';
|
||||
import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js';
|
||||
|
||||
export const refs = {
|
||||
UserLite: packedUserLiteSchema,
|
||||
@@ -78,6 +79,8 @@ export const refs = {
|
||||
Signin: packedSigninSchema,
|
||||
RoleLite: packedRoleLiteSchema,
|
||||
Role: packedRoleSchema,
|
||||
ReversiGameLite: packedReversiGameLiteSchema,
|
||||
ReversiGameDetailed: packedReversiGameDetailedSchema,
|
||||
};
|
||||
|
||||
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
|
||||
|
||||
23
packages/backend/src/misc/promise-tracker.ts
Normal file
23
packages/backend/src/misc/promise-tracker.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
const promiseRefs: Set<WeakRef<Promise<unknown>>> = new Set();
|
||||
|
||||
/**
|
||||
* This tracks promises that other modules decided not to wait for,
|
||||
* and makes sure they are all settled before fully closing down the server.
|
||||
*/
|
||||
export function trackPromise(promise: Promise<unknown>) {
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
return;
|
||||
}
|
||||
const ref = new WeakRef(promise);
|
||||
promiseRefs.add(ref);
|
||||
promise.finally(() => promiseRefs.delete(ref));
|
||||
}
|
||||
|
||||
export async function allSettled(): Promise<void> {
|
||||
await Promise.allSettled([...promiseRefs].map(r => r.deref()));
|
||||
}
|
||||
@@ -7,6 +7,7 @@ export class StatusError extends Error {
|
||||
public statusCode: number;
|
||||
public statusMessage?: string;
|
||||
public isClientError: boolean;
|
||||
public isRetryable: boolean;
|
||||
|
||||
constructor(message: string, statusCode: number, statusMessage?: string) {
|
||||
super(message);
|
||||
@@ -14,5 +15,6 @@ export class StatusError extends Error {
|
||||
this.statusCode = statusCode;
|
||||
this.statusMessage = statusMessage;
|
||||
this.isClientError = typeof this.statusCode === 'number' && this.statusCode >= 400 && this.statusCode < 500;
|
||||
this.isRetryable = !this.isClientError || this.statusCode === 429;
|
||||
}
|
||||
}
|
||||
|
||||
57
packages/backend/src/models/BubbleGameRecord.ts
Normal file
57
packages/backend/src/models/BubbleGameRecord.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
|
||||
@Entity('bubble_game_record')
|
||||
export class MiBubbleGameRecord {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
})
|
||||
public userId: MiUser['id'];
|
||||
|
||||
@ManyToOne(type => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: MiUser | null;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone')
|
||||
public seededAt: Date;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
})
|
||||
public seed: string;
|
||||
|
||||
@Column('integer')
|
||||
public gameVersion: number;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128,
|
||||
})
|
||||
public gameMode: string;
|
||||
|
||||
@Index()
|
||||
@Column('integer')
|
||||
public score: number;
|
||||
|
||||
@Column('jsonb', {
|
||||
default: [],
|
||||
})
|
||||
public logs: any[];
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public isVerified: boolean;
|
||||
}
|
||||
@@ -191,6 +191,29 @@ export class MiMeta {
|
||||
})
|
||||
public hcaptchaSecretKey: string | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public enableMcaptcha: boolean;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
nullable: true,
|
||||
})
|
||||
public mcaptchaSitekey: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
nullable: true,
|
||||
})
|
||||
public mcaptchaSecretKey: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
nullable: true,
|
||||
})
|
||||
public mcaptchaInstanceUrl: string | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
@@ -467,7 +490,7 @@ export class MiMeta {
|
||||
nullable: true,
|
||||
})
|
||||
public truemailInstance: string | null;
|
||||
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
nullable: true,
|
||||
|
||||
@@ -11,9 +11,6 @@ import { MiChannel } from './Channel.js';
|
||||
import type { MiDriveFile } from './DriveFile.js';
|
||||
|
||||
@Entity('note')
|
||||
@Index('IDX_NOTE_TAGS', { synchronize: false })
|
||||
@Index('IDX_NOTE_MENTIONS', { synchronize: false })
|
||||
@Index('IDX_NOTE_VISIBLE_USER_IDS', { synchronize: false })
|
||||
export class MiNote {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
@@ -133,7 +130,7 @@ export class MiNote {
|
||||
})
|
||||
public url: string | null;
|
||||
|
||||
@Index()
|
||||
@Index('IDX_NOTE_FILE_IDS', { synchronize: false })
|
||||
@Column({
|
||||
...id(),
|
||||
array: true, default: '{}',
|
||||
@@ -145,14 +142,14 @@ export class MiNote {
|
||||
})
|
||||
public attachedFileTypes: string[];
|
||||
|
||||
@Index()
|
||||
@Index('IDX_NOTE_VISIBLE_USER_IDS', { synchronize: false })
|
||||
@Column({
|
||||
...id(),
|
||||
array: true, default: '{}',
|
||||
})
|
||||
public visibleUserIds: MiUser['id'][];
|
||||
|
||||
@Index()
|
||||
@Index('IDX_NOTE_MENTIONS', { synchronize: false })
|
||||
@Column({
|
||||
...id(),
|
||||
array: true, default: '{}',
|
||||
@@ -174,7 +171,7 @@ export class MiNote {
|
||||
})
|
||||
public emojis: string[];
|
||||
|
||||
@Index()
|
||||
@Index('IDX_NOTE_TAGS', { synchronize: false })
|
||||
@Column('varchar', {
|
||||
length: 128, array: true, default: '{}',
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook } from './_.js';
|
||||
import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, MiBubbleGameRecord, MiReversiGame } from './_.js';
|
||||
import type { DataSource } from 'typeorm';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
|
||||
@@ -399,6 +399,18 @@ const $userMemosRepository: Provider = {
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $bubbleGameRecordsRepository: Provider = {
|
||||
provide: DI.bubbleGameRecordsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $reversiGamesRepository: Provider = {
|
||||
provide: DI.reversiGamesRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiReversiGame),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
],
|
||||
@@ -468,6 +480,8 @@ const $userMemosRepository: Provider = {
|
||||
$flashsRepository,
|
||||
$flashLikesRepository,
|
||||
$userMemosRepository,
|
||||
$bubbleGameRecordsRepository,
|
||||
$reversiGamesRepository,
|
||||
],
|
||||
exports: [
|
||||
$usersRepository,
|
||||
@@ -535,6 +549,8 @@ const $userMemosRepository: Provider = {
|
||||
$flashsRepository,
|
||||
$flashLikesRepository,
|
||||
$userMemosRepository,
|
||||
$bubbleGameRecordsRepository,
|
||||
$reversiGamesRepository,
|
||||
],
|
||||
})
|
||||
export class RepositoryModule {}
|
||||
|
||||
127
packages/backend/src/models/ReversiGame.ts
Normal file
127
packages/backend/src/models/ReversiGame.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
|
||||
@Entity('reversi_game')
|
||||
export class MiReversiGame {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
comment: 'The started date of the ReversiGame.',
|
||||
})
|
||||
public startedAt: Date | null;
|
||||
|
||||
@Column(id())
|
||||
public user1Id: MiUser['id'];
|
||||
|
||||
@ManyToOne(type => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user1: MiUser | null;
|
||||
|
||||
@Column(id())
|
||||
public user2Id: MiUser['id'];
|
||||
|
||||
@ManyToOne(type => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user2: MiUser | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public user1Ready: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public user2Ready: boolean;
|
||||
|
||||
/**
|
||||
* どちらのプレイヤーが先行(黒)か
|
||||
* 1 ... user1
|
||||
* 2 ... user2
|
||||
*/
|
||||
@Column('integer', {
|
||||
nullable: true,
|
||||
})
|
||||
public black: number | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public isStarted: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public isEnded: boolean;
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
})
|
||||
public winnerId: MiUser['id'] | null;
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
})
|
||||
public surrendered: MiUser['id'] | null;
|
||||
|
||||
@Column('jsonb', {
|
||||
default: [],
|
||||
})
|
||||
public logs: {
|
||||
at: number;
|
||||
color: boolean;
|
||||
pos: number;
|
||||
}[];
|
||||
|
||||
@Column('varchar', {
|
||||
array: true, length: 64,
|
||||
})
|
||||
public map: string[];
|
||||
|
||||
@Column('varchar', {
|
||||
length: 32,
|
||||
})
|
||||
public bw: string;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public isLlotheo: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public canPutEverywhere: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public loopedBoard: boolean;
|
||||
|
||||
@Column('jsonb', {
|
||||
nullable: true, default: null,
|
||||
})
|
||||
public form1: any | null;
|
||||
|
||||
@Column('jsonb', {
|
||||
nullable: true, default: null,
|
||||
})
|
||||
public form2: any | null;
|
||||
|
||||
/**
|
||||
* ログのposを文字列としてすべて連結したもののCRC32値
|
||||
*/
|
||||
@Column('varchar', {
|
||||
length: 32, nullable: true,
|
||||
})
|
||||
public crc32: string | null;
|
||||
}
|
||||
@@ -68,6 +68,9 @@ import { MiRoleAssignment } from '@/models/RoleAssignment.js';
|
||||
import { MiFlash } from '@/models/Flash.js';
|
||||
import { MiFlashLike } from '@/models/FlashLike.js';
|
||||
import { MiUserListFavorite } from '@/models/UserListFavorite.js';
|
||||
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
|
||||
import { MiReversiGame } from '@/models/ReversiGame.js';
|
||||
|
||||
import type { Repository } from 'typeorm';
|
||||
|
||||
export {
|
||||
@@ -136,6 +139,8 @@ export {
|
||||
MiFlash,
|
||||
MiFlashLike,
|
||||
MiUserMemo,
|
||||
MiBubbleGameRecord,
|
||||
MiReversiGame,
|
||||
};
|
||||
|
||||
export type AbuseUserReportsRepository = Repository<MiAbuseUserReport>;
|
||||
@@ -203,3 +208,5 @@ export type RoleAssignmentsRepository = Repository<MiRoleAssignment>;
|
||||
export type FlashsRepository = Repository<MiFlash>;
|
||||
export type FlashLikesRepository = Repository<MiFlashLike>;
|
||||
export type UserMemoRepository = Repository<MiUserMemo>;
|
||||
export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord>;
|
||||
export type ReversiGamesRepository = Repository<MiReversiGame>;
|
||||
|
||||
@@ -148,6 +148,10 @@ export const packedNoteSchema = {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
localOnly: {
|
||||
|
||||
234
packages/backend/src/models/json-schema/reversi-game.ts
Normal file
234
packages/backend/src/models/json-schema/reversi-game.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export const packedReversiGameLiteSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
startedAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'date-time',
|
||||
},
|
||||
isStarted: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isEnded: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
form1: {
|
||||
type: 'any',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
form2: {
|
||||
type: 'any',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
user1Ready: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user2Ready: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user1Id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
user2Id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
user1: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'User',
|
||||
},
|
||||
user2: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'User',
|
||||
},
|
||||
winnerId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'id',
|
||||
},
|
||||
winner: {
|
||||
type: 'object',
|
||||
optional: false, nullable: true,
|
||||
ref: 'User',
|
||||
},
|
||||
surrendered: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'id',
|
||||
},
|
||||
black: {
|
||||
type: 'number',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
bw: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isLlotheo: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
canPutEverywhere: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
loopedBoard: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const packedReversiGameDetailedSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
startedAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'date-time',
|
||||
},
|
||||
isStarted: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isEnded: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
form1: {
|
||||
type: 'any',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
form2: {
|
||||
type: 'any',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
user1Ready: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user2Ready: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user1Id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
user2Id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
user1: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'User',
|
||||
},
|
||||
user2: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'User',
|
||||
},
|
||||
winnerId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'id',
|
||||
},
|
||||
winner: {
|
||||
type: 'object',
|
||||
optional: false, nullable: true,
|
||||
ref: 'User',
|
||||
},
|
||||
surrendered: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'id',
|
||||
},
|
||||
black: {
|
||||
type: 'number',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
bw: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isLlotheo: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
canPutEverywhere: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
loopedBoard: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
logs: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
at: {
|
||||
type: 'number',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
color: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
pos: {
|
||||
type: 'number',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
map: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -76,6 +76,8 @@ import { MiRoleAssignment } from '@/models/RoleAssignment.js';
|
||||
import { MiFlash } from '@/models/Flash.js';
|
||||
import { MiFlashLike } from '@/models/FlashLike.js';
|
||||
import { MiUserMemo } from '@/models/UserMemo.js';
|
||||
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
|
||||
import { MiReversiGame } from '@/models/ReversiGame.js';
|
||||
|
||||
import { Config } from '@/config.js';
|
||||
import MisskeyLogger from '@/logger.js';
|
||||
@@ -190,6 +192,8 @@ export const entities = [
|
||||
MiFlash,
|
||||
MiFlashLike,
|
||||
MiUserMemo,
|
||||
MiBubbleGameRecord,
|
||||
MiReversiGame,
|
||||
...charts,
|
||||
];
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmo
|
||||
import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js';
|
||||
import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js';
|
||||
import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js';
|
||||
import { ExportClipsProcessorService } from './processors/ExportClipsProcessorService.js';
|
||||
import { ExportUserListsProcessorService } from './processors/ExportUserListsProcessorService.js';
|
||||
import { ExportAntennasProcessorService } from './processors/ExportAntennasProcessorService.js';
|
||||
import { ImportBlockingProcessorService } from './processors/ImportBlockingProcessorService.js';
|
||||
@@ -53,6 +54,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
|
||||
DeleteDriveFilesProcessorService,
|
||||
ExportCustomEmojisProcessorService,
|
||||
ExportNotesProcessorService,
|
||||
ExportClipsProcessorService,
|
||||
ExportFavoritesProcessorService,
|
||||
ExportFollowingProcessorService,
|
||||
ExportMutingProcessorService,
|
||||
|
||||
@@ -16,6 +16,7 @@ import { InboxProcessorService } from './processors/InboxProcessorService.js';
|
||||
import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js';
|
||||
import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmojisProcessorService.js';
|
||||
import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js';
|
||||
import { ExportClipsProcessorService } from './processors/ExportClipsProcessorService.js';
|
||||
import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js';
|
||||
import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js';
|
||||
import { ExportBlockingProcessorService } from './processors/ExportBlockingProcessorService.js';
|
||||
@@ -91,6 +92,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService,
|
||||
private exportCustomEmojisProcessorService: ExportCustomEmojisProcessorService,
|
||||
private exportNotesProcessorService: ExportNotesProcessorService,
|
||||
private exportClipsProcessorService: ExportClipsProcessorService,
|
||||
private exportFavoritesProcessorService: ExportFavoritesProcessorService,
|
||||
private exportFollowingProcessorService: ExportFollowingProcessorService,
|
||||
private exportMutingProcessorService: ExportMutingProcessorService,
|
||||
@@ -164,6 +166,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job);
|
||||
case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job);
|
||||
case 'exportNotes': return this.exportNotesProcessorService.process(job);
|
||||
case 'exportClips': return this.exportClipsProcessorService.process(job);
|
||||
case 'exportFavorites': return this.exportFavoritesProcessorService.process(job);
|
||||
case 'exportFollowing': return this.exportFollowingProcessorService.process(job);
|
||||
case 'exportMuting': return this.exportMutingProcessorService.process(job);
|
||||
|
||||
@@ -72,7 +72,7 @@ export class DeliverProcessorService {
|
||||
}
|
||||
|
||||
try {
|
||||
await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content);
|
||||
await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, job.data.digest);
|
||||
|
||||
// Update stats
|
||||
this.federatedInstanceService.fetch(host).then(i => {
|
||||
@@ -111,7 +111,7 @@ export class DeliverProcessorService {
|
||||
|
||||
if (res instanceof StatusError) {
|
||||
// 4xx
|
||||
if (res.isClientError) {
|
||||
if (!res.isRetryable) {
|
||||
// 相手が閉鎖していることを明示しているため、配送停止する
|
||||
if (job.data.isSharedInbox && res.statusCode === 410) {
|
||||
this.federatedInstanceService.fetch(host).then(i => {
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import { Writable } from 'node:stream';
|
||||
import { Inject, Injectable, StreamableFile } from '@nestjs/common';
|
||||
import { MoreThan } from 'typeorm';
|
||||
import { format as dateFormat } from 'date-fns';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { ClipNotesRepository, ClipsRepository, MiClip, MiClipNote, MiUser, NotesRepository, PollsRepository, UsersRepository } from '@/models/_.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { DriveService } from '@/core/DriveService.js';
|
||||
import { createTemp } from '@/misc/create-temp.js';
|
||||
import type { MiPoll } from '@/models/Poll.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||
import { Packed } from '@/misc/json-schema.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { DbJobDataWithUser } from '../types.js';
|
||||
|
||||
@Injectable()
|
||||
export class ExportClipsProcessorService {
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.pollsRepository)
|
||||
private pollsRepository: PollsRepository,
|
||||
|
||||
@Inject(DI.clipsRepository)
|
||||
private clipsRepository: ClipsRepository,
|
||||
|
||||
@Inject(DI.clipNotesRepository)
|
||||
private clipNotesRepository: ClipNotesRepository,
|
||||
|
||||
private driveService: DriveService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('export-clips');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
||||
this.logger.info(`Exporting clips of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create temp file
|
||||
const [path, cleanup] = await createTemp();
|
||||
|
||||
this.logger.info(`Temp file is ${path}`);
|
||||
|
||||
try {
|
||||
const stream = Writable.toWeb(fs.createWriteStream(path, { flags: 'a' }));
|
||||
const writer = stream.getWriter();
|
||||
writer.closed.catch(this.logger.error);
|
||||
|
||||
await writer.write('[');
|
||||
|
||||
await this.processClips(writer, user, job);
|
||||
|
||||
await writer.write(']');
|
||||
await writer.close();
|
||||
|
||||
this.logger.succ(`Exported to: ${path}`);
|
||||
|
||||
const fileName = 'clips-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json';
|
||||
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
|
||||
|
||||
this.logger.succ(`Exported to: ${driveFile.id}`);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
async processClips(writer: WritableStreamDefaultWriter, user: MiUser, job: Bull.Job<DbJobDataWithUser>) {
|
||||
let exportedClipsCount = 0;
|
||||
let cursor: MiClip['id'] | null = null;
|
||||
|
||||
while (true) {
|
||||
const clips = await this.clipsRepository.find({
|
||||
where: {
|
||||
userId: user.id,
|
||||
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||
},
|
||||
take: 100,
|
||||
order: {
|
||||
id: 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (clips.length === 0) {
|
||||
job.updateProgress(100);
|
||||
break;
|
||||
}
|
||||
|
||||
cursor = clips.at(-1)?.id ?? null;
|
||||
|
||||
for (const clip of clips) {
|
||||
// Stringify but remove the last `]}`
|
||||
const content = JSON.stringify(this.serializeClip(clip)).slice(0, -2);
|
||||
const isFirst = exportedClipsCount === 0;
|
||||
await writer.write(isFirst ? content : ',\n' + content);
|
||||
|
||||
await this.processClipNotes(writer, clip.id);
|
||||
|
||||
await writer.write(']}');
|
||||
exportedClipsCount++;
|
||||
}
|
||||
|
||||
const total = await this.clipsRepository.countBy({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
job.updateProgress(exportedClipsCount / total);
|
||||
}
|
||||
}
|
||||
|
||||
async processClipNotes(writer: WritableStreamDefaultWriter, clipId: string): Promise<void> {
|
||||
let exportedClipNotesCount = 0;
|
||||
let cursor: MiClipNote['id'] | null = null;
|
||||
|
||||
while (true) {
|
||||
const clipNotes = await this.clipNotesRepository.find({
|
||||
where: {
|
||||
clipId,
|
||||
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||
},
|
||||
take: 100,
|
||||
order: {
|
||||
id: 1,
|
||||
},
|
||||
relations: ['note', 'note.user'],
|
||||
}) as (MiClipNote & { note: MiNote & { user: MiUser } })[];
|
||||
|
||||
if (clipNotes.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
cursor = clipNotes.at(-1)?.id ?? null;
|
||||
|
||||
for (const clipNote of clipNotes) {
|
||||
let poll: MiPoll | undefined;
|
||||
if (clipNote.note.hasPoll) {
|
||||
poll = await this.pollsRepository.findOneByOrFail({ noteId: clipNote.note.id });
|
||||
}
|
||||
const content = JSON.stringify(this.serializeClipNote(clipNote, poll));
|
||||
const isFirst = exportedClipNotesCount === 0;
|
||||
await writer.write(isFirst ? content : ',\n' + content);
|
||||
|
||||
exportedClipNotesCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private serializeClip(clip: MiClip): Record<string, unknown> {
|
||||
return {
|
||||
id: clip.id,
|
||||
name: clip.name,
|
||||
description: clip.description,
|
||||
lastClippedAt: clip.lastClippedAt?.toISOString(),
|
||||
clipNotes: [],
|
||||
};
|
||||
}
|
||||
|
||||
private serializeClipNote(clip: MiClipNote & { note: MiNote & { user: MiUser } }, poll: MiPoll | undefined): Record<string, unknown> {
|
||||
return {
|
||||
id: clip.id,
|
||||
createdAt: this.idService.parse(clip.id).date.toISOString(),
|
||||
note: {
|
||||
id: clip.note.id,
|
||||
text: clip.note.text,
|
||||
createdAt: this.idService.parse(clip.note.id).date.toISOString(),
|
||||
fileIds: clip.note.fileIds,
|
||||
replyId: clip.note.replyId,
|
||||
renoteId: clip.note.renoteId,
|
||||
poll: poll,
|
||||
cw: clip.note.cw,
|
||||
visibility: clip.note.visibility,
|
||||
visibleUserIds: clip.note.visibleUserIds,
|
||||
localOnly: clip.note.localOnly,
|
||||
reactionAcceptance: clip.note.reactionAcceptance,
|
||||
uri: clip.note.uri,
|
||||
url: clip.note.url,
|
||||
user: {
|
||||
id: clip.note.user.id,
|
||||
name: clip.note.user.name,
|
||||
username: clip.note.user.username,
|
||||
host: clip.note.user.host,
|
||||
uri: clip.note.user.uri,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ export class InboxProcessorService {
|
||||
} catch (err) {
|
||||
// 対象が4xxならスキップ
|
||||
if (err instanceof StatusError) {
|
||||
if (err.isClientError) {
|
||||
if (!err.isRetryable) {
|
||||
throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`);
|
||||
}
|
||||
throw new Error(`Error in actor ${activity.actor} - ${err.statusCode}`);
|
||||
|
||||
@@ -71,7 +71,7 @@ export class WebhookDeliverProcessorService {
|
||||
|
||||
if (res instanceof StatusError) {
|
||||
// 4xx
|
||||
if (res.isClientError) {
|
||||
if (!res.isRetryable) {
|
||||
throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,9 @@ export type DeliverJobData = {
|
||||
/** Actor */
|
||||
user: ThinUser;
|
||||
/** Activity */
|
||||
content: unknown;
|
||||
content: string;
|
||||
/** Digest header */
|
||||
digest: string;
|
||||
/** inbox URL to deliver */
|
||||
to: string;
|
||||
/** whether it is sharedInbox */
|
||||
|
||||
@@ -9,7 +9,7 @@ import { dirname } from 'node:path';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import rename from 'rename';
|
||||
import sharp from 'sharp';
|
||||
import { sharpBmp } from 'sharp-read-bmp';
|
||||
import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { MiDriveFile, DriveFilesRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
@@ -168,11 +168,35 @@ export class FileServerService {
|
||||
}
|
||||
|
||||
if (!image) {
|
||||
image = {
|
||||
data: fs.createReadStream(file.path),
|
||||
ext: file.ext,
|
||||
type: file.mime,
|
||||
};
|
||||
if (request.headers.range && file.file.size > 0) {
|
||||
const range = request.headers.range as string;
|
||||
const parts = range.replace(/bytes=/, '').split('-');
|
||||
const start = parseInt(parts[0], 10);
|
||||
let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
|
||||
if (end > file.file.size) {
|
||||
end = file.file.size - 1;
|
||||
}
|
||||
const chunksize = end - start + 1;
|
||||
|
||||
image = {
|
||||
data: fs.createReadStream(file.path, {
|
||||
start,
|
||||
end,
|
||||
}),
|
||||
ext: file.ext,
|
||||
type: file.mime,
|
||||
};
|
||||
|
||||
reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
|
||||
reply.header('Accept-Ranges', 'bytes');
|
||||
reply.header('Content-Length', chunksize);
|
||||
} else {
|
||||
image = {
|
||||
data: fs.createReadStream(file.path),
|
||||
ext: file.ext,
|
||||
type: file.mime,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if ('pipe' in image.data && typeof image.data.pipe === 'function') {
|
||||
@@ -203,11 +227,54 @@ export class FileServerService {
|
||||
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.mime) ? file.mime : 'application/octet-stream');
|
||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||
reply.header('Content-Disposition', contentDisposition('inline', filename));
|
||||
|
||||
if (request.headers.range && file.file.size > 0) {
|
||||
const range = request.headers.range as string;
|
||||
const parts = range.replace(/bytes=/, '').split('-');
|
||||
const start = parseInt(parts[0], 10);
|
||||
let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
|
||||
if (end > file.file.size) {
|
||||
end = file.file.size - 1;
|
||||
}
|
||||
const chunksize = end - start + 1;
|
||||
const fileStream = fs.createReadStream(file.path, {
|
||||
start,
|
||||
end,
|
||||
});
|
||||
reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
|
||||
reply.header('Accept-Ranges', 'bytes');
|
||||
reply.header('Content-Length', chunksize);
|
||||
reply.code(206);
|
||||
return fileStream;
|
||||
}
|
||||
|
||||
return fs.createReadStream(file.path);
|
||||
} else {
|
||||
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.file.type) ? file.file.type : 'application/octet-stream');
|
||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||
reply.header('Content-Disposition', contentDisposition('inline', file.filename));
|
||||
|
||||
if (request.headers.range && file.file.size > 0) {
|
||||
const range = request.headers.range as string;
|
||||
const parts = range.replace(/bytes=/, '').split('-');
|
||||
const start = parseInt(parts[0], 10);
|
||||
let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
|
||||
console.log(end);
|
||||
if (end > file.file.size) {
|
||||
end = file.file.size - 1;
|
||||
}
|
||||
const chunksize = end - start + 1;
|
||||
const fileStream = fs.createReadStream(file.path, {
|
||||
start,
|
||||
end,
|
||||
});
|
||||
reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
|
||||
reply.header('Accept-Ranges', 'bytes');
|
||||
reply.header('Content-Length', chunksize);
|
||||
reply.code(206);
|
||||
return fileStream;
|
||||
}
|
||||
|
||||
return fs.createReadStream(file.path);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -340,11 +407,35 @@ export class FileServerService {
|
||||
}
|
||||
|
||||
if (!image) {
|
||||
image = {
|
||||
data: fs.createReadStream(file.path),
|
||||
ext: file.ext,
|
||||
type: file.mime,
|
||||
};
|
||||
if (request.headers.range && file.file && file.file.size > 0) {
|
||||
const range = request.headers.range as string;
|
||||
const parts = range.replace(/bytes=/, '').split('-');
|
||||
const start = parseInt(parts[0], 10);
|
||||
let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
|
||||
if (end > file.file.size) {
|
||||
end = file.file.size - 1;
|
||||
}
|
||||
const chunksize = end - start + 1;
|
||||
|
||||
image = {
|
||||
data: fs.createReadStream(file.path, {
|
||||
start,
|
||||
end,
|
||||
}),
|
||||
ext: file.ext,
|
||||
type: file.mime,
|
||||
};
|
||||
|
||||
reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
|
||||
reply.header('Accept-Ranges', 'bytes');
|
||||
reply.header('Content-Length', chunksize);
|
||||
} else {
|
||||
image = {
|
||||
data: fs.createReadStream(file.path),
|
||||
ext: file.ext,
|
||||
type: file.mime,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if ('cleanup' in file) {
|
||||
|
||||
@@ -22,9 +22,13 @@ import { SigninApiService } from './api/SigninApiService.js';
|
||||
import { SigninService } from './api/SigninService.js';
|
||||
import { SignupApiService } from './api/SignupApiService.js';
|
||||
import { StreamingApiServerService } from './api/StreamingApiServerService.js';
|
||||
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
|
||||
import { ClientServerService } from './web/ClientServerService.js';
|
||||
import { FeedService } from './web/FeedService.js';
|
||||
import { UrlPreviewService } from './web/UrlPreviewService.js';
|
||||
import { ClientLoggerService } from './web/ClientLoggerService.js';
|
||||
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
||||
|
||||
import { MainChannelService } from './api/stream/channels/main.js';
|
||||
import { AdminChannelService } from './api/stream/channels/admin.js';
|
||||
import { AntennaChannelService } from './api/stream/channels/antenna.js';
|
||||
@@ -38,10 +42,9 @@ import { LocalTimelineChannelService } from './api/stream/channels/local-timelin
|
||||
import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js';
|
||||
import { ServerStatsChannelService } from './api/stream/channels/server-stats.js';
|
||||
import { UserListChannelService } from './api/stream/channels/user-list.js';
|
||||
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
|
||||
import { ClientLoggerService } from './web/ClientLoggerService.js';
|
||||
import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js';
|
||||
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
||||
import { ReversiChannelService } from './api/stream/channels/reversi.js';
|
||||
import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -77,6 +80,8 @@ import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
||||
GlobalTimelineChannelService,
|
||||
HashtagChannelService,
|
||||
RoleTimelineChannelService,
|
||||
ReversiChannelService,
|
||||
ReversiGameChannelService,
|
||||
HomeTimelineChannelService,
|
||||
HybridTimelineChannelService,
|
||||
LocalTimelineChannelService,
|
||||
|
||||
@@ -208,6 +208,7 @@ import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js';
|
||||
import * as ep___i_exportFollowing from './endpoints/i/export-following.js';
|
||||
import * as ep___i_exportMute from './endpoints/i/export-mute.js';
|
||||
import * as ep___i_exportNotes from './endpoints/i/export-notes.js';
|
||||
import * as ep___i_exportClips from './endpoints/i/export-clips.js';
|
||||
import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js';
|
||||
import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js';
|
||||
import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js';
|
||||
@@ -363,6 +364,14 @@ import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
|
||||
import * as ep___fetchRss from './endpoints/fetch-rss.js';
|
||||
import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js';
|
||||
import * as ep___retention from './endpoints/retention.js';
|
||||
import * as ep___bubbleGame_register from './endpoints/bubble-game/register.js';
|
||||
import * as ep___bubbleGame_ranking from './endpoints/bubble-game/ranking.js';
|
||||
import * as ep___reversi_cancelMatch from './endpoints/reversi/cancel-match.js';
|
||||
import * as ep___reversi_games from './endpoints/reversi/games.js';
|
||||
import * as ep___reversi_match from './endpoints/reversi/match.js';
|
||||
import * as ep___reversi_invitations from './endpoints/reversi/invitations.js';
|
||||
import * as ep___reversi_showGame from './endpoints/reversi/show-game.js';
|
||||
import * as ep___reversi_surrender from './endpoints/reversi/surrender.js';
|
||||
import { GetterService } from './GetterService.js';
|
||||
import { ApiLoggerService } from './ApiLoggerService.js';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
@@ -569,6 +578,7 @@ const $i_exportBlocking: Provider = { provide: 'ep:i/export-blocking', useClass:
|
||||
const $i_exportFollowing: Provider = { provide: 'ep:i/export-following', useClass: ep___i_exportFollowing.default };
|
||||
const $i_exportMute: Provider = { provide: 'ep:i/export-mute', useClass: ep___i_exportMute.default };
|
||||
const $i_exportNotes: Provider = { provide: 'ep:i/export-notes', useClass: ep___i_exportNotes.default };
|
||||
const $i_exportClips: Provider = { provide: 'ep:i/export-clips', useClass: ep___i_exportClips.default };
|
||||
const $i_exportFavorites: Provider = { provide: 'ep:i/export-favorites', useClass: ep___i_exportFavorites.default };
|
||||
const $i_exportUserLists: Provider = { provide: 'ep:i/export-user-lists', useClass: ep___i_exportUserLists.default };
|
||||
const $i_exportAntennas: Provider = { provide: 'ep:i/export-antennas', useClass: ep___i_exportAntennas.default };
|
||||
@@ -724,6 +734,14 @@ const $users_updateMemo: Provider = { provide: 'ep:users/update-memo', useClass:
|
||||
const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default };
|
||||
const $fetchExternalResources: Provider = { provide: 'ep:fetch-external-resources', useClass: ep___fetchExternalResources.default };
|
||||
const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default };
|
||||
const $bubbleGame_register: Provider = { provide: 'ep:bubble-game/register', useClass: ep___bubbleGame_register.default };
|
||||
const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useClass: ep___bubbleGame_ranking.default };
|
||||
const $reversi_cancelMatch: Provider = { provide: 'ep:reversi/cancel-match', useClass: ep___reversi_cancelMatch.default };
|
||||
const $reversi_games: Provider = { provide: 'ep:reversi/games', useClass: ep___reversi_games.default };
|
||||
const $reversi_match: Provider = { provide: 'ep:reversi/match', useClass: ep___reversi_match.default };
|
||||
const $reversi_invitations: Provider = { provide: 'ep:reversi/invitations', useClass: ep___reversi_invitations.default };
|
||||
const $reversi_showGame: Provider = { provide: 'ep:reversi/show-game', useClass: ep___reversi_showGame.default };
|
||||
const $reversi_surrender: Provider = { provide: 'ep:reversi/surrender', useClass: ep___reversi_surrender.default };
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -934,6 +952,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$i_exportFollowing,
|
||||
$i_exportMute,
|
||||
$i_exportNotes,
|
||||
$i_exportClips,
|
||||
$i_exportFavorites,
|
||||
$i_exportUserLists,
|
||||
$i_exportAntennas,
|
||||
@@ -1089,6 +1108,14 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$fetchRss,
|
||||
$fetchExternalResources,
|
||||
$retention,
|
||||
$bubbleGame_register,
|
||||
$bubbleGame_ranking,
|
||||
$reversi_cancelMatch,
|
||||
$reversi_games,
|
||||
$reversi_match,
|
||||
$reversi_invitations,
|
||||
$reversi_showGame,
|
||||
$reversi_surrender,
|
||||
],
|
||||
exports: [
|
||||
$admin_meta,
|
||||
@@ -1293,6 +1320,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$i_exportFollowing,
|
||||
$i_exportMute,
|
||||
$i_exportNotes,
|
||||
$i_exportClips,
|
||||
$i_exportFavorites,
|
||||
$i_exportUserLists,
|
||||
$i_exportAntennas,
|
||||
@@ -1445,6 +1473,14 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$fetchRss,
|
||||
$fetchExternalResources,
|
||||
$retention,
|
||||
$bubbleGame_register,
|
||||
$bubbleGame_ranking,
|
||||
$reversi_cancelMatch,
|
||||
$reversi_games,
|
||||
$reversi_match,
|
||||
$reversi_invitations,
|
||||
$reversi_showGame,
|
||||
$reversi_surrender,
|
||||
],
|
||||
})
|
||||
export class EndpointsModule {}
|
||||
|
||||
@@ -65,6 +65,7 @@ export class SignupApiService {
|
||||
'hcaptcha-response'?: string;
|
||||
'g-recaptcha-response'?: string;
|
||||
'turnstile-response'?: string;
|
||||
'm-captcha-response'?: string;
|
||||
}
|
||||
}>,
|
||||
reply: FastifyReply,
|
||||
@@ -82,6 +83,12 @@ export class SignupApiService {
|
||||
});
|
||||
}
|
||||
|
||||
if (instance.enableMcaptcha && instance.mcaptchaSecretKey && instance.mcaptchaSitekey && instance.mcaptchaInstanceUrl) {
|
||||
await this.captchaService.verifyMcaptcha(instance.mcaptchaSecretKey, instance.mcaptchaSitekey, instance.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
|
||||
throw new FastifyReplyError(400, err);
|
||||
});
|
||||
}
|
||||
|
||||
if (instance.enableRecaptcha && instance.recaptchaSecretKey) {
|
||||
await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
|
||||
throw new FastifyReplyError(400, err);
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Schema } from '@/misc/json-schema.js';
|
||||
import { permissions } from 'misskey-js';
|
||||
import type { Schema } from '@/misc/json-schema.js';
|
||||
import { RolePolicies } from '@/core/RoleService.js';
|
||||
|
||||
import * as ep___admin_meta from './endpoints/admin/meta.js';
|
||||
@@ -209,6 +209,7 @@ import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js';
|
||||
import * as ep___i_exportFollowing from './endpoints/i/export-following.js';
|
||||
import * as ep___i_exportMute from './endpoints/i/export-mute.js';
|
||||
import * as ep___i_exportNotes from './endpoints/i/export-notes.js';
|
||||
import * as ep___i_exportClips from './endpoints/i/export-clips.js';
|
||||
import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js';
|
||||
import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js';
|
||||
import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js';
|
||||
@@ -364,6 +365,14 @@ import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
|
||||
import * as ep___fetchRss from './endpoints/fetch-rss.js';
|
||||
import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js';
|
||||
import * as ep___retention from './endpoints/retention.js';
|
||||
import * as ep___bubbleGame_register from './endpoints/bubble-game/register.js';
|
||||
import * as ep___bubbleGame_ranking from './endpoints/bubble-game/ranking.js';
|
||||
import * as ep___reversi_cancelMatch from './endpoints/reversi/cancel-match.js';
|
||||
import * as ep___reversi_games from './endpoints/reversi/games.js';
|
||||
import * as ep___reversi_match from './endpoints/reversi/match.js';
|
||||
import * as ep___reversi_invitations from './endpoints/reversi/invitations.js';
|
||||
import * as ep___reversi_showGame from './endpoints/reversi/show-game.js';
|
||||
import * as ep___reversi_surrender from './endpoints/reversi/surrender.js';
|
||||
|
||||
const eps = [
|
||||
['admin/meta', ep___admin_meta],
|
||||
@@ -568,6 +577,7 @@ const eps = [
|
||||
['i/export-following', ep___i_exportFollowing],
|
||||
['i/export-mute', ep___i_exportMute],
|
||||
['i/export-notes', ep___i_exportNotes],
|
||||
['i/export-clips', ep___i_exportClips],
|
||||
['i/export-favorites', ep___i_exportFavorites],
|
||||
['i/export-user-lists', ep___i_exportUserLists],
|
||||
['i/export-antennas', ep___i_exportAntennas],
|
||||
@@ -723,6 +733,14 @@ const eps = [
|
||||
['fetch-rss', ep___fetchRss],
|
||||
['fetch-external-resources', ep___fetchExternalResources],
|
||||
['retention', ep___retention],
|
||||
['bubble-game/register', ep___bubbleGame_register],
|
||||
['bubble-game/ranking', ep___bubbleGame_ranking],
|
||||
['reversi/cancel-match', ep___reversi_cancelMatch],
|
||||
['reversi/games', ep___reversi_games],
|
||||
['reversi/match', ep___reversi_match],
|
||||
['reversi/invitations', ep___reversi_invitations],
|
||||
['reversi/show-game', ep___reversi_showGame],
|
||||
['reversi/surrender', ep___reversi_surrender],
|
||||
];
|
||||
|
||||
interface IEndpointMetaBase {
|
||||
|
||||
@@ -41,6 +41,18 @@ export const meta = {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
enableMcaptcha: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
mcaptchaSiteKey: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
mcaptchaInstanceUrl: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
enableRecaptcha: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
@@ -163,6 +175,10 @@ export const meta = {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
mcaptchaSecretKey: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
recaptchaSecretKey: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
@@ -468,6 +484,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||
enableHcaptcha: instance.enableHcaptcha,
|
||||
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
||||
enableMcaptcha: instance.enableMcaptcha,
|
||||
mcaptchaSiteKey: instance.mcaptchaSitekey,
|
||||
mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl,
|
||||
enableRecaptcha: instance.enableRecaptcha,
|
||||
recaptchaSiteKey: instance.recaptchaSiteKey,
|
||||
enableTurnstile: instance.enableTurnstile,
|
||||
@@ -498,6 +517,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
sensitiveWords: instance.sensitiveWords,
|
||||
preservedUsernames: instance.preservedUsernames,
|
||||
hcaptchaSecretKey: instance.hcaptchaSecretKey,
|
||||
mcaptchaSecretKey: instance.mcaptchaSecretKey,
|
||||
recaptchaSecretKey: instance.recaptchaSecretKey,
|
||||
turnstileSecretKey: instance.turnstileSecretKey,
|
||||
sensitiveMediaDetection: instance.sensitiveMediaDetection,
|
||||
|
||||
@@ -63,6 +63,10 @@ export const paramDef = {
|
||||
enableHcaptcha: { type: 'boolean' },
|
||||
hcaptchaSiteKey: { type: 'string', nullable: true },
|
||||
hcaptchaSecretKey: { type: 'string', nullable: true },
|
||||
enableMcaptcha: { type: 'boolean' },
|
||||
mcaptchaSiteKey: { type: 'string', nullable: true },
|
||||
mcaptchaInstanceUrl: { type: 'string', nullable: true },
|
||||
mcaptchaSecretKey: { type: 'string', nullable: true },
|
||||
enableRecaptcha: { type: 'boolean' },
|
||||
recaptchaSiteKey: { type: 'string', nullable: true },
|
||||
recaptchaSecretKey: { type: 'string', nullable: true },
|
||||
@@ -269,6 +273,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
set.hcaptchaSecretKey = ps.hcaptchaSecretKey;
|
||||
}
|
||||
|
||||
if (ps.enableMcaptcha !== undefined) {
|
||||
set.enableMcaptcha = ps.enableMcaptcha;
|
||||
}
|
||||
|
||||
if (ps.mcaptchaSiteKey !== undefined) {
|
||||
set.mcaptchaSitekey = ps.mcaptchaSiteKey;
|
||||
}
|
||||
|
||||
if (ps.mcaptchaInstanceUrl !== undefined) {
|
||||
set.mcaptchaInstanceUrl = ps.mcaptchaInstanceUrl;
|
||||
}
|
||||
|
||||
if (ps.mcaptchaSecretKey !== undefined) {
|
||||
set.mcaptchaSecretKey = ps.mcaptchaSecretKey;
|
||||
}
|
||||
|
||||
if (ps.enableRecaptcha !== undefined) {
|
||||
set.enableRecaptcha = ps.enableRecaptcha;
|
||||
}
|
||||
@@ -472,7 +492,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
set.verifymailAuthKey = ps.verifymailAuthKey;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (ps.enableTruemailApi !== undefined) {
|
||||
set.enableTruemailApi = ps.enableTruemailApi;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -92,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
|
||||
antenna.isActive = true;
|
||||
antenna.lastUsedAt = new Date();
|
||||
this.antennasRepository.update(antenna.id, antenna);
|
||||
trackPromise(this.antennasRepository.update(antenna.id, antenna));
|
||||
|
||||
if (needPublishEvent) {
|
||||
this.globalEventService.publishInternalEvent('antennaUpdated', antenna);
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { MoreThan } from 'typeorm';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { BubbleGameRecordsRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
|
||||
export const meta = {
|
||||
allowGet: true,
|
||||
cacheSec: 60,
|
||||
|
||||
errors: {
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
id: { type: 'string', format: 'misskey:id' },
|
||||
score: { type: 'integer' },
|
||||
user: { ref: 'UserLite' },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
gameMode: { type: 'string' },
|
||||
},
|
||||
required: ['gameMode'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.bubbleGameRecordsRepository)
|
||||
private bubbleGameRecordsRepository: BubbleGameRecordsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps) => {
|
||||
const records = await this.bubbleGameRecordsRepository.find({
|
||||
where: {
|
||||
gameMode: ps.gameMode,
|
||||
seededAt: MoreThan(new Date(Date.now() - 1000 * 60 * 60 * 24 * 7)),
|
||||
},
|
||||
order: {
|
||||
score: 'DESC',
|
||||
},
|
||||
take: 10,
|
||||
relations: ['user'],
|
||||
});
|
||||
|
||||
const users = await this.userEntityService.packMany(records.map(r => r.user!), null, { detail: false });
|
||||
|
||||
return records.map(r => ({
|
||||
id: r.id,
|
||||
score: r.score,
|
||||
user: users.find(u => u.id === r.user!.id),
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* 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 { IdService } from '@/core/IdService.js';
|
||||
import type { BubbleGameRecordsRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 120,
|
||||
minInterval: ms('30sec'),
|
||||
},
|
||||
|
||||
errors: {
|
||||
invalidSeed: {
|
||||
message: 'Provided seed is invalid.',
|
||||
code: 'INVALID_SEED',
|
||||
id: 'eb627bc7-574b-4a52-a860-3c3eae772b88',
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
score: { type: 'integer', minimum: 0 },
|
||||
seed: { type: 'string', minLength: 1, maxLength: 1024 },
|
||||
logs: { type: 'array' },
|
||||
gameMode: { type: 'string' },
|
||||
gameVersion: { type: 'integer' },
|
||||
},
|
||||
required: ['score', 'seed', 'logs', 'gameMode', 'gameVersion'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.bubbleGameRecordsRepository)
|
||||
private bubbleGameRecordsRepository: BubbleGameRecordsRepository,
|
||||
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const seedDate = new Date(parseInt(ps.seed, 10));
|
||||
const now = new Date();
|
||||
|
||||
// シードが未来なのは通常のプレイではありえないので弾く
|
||||
if (seedDate.getTime() > now.getTime()) {
|
||||
throw new ApiError(meta.errors.invalidSeed);
|
||||
}
|
||||
|
||||
// シードが古すぎる(5時間以上前)のも弾く
|
||||
if (seedDate.getTime() < now.getTime() - 1000 * 60 * 60 * 5) {
|
||||
throw new ApiError(meta.errors.invalidSeed);
|
||||
}
|
||||
|
||||
await this.bubbleGameRecordsRepository.insert({
|
||||
id: this.idService.gen(now.getTime()),
|
||||
seed: ps.seed,
|
||||
seededAt: seedDate,
|
||||
userId: me.id,
|
||||
score: ps.score,
|
||||
logs: ps.logs,
|
||||
gameMode: ps.gameMode,
|
||||
gameVersion: ps.gameVersion,
|
||||
isVerified: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ export const paramDef = {
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
|
||||
type: { type: 'string', nullable: true, pattern: /^[a-zA-Z\/\-*]+$/.toString().slice(1, -1) },
|
||||
sort: { type: 'string', nullable: true, enum: ['+createdAt', '-createdAt', '+name', '-name', '+size', '-size'] },
|
||||
sort: { type: 'string', nullable: true, enum: ['+createdAt', '-createdAt', '+name', '-name', '+size', '-size', null] },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@@ -74,7 +74,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
}
|
||||
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId);
|
||||
query.andWhere(':file = ANY(note.fileIds)', { file: file.id });
|
||||
query.andWhere(':file <@ note.fileIds', { file: [file.id] });
|
||||
|
||||
const notes = await query.limit(ps.limit).getMany();
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ export const paramDef = {
|
||||
'-firstRetrievedAt',
|
||||
'+latestRequestReceivedAt',
|
||||
'-latestRequestReceivedAt',
|
||||
null,
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UsersRepository } from '@/models/_.js';
|
||||
import { safeForSql } from "@/misc/safe-for-sql.js";
|
||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
@@ -47,8 +48,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private userEntityService: UserEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
if (!safeForSql(normalizeForSearch(ps.tag))) throw new Error('Injection');
|
||||
const query = this.usersRepository.createQueryBuilder('user')
|
||||
.where(':tag = ANY(user.tags)', { tag: normalizeForSearch(ps.tag) })
|
||||
.where(':tag <@ user.tags', { tag: [normalizeForSearch(ps.tag)] })
|
||||
.andWhere('user.isSuspended = FALSE');
|
||||
|
||||
const recent = new Date(Date.now() - (1000 * 60 * 60 * 24 * 5));
|
||||
|
||||
@@ -103,13 +103,13 @@ export const meta = {
|
||||
items: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
"ble",
|
||||
"cable",
|
||||
"hybrid",
|
||||
"internal",
|
||||
"nfc",
|
||||
"smart-card",
|
||||
"usb",
|
||||
'ble',
|
||||
'cable',
|
||||
'hybrid',
|
||||
'internal',
|
||||
'nfc',
|
||||
'smart-card',
|
||||
'usb',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -123,8 +123,8 @@ export const meta = {
|
||||
authenticatorAttachment: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
"cross-platform",
|
||||
"platform",
|
||||
'cross-platform',
|
||||
'platform',
|
||||
],
|
||||
},
|
||||
requireResidentKey: {
|
||||
@@ -133,9 +133,9 @@ export const meta = {
|
||||
userVerification: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
"discouraged",
|
||||
"preferred",
|
||||
"required",
|
||||
'discouraged',
|
||||
'preferred',
|
||||
'required',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -144,10 +144,11 @@ export const meta = {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
enum: [
|
||||
"direct",
|
||||
"enterprise",
|
||||
"indirect",
|
||||
"none",
|
||||
'direct',
|
||||
'enterprise',
|
||||
'indirect',
|
||||
'none',
|
||||
null,
|
||||
],
|
||||
},
|
||||
extensions: {
|
||||
|
||||
35
packages/backend/src/server/api/endpoints/i/export-clips.ts
Normal file
35
packages/backend/src/server/api/endpoints/i/export-clips.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
|
||||
export const meta = {
|
||||
secure: true,
|
||||
requireCredential: true,
|
||||
limit: {
|
||||
duration: ms('1day'),
|
||||
max: 1,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
this.queueService.createExportClipsJob(me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -108,6 +108,18 @@ export const meta = {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
enableMcaptcha: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
mcaptchaSiteKey: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
mcaptchaInstanceUrl: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
enableRecaptcha: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
@@ -351,6 +363,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||
enableHcaptcha: instance.enableHcaptcha,
|
||||
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
||||
enableMcaptcha: instance.enableMcaptcha,
|
||||
mcaptchaSiteKey: instance.mcaptchaSitekey,
|
||||
mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl,
|
||||
enableRecaptcha: instance.enableRecaptcha,
|
||||
recaptchaSiteKey: instance.recaptchaSiteKey,
|
||||
enableTurnstile: instance.enableTurnstile,
|
||||
|
||||
@@ -34,11 +34,10 @@ describe('api:notes/create', () => {
|
||||
.toBe(VALID);
|
||||
});
|
||||
|
||||
// TODO
|
||||
//test('null post', () => {
|
||||
// expect(v({ text: null }))
|
||||
// .toBe(INVALID);
|
||||
//});
|
||||
test('null post', () => {
|
||||
expect(v({ text: null }))
|
||||
.toBe(INVALID);
|
||||
});
|
||||
|
||||
test('0 characters post', () => {
|
||||
expect(v({ text: '' }))
|
||||
@@ -49,6 +48,11 @@ describe('api:notes/create', () => {
|
||||
expect(v({ text: await tooLong }))
|
||||
.toBe(INVALID);
|
||||
});
|
||||
|
||||
test('whitespace-only post', () => {
|
||||
expect(v({ text: ' ' }))
|
||||
.toBe(INVALID);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cw', () => {
|
||||
|
||||
@@ -172,13 +172,33 @@ export const paramDef = {
|
||||
},
|
||||
},
|
||||
// (re)note with text, files and poll are optional
|
||||
anyOf: [
|
||||
{ required: ['text'] },
|
||||
{ required: ['renoteId'] },
|
||||
{ required: ['fileIds'] },
|
||||
{ required: ['mediaIds'] },
|
||||
{ required: ['poll'] },
|
||||
],
|
||||
if: {
|
||||
properties: {
|
||||
renoteId: {
|
||||
type: 'null',
|
||||
},
|
||||
fileIds: {
|
||||
type: 'null',
|
||||
},
|
||||
mediaIds: {
|
||||
type: 'null',
|
||||
},
|
||||
poll: {
|
||||
type: 'null',
|
||||
},
|
||||
},
|
||||
},
|
||||
then: {
|
||||
properties: {
|
||||
text: {
|
||||
type: 'string',
|
||||
minLength: 1,
|
||||
maxLength: MAX_NOTE_TEXT_LENGTH,
|
||||
pattern: '[^\\s]+',
|
||||
},
|
||||
},
|
||||
required: ['text'],
|
||||
},
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -61,9 +61,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where(`'{"${me.id}"}' <@ note.mentions`)
|
||||
.orWhere(`'{"${me.id}"}' <@ note.visibleUserIds`);
|
||||
qb // このmeIdAsListパラメータはqueryServiceのgenerateVisibilityQueryでセットされる
|
||||
.where(':meIdAsList <@ note.mentions')
|
||||
.orWhere(':meIdAsList <@ note.visibleUserIds');
|
||||
}))
|
||||
// Avoid scanning primary key index
|
||||
.orderBy('CONCAT(note.id)', 'DESC')
|
||||
|
||||
@@ -87,14 +87,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
try {
|
||||
if (ps.tag) {
|
||||
if (!safeForSql(normalizeForSearch(ps.tag))) throw new Error('Injection');
|
||||
query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`);
|
||||
query.andWhere(':tag <@ note.tags', { tag: [normalizeForSearch(ps.tag)] });
|
||||
} else {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
for (const tags of ps.query!) {
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
for (const tag of tags) {
|
||||
if (!safeForSql(normalizeForSearch(tag))) throw new Error('Injection');
|
||||
qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`);
|
||||
qb.andWhere(':tag <@ note.tags', { tag: [normalizeForSearch(tag)] });
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
}
|
||||
|
||||
// Get mutee
|
||||
const mutee = await getterService.getUser(ps.userId).catch(err => {
|
||||
const mutee = await this.getterService.getUser(ps.userId).catch(err => {
|
||||
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||
throw err;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ReversiService } from '@/core/ReversiService.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
errors: {
|
||||
},
|
||||
|
||||
res: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private reversiService: ReversiService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
if (ps.userId) {
|
||||
await this.reversiService.matchSpecificUserCancel(me, ps.userId);
|
||||
return;
|
||||
} else {
|
||||
await this.reversiService.matchAnyUserCancel(me);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
61
packages/backend/src/server/api/endpoints/reversi/games.ts
Normal file
61
packages/backend/src/server/api/endpoints/reversi/games.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Brackets } from 'typeorm';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { ReversiGamesRepository } from '@/models/_.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: false,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: { ref: 'ReversiGameLite' },
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
my: { type: 'boolean', default: false },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.reversiGamesRepository)
|
||||
private reversiGamesRepository: ReversiGamesRepository,
|
||||
|
||||
private reversiGameEntityService: ReversiGameEntityService,
|
||||
private queryService: QueryService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.reversiGamesRepository.createQueryBuilder('game'), ps.sinceId, ps.untilId)
|
||||
.andWhere('game.isStarted = TRUE');
|
||||
|
||||
if (ps.my && me) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('game.user1Id = :userId', { userId: me.id })
|
||||
.orWhere('game.user2Id = :userId', { userId: me.id });
|
||||
}));
|
||||
}
|
||||
|
||||
const games = await query.take(ps.limit).getMany();
|
||||
|
||||
return await this.reversiGameEntityService.packLiteMany(games, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* 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 { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ReversiService } from '@/core/ReversiService.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:account',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: { ref: 'UserLite' },
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private userEntityService: UserEntityService,
|
||||
private reversiService: ReversiService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const invitations = await this.reversiService.getInvitations(me);
|
||||
|
||||
return await this.userEntityService.packMany(invitations, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
66
packages/backend/src/server/api/endpoints/reversi/match.ts
Normal file
66
packages/backend/src/server/api/endpoints/reversi/match.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ReversiService } from '@/core/ReversiService.js';
|
||||
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { GetterService } from '../../GetterService.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
errors: {
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: '0b4f0559-b484-4e31-9581-3f73cee89b28',
|
||||
},
|
||||
|
||||
isYourself: {
|
||||
message: 'Target user is yourself.',
|
||||
code: 'TARGET_IS_YOURSELF',
|
||||
id: '96fd7bd6-d2bc-426c-a865-d055dcd2828e',
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private getterService: GetterService,
|
||||
private reversiService: ReversiService,
|
||||
private reversiGameEntityService: ReversiGameEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
if (ps.userId === me.id) throw new ApiError(meta.errors.isYourself);
|
||||
|
||||
const target = ps.userId ? await this.getterService.getUser(ps.userId).catch(err => {
|
||||
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||
throw err;
|
||||
}) : null;
|
||||
|
||||
const game = target ? await this.reversiService.matchSpecificUser(me, target) : await this.reversiService.matchAnyUser(me);
|
||||
|
||||
if (game == null) return;
|
||||
|
||||
return await this.reversiGameEntityService.packDetail(game, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ReversiService } from '@/core/ReversiService.js';
|
||||
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: false,
|
||||
|
||||
errors: {
|
||||
noSuchGame: {
|
||||
message: 'No such game.',
|
||||
code: 'NO_SUCH_GAME',
|
||||
id: 'f13a03db-fae1-46c9-87f3-43c8165419e1',
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'ReversiGameDetailed',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
gameId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['gameId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private reversiService: ReversiService,
|
||||
private reversiGameEntityService: ReversiGameEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const game = await this.reversiService.get(ps.gameId);
|
||||
|
||||
if (game == null) {
|
||||
throw new ApiError(meta.errors.noSuchGame);
|
||||
}
|
||||
|
||||
return await this.reversiGameEntityService.packDetail(game, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ReversiService } from '@/core/ReversiService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
errors: {
|
||||
noSuchGame: {
|
||||
message: 'No such game.',
|
||||
code: 'NO_SUCH_GAME',
|
||||
id: 'ace0b11f-e0a6-4076-a30d-e8284c81b2df',
|
||||
},
|
||||
|
||||
alreadyEnded: {
|
||||
message: 'That game has already ended.',
|
||||
code: 'ALREADY_ENDED',
|
||||
id: '6c2ad4a6-cbf1-4a5b-b187-b772826cfc6d',
|
||||
},
|
||||
|
||||
accessDenied: {
|
||||
message: 'Access denied.',
|
||||
code: 'ACCESS_DENIED',
|
||||
id: '6e04164b-a992-4c93-8489-2123069973e1',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
gameId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['gameId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private reversiService: ReversiService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const game = await this.reversiService.get(ps.gameId);
|
||||
|
||||
if (game == null) {
|
||||
throw new ApiError(meta.errors.noSuchGame);
|
||||
}
|
||||
|
||||
if (game.isEnded) {
|
||||
throw new ApiError(meta.errors.alreadyEnded);
|
||||
}
|
||||
|
||||
if ((game.user1Id !== me.id) && (game.user2Id !== me.id)) {
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
}
|
||||
|
||||
await this.reversiService.surrender(game, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { schemas, convertSchemaToOpenApiSchema } from './schemas.js';
|
||||
|
||||
export function genOpenapiSpec(config: Config) {
|
||||
const spec = {
|
||||
openapi: '3.0.0',
|
||||
openapi: '3.1.0',
|
||||
|
||||
info: {
|
||||
version: config.version,
|
||||
@@ -56,7 +56,7 @@ export function genOpenapiSpec(config: Config) {
|
||||
}
|
||||
}
|
||||
|
||||
const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res) : {};
|
||||
const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res, 'res') : {};
|
||||
|
||||
let desc = (endpoint.meta.description ? endpoint.meta.description : 'No description provided.') + '\n\n';
|
||||
|
||||
@@ -71,7 +71,7 @@ export function genOpenapiSpec(config: Config) {
|
||||
}
|
||||
|
||||
const requestType = endpoint.meta.requireFile ? 'multipart/form-data' : 'application/json';
|
||||
const schema = { ...endpoint.params };
|
||||
const schema = { ...convertSchemaToOpenApiSchema(endpoint.params, 'param') };
|
||||
|
||||
if (endpoint.meta.requireFile) {
|
||||
schema.properties = {
|
||||
@@ -210,7 +210,9 @@ export function genOpenapiSpec(config: Config) {
|
||||
};
|
||||
|
||||
spec.paths['/' + endpoint.name] = {
|
||||
...(endpoint.meta.allowGet ? { get: info } : {}),
|
||||
...(endpoint.meta.allowGet ? {
|
||||
get: info,
|
||||
} : {}),
|
||||
post: info,
|
||||
};
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user