Merge branch 'develop' into ed25519
This commit is contained in:
commit
d200da8690
278 changed files with 8215 additions and 3937 deletions
141
.github/workflows/check-misskey-js-autogen.yml
vendored
141
.github/workflows/check-misskey-js-autogen.yml
vendored
|
@ -5,24 +5,23 @@ on:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
- develop
|
- develop
|
||||||
|
- improve-misskey-js-autogen-check
|
||||||
paths:
|
paths:
|
||||||
- packages/backend/**
|
- packages/backend/**
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-misskey-js-autogen:
|
# pull_request_target safety: permissions: read-all, and there are no secrets used in this job
|
||||||
|
generate-misskey-js:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: write
|
contents: read
|
||||||
|
if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }}
|
||||||
env:
|
|
||||||
api_json_name: "api-head.json"
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.1
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||||
|
|
||||||
- name: setup pnpm
|
- name: setup pnpm
|
||||||
uses: pnpm/action-setup@v3
|
uses: pnpm/action-setup@v3
|
||||||
|
@ -39,79 +38,81 @@ jobs:
|
||||||
- name: install dependencies
|
- name: install dependencies
|
||||||
run: pnpm i --frozen-lockfile
|
run: pnpm i --frozen-lockfile
|
||||||
|
|
||||||
- name: wait get-api-diff
|
# generate api.json
|
||||||
uses: lewagon/wait-on-check-action@v1.3.3
|
- name: Copy Config
|
||||||
|
run: cp .config/example.yml .config/default.yml
|
||||||
|
- name: Build
|
||||||
|
run: pnpm build
|
||||||
|
- name: Generate API JSON
|
||||||
|
run: pnpm --filter backend generate-api-json
|
||||||
|
|
||||||
|
# build misskey js
|
||||||
|
- name: Build misskey-js
|
||||||
|
run: |-
|
||||||
|
cp packages/backend/built/api.json packages/misskey-js/generator/api.json
|
||||||
|
pnpm run --filter misskey-js-type-generator generate
|
||||||
|
|
||||||
|
# packages/misskey-js/generator/built/autogen
|
||||||
|
- name: Upload Generated
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
name: generated-misskey-js
|
||||||
check-regexp: get-from-misskey .+
|
path: packages/misskey-js/generator/built/autogen
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
wait-interval: 30
|
|
||||||
|
|
||||||
- name: Download artifact
|
# pull_request_target safety: permissions: read-all, and there are no secrets used in this job
|
||||||
uses: actions/github-script@v7.0.1
|
get-actual-misskey-js:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }}
|
||||||
|
steps:
|
||||||
|
- name: checkout
|
||||||
|
uses: actions/checkout@v4.1.1
|
||||||
with:
|
with:
|
||||||
script: |
|
submodules: true
|
||||||
const fs = require('fs');
|
ref: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||||
|
|
||||||
const workflows = await github.rest.actions.listWorkflowRunsForRepo({
|
- name: Upload From Merged
|
||||||
owner: context.repo.owner,
|
uses: actions/upload-artifact@v4
|
||||||
repo: context.repo.repo,
|
with:
|
||||||
head_sha: `${{ github.event.pull_request.head.sha }}`
|
name: actual-misskey-js
|
||||||
}).then(x => x.data.workflow_runs);
|
path: packages/misskey-js/src/autogen
|
||||||
|
|
||||||
console.log(workflows.map(x => ({name: x.name, title: x.display_title})));
|
# pull_request_target safety: nothing is cloned from repository
|
||||||
|
comment-misskey-js-autogen:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [generate-misskey-js, get-actual-misskey-js]
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: download generated-misskey-js
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: generated-misskey-js
|
||||||
|
path: misskey-js-generated
|
||||||
|
|
||||||
const run_id = workflows.find(x => x.name.includes("Get api.json from Misskey")).id;
|
- name: download actual-misskey-js
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: actual-misskey-js
|
||||||
|
path: misskey-js-actual
|
||||||
|
|
||||||
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
- name: check misskey-js changes
|
||||||
owner: context.repo.owner,
|
id: check-changes
|
||||||
repo: context.repo.repo,
|
run: |
|
||||||
run_id: run_id,
|
diff -r -u --label=generated --label=on-tree ./misskey-js-generated ./misskey-js-actual > misskey-js.diff || true
|
||||||
});
|
|
||||||
|
|
||||||
let matchArtifacts = allArtifacts.data.artifacts.filter((artifact) => {
|
if [ -s misskey-js.diff ]; then
|
||||||
return artifact.name.startsWith("api-artifact-") || artifact.name == "api-artifact"
|
echo "changes=true" >> $GITHUB_OUTPUT
|
||||||
});
|
else
|
||||||
|
echo "changes=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
await Promise.all(matchArtifacts.map(async (artifact) => {
|
- name: Print full diff
|
||||||
let download = await github.rest.actions.downloadArtifact({
|
run: cat ./misskey-js.diff
|
||||||
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: get head checksum
|
|
||||||
run: |-
|
|
||||||
checksum=$(realpath head_checksum)
|
|
||||||
|
|
||||||
cd packages/misskey-js/src
|
|
||||||
find autogen -type f -exec sh -c 'echo $(sed -E "s/^\s+\*\s+generatedAt:.+$//" {} | sha256sum | cut -d" " -f 1) {}' \; > $checksum
|
|
||||||
cd ../../..
|
|
||||||
|
|
||||||
- name: build autogen
|
|
||||||
run: |-
|
|
||||||
checksum=$(realpath ${api_json_name}_checksum)
|
|
||||||
mv $api_json_name packages/misskey-js/generator/api.json
|
|
||||||
|
|
||||||
cd packages/misskey-js/generator
|
|
||||||
pnpm run generate
|
|
||||||
cd built
|
|
||||||
find autogen -type f -exec sh -c 'echo $(sed -E "s/^\s+\*\s+generatedAt:.+$//" {} | sha256sum | cut -d" " -f 1) {}' \; > $checksum
|
|
||||||
cd ../../../..
|
|
||||||
|
|
||||||
- name: check update for type definitions
|
|
||||||
run: diff head_checksum ${api_json_name}_checksum
|
|
||||||
|
|
||||||
- name: send message
|
- name: send message
|
||||||
if: failure()
|
if: steps.check-changes.outputs.changes == 'true'
|
||||||
uses: thollander/actions-comment-pull-request@v2
|
uses: thollander/actions-comment-pull-request@v2
|
||||||
with:
|
with:
|
||||||
comment_tag: check-misskey-js-autogen
|
comment_tag: check-misskey-js-autogen
|
||||||
|
@ -125,7 +126,7 @@ jobs:
|
||||||
```
|
```
|
||||||
|
|
||||||
- name: send message
|
- name: send message
|
||||||
if: success()
|
if: steps.check-changes.outputs.changes == 'false'
|
||||||
uses: thollander/actions-comment-pull-request@v2
|
uses: thollander/actions-comment-pull-request@v2
|
||||||
with:
|
with:
|
||||||
comment_tag: check-misskey-js-autogen
|
comment_tag: check-misskey-js-autogen
|
||||||
|
|
75
.github/workflows/check-spdx-license-id.yml
vendored
Normal file
75
.github/workflows/check-spdx-license-id.yml
vendored
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
name: Check SPDX-License-Identifier
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- develop
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-spdx-license-id:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4.1.1
|
||||||
|
- name: Check
|
||||||
|
run: |
|
||||||
|
counter=0
|
||||||
|
|
||||||
|
search() {
|
||||||
|
local directory="$1"
|
||||||
|
find "$directory" -type f \
|
||||||
|
'(' \
|
||||||
|
-name "*.cjs" -and -not -name '*.config.cjs' -o \
|
||||||
|
-name "*.html" -o \
|
||||||
|
-name "*.js" -and -not -name '*.config.js' -o \
|
||||||
|
-name "*.mjs" -and -not -name '*.config.mjs' -o \
|
||||||
|
-name "*.scss" -o \
|
||||||
|
-name "*.ts" -and -not -name '*.config.ts' -o \
|
||||||
|
-name "*.vue" \
|
||||||
|
')' -and \
|
||||||
|
-not -name '*eslint*'
|
||||||
|
}
|
||||||
|
|
||||||
|
check() {
|
||||||
|
local file="$1"
|
||||||
|
if ! (
|
||||||
|
grep -q "SPDX-FileCopyrightText: syuilo and misskey-project" "$file" ||
|
||||||
|
grep -q "SPDX-License-Identifier: AGPL-3.0-only" "$file"
|
||||||
|
); then
|
||||||
|
echo "Missing: $file"
|
||||||
|
((counter++))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
directories=(
|
||||||
|
"cypress/e2e"
|
||||||
|
"packages/backend/migration"
|
||||||
|
"packages/backend/src"
|
||||||
|
"packages/backend/test"
|
||||||
|
"packages/frontend/.storybook"
|
||||||
|
"packages/frontend/@types"
|
||||||
|
"packages/frontend/lib"
|
||||||
|
"packages/frontend/public"
|
||||||
|
"packages/frontend/src"
|
||||||
|
"packages/frontend/test"
|
||||||
|
"packages/misskey-bubble-game/src"
|
||||||
|
"packages/misskey-reversi/src"
|
||||||
|
"packages/sw/src"
|
||||||
|
"scripts"
|
||||||
|
)
|
||||||
|
|
||||||
|
for directory in "${directories[@]}"; do
|
||||||
|
for file in $(search $directory); do
|
||||||
|
check "$file"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $counter -gt 0 ]; then
|
||||||
|
echo "SPDX-License-Identifier is missing in $counter files."
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "SPDX-License-Identifier is certainly described in all target files!"
|
||||||
|
exit 0
|
||||||
|
fi
|
|
@ -50,12 +50,9 @@ jobs:
|
||||||
|
|
||||||
- name: Get PR ref
|
- name: Get PR ref
|
||||||
id: get-ref
|
id: get-ref
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
run: |
|
||||||
PR_NUMBER=$(jq --raw-output .issue.number $GITHUB_EVENT_PATH)
|
PR_REF="refs/pull/${{ github.event.issue.number }}/head"
|
||||||
PR_REF=$(gh pr view $PR_NUMBER --json headRefName -q '.headRefName')
|
echo "pr-ref=$PR_REF" >> $GITHUB_OUTPUT
|
||||||
echo "pr-ref=$PR_REF" > $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Extract wait time
|
- name: Extract wait time
|
||||||
id: get-wait-time
|
id: get-wait-time
|
||||||
|
|
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
|
@ -92,6 +92,6 @@ jobs:
|
||||||
- run: pnpm i --frozen-lockfile
|
- run: pnpm i --frozen-lockfile
|
||||||
- run: pnpm --filter misskey-js run build
|
- run: pnpm --filter misskey-js run build
|
||||||
if: ${{ matrix.workspace == 'backend' }}
|
if: ${{ matrix.workspace == 'backend' }}
|
||||||
- run: pnpm --filter misskey-reversi run build:tsc
|
- run: pnpm --filter misskey-reversi run build
|
||||||
if: ${{ matrix.workspace == 'backend' }}
|
if: ${{ matrix.workspace == 'backend' }}
|
||||||
- run: pnpm --filter ${{ matrix.workspace }} run typecheck
|
- run: pnpm --filter ${{ matrix.workspace }} run typecheck
|
||||||
|
|
7
.github/workflows/storybook.yml
vendored
7
.github/workflows/storybook.yml
vendored
|
@ -87,12 +87,13 @@ jobs:
|
||||||
if [ "$CHROMATIC_PARAMETER" = " --skip" ]; then
|
if [ "$CHROMATIC_PARAMETER" = " --skip" ]; then
|
||||||
echo "skip=true" >> $GITHUB_OUTPUT
|
echo "skip=true" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
BRANCH="${{ github.event.pull_request.head.user.login }}:${{ github.event.pull_request.head.ref }}"
|
BRANCH="${{ github.event.pull_request.head.user.login }}:$HEAD_REF"
|
||||||
if [ "$BRANCH" = "misskey-dev:${{ github.event.pull_request.head.ref }}" ]; then
|
if [ "$BRANCH" = "misskey-dev:$HEAD_REF" ]; then
|
||||||
BRANCH="${{ github.event.pull_request.head.ref }}"
|
BRANCH="$HEAD_REF"
|
||||||
fi
|
fi
|
||||||
pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static --branch-name $BRANCH $(echo "$CHROMATIC_PARAMETER")
|
pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static --branch-name $BRANCH $(echo "$CHROMATIC_PARAMETER")
|
||||||
env:
|
env:
|
||||||
|
HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
||||||
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||||
- name: Notify that Chromatic detects changes
|
- name: Notify that Chromatic detects changes
|
||||||
uses: actions/github-script@v7.0.1
|
uses: actions/github-script@v7.0.1
|
||||||
|
|
2
.github/workflows/test-backend.yml
vendored
2
.github/workflows/test-backend.yml
vendored
|
@ -45,6 +45,8 @@ jobs:
|
||||||
with:
|
with:
|
||||||
version: 8
|
version: 8
|
||||||
run_install: false
|
run_install: false
|
||||||
|
- name: Install FFmpeg
|
||||||
|
uses: FedericoCarboni/setup-ffmpeg@v3
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.2
|
||||||
with:
|
with:
|
||||||
|
|
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
|
@ -4,7 +4,6 @@
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"Vue.volar",
|
"Vue.volar",
|
||||||
"Orta.vscode-jest",
|
"Orta.vscode-jest",
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"mrmlnc.vscode-json5"
|
"mrmlnc.vscode-json5"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -7,7 +7,7 @@
|
||||||
"*.test.ts": "typescript"
|
"*.test.ts": "typescript"
|
||||||
},
|
},
|
||||||
"jest.jestCommandLine": "pnpm run jest",
|
"jest.jestCommandLine": "pnpm run jest",
|
||||||
"jest.autoRun": "off",
|
"jest.runMode": "on-demand",
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll": "explicit"
|
"source.fixAll": "explicit"
|
||||||
},
|
},
|
||||||
|
|
70
CHANGELOG.md
70
CHANGELOG.md
|
@ -1,18 +1,84 @@
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Note
|
||||||
|
- コントロールパネル内にあるサマリープロキシの設定個所がセキュリティから全般へ変更となります。
|
||||||
|
- 悪意のある第三者がリモートユーザーになりすましたアクティビティを受け取れてしまう問題を修正しました。詳しくは[GitHub security advisory](https://github.com/misskey-dev/misskey/security/advisories/GHSA-2vxv-pv3m-3wvj)をご覧ください。
|
||||||
|
|
||||||
### General
|
### General
|
||||||
-
|
- Enhance: URLプレビューの有効化・無効化を設定できるように #13569
|
||||||
|
- Enhance: アンテナでBotによるノートを除外できるように
|
||||||
|
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/545)
|
||||||
|
- Enhance: クリップのノート数を表示するように
|
||||||
|
- Enhance: コンディショナルロールの条件として以下を新たに追加 (#13667)
|
||||||
|
- 猫ユーザーか
|
||||||
|
- botユーザーか
|
||||||
|
- サスペンド済みユーザーか
|
||||||
|
- 鍵アカウントユーザーか
|
||||||
|
- 「アカウントを見つけやすくする」が有効なユーザーか
|
||||||
|
- Fix: Play作成時に設定した公開範囲が機能していない問題を修正
|
||||||
|
- Fix: 正規化されていない状態のhashtagが連合されてきたhtmlに含まれているとhashtagが正しくhashtagに復元されない問題を修正
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
|
- Feat: アップロードするファイルの名前をランダム文字列にできるように
|
||||||
- Enhance: 自分のノートの添付ファイルから直接ファイルの詳細ページに飛べるように
|
- Enhance: 自分のノートの添付ファイルから直接ファイルの詳細ページに飛べるように
|
||||||
- Enhance: 広告がMisskeyと同一ドメインの場合はRouterで遷移するように
|
- Enhance: 広告がMisskeyと同一ドメインの場合はRouterで遷移するように
|
||||||
- Enhance: リアクション・いいねの総数を表示するように
|
- Enhance: リアクション・いいねの総数を表示するように
|
||||||
- Enhance: リアクション受け入れが「いいねのみ」の場合はリアクション絵文字一覧を表示しないように
|
- Enhance: リアクション受け入れが「いいねのみ」の場合はリアクション絵文字一覧を表示しないように
|
||||||
|
- Enhance: 設定>プラグインのページからプラグインの簡易的なログやエラーを見られるように
|
||||||
|
- 実装の都合により、プラグインは1つエラーを起こした時に即時停止するようになりました
|
||||||
|
- Enhance: ページのデザインを変更
|
||||||
|
- Enhance: 2要素認証(ワンタイムパスワード)の入力欄を改善
|
||||||
|
- Enhance: 「今日誕生日のフォロー中ユーザー」ウィジェットを手動でリロードできるように
|
||||||
|
- Enhance: 映像・音声の再生にブラウザのネイティブプレイヤーを使用できるように
|
||||||
|
- Enhance: 映像・音声の再生メニューに「再生速度」「ループ再生」「ピクチャインピクチャ」を追加
|
||||||
|
- Enhance: 映像・音声の再生にキーボードショートカットが使えるように
|
||||||
|
- Enhance: ノートについているリアクションの「もっと!」から、リアクションの一覧を表示できるように
|
||||||
|
- Enhance: リプライにて引用がある場合テキストが空でもノートできるように
|
||||||
|
- 引用したいノートのURLをコピーしリプライ投稿画面にペーストして添付することで達成できます
|
||||||
|
- Enhance: フォローするかどうかの確認ダイアログを出せるように
|
||||||
|
- Enhance: Playを手動でリロードできるように
|
||||||
|
- Enhance: 通報のコメント内のリンクをクリックした際、ウィンドウで開くように
|
||||||
|
- Chore: AiScriptを0.18.0にバージョンアップ
|
||||||
- Fix: 一部のページ内リンクが正しく動作しない問題を修正
|
- Fix: 一部のページ内リンクが正しく動作しない問題を修正
|
||||||
- Fix: 周年の実績が閏年を考慮しない問題を修正
|
- Fix: 周年の実績が閏年を考慮しない問題を修正
|
||||||
|
- Fix: ローカルURLのプレビューポップアップが左上に表示される
|
||||||
|
- Fix: WebGL2をサポートしないブラウザで「季節に応じた画面の演出」が有効になっているとき、Misskeyが起動できなくなる問題を修正
|
||||||
|
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/459)
|
||||||
|
- Fix: ページタイトルでローカルユーザーとリモートユーザーの区別がつかない問題を修正
|
||||||
|
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/528)
|
||||||
|
- Fix: コードブロックのシンタックスハイライトで使用される定義ファイルをCDNから取得するように #13177
|
||||||
|
- CDNから取得せずMisskey本体にバンドルする場合は`pacakges/frontend/vite.config.ts`を修正してください。
|
||||||
|
- Fix: タイムゾーンによっては、「今日誕生日のフォロー中ユーザー」ウィジェットが正しく動作しない問題を修正
|
||||||
|
- Fix: CWのみの引用リノートが詳細ページで純粋なリノートとして誤って扱われてしまう問題を修正
|
||||||
|
- Fix: ノート詳細ページにおいてCW付き引用リノートのCWボタンのラベルに「引用」が含まれていない問題を修正
|
||||||
|
- Fix: ダイアログの入力で字数制限に違反していてもEnterキーが押せてしまう問題を修正
|
||||||
|
- Fix: ダイレクト投稿の宛先が保存されない問題を修正
|
||||||
|
- Fix: Playのページを離れたときに、Playが正常に初期化されない問題を修正
|
||||||
|
- Fix: ページのOGP URLが間違っているのを修正
|
||||||
|
- Fix: リバーシの対局を正しく共有できないことがある問題を修正
|
||||||
|
- Fix: 通知をグループ化している際に、人数が正常に表示されないことがある問題を修正
|
||||||
|
- Fix: 連合なしの状態の読み書きができない問題を修正
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
-
|
- Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに
|
||||||
|
- Enhance: misskey-dev/summaly@5.1.0の取り込み(プレビュー生成処理の効率化)
|
||||||
|
- Fix: リモートから配送されたアクティビティにJSON-LD compactionをかける
|
||||||
|
- Fix: フォローリクエストを作成する際に既存のものは削除するように
|
||||||
|
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/440)
|
||||||
|
- Fix: エンドポイント`notes/translate`のエラーを改善
|
||||||
|
- Fix: CleanRemoteFilesProcessorService report progress from 100% (#13632)
|
||||||
|
- Fix: 一部の音声ファイルが映像ファイルとして扱われる問題を修正
|
||||||
|
- Fix: リプライのみの引用リノートと、CWのみの引用リノートが純粋なリノートとして誤って扱われてしまう問題を修正
|
||||||
|
- Fix: 登録にメール認証が必須になっている場合、登録されているメールアドレスを削除できないように
|
||||||
|
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/606)
|
||||||
|
- Fix: Add Cache-Control to Bull Board
|
||||||
|
- Fix: nginx経由で/files/にRangeリクエストされた場合に正しく応答できないのを修正
|
||||||
|
- Fix: 一部のタイムラインのストリーミングでインスタンスミュートが効かない問題を修正
|
||||||
|
- Fix: グローバルタイムラインで返信が表示されないことがある問題を修正
|
||||||
|
- Fix: リノートをミュートしたユーザの投稿のリノートがミュートされる問題を修正
|
||||||
|
- Fix: AP Link等は添付ファイル扱いしないようになど (#13754)
|
||||||
|
- Enhance: ドライブのファイルがNSFWかどうか個別に連合されるように (#13756)
|
||||||
|
- 可能な場合、ノートの添付ファイルのセンシティブ判定がファイル単位になります
|
||||||
|
|
||||||
## 2024.3.1
|
## 2024.3.1
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
describe('Before setup instance', () => {
|
describe('Before setup instance', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.resetState();
|
cy.resetState();
|
|
@ -1,3 +1,8 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
describe('Router transition', () => {
|
describe('Router transition', () => {
|
||||||
describe('Redirect', () => {
|
describe('Redirect', () => {
|
||||||
// サーバの初期化。ルートのテストに関しては各describeごとに1度だけ実行で十分だと思う(使いまわした方が早い)
|
// サーバの初期化。ルートのテストに関しては各describeごとに1度だけ実行で十分だと思う(使いまわした方が早い)
|
|
@ -1,3 +1,8 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
/* flaky
|
/* flaky
|
||||||
describe('After user signed in', () => {
|
describe('After user signed in', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
|
@ -30,9 +30,13 @@ Cypress.Commands.add('visitHome', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add('resetState', () => {
|
Cypress.Commands.add('resetState', () => {
|
||||||
cy.window(win => {
|
// iframe.contentWindow.indexedDB.deleteDatabase() がchromeのバグで使用できないため、indexedDBを無効化している。
|
||||||
|
// see https://github.com/misskey-dev/misskey/issues/13605#issuecomment-2053652123
|
||||||
|
/*
|
||||||
|
cy.window().then(win => {
|
||||||
win.indexedDB.deleteDatabase('keyval-store');
|
win.indexedDB.deleteDatabase('keyval-store');
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
cy.request('POST', '/api/reset-db', {}).as('reset');
|
cy.request('POST', '/api/reset-db', {}).as('reset');
|
||||||
cy.get('@reset').its('status').should('equal', 204);
|
cy.get('@reset').its('status').should('equal', 204);
|
||||||
cy.reload(true);
|
cy.reload(true);
|
19
cypress/support/index.ts
Normal file
19
cypress/support/index.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
declare global {
|
||||||
|
namespace Cypress {
|
||||||
|
interface Chainable {
|
||||||
|
login(username: string, password: string): Chainable<void>;
|
||||||
|
|
||||||
|
registerUser(
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
isAdmin?: boolean
|
||||||
|
): Chainable<void>;
|
||||||
|
|
||||||
|
resetState(): Chainable<void>;
|
||||||
|
|
||||||
|
visitHome(): Chainable<void>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {}
|
8
cypress/tsconfig.json
Normal file
8
cypress/tsconfig.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["dom", "es5"],
|
||||||
|
"target": "es5",
|
||||||
|
"types": ["cypress", "node"]
|
||||||
|
},
|
||||||
|
"include": ["./**/*.ts"]
|
||||||
|
}
|
158
locales/index.d.ts
vendored
158
locales/index.d.ts
vendored
|
@ -1616,6 +1616,10 @@ export interface Locale extends ILocale {
|
||||||
* 除外キーワード
|
* 除外キーワード
|
||||||
*/
|
*/
|
||||||
"antennaExcludeKeywords": string;
|
"antennaExcludeKeywords": string;
|
||||||
|
/**
|
||||||
|
* Botアカウントを除外
|
||||||
|
*/
|
||||||
|
"antennaExcludeBots": string;
|
||||||
/**
|
/**
|
||||||
* スペースで区切るとAND指定になり、改行で区切るとOR指定になります
|
* スペースで区切るとAND指定になり、改行で区切るとOR指定になります
|
||||||
*/
|
*/
|
||||||
|
@ -4912,6 +4916,46 @@ export interface Locale extends ILocale {
|
||||||
* リトライ
|
* リトライ
|
||||||
*/
|
*/
|
||||||
"gameRetry": string;
|
"gameRetry": string;
|
||||||
|
/**
|
||||||
|
* 使用しない場合は空欄にしてください
|
||||||
|
*/
|
||||||
|
"notUsePleaseLeaveBlank": string;
|
||||||
|
/**
|
||||||
|
* ワンタイムパスワードを使う
|
||||||
|
*/
|
||||||
|
"useTotp": string;
|
||||||
|
/**
|
||||||
|
* バックアップコードを使う
|
||||||
|
*/
|
||||||
|
"useBackupCode": string;
|
||||||
|
/**
|
||||||
|
* アプリを起動
|
||||||
|
*/
|
||||||
|
"launchApp": string;
|
||||||
|
/**
|
||||||
|
* 動画・音声の再生にブラウザのUIを使用する
|
||||||
|
*/
|
||||||
|
"useNativeUIForVideoAudioPlayer": string;
|
||||||
|
/**
|
||||||
|
* オリジナルのファイル名を保持
|
||||||
|
*/
|
||||||
|
"keepOriginalFilename": string;
|
||||||
|
/**
|
||||||
|
* この設定をオフにすると、アップロード時にファイル名が自動でランダム文字列に置き換えられます。
|
||||||
|
*/
|
||||||
|
"keepOriginalFilenameDescription": string;
|
||||||
|
/**
|
||||||
|
* 説明文はありません
|
||||||
|
*/
|
||||||
|
"noDescription": string;
|
||||||
|
/**
|
||||||
|
* フォローの際常に確認する
|
||||||
|
*/
|
||||||
|
"alwaysConfirmFollow": string;
|
||||||
|
/**
|
||||||
|
* お問い合わせ
|
||||||
|
*/
|
||||||
|
"inquiry": string;
|
||||||
"_bubbleGame": {
|
"_bubbleGame": {
|
||||||
/**
|
/**
|
||||||
* 遊び方
|
* 遊び方
|
||||||
|
@ -6552,6 +6596,26 @@ export interface Locale extends ILocale {
|
||||||
* リモートユーザー
|
* リモートユーザー
|
||||||
*/
|
*/
|
||||||
"isRemote": string;
|
"isRemote": string;
|
||||||
|
/**
|
||||||
|
* 猫ユーザー
|
||||||
|
*/
|
||||||
|
"isCat": string;
|
||||||
|
/**
|
||||||
|
* botユーザー
|
||||||
|
*/
|
||||||
|
"isBot": string;
|
||||||
|
/**
|
||||||
|
* サスペンド済みユーザー
|
||||||
|
*/
|
||||||
|
"isSuspended": string;
|
||||||
|
/**
|
||||||
|
* 鍵アカウントユーザー
|
||||||
|
*/
|
||||||
|
"isLocked": string;
|
||||||
|
/**
|
||||||
|
* 「アカウントを見つけやすくする」が有効なユーザー
|
||||||
|
*/
|
||||||
|
"isExplorable": string;
|
||||||
/**
|
/**
|
||||||
* アカウント作成から~以内
|
* アカウント作成から~以内
|
||||||
*/
|
*/
|
||||||
|
@ -6805,6 +6869,10 @@ export interface Locale extends ILocale {
|
||||||
* ソースを表示
|
* ソースを表示
|
||||||
*/
|
*/
|
||||||
"viewSource": string;
|
"viewSource": string;
|
||||||
|
/**
|
||||||
|
* ログを表示
|
||||||
|
*/
|
||||||
|
"viewLog": string;
|
||||||
};
|
};
|
||||||
"_preferencesBackups": {
|
"_preferencesBackups": {
|
||||||
/**
|
/**
|
||||||
|
@ -7522,13 +7590,9 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"step1": ParameterizedString<"a" | "b">;
|
"step1": ParameterizedString<"a" | "b">;
|
||||||
/**
|
/**
|
||||||
* 次に、表示されているQRコードをアプリでスキャンします。
|
* 次に、表示されているQRコードをアプリでスキャンするか、ボタンをクリックして端末上でアプリを開きます。
|
||||||
*/
|
*/
|
||||||
"step2": string;
|
"step2": string;
|
||||||
/**
|
|
||||||
* QRコードをクリックすると、お使いの端末にインストールされている認証アプリやキーリングに登録できます。
|
|
||||||
*/
|
|
||||||
"step2Click": string;
|
|
||||||
/**
|
/**
|
||||||
* デスクトップアプリを使用する場合は次のURIを入力します
|
* デスクトップアプリを使用する場合は次のURIを入力します
|
||||||
*/
|
*/
|
||||||
|
@ -7621,6 +7685,10 @@ export interface Locale extends ILocale {
|
||||||
* バックアップコードが全て使用されました。認証アプリを利用できない場合、これ以上アカウントにアクセスできなくなります。認証アプリを再登録してください。
|
* バックアップコードが全て使用されました。認証アプリを利用できない場合、これ以上アカウントにアクセスできなくなります。認証アプリを再登録してください。
|
||||||
*/
|
*/
|
||||||
"backupCodesExhaustedWarning": string;
|
"backupCodesExhaustedWarning": string;
|
||||||
|
/**
|
||||||
|
* 詳細なガイドはこちら
|
||||||
|
*/
|
||||||
|
"moreDetailedGuideHere": string;
|
||||||
};
|
};
|
||||||
"_permissions": {
|
"_permissions": {
|
||||||
/**
|
/**
|
||||||
|
@ -8631,6 +8699,10 @@ export interface Locale extends ILocale {
|
||||||
* 説明
|
* 説明
|
||||||
*/
|
*/
|
||||||
"summary": string;
|
"summary": string;
|
||||||
|
/**
|
||||||
|
* 非公開に設定するとプロフィールに表示されなくなりますが、URLを知っている人は引き続きアクセスできます。
|
||||||
|
*/
|
||||||
|
"visibilityDescription": string;
|
||||||
};
|
};
|
||||||
"_pages": {
|
"_pages": {
|
||||||
/**
|
/**
|
||||||
|
@ -8802,6 +8874,14 @@ export interface Locale extends ILocale {
|
||||||
* ボタン
|
* ボタン
|
||||||
*/
|
*/
|
||||||
"button": string;
|
"button": string;
|
||||||
|
/**
|
||||||
|
* 動的ブロック
|
||||||
|
*/
|
||||||
|
"dynamic": string;
|
||||||
|
/**
|
||||||
|
* このブロックは廃止されています。今後は{play}を利用してください。
|
||||||
|
*/
|
||||||
|
"dynamicDescription": ParameterizedString<"play">;
|
||||||
/**
|
/**
|
||||||
* ノート埋め込み
|
* ノート埋め込み
|
||||||
*/
|
*/
|
||||||
|
@ -9756,6 +9836,74 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"header": string;
|
"header": string;
|
||||||
};
|
};
|
||||||
|
"_urlPreviewSetting": {
|
||||||
|
/**
|
||||||
|
* URLプレビューの設定
|
||||||
|
*/
|
||||||
|
"title": string;
|
||||||
|
/**
|
||||||
|
* URLプレビューを有効にする
|
||||||
|
*/
|
||||||
|
"enable": string;
|
||||||
|
/**
|
||||||
|
* プレビュー取得時のタイムアウト(ms)
|
||||||
|
*/
|
||||||
|
"timeout": string;
|
||||||
|
/**
|
||||||
|
* プレビュー取得の所要時間がこの値を超えた場合、プレビューは生成されません。
|
||||||
|
*/
|
||||||
|
"timeoutDescription": string;
|
||||||
|
/**
|
||||||
|
* Content-Lengthの最大値(byte)
|
||||||
|
*/
|
||||||
|
"maximumContentLength": string;
|
||||||
|
/**
|
||||||
|
* Content-Lengthがこの値を超えた場合、プレビューは生成されません。
|
||||||
|
*/
|
||||||
|
"maximumContentLengthDescription": string;
|
||||||
|
/**
|
||||||
|
* Content-Lengthが取得できた場合のみプレビューを生成
|
||||||
|
*/
|
||||||
|
"requireContentLength": string;
|
||||||
|
/**
|
||||||
|
* 相手サーバがContent-Lengthを返さない場合、プレビューは生成されません。
|
||||||
|
*/
|
||||||
|
"requireContentLengthDescription": string;
|
||||||
|
/**
|
||||||
|
* User-Agent
|
||||||
|
*/
|
||||||
|
"userAgent": string;
|
||||||
|
/**
|
||||||
|
* プレビュー取得時に使用されるUser-Agentを設定します。空欄の場合、デフォルトのUser-Agentが使用されます。
|
||||||
|
*/
|
||||||
|
"userAgentDescription": string;
|
||||||
|
/**
|
||||||
|
* プレビューを生成するプロキシのエンドポイント
|
||||||
|
*/
|
||||||
|
"summaryProxy": string;
|
||||||
|
/**
|
||||||
|
* Misskey本体ではなく、サマリープロキシを使用してプレビューを生成します。
|
||||||
|
*/
|
||||||
|
"summaryProxyDescription": string;
|
||||||
|
/**
|
||||||
|
* プロキシには下記パラメータがクエリ文字列として連携されます。プロキシ側がこれらをサポートしない場合、設定値は無視されます。
|
||||||
|
*/
|
||||||
|
"summaryProxyDescription2": string;
|
||||||
|
};
|
||||||
|
"_mediaControls": {
|
||||||
|
/**
|
||||||
|
* ピクチャインピクチャ
|
||||||
|
*/
|
||||||
|
"pip": string;
|
||||||
|
/**
|
||||||
|
* 再生速度
|
||||||
|
*/
|
||||||
|
"playbackRate": string;
|
||||||
|
/**
|
||||||
|
* ループ再生
|
||||||
|
*/
|
||||||
|
"loop": string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
declare const locales: {
|
declare const locales: {
|
||||||
[lang: string]: Locale;
|
[lang: string]: Locale;
|
||||||
|
|
|
@ -400,6 +400,7 @@ name: "名前"
|
||||||
antennaSource: "受信ソース"
|
antennaSource: "受信ソース"
|
||||||
antennaKeywords: "受信キーワード"
|
antennaKeywords: "受信キーワード"
|
||||||
antennaExcludeKeywords: "除外キーワード"
|
antennaExcludeKeywords: "除外キーワード"
|
||||||
|
antennaExcludeBots: "Botアカウントを除外"
|
||||||
antennaKeywordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります"
|
antennaKeywordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります"
|
||||||
notifyAntenna: "新しいノートを通知する"
|
notifyAntenna: "新しいノートを通知する"
|
||||||
withFileAntenna: "ファイルが添付されたノートのみ"
|
withFileAntenna: "ファイルが添付されたノートのみ"
|
||||||
|
@ -1224,6 +1225,16 @@ enableHorizontalSwipe: "スワイプしてタブを切り替える"
|
||||||
loading: "読み込み中"
|
loading: "読み込み中"
|
||||||
surrender: "やめる"
|
surrender: "やめる"
|
||||||
gameRetry: "リトライ"
|
gameRetry: "リトライ"
|
||||||
|
notUsePleaseLeaveBlank: "使用しない場合は空欄にしてください"
|
||||||
|
useTotp: "ワンタイムパスワードを使う"
|
||||||
|
useBackupCode: "バックアップコードを使う"
|
||||||
|
launchApp: "アプリを起動"
|
||||||
|
useNativeUIForVideoAudioPlayer: "動画・音声の再生にブラウザのUIを使用する"
|
||||||
|
keepOriginalFilename: "オリジナルのファイル名を保持"
|
||||||
|
keepOriginalFilenameDescription: "この設定をオフにすると、アップロード時にファイル名が自動でランダム文字列に置き換えられます。"
|
||||||
|
noDescription: "説明文はありません"
|
||||||
|
alwaysConfirmFollow: "フォローの際常に確認する"
|
||||||
|
inquiry: "お問い合わせ"
|
||||||
|
|
||||||
_bubbleGame:
|
_bubbleGame:
|
||||||
howToPlay: "遊び方"
|
howToPlay: "遊び方"
|
||||||
|
@ -1693,6 +1704,11 @@ _role:
|
||||||
roleAssignedTo: "マニュアルロールにアサイン済み"
|
roleAssignedTo: "マニュアルロールにアサイン済み"
|
||||||
isLocal: "ローカルユーザー"
|
isLocal: "ローカルユーザー"
|
||||||
isRemote: "リモートユーザー"
|
isRemote: "リモートユーザー"
|
||||||
|
isCat: "猫ユーザー"
|
||||||
|
isBot: "botユーザー"
|
||||||
|
isSuspended: "サスペンド済みユーザー"
|
||||||
|
isLocked: "鍵アカウントユーザー"
|
||||||
|
isExplorable: "「アカウントを見つけやすくする」が有効なユーザー"
|
||||||
createdLessThan: "アカウント作成から~以内"
|
createdLessThan: "アカウント作成から~以内"
|
||||||
createdMoreThan: "アカウント作成から~経過"
|
createdMoreThan: "アカウント作成から~経過"
|
||||||
followersLessThanOrEq: "フォロワー数が~以下"
|
followersLessThanOrEq: "フォロワー数が~以下"
|
||||||
|
@ -1772,6 +1788,7 @@ _plugin:
|
||||||
installWarn: "信頼できないプラグインはインストールしないでください。"
|
installWarn: "信頼できないプラグインはインストールしないでください。"
|
||||||
manage: "プラグインの管理"
|
manage: "プラグインの管理"
|
||||||
viewSource: "ソースを表示"
|
viewSource: "ソースを表示"
|
||||||
|
viewLog: "ログを表示"
|
||||||
|
|
||||||
_preferencesBackups:
|
_preferencesBackups:
|
||||||
list: "作成したバックアップ"
|
list: "作成したバックアップ"
|
||||||
|
@ -1978,8 +1995,7 @@ _2fa:
|
||||||
alreadyRegistered: "既に設定は完了しています。"
|
alreadyRegistered: "既に設定は完了しています。"
|
||||||
registerTOTP: "認証アプリの設定を開始"
|
registerTOTP: "認証アプリの設定を開始"
|
||||||
step1: "まず、{a}や{b}などの認証アプリをお使いのデバイスにインストールします。"
|
step1: "まず、{a}や{b}などの認証アプリをお使いのデバイスにインストールします。"
|
||||||
step2: "次に、表示されているQRコードをアプリでスキャンします。"
|
step2: "次に、表示されているQRコードをアプリでスキャンするか、ボタンをクリックして端末上でアプリを開きます。"
|
||||||
step2Click: "QRコードをクリックすると、お使いの端末にインストールされている認証アプリやキーリングに登録できます。"
|
|
||||||
step2Uri: "デスクトップアプリを使用する場合は次のURIを入力します"
|
step2Uri: "デスクトップアプリを使用する場合は次のURIを入力します"
|
||||||
step3Title: "確認コードを入力"
|
step3Title: "確認コードを入力"
|
||||||
step3: "アプリに表示されている確認コード(トークン)を入力します。"
|
step3: "アプリに表示されている確認コード(トークン)を入力します。"
|
||||||
|
@ -2003,6 +2019,7 @@ _2fa:
|
||||||
backupCodesDescription: "認証アプリが使用できなくなった場合、以下のバックアップコードを使ってアカウントにアクセスできます。これらのコードは必ず安全な場所に保管してください。各コードは一回だけ使用できます。"
|
backupCodesDescription: "認証アプリが使用できなくなった場合、以下のバックアップコードを使ってアカウントにアクセスできます。これらのコードは必ず安全な場所に保管してください。各コードは一回だけ使用できます。"
|
||||||
backupCodeUsedWarning: "バックアップコードが使用されました。認証アプリが使えなくなっている場合、なるべく早く認証アプリを再設定してください。"
|
backupCodeUsedWarning: "バックアップコードが使用されました。認証アプリが使えなくなっている場合、なるべく早く認証アプリを再設定してください。"
|
||||||
backupCodesExhaustedWarning: "バックアップコードが全て使用されました。認証アプリを利用できない場合、これ以上アカウントにアクセスできなくなります。認証アプリを再登録してください。"
|
backupCodesExhaustedWarning: "バックアップコードが全て使用されました。認証アプリを利用できない場合、これ以上アカウントにアクセスできなくなります。認証アプリを再登録してください。"
|
||||||
|
moreDetailedGuideHere: "詳細なガイドはこちら"
|
||||||
|
|
||||||
_permissions:
|
_permissions:
|
||||||
"read:account": "アカウントの情報を見る"
|
"read:account": "アカウントの情報を見る"
|
||||||
|
@ -2279,6 +2296,7 @@ _play:
|
||||||
title: "タイトル"
|
title: "タイトル"
|
||||||
script: "スクリプト"
|
script: "スクリプト"
|
||||||
summary: "説明"
|
summary: "説明"
|
||||||
|
visibilityDescription: "非公開に設定するとプロフィールに表示されなくなりますが、URLを知っている人は引き続きアクセスできます。"
|
||||||
|
|
||||||
_pages:
|
_pages:
|
||||||
newPage: "ページの作成"
|
newPage: "ページの作成"
|
||||||
|
@ -2324,6 +2342,8 @@ _pages:
|
||||||
section: "セクション"
|
section: "セクション"
|
||||||
image: "画像"
|
image: "画像"
|
||||||
button: "ボタン"
|
button: "ボタン"
|
||||||
|
dynamic: "動的ブロック"
|
||||||
|
dynamicDescription: "このブロックは廃止されています。今後は{play}を利用してください。"
|
||||||
|
|
||||||
note: "ノート埋め込み"
|
note: "ノート埋め込み"
|
||||||
_note:
|
_note:
|
||||||
|
@ -2599,3 +2619,22 @@ _offlineScreen:
|
||||||
title: "オフライン - サーバーに接続できません"
|
title: "オフライン - サーバーに接続できません"
|
||||||
header: "サーバーに接続できません"
|
header: "サーバーに接続できません"
|
||||||
|
|
||||||
|
_urlPreviewSetting:
|
||||||
|
title: "URLプレビューの設定"
|
||||||
|
enable: "URLプレビューを有効にする"
|
||||||
|
timeout: "プレビュー取得時のタイムアウト(ms)"
|
||||||
|
timeoutDescription: "プレビュー取得の所要時間がこの値を超えた場合、プレビューは生成されません。"
|
||||||
|
maximumContentLength: "Content-Lengthの最大値(byte)"
|
||||||
|
maximumContentLengthDescription: "Content-Lengthがこの値を超えた場合、プレビューは生成されません。"
|
||||||
|
requireContentLength: "Content-Lengthが取得できた場合のみプレビューを生成"
|
||||||
|
requireContentLengthDescription: "相手サーバがContent-Lengthを返さない場合、プレビューは生成されません。"
|
||||||
|
userAgent: "User-Agent"
|
||||||
|
userAgentDescription: "プレビュー取得時に使用されるUser-Agentを設定します。空欄の場合、デフォルトのUser-Agentが使用されます。"
|
||||||
|
summaryProxy: "プレビューを生成するプロキシのエンドポイント"
|
||||||
|
summaryProxyDescription: "Misskey本体ではなく、サマリープロキシを使用してプレビューを生成します。"
|
||||||
|
summaryProxyDescription2: "プロキシには下記パラメータがクエリ文字列として連携されます。プロキシ側がこれらをサポートしない場合、設定値は無視されます。"
|
||||||
|
|
||||||
|
_mediaControls:
|
||||||
|
pip: "ピクチャインピクチャ"
|
||||||
|
playbackRate: "再生速度"
|
||||||
|
loop: "ループ再生"
|
||||||
|
|
|
@ -56,9 +56,12 @@
|
||||||
"postcss": "8.4.35",
|
"postcss": "8.4.35",
|
||||||
"tar": "6.2.0",
|
"tar": "6.2.0",
|
||||||
"terser": "5.28.1",
|
"terser": "5.28.1",
|
||||||
"typescript": "5.3.3"
|
"typescript": "5.3.3",
|
||||||
|
"esbuild": "0.19.11",
|
||||||
|
"glob": "10.3.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.11.28",
|
||||||
"@typescript-eslint/eslint-plugin": "7.1.0",
|
"@typescript-eslint/eslint-plugin": "7.1.0",
|
||||||
"@typescript-eslint/parser": "7.1.0",
|
"@typescript-eslint/parser": "7.1.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
|
|
|
@ -19,5 +19,6 @@
|
||||||
},
|
},
|
||||||
"target": "es2022"
|
"target": "es2022"
|
||||||
},
|
},
|
||||||
"minify": false
|
"minify": false,
|
||||||
|
"sourceMaps": "inline"
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,6 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<redoc spec-url="/api.json" expand-responses="200" expand-single-schema-field="true"></redoc>
|
<redoc spec-url="/api.json" expand-responses="200" expand-single-schema-field="true"></redoc>
|
||||||
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
|
<script src="https://cdn.redoc.ly/redoc/v2.1.3/bundles/redoc.standalone.js" integrity="sha256-u4DgqzYXoArvNF/Ymw3puKexfOC6lYfw0sfmeliBJ1I=" crossorigin="anonymous"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { loadConfig } from './built/config.js'
|
|
||||||
import { genOpenapiSpec } from './built/server/api/openapi/gen-spec.js'
|
|
||||||
import { writeFileSync } from "node:fs";
|
|
||||||
|
|
||||||
const config = loadConfig();
|
|
||||||
const spec = genOpenapiSpec(config, true);
|
|
||||||
|
|
||||||
writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8');
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
export class UserBlacklistAnntena1689325027964 {
|
export class UserBlacklistAnntena1689325027964 {
|
||||||
name = 'UserBlacklistAnntena1689325027964'
|
name = 'UserBlacklistAnntena1689325027964'
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
export class FixRenoteMuting1690417561185 {
|
export class FixRenoteMuting1690417561185 {
|
||||||
name = 'FixRenoteMuting1690417561185'
|
name = 'FixRenoteMuting1690417561185'
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
export class ChangeCacheRemoteFilesDefault1690417561186 {
|
export class ChangeCacheRemoteFilesDefault1690417561186 {
|
||||||
name = 'ChangeCacheRemoteFilesDefault1690417561186'
|
name = 'ChangeCacheRemoteFilesDefault1690417561186'
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
export class Fix1690417561187 {
|
export class Fix1690417561187 {
|
||||||
name = 'Fix1690417561187'
|
name = 'Fix1690417561187'
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
export class User2faBackupCodes1690569881926 {
|
export class User2faBackupCodes1690569881926 {
|
||||||
name = 'User2faBackupCodes1690569881926'
|
name = 'User2faBackupCodes1690569881926'
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
export class RefineAnnouncement1691649257651 {
|
export class RefineAnnouncement1691649257651 {
|
||||||
name = 'RefineAnnouncement1691649257651'
|
name = 'RefineAnnouncement1691649257651'
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
export class RefineAnnouncement21691657412740 {
|
export class RefineAnnouncement21691657412740 {
|
||||||
name = 'RefineAnnouncement21691657412740'
|
name = 'RefineAnnouncement21691657412740'
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
export class VerifiedLinks1695260774117 {
|
export class VerifiedLinks1695260774117 {
|
||||||
name = 'VerifiedLinks1695260774117'
|
name = 'VerifiedLinks1695260774117'
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
export class FollowingNotify1695288787870 {
|
export class FollowingNotify1695288787870 {
|
||||||
name = 'FollowingNotify1695288787870'
|
name = 'FollowingNotify1695288787870'
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
export class ShortName1695440131671 {
|
export class ShortName1695440131671 {
|
||||||
name = 'ShortName1695440131671'
|
name = 'ShortName1695440131671'
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
export class MutingNotificationTypes1695605508898 {
|
export class MutingNotificationTypes1695605508898 {
|
||||||
name = 'MutingNotificationTypes1695605508898'
|
name = 'MutingNotificationTypes1695605508898'
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
export class NoteUpdatedAt1695901659683 {
|
export class NoteUpdatedAt1695901659683 {
|
||||||
name = 'NoteUpdatedAt1695901659683'
|
name = 'NoteUpdatedAt1695901659683'
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
export class UserListMembership1696323464251 {
|
export class UserListMembership1696323464251 {
|
||||||
name = 'UserListMembership1696323464251'
|
name = 'UserListMembership1696323464251'
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
export class Hibernation1696331570827 {
|
export class Hibernation1696331570827 {
|
||||||
name = 'Hibernation1696331570827'
|
name = 'Hibernation1696331570827'
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
export class Clean1696332072038 {
|
export class Clean1696332072038 {
|
||||||
name = 'Clean1696332072038'
|
name = 'Clean1696332072038'
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
export class HardMute1700383825690 {
|
export class HardMute1700383825690 {
|
||||||
name = 'HardMute1700383825690'
|
name = 'HardMute1700383825690'
|
||||||
|
|
||||||
|
|
42
packages/backend/migration/1710512074000-url-preview-meta.js
Normal file
42
packages/backend/migration/1710512074000-url-preview-meta.js
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class UrlPreviewMeta1710512074000 {
|
||||||
|
name = 'UrlPreviewMeta1710512074000'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`
|
||||||
|
alter table meta
|
||||||
|
rename column "summalyProxy" to "urlPreviewSummaryProxyUrl";
|
||||||
|
alter table meta
|
||||||
|
add "urlPreviewEnabled" boolean default true not null;
|
||||||
|
alter table meta
|
||||||
|
add "urlPreviewTimeout" integer default 10000 not null;
|
||||||
|
alter table meta
|
||||||
|
add "urlPreviewMaximumContentLength" bigint default 10485760 not null;
|
||||||
|
alter table meta
|
||||||
|
add "urlPreviewRequireContentLength" boolean default false not null;
|
||||||
|
alter table meta
|
||||||
|
add "urlPreviewUserAgent" varchar(1024) default null;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`
|
||||||
|
alter table meta
|
||||||
|
rename column "urlPreviewSummaryProxyUrl" to "summalyProxy";
|
||||||
|
alter table meta
|
||||||
|
drop column "urlPreviewEnabled";
|
||||||
|
alter table meta
|
||||||
|
drop column "urlPreviewTimeout";
|
||||||
|
alter table meta
|
||||||
|
drop column "urlPreviewMaximumContentLength";
|
||||||
|
alter table meta
|
||||||
|
drop column "urlPreviewRequireContentLength";
|
||||||
|
alter table meta
|
||||||
|
drop column "urlPreviewUserAgent";
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class AntennaExcludeBots1710919614510 {
|
||||||
|
name = 'AntennaExcludeBots1710919614510'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "antenna" ADD "excludeBots" boolean NOT NULL DEFAULT false`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "excludeBots"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,14 +11,14 @@
|
||||||
"start:test": "cross-env NODE_ENV=test node ./built/boot/entry.js",
|
"start:test": "cross-env NODE_ENV=test node ./built/boot/entry.js",
|
||||||
"migrate": "pnpm typeorm migration:run -d ormconfig.js",
|
"migrate": "pnpm typeorm migration:run -d ormconfig.js",
|
||||||
"revert": "pnpm typeorm migration:revert -d ormconfig.js",
|
"revert": "pnpm typeorm migration:revert -d ormconfig.js",
|
||||||
"check:connect": "node ./check_connect.js",
|
"check:connect": "node ./scripts/check_connect.js",
|
||||||
"build": "swc src -d built -D",
|
"build": "swc src -d built -D",
|
||||||
"build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc",
|
"build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc",
|
||||||
"watch:swc": "swc src -d built -D -w",
|
"watch:swc": "swc src -d built -D -w",
|
||||||
"build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
|
"build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
|
||||||
"watch": "node watch.mjs",
|
"watch": "node ./scripts/watch.mjs",
|
||||||
"restart": "pnpm build && pnpm start",
|
"restart": "pnpm build && pnpm start",
|
||||||
"dev": "nodemon -w src -e ts,js,mjs,cjs,json --exec \"cross-env NODE_ENV=development pnpm run restart\"",
|
"dev": "node ./scripts/dev.mjs",
|
||||||
"typecheck": "tsc --noEmit && tsc -p test --noEmit",
|
"typecheck": "tsc --noEmit && tsc -p test --noEmit",
|
||||||
"eslint": "eslint --quiet \"src/**/*.ts\"",
|
"eslint": "eslint --quiet \"src/**/*.ts\"",
|
||||||
"lint": "pnpm typecheck && pnpm eslint",
|
"lint": "pnpm typecheck && pnpm eslint",
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
|
"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
|
||||||
"test-and-coverage": "pnpm jest-and-coverage",
|
"test-and-coverage": "pnpm jest-and-coverage",
|
||||||
"test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e",
|
"test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e",
|
||||||
"generate-api-json": "pnpm build && node ./generate_api_json.js"
|
"generate-api-json": "pnpm build && node ./scripts/generate_api_json.js"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@swc/core-android-arm64": "1.3.11",
|
"@swc/core-android-arm64": "1.3.11",
|
||||||
|
@ -81,7 +81,7 @@
|
||||||
"@fastify/view": "8.2.0",
|
"@fastify/view": "8.2.0",
|
||||||
"@misskey-dev/node-http-message-signatures": "0.0.8",
|
"@misskey-dev/node-http-message-signatures": "0.0.8",
|
||||||
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
||||||
"@misskey-dev/summaly": "5.0.3",
|
"@misskey-dev/summaly": "5.1.0",
|
||||||
"@nestjs/common": "10.3.3",
|
"@nestjs/common": "10.3.3",
|
||||||
"@nestjs/core": "10.3.3",
|
"@nestjs/core": "10.3.3",
|
||||||
"@nestjs/testing": "10.3.3",
|
"@nestjs/testing": "10.3.3",
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Redis from 'ioredis';
|
import Redis from 'ioredis';
|
||||||
import { loadConfig } from './built/config.js';
|
import { loadConfig } from '../built/config.js';
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
const redis = new Redis(config.redis);
|
const redis = new Redis(config.redis);
|
61
packages/backend/scripts/dev.mjs
Normal file
61
packages/backend/scripts/dev.mjs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execa, execaNode } from 'execa';
|
||||||
|
|
||||||
|
/** @type {import('execa').ExecaChildProcess | undefined} */
|
||||||
|
let backendProcess;
|
||||||
|
|
||||||
|
async function execBuildAssets() {
|
||||||
|
await execa('pnpm', ['run', 'build-assets'], {
|
||||||
|
cwd: '../../',
|
||||||
|
stdout: process.stdout,
|
||||||
|
stderr: process.stderr,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function execStart() {
|
||||||
|
// pnpm run start を呼び出したいが、windowsだとプロセスグループ単位でのkillが出来ずゾンビプロセス化するので
|
||||||
|
// 上記と同等の動きをするコマンドで子・孫プロセスを作らないようにしたい
|
||||||
|
backendProcess = execaNode('./built/boot/entry.js', [], {
|
||||||
|
stdout: process.stdout,
|
||||||
|
stderr: process.stderr,
|
||||||
|
env: {
|
||||||
|
'NODE_ENV': 'development',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function killProc() {
|
||||||
|
if (backendProcess) {
|
||||||
|
backendProcess.kill();
|
||||||
|
await new Promise(resolve => backendProcess.on('exit', resolve));
|
||||||
|
backendProcess = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
execaNode(
|
||||||
|
'./node_modules/nodemon/bin/nodemon.js',
|
||||||
|
[
|
||||||
|
'-w', 'src',
|
||||||
|
'-e', 'ts,js,mjs,cjs,json',
|
||||||
|
'--exec', 'pnpm', 'run', 'build',
|
||||||
|
],
|
||||||
|
{
|
||||||
|
stdio: [process.stdin, process.stdout, process.stderr, 'ipc'],
|
||||||
|
})
|
||||||
|
.on('message', async (message) => {
|
||||||
|
if (message.type === 'exit') {
|
||||||
|
// かならずbuild->build-assetsの順番で呼び出したいので、
|
||||||
|
// 少々トリッキーだがnodemonからのexitイベントを利用してbuild-assets->startを行う。
|
||||||
|
// pnpm restartをbuildが終わる前にbuild-assetsが動いてしまうので、バラバラに呼び出す必要がある
|
||||||
|
|
||||||
|
await killProc();
|
||||||
|
await execBuildAssets();
|
||||||
|
execStart();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})();
|
13
packages/backend/scripts/generate_api_json.js
Normal file
13
packages/backend/scripts/generate_api_json.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { loadConfig } from '../built/config.js'
|
||||||
|
import { genOpenapiSpec } from '../built/server/api/openapi/gen-spec.js'
|
||||||
|
import { writeFileSync } from "node:fs";
|
||||||
|
|
||||||
|
const config = loadConfig();
|
||||||
|
const spec = genOpenapiSpec(config, true);
|
||||||
|
|
||||||
|
writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8');
|
|
@ -305,7 +305,7 @@ export class AccountMoveService {
|
||||||
let resultUser: MiLocalUser | MiRemoteUser | null = null;
|
let resultUser: MiLocalUser | MiRemoteUser | null = null;
|
||||||
|
|
||||||
if (this.userEntityService.isRemoteUser(dst)) {
|
if (this.userEntityService.isRemoteUser(dst)) {
|
||||||
if ((new Date()).getTime() - (dst.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) {
|
if (Date.now() - (dst.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) {
|
||||||
await this.apPersonService.updatePerson(dst.uri);
|
await this.apPersonService.updatePerson(dst.uri);
|
||||||
}
|
}
|
||||||
dst = await this.apPersonService.fetchPerson(dst.uri) ?? dst;
|
dst = await this.apPersonService.fetchPerson(dst.uri) ?? dst;
|
||||||
|
@ -321,7 +321,7 @@ export class AccountMoveService {
|
||||||
if (!src) continue; // oldAccountを探してもこのサーバーに存在しない場合はフォロー関係もないということなのでスルー
|
if (!src) continue; // oldAccountを探してもこのサーバーに存在しない場合はフォロー関係もないということなのでスルー
|
||||||
|
|
||||||
if (this.userEntityService.isRemoteUser(dst)) {
|
if (this.userEntityService.isRemoteUser(dst)) {
|
||||||
if ((new Date()).getTime() - (src.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) {
|
if (Date.now() - (src.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) {
|
||||||
await this.apPersonService.updatePerson(srcUri);
|
await this.apPersonService.updatePerson(srcUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -92,7 +92,7 @@ export class AntennaService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async addNoteToAntennas(note: MiNote, noteUser: { id: MiUser['id']; username: string; host: string | null; }): Promise<void> {
|
public async addNoteToAntennas(note: MiNote, noteUser: { id: MiUser['id']; username: string; host: string | null; isBot: boolean; }): Promise<void> {
|
||||||
const antennas = await this.getAntennas();
|
const antennas = await this.getAntennas();
|
||||||
const antennasWithMatchResult = await Promise.all(antennas.map(antenna => this.checkHitAntenna(antenna, note, noteUser).then(hit => [antenna, hit] as const)));
|
const antennasWithMatchResult = await Promise.all(antennas.map(antenna => this.checkHitAntenna(antenna, note, noteUser).then(hit => [antenna, hit] as const)));
|
||||||
const matchedAntennas = antennasWithMatchResult.filter(([, hit]) => hit).map(([antenna]) => antenna);
|
const matchedAntennas = antennasWithMatchResult.filter(([, hit]) => hit).map(([antenna]) => antenna);
|
||||||
|
@ -110,10 +110,12 @@ export class AntennaService implements OnApplicationShutdown {
|
||||||
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
|
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async checkHitAntenna(antenna: MiAntenna, note: (MiNote | Packed<'Note'>), noteUser: { id: MiUser['id']; username: string; host: string | null; }): Promise<boolean> {
|
public async checkHitAntenna(antenna: MiAntenna, note: (MiNote | Packed<'Note'>), noteUser: { id: MiUser['id']; username: string; host: string | null; isBot: boolean; }): Promise<boolean> {
|
||||||
if (note.visibility === 'specified') return false;
|
if (note.visibility === 'specified') return false;
|
||||||
if (note.visibility === 'followers') return false;
|
if (note.visibility === 'followers') return false;
|
||||||
|
|
||||||
|
if (antenna.excludeBots && noteUser.isBot) return false;
|
||||||
|
|
||||||
if (antenna.localOnly && noteUser.host != null) return false;
|
if (antenna.localOnly && noteUser.host != null) return false;
|
||||||
|
|
||||||
if (!antenna.withReplies && note.replyId != null) return false;
|
if (!antenna.withReplies && note.replyId != null) return false;
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
||||||
import Redis from 'ioredis';
|
import Redis from 'ioredis';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
|
|
@ -127,7 +127,7 @@ import { ApMfmService } from './activitypub/ApMfmService.js';
|
||||||
import { ApRendererService } from './activitypub/ApRendererService.js';
|
import { ApRendererService } from './activitypub/ApRendererService.js';
|
||||||
import { ApRequestService } from './activitypub/ApRequestService.js';
|
import { ApRequestService } from './activitypub/ApRequestService.js';
|
||||||
import { ApResolverService } from './activitypub/ApResolverService.js';
|
import { ApResolverService } from './activitypub/ApResolverService.js';
|
||||||
import { LdSignatureService } from './activitypub/LdSignatureService.js';
|
import { JsonLdService } from './activitypub/JsonLdService.js';
|
||||||
import { RemoteLoggerService } from './RemoteLoggerService.js';
|
import { RemoteLoggerService } from './RemoteLoggerService.js';
|
||||||
import { RemoteUserResolveService } from './RemoteUserResolveService.js';
|
import { RemoteUserResolveService } from './RemoteUserResolveService.js';
|
||||||
import { WebfingerService } from './WebfingerService.js';
|
import { WebfingerService } from './WebfingerService.js';
|
||||||
|
@ -266,7 +266,7 @@ const $ApMfmService: Provider = { provide: 'ApMfmService', useExisting: ApMfmSer
|
||||||
const $ApRendererService: Provider = { provide: 'ApRendererService', useExisting: ApRendererService };
|
const $ApRendererService: Provider = { provide: 'ApRendererService', useExisting: ApRendererService };
|
||||||
const $ApRequestService: Provider = { provide: 'ApRequestService', useExisting: ApRequestService };
|
const $ApRequestService: Provider = { provide: 'ApRequestService', useExisting: ApRequestService };
|
||||||
const $ApResolverService: Provider = { provide: 'ApResolverService', useExisting: ApResolverService };
|
const $ApResolverService: Provider = { provide: 'ApResolverService', useExisting: ApResolverService };
|
||||||
const $LdSignatureService: Provider = { provide: 'LdSignatureService', useExisting: LdSignatureService };
|
const $JsonLdService: Provider = { provide: 'JsonLdService', useExisting: JsonLdService };
|
||||||
const $RemoteLoggerService: Provider = { provide: 'RemoteLoggerService', useExisting: RemoteLoggerService };
|
const $RemoteLoggerService: Provider = { provide: 'RemoteLoggerService', useExisting: RemoteLoggerService };
|
||||||
const $RemoteUserResolveService: Provider = { provide: 'RemoteUserResolveService', useExisting: RemoteUserResolveService };
|
const $RemoteUserResolveService: Provider = { provide: 'RemoteUserResolveService', useExisting: RemoteUserResolveService };
|
||||||
const $WebfingerService: Provider = { provide: 'WebfingerService', useExisting: WebfingerService };
|
const $WebfingerService: Provider = { provide: 'WebfingerService', useExisting: WebfingerService };
|
||||||
|
@ -406,7 +406,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
ApRendererService,
|
ApRendererService,
|
||||||
ApRequestService,
|
ApRequestService,
|
||||||
ApResolverService,
|
ApResolverService,
|
||||||
LdSignatureService,
|
JsonLdService,
|
||||||
RemoteLoggerService,
|
RemoteLoggerService,
|
||||||
RemoteUserResolveService,
|
RemoteUserResolveService,
|
||||||
WebfingerService,
|
WebfingerService,
|
||||||
|
@ -542,7 +542,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$ApRendererService,
|
$ApRendererService,
|
||||||
$ApRequestService,
|
$ApRequestService,
|
||||||
$ApResolverService,
|
$ApResolverService,
|
||||||
$LdSignatureService,
|
$JsonLdService,
|
||||||
$RemoteLoggerService,
|
$RemoteLoggerService,
|
||||||
$RemoteUserResolveService,
|
$RemoteUserResolveService,
|
||||||
$WebfingerService,
|
$WebfingerService,
|
||||||
|
@ -678,7 +678,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
ApRendererService,
|
ApRendererService,
|
||||||
ApRequestService,
|
ApRequestService,
|
||||||
ApResolverService,
|
ApResolverService,
|
||||||
LdSignatureService,
|
JsonLdService,
|
||||||
RemoteLoggerService,
|
RemoteLoggerService,
|
||||||
RemoteUserResolveService,
|
RemoteUserResolveService,
|
||||||
WebfingerService,
|
WebfingerService,
|
||||||
|
@ -813,7 +813,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$ApRendererService,
|
$ApRendererService,
|
||||||
$ApRequestService,
|
$ApRequestService,
|
||||||
$ApResolverService,
|
$ApResolverService,
|
||||||
$LdSignatureService,
|
$JsonLdService,
|
||||||
$RemoteLoggerService,
|
$RemoteLoggerService,
|
||||||
$RemoteUserResolveService,
|
$RemoteUserResolveService,
|
||||||
$WebfingerService,
|
$WebfingerService,
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { query } from '@/misc/prelude/url.js';
|
||||||
import type { Serialized } from '@/types.js';
|
import type { Serialized } from '@/types.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
|
||||||
const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
|
const parseEmojiStrRegexp = /^([-\w]+)(?:@([\w.-]+))?$/;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CustomEmojiService implements OnApplicationShutdown {
|
export class CustomEmojiService implements OnApplicationShutdown {
|
||||||
|
|
|
@ -13,7 +13,7 @@ import type { NotesRepository } from '@/models/_.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { FanoutTimelineName, FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
import { FanoutTimelineName, FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import { isPureRenote } from '@/misc/is-pure-renote.js';
|
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { isReply } from '@/misc/is-reply.js';
|
import { isReply } from '@/misc/is-reply.js';
|
||||||
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
||||||
|
@ -95,7 +95,7 @@ export class FanoutTimelineEndpointService {
|
||||||
|
|
||||||
if (ps.excludePureRenotes) {
|
if (ps.excludePureRenotes) {
|
||||||
const parentFilter = filter;
|
const parentFilter = filter;
|
||||||
filter = (note) => !isPureRenote(note) && parentFilter(note);
|
filter = (note) => (!isRenote(note) || isQuote(note)) && parentFilter(note);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ps.me) {
|
if (ps.me) {
|
||||||
|
@ -116,7 +116,7 @@ export class FanoutTimelineEndpointService {
|
||||||
filter = (note) => {
|
filter = (note) => {
|
||||||
if (isUserRelated(note, userIdsWhoBlockingMe, ps.ignoreAuthorFromBlock)) return false;
|
if (isUserRelated(note, userIdsWhoBlockingMe, ps.ignoreAuthorFromBlock)) return false;
|
||||||
if (isUserRelated(note, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false;
|
if (isUserRelated(note, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false;
|
||||||
if (isPureRenote(note) && isUserRelated(note, userIdsWhoMeMutingRenotes, ps.ignoreAuthorFromMute)) return false;
|
if (!ps.ignoreAuthorFromMute && isRenote(note) && !isQuote(note) && userIdsWhoMeMutingRenotes.has(note.userId)) return false;
|
||||||
if (isInstanceMuted(note, userMutedInstances)) return false;
|
if (isInstanceMuted(note, userMutedInstances)) return false;
|
||||||
|
|
||||||
return parentFilter(note);
|
return parentFilter(note);
|
||||||
|
|
|
@ -14,11 +14,12 @@ import FFmpeg from 'fluent-ffmpeg';
|
||||||
import isSvg from 'is-svg';
|
import isSvg from 'is-svg';
|
||||||
import probeImageSize from 'probe-image-size';
|
import probeImageSize from 'probe-image-size';
|
||||||
import { type predictionType } from 'nsfwjs';
|
import { type predictionType } from 'nsfwjs';
|
||||||
import sharp from 'sharp';
|
|
||||||
import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
|
import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
|
||||||
import { encode } from 'blurhash';
|
import { encode } from 'blurhash';
|
||||||
import { createTempDir } from '@/misc/create-temp.js';
|
import { createTempDir } from '@/misc/create-temp.js';
|
||||||
import { AiService } from '@/core/AiService.js';
|
import { AiService } from '@/core/AiService.js';
|
||||||
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
|
import type Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
|
||||||
export type FileInfo = {
|
export type FileInfo = {
|
||||||
|
@ -49,9 +50,13 @@ const TYPE_SVG = {
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FileInfoService {
|
export class FileInfoService {
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private aiService: AiService,
|
private aiService: AiService,
|
||||||
|
private loggerService: LoggerService,
|
||||||
) {
|
) {
|
||||||
|
this.logger = this.loggerService.getLogger('file-info');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -317,6 +322,34 @@ export class FileInfoService {
|
||||||
return mime;
|
return mime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ビデオファイルにビデオトラックがあるかどうかチェック
|
||||||
|
* (ない場合:m4a, webmなど)
|
||||||
|
*
|
||||||
|
* @param path ファイルパス
|
||||||
|
* @returns ビデオトラックがあるかどうか(エラー発生時は常に`true`を返す)
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
private hasVideoTrackOnVideoFile(path: string): Promise<boolean> {
|
||||||
|
const sublogger = this.logger.createSubLogger('ffprobe');
|
||||||
|
sublogger.info(`Checking the video file. File path: ${path}`);
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
try {
|
||||||
|
FFmpeg.ffprobe(path, (err, metadata) => {
|
||||||
|
if (err) {
|
||||||
|
sublogger.warn(`Could not check the video file. Returns true. File path: ${path}`, err);
|
||||||
|
resolve(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(metadata.streams.some((stream) => stream.codec_type === 'video'));
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
sublogger.warn(`Could not check the video file. Returns true. File path: ${path}`, err as Error);
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect MIME Type and extension
|
* Detect MIME Type and extension
|
||||||
*/
|
*/
|
||||||
|
@ -339,6 +372,20 @@ export class FileInfoService {
|
||||||
return TYPE_SVG;
|
return TYPE_SVG;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((type.mime.startsWith('video') || type.mime === 'application/ogg') && !(await this.hasVideoTrackOnVideoFile(path))) {
|
||||||
|
const newMime = `audio/${type.mime.split('/')[1]}`;
|
||||||
|
if (newMime === 'audio/mp4') {
|
||||||
|
return {
|
||||||
|
mime: 'audio/mp4',
|
||||||
|
ext: 'm4a',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
mime: newMime,
|
||||||
|
ext: type.ext,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mime: this.fixMime(type.mime),
|
mime: this.fixMime(type.mime),
|
||||||
ext: type.ext,
|
ext: type.ext,
|
||||||
|
|
|
@ -6,10 +6,11 @@
|
||||||
import { URL } from 'node:url';
|
import { URL } from 'node:url';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as parse5 from 'parse5';
|
import * as parse5 from 'parse5';
|
||||||
import { Window } from 'happy-dom';
|
import { Window, XMLSerializer } from 'happy-dom';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { intersperse } from '@/misc/prelude/array.js';
|
import { intersperse } from '@/misc/prelude/array.js';
|
||||||
|
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||||
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js';
|
import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js';
|
||||||
|
@ -33,6 +34,8 @@ export class MfmService {
|
||||||
// some AP servers like Pixelfed use br tags as well as newlines
|
// some AP servers like Pixelfed use br tags as well as newlines
|
||||||
html = html.replace(/<br\s?\/?>\r?\n/gi, '\n');
|
html = html.replace(/<br\s?\/?>\r?\n/gi, '\n');
|
||||||
|
|
||||||
|
const normalizedHashtagNames = hashtagNames == null ? undefined : new Set<string>(hashtagNames.map(x => normalizeForSearch(x)));
|
||||||
|
|
||||||
const dom = parse5.parseFragment(html);
|
const dom = parse5.parseFragment(html);
|
||||||
|
|
||||||
let text = '';
|
let text = '';
|
||||||
|
@ -85,7 +88,7 @@ export class MfmService {
|
||||||
const href = node.attrs.find(x => x.name === 'href');
|
const href = node.attrs.find(x => x.name === 'href');
|
||||||
|
|
||||||
// ハッシュタグ
|
// ハッシュタグ
|
||||||
if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) {
|
if (normalizedHashtagNames && href && normalizedHashtagNames.has(normalizeForSearch(txt))) {
|
||||||
text += txt;
|
text += txt;
|
||||||
// メンション
|
// メンション
|
||||||
} else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) {
|
} else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) {
|
||||||
|
@ -244,6 +247,8 @@ export class MfmService {
|
||||||
|
|
||||||
const doc = window.document;
|
const doc = window.document;
|
||||||
|
|
||||||
|
const body = doc.createElement('p');
|
||||||
|
|
||||||
function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
|
function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
|
||||||
if (children) {
|
if (children) {
|
||||||
for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child);
|
for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child);
|
||||||
|
@ -454,8 +459,8 @@ export class MfmService {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
appendChildren(nodes, doc.body);
|
appendChildren(nodes, body);
|
||||||
|
|
||||||
return `<p>${doc.body.innerHTML}</p>`;
|
return new XMLSerializer().serializeToString(body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -306,7 +306,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check blocking
|
// Check blocking
|
||||||
if (data.renote && !this.isQuote(data)) {
|
if (this.isRenote(data) && !this.isQuote(data)) {
|
||||||
if (data.renote.userHost === null) {
|
if (data.renote.userHost === null) {
|
||||||
if (data.renote.userId !== user.id) {
|
if (data.renote.userId !== user.id) {
|
||||||
const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id);
|
const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id);
|
||||||
|
@ -641,7 +641,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it is renote
|
// If it is renote
|
||||||
if (data.renote) {
|
if (this.isRenote(data)) {
|
||||||
const type = this.isQuote(data) ? 'quote' : 'renote';
|
const type = this.isQuote(data) ? 'quote' : 'renote';
|
||||||
|
|
||||||
// Notify
|
// Notify
|
||||||
|
@ -725,9 +725,20 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private isQuote(note: Option): note is Option & { renote: MiNote } {
|
private isRenote(note: Option): note is Option & { renote: MiNote } {
|
||||||
// sync with misc/is-quote.ts
|
return note.renote != null;
|
||||||
return !!note.renote && (!!note.text || !!note.cw || (!!note.files && !!note.files.length) || !!note.poll);
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private isQuote(note: Option & { renote: MiNote }): note is Option & { renote: MiNote } & (
|
||||||
|
{ text: string } | { cw: string } | { reply: MiNote } | { poll: IPoll } | { files: MiDriveFile[] }
|
||||||
|
) {
|
||||||
|
// NOTE: SYNC WITH misc/is-quote.ts
|
||||||
|
return note.text != null ||
|
||||||
|
note.reply != null ||
|
||||||
|
note.cw != null ||
|
||||||
|
note.poll != null ||
|
||||||
|
(note.files != null && note.files.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -795,7 +806,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
private async renderNoteOrRenoteActivity(data: Option, note: MiNote) {
|
private async renderNoteOrRenoteActivity(data: Option, note: MiNote) {
|
||||||
if (data.localOnly) return null;
|
if (data.localOnly) return null;
|
||||||
|
|
||||||
const content = data.renote && !this.isQuote(data)
|
const content = this.isRenote(data) && !this.isQuote(data)
|
||||||
? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note)
|
? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note)
|
||||||
: this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false), note);
|
: this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false), note);
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ import { bindThis } from '@/decorators.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { SearchService } from '@/core/SearchService.js';
|
import { SearchService } from '@/core/SearchService.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
import { isPureRenote } from '@/misc/is-pure-renote.js';
|
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NoteDeleteService {
|
export class NoteDeleteService {
|
||||||
|
@ -79,7 +79,7 @@ export class NoteDeleteService {
|
||||||
let renote: MiNote | null = null;
|
let renote: MiNote | null = null;
|
||||||
|
|
||||||
// if deleted note is renote
|
// if deleted note is renote
|
||||||
if (isPureRenote(note)) {
|
if (isRenote(note) && !isQuote(note)) {
|
||||||
renote = await this.notesRepository.findOneBy({
|
renote = await this.notesRepository.findOneBy({
|
||||||
id: note.renoteId,
|
id: note.renoteId,
|
||||||
});
|
});
|
||||||
|
|
|
@ -101,7 +101,7 @@ export class PushNotificationService implements OnApplicationShutdown {
|
||||||
type,
|
type,
|
||||||
body: (type === 'notification' || type === 'unreadAntennaNote') ? truncateBody(type, body) : body,
|
body: (type === 'notification' || type === 'unreadAntennaNote') ? truncateBody(type, body) : body,
|
||||||
userId,
|
userId,
|
||||||
dateTime: (new Date()).getTime(),
|
dateTime: Date.now(),
|
||||||
}), {
|
}), {
|
||||||
proxy: this.config.proxy,
|
proxy: this.config.proxy,
|
||||||
}).catch((err: any) => {
|
}).catch((err: any) => {
|
||||||
|
|
|
@ -205,45 +205,79 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
private evalCond(user: MiUser, roles: MiRole[], value: RoleCondFormulaValue): boolean {
|
private evalCond(user: MiUser, roles: MiRole[], value: RoleCondFormulaValue): boolean {
|
||||||
try {
|
try {
|
||||||
switch (value.type) {
|
switch (value.type) {
|
||||||
|
// ~かつ~
|
||||||
case 'and': {
|
case 'and': {
|
||||||
return value.values.every(v => this.evalCond(user, roles, v));
|
return value.values.every(v => this.evalCond(user, roles, v));
|
||||||
}
|
}
|
||||||
|
// ~または~
|
||||||
case 'or': {
|
case 'or': {
|
||||||
return value.values.some(v => this.evalCond(user, roles, v));
|
return value.values.some(v => this.evalCond(user, roles, v));
|
||||||
}
|
}
|
||||||
|
// ~ではない
|
||||||
case 'not': {
|
case 'not': {
|
||||||
return !this.evalCond(user, roles, value.value);
|
return !this.evalCond(user, roles, value.value);
|
||||||
}
|
}
|
||||||
|
// マニュアルロールがアサインされている
|
||||||
case 'roleAssignedTo': {
|
case 'roleAssignedTo': {
|
||||||
return roles.some(r => r.id === value.roleId);
|
return roles.some(r => r.id === value.roleId);
|
||||||
}
|
}
|
||||||
|
// ローカルユーザのみ
|
||||||
case 'isLocal': {
|
case 'isLocal': {
|
||||||
return this.userEntityService.isLocalUser(user);
|
return this.userEntityService.isLocalUser(user);
|
||||||
}
|
}
|
||||||
|
// リモートユーザのみ
|
||||||
case 'isRemote': {
|
case 'isRemote': {
|
||||||
return this.userEntityService.isRemoteUser(user);
|
return this.userEntityService.isRemoteUser(user);
|
||||||
}
|
}
|
||||||
|
// サスペンド済みユーザである
|
||||||
|
case 'isSuspended': {
|
||||||
|
return user.isSuspended;
|
||||||
|
}
|
||||||
|
// 鍵アカウントユーザである
|
||||||
|
case 'isLocked': {
|
||||||
|
return user.isLocked;
|
||||||
|
}
|
||||||
|
// botユーザである
|
||||||
|
case 'isBot': {
|
||||||
|
return user.isBot;
|
||||||
|
}
|
||||||
|
// 猫である
|
||||||
|
case 'isCat': {
|
||||||
|
return user.isCat;
|
||||||
|
}
|
||||||
|
// 「ユーザを見つけやすくする」が有効なアカウント
|
||||||
|
case 'isExplorable': {
|
||||||
|
return user.isExplorable;
|
||||||
|
}
|
||||||
|
// ユーザが作成されてから指定期間経過した
|
||||||
case 'createdLessThan': {
|
case 'createdLessThan': {
|
||||||
return this.idService.parse(user.id).date.getTime() > (Date.now() - (value.sec * 1000));
|
return this.idService.parse(user.id).date.getTime() > (Date.now() - (value.sec * 1000));
|
||||||
}
|
}
|
||||||
|
// ユーザが作成されてから指定期間経っていない
|
||||||
case 'createdMoreThan': {
|
case 'createdMoreThan': {
|
||||||
return this.idService.parse(user.id).date.getTime() < (Date.now() - (value.sec * 1000));
|
return this.idService.parse(user.id).date.getTime() < (Date.now() - (value.sec * 1000));
|
||||||
}
|
}
|
||||||
|
// フォロワー数が指定値以下
|
||||||
case 'followersLessThanOrEq': {
|
case 'followersLessThanOrEq': {
|
||||||
return user.followersCount <= value.value;
|
return user.followersCount <= value.value;
|
||||||
}
|
}
|
||||||
|
// フォロワー数が指定値以上
|
||||||
case 'followersMoreThanOrEq': {
|
case 'followersMoreThanOrEq': {
|
||||||
return user.followersCount >= value.value;
|
return user.followersCount >= value.value;
|
||||||
}
|
}
|
||||||
|
// フォロー数が指定値以下
|
||||||
case 'followingLessThanOrEq': {
|
case 'followingLessThanOrEq': {
|
||||||
return user.followingCount <= value.value;
|
return user.followingCount <= value.value;
|
||||||
}
|
}
|
||||||
|
// フォロー数が指定値以上
|
||||||
case 'followingMoreThanOrEq': {
|
case 'followingMoreThanOrEq': {
|
||||||
return user.followingCount >= value.value;
|
return user.followingCount >= value.value;
|
||||||
}
|
}
|
||||||
|
// ノート数が指定値以下
|
||||||
case 'notesLessThanOrEq': {
|
case 'notesLessThanOrEq': {
|
||||||
return user.notesCount <= value.value;
|
return user.notesCount <= value.value;
|
||||||
}
|
}
|
||||||
|
// ノート数が指定値以上
|
||||||
case 'notesMoreThanOrEq': {
|
case 'notesMoreThanOrEq': {
|
||||||
return user.notesCount >= value.value;
|
return user.notesCount >= value.value;
|
||||||
}
|
}
|
||||||
|
|
|
@ -511,6 +511,12 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
if (blocking) throw new Error('blocking');
|
if (blocking) throw new Error('blocking');
|
||||||
if (blocked) throw new Error('blocked');
|
if (blocked) throw new Error('blocked');
|
||||||
|
|
||||||
|
// Remove old follow requests before creating a new one.
|
||||||
|
await this.followRequestsRepository.delete({
|
||||||
|
followeeId: followee.id,
|
||||||
|
followerId: follower.id,
|
||||||
|
});
|
||||||
|
|
||||||
const followRequest = await this.followRequestsRepository.insert({
|
const followRequest = await this.followRequestsRepository.insert({
|
||||||
id: this.idService.gen(),
|
id: this.idService.gen(),
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
|
|
|
@ -27,8 +27,9 @@ import { bindThis } from '@/decorators.js';
|
||||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||||
import { isNotNull } from '@/misc/is-not-null.js';
|
import { isNotNull } from '@/misc/is-not-null.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { LdSignatureService } from './LdSignatureService.js';
|
import { JsonLdService } from './JsonLdService.js';
|
||||||
import { ApMfmService } from './ApMfmService.js';
|
import { ApMfmService } from './ApMfmService.js';
|
||||||
|
import { CONTEXT } from './misc/contexts.js';
|
||||||
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
|
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
|
||||||
import type { PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures';
|
import type { PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures';
|
||||||
|
|
||||||
|
@ -56,7 +57,7 @@ export class ApRendererService {
|
||||||
private customEmojiService: CustomEmojiService,
|
private customEmojiService: CustomEmojiService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private driveFileEntityService: DriveFileEntityService,
|
private driveFileEntityService: DriveFileEntityService,
|
||||||
private ldSignatureService: LdSignatureService,
|
private jsonLdService: JsonLdService,
|
||||||
private userKeypairService: UserKeypairService,
|
private userKeypairService: UserKeypairService,
|
||||||
private apMfmService: ApMfmService,
|
private apMfmService: ApMfmService,
|
||||||
private mfmService: MfmService,
|
private mfmService: MfmService,
|
||||||
|
@ -166,6 +167,7 @@ export class ApRendererService {
|
||||||
mediaType: file.webpublicType ?? file.type,
|
mediaType: file.webpublicType ?? file.type,
|
||||||
url: this.driveFileEntityService.getPublicUrl(file),
|
url: this.driveFileEntityService.getPublicUrl(file),
|
||||||
name: file.comment,
|
name: file.comment,
|
||||||
|
sensitive: file.isSensitive,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -620,47 +622,16 @@ export class ApRendererService {
|
||||||
x.id = `${this.config.url}/${randomUUID()}`;
|
x.id = `${this.config.url}/${randomUUID()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.assign({
|
return Object.assign({ '@context': CONTEXT }, x as T & { id: string });
|
||||||
'@context': [
|
|
||||||
'https://www.w3.org/ns/activitystreams',
|
|
||||||
'https://w3id.org/security/v1',
|
|
||||||
{
|
|
||||||
Key: 'sec:Key',
|
|
||||||
// as non-standards
|
|
||||||
manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
|
|
||||||
sensitive: 'as:sensitive',
|
|
||||||
Hashtag: 'as:Hashtag',
|
|
||||||
quoteUrl: 'as:quoteUrl',
|
|
||||||
// Mastodon
|
|
||||||
toot: 'http://joinmastodon.org/ns#',
|
|
||||||
Emoji: 'toot:Emoji',
|
|
||||||
featured: 'toot:featured',
|
|
||||||
discoverable: 'toot:discoverable',
|
|
||||||
// schema
|
|
||||||
schema: 'http://schema.org#',
|
|
||||||
PropertyValue: 'schema:PropertyValue',
|
|
||||||
value: 'schema:value',
|
|
||||||
// Misskey
|
|
||||||
misskey: 'https://misskey-hub.net/ns#',
|
|
||||||
'_misskey_content': 'misskey:_misskey_content',
|
|
||||||
'_misskey_quote': 'misskey:_misskey_quote',
|
|
||||||
'_misskey_reaction': 'misskey:_misskey_reaction',
|
|
||||||
'_misskey_votes': 'misskey:_misskey_votes',
|
|
||||||
'_misskey_summary': 'misskey:_misskey_summary',
|
|
||||||
'isCat': 'misskey:isCat',
|
|
||||||
additionalPublicKeys: 'misskey:additionalPublicKeys',
|
|
||||||
// vcard
|
|
||||||
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}, x as T & { id: string });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async attachLdSignature(activity: any, user: { id: MiUser['id']; host: null; }, key: PrivateKeyWithPem): Promise<IActivity> {
|
public async attachLdSignature(activity: any, user: { id: MiUser['id']; host: null; }, key: PrivateKeyWithPem): Promise<IActivity> {
|
||||||
const ldSignature = this.ldSignatureService.use();
|
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||||
ldSignature.debug = false;
|
|
||||||
activity = await ldSignature.signRsaSignature2017(activity, key.privateKeyPem, key.keyId);
|
const jsonLd = this.jsonLdService.use();
|
||||||
|
jsonLd.debug = false;
|
||||||
|
activity = await jsonLd.signRsaSignature2017(activity, key.privateKeyPem, key.keyId);
|
||||||
|
|
||||||
return activity;
|
return activity;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,14 +7,14 @@ import * as crypto from 'node:crypto';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { CONTEXTS } from './misc/contexts.js';
|
import { CONTEXT, PRELOADED_CONTEXTS } from './misc/contexts.js';
|
||||||
import { validateContentTypeSetAsJsonLD } from './misc/validator.js';
|
import { validateContentTypeSetAsJsonLD } from './misc/validator.js';
|
||||||
import type { JsonLdDocument } from 'jsonld';
|
import type { JsonLdDocument } from 'jsonld';
|
||||||
import type { JsonLd, RemoteDocument } from 'jsonld/jsonld-spec.js';
|
import type { JsonLd as JsonLdObject, RemoteDocument } from 'jsonld/jsonld-spec.js';
|
||||||
|
|
||||||
// RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017
|
// RsaSignature2017 implementation is based on https://github.com/transmute-industries/RsaSignature2017
|
||||||
|
|
||||||
class LdSignature {
|
class JsonLd {
|
||||||
public debug = false;
|
public debug = false;
|
||||||
public preLoad = true;
|
public preLoad = true;
|
||||||
public loderTimeout = 5000;
|
public loderTimeout = 5000;
|
||||||
|
@ -89,10 +89,18 @@ class LdSignature {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async normalize(data: JsonLdDocument): Promise<string> {
|
public async compact(data: any, context: any = CONTEXT): Promise<JsonLdDocument> {
|
||||||
const customLoader = this.getLoader();
|
const customLoader = this.getLoader();
|
||||||
// XXX: Importing jsonld dynamically since Jest frequently fails to import it statically
|
// XXX: Importing jsonld dynamically since Jest frequently fails to import it statically
|
||||||
// https://github.com/misskey-dev/misskey/pull/9894#discussion_r1103753595
|
// https://github.com/misskey-dev/misskey/pull/9894#discussion_r1103753595
|
||||||
|
return (await import('jsonld')).default.compact(data, context, {
|
||||||
|
documentLoader: customLoader,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async normalize(data: JsonLdDocument): Promise<string> {
|
||||||
|
const customLoader = this.getLoader();
|
||||||
return (await import('jsonld')).default.normalize(data, {
|
return (await import('jsonld')).default.normalize(data, {
|
||||||
documentLoader: customLoader,
|
documentLoader: customLoader,
|
||||||
});
|
});
|
||||||
|
@ -104,11 +112,11 @@ class LdSignature {
|
||||||
if (!/^https?:\/\//.test(url)) throw new Error(`Invalid URL ${url}`);
|
if (!/^https?:\/\//.test(url)) throw new Error(`Invalid URL ${url}`);
|
||||||
|
|
||||||
if (this.preLoad) {
|
if (this.preLoad) {
|
||||||
if (url in CONTEXTS) {
|
if (url in PRELOADED_CONTEXTS) {
|
||||||
if (this.debug) console.debug(`HIT: ${url}`);
|
if (this.debug) console.debug(`HIT: ${url}`);
|
||||||
return {
|
return {
|
||||||
contextUrl: undefined,
|
contextUrl: undefined,
|
||||||
document: CONTEXTS[url],
|
document: PRELOADED_CONTEXTS[url],
|
||||||
documentUrl: url,
|
documentUrl: url,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -125,7 +133,7 @@ class LdSignature {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async fetchDocument(url: string): Promise<JsonLd> {
|
private async fetchDocument(url: string): Promise<JsonLdObject> {
|
||||||
const json = await this.httpRequestService.send(
|
const json = await this.httpRequestService.send(
|
||||||
url,
|
url,
|
||||||
{
|
{
|
||||||
|
@ -146,7 +154,7 @@ class LdSignature {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return json as JsonLd;
|
return json as JsonLdObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -158,14 +166,14 @@ class LdSignature {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LdSignatureService {
|
export class JsonLdService {
|
||||||
constructor(
|
constructor(
|
||||||
private httpRequestService: HttpRequestService,
|
private httpRequestService: HttpRequestService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public use(): LdSignature {
|
public use(): JsonLd {
|
||||||
return new LdSignature(this.httpRequestService);
|
return new JsonLd(this.httpRequestService);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,7 +3,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { JsonLd } from 'jsonld/jsonld-spec.js';
|
import type { Context, JsonLd } from 'jsonld/jsonld-spec.js';
|
||||||
|
|
||||||
/* eslint:disable:quotemark indent */
|
/* eslint:disable:quotemark indent */
|
||||||
const id_v1 = {
|
const id_v1 = {
|
||||||
|
@ -526,7 +526,42 @@ const activitystreams = {
|
||||||
},
|
},
|
||||||
} satisfies JsonLd;
|
} satisfies JsonLd;
|
||||||
|
|
||||||
export const CONTEXTS: Record<string, JsonLd> = {
|
const context_iris = [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
'https://w3id.org/security/v1',
|
||||||
|
];
|
||||||
|
|
||||||
|
const extension_context_definition = {
|
||||||
|
Key: 'sec:Key',
|
||||||
|
// as non-standards
|
||||||
|
manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
|
||||||
|
sensitive: 'as:sensitive',
|
||||||
|
Hashtag: 'as:Hashtag',
|
||||||
|
quoteUrl: 'as:quoteUrl',
|
||||||
|
// Mastodon
|
||||||
|
toot: 'http://joinmastodon.org/ns#',
|
||||||
|
Emoji: 'toot:Emoji',
|
||||||
|
featured: 'toot:featured',
|
||||||
|
discoverable: 'toot:discoverable',
|
||||||
|
// schema
|
||||||
|
schema: 'http://schema.org#',
|
||||||
|
PropertyValue: 'schema:PropertyValue',
|
||||||
|
value: 'schema:value',
|
||||||
|
// Misskey
|
||||||
|
misskey: 'https://misskey-hub.net/ns#',
|
||||||
|
'_misskey_content': 'misskey:_misskey_content',
|
||||||
|
'_misskey_quote': 'misskey:_misskey_quote',
|
||||||
|
'_misskey_reaction': 'misskey:_misskey_reaction',
|
||||||
|
'_misskey_votes': 'misskey:_misskey_votes',
|
||||||
|
'_misskey_summary': 'misskey:_misskey_summary',
|
||||||
|
'isCat': 'misskey:isCat',
|
||||||
|
// vcard
|
||||||
|
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
||||||
|
} satisfies Context;
|
||||||
|
|
||||||
|
export const CONTEXT: (string | Context)[] = [...context_iris, extension_context_definition];
|
||||||
|
|
||||||
|
export const PRELOADED_CONTEXTS: Record<string, JsonLd> = {
|
||||||
'https://w3id.org/identity/v1': id_v1,
|
'https://w3id.org/identity/v1': id_v1,
|
||||||
'https://w3id.org/security/v1': security_v1,
|
'https://w3id.org/security/v1': security_v1,
|
||||||
'https://www.w3.org/ns/activitystreams': activitystreams,
|
'https://www.w3.org/ns/activitystreams': activitystreams,
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { bindThis } from '@/decorators.js';
|
||||||
import { checkHttps } from '@/misc/check-https.js';
|
import { checkHttps } from '@/misc/check-https.js';
|
||||||
import { ApResolverService } from '../ApResolverService.js';
|
import { ApResolverService } from '../ApResolverService.js';
|
||||||
import { ApLoggerService } from '../ApLoggerService.js';
|
import { ApLoggerService } from '../ApLoggerService.js';
|
||||||
import type { IObject } from '../type.js';
|
import { isDocument, type IObject } from '../type.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApImageService {
|
export class ApImageService {
|
||||||
|
@ -39,7 +39,7 @@ export class ApImageService {
|
||||||
* Imageを作成します。
|
* Imageを作成します。
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async createImage(actor: MiRemoteUser, value: string | IObject): Promise<MiDriveFile> {
|
public async createImage(actor: MiRemoteUser, value: string | IObject): Promise<MiDriveFile | null> {
|
||||||
// 投稿者が凍結されていたらスキップ
|
// 投稿者が凍結されていたらスキップ
|
||||||
if (actor.isSuspended) {
|
if (actor.isSuspended) {
|
||||||
throw new Error('actor has been suspended');
|
throw new Error('actor has been suspended');
|
||||||
|
@ -47,16 +47,18 @@ export class ApImageService {
|
||||||
|
|
||||||
const image = await this.apResolverService.createResolver().resolve(value);
|
const image = await this.apResolverService.createResolver().resolve(value);
|
||||||
|
|
||||||
|
if (!isDocument(image)) return null;
|
||||||
|
|
||||||
if (image.url == null) {
|
if (image.url == null) {
|
||||||
throw new Error('invalid image: url not provided');
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof image.url !== 'string') {
|
if (typeof image.url !== 'string') {
|
||||||
throw new Error('invalid image: unexpected type of url: ' + JSON.stringify(image.url, null, 2));
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!checkHttps(image.url)) {
|
if (!checkHttps(image.url)) {
|
||||||
throw new Error('invalid image: unexpected schema of url: ' + image.url);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.info(`Creating the Image: ${image.url}`);
|
this.logger.info(`Creating the Image: ${image.url}`);
|
||||||
|
@ -86,12 +88,11 @@ export class ApImageService {
|
||||||
/**
|
/**
|
||||||
* Imageを解決します。
|
* Imageを解決します。
|
||||||
*
|
*
|
||||||
* Misskeyに対象のImageが登録されていればそれを返し、そうでなければ
|
* ImageをリモートサーバーからフェッチしてMisskeyに登録しそれを返します。
|
||||||
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
|
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async resolveImage(actor: MiRemoteUser, value: string | IObject): Promise<MiDriveFile> {
|
public async resolveImage(actor: MiRemoteUser, value: string | IObject): Promise<MiDriveFile | null> {
|
||||||
// TODO
|
// TODO: Misskeyに対象のImageが登録されていればそれを返す
|
||||||
|
|
||||||
// リモートサーバーからフェッチしてきて登録
|
// リモートサーバーからフェッチしてきて登録
|
||||||
return await this.createImage(actor, value);
|
return await this.createImage(actor, value);
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||||
import promiseLimit from 'promise-limit';
|
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { PollsRepository, EmojisRepository } from '@/models/_.js';
|
import type { PollsRepository, EmojisRepository } from '@/models/_.js';
|
||||||
|
@ -209,15 +208,13 @@ export class ApNoteService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添付ファイル
|
// 添付ファイル
|
||||||
// TODO: attachmentは必ずしもImageではない
|
const files: MiDriveFile[] = [];
|
||||||
// TODO: attachmentは必ずしも配列ではない
|
|
||||||
const limit = promiseLimit<MiDriveFile>(2);
|
for (const attach of toArray(note.attachment)) {
|
||||||
const files = (await Promise.all(toArray(note.attachment).map(attach => (
|
attach.sensitive ??= note.sensitive;
|
||||||
limit(() => this.apImageService.resolveImage(actor, {
|
const file = await this.apImageService.resolveImage(actor, attach);
|
||||||
...attach,
|
if (file) files.push(file);
|
||||||
sensitive: note.sensitive, // Noteがsensitiveなら添付もsensitiveにする
|
}
|
||||||
}))
|
|
||||||
))));
|
|
||||||
|
|
||||||
// リプライ
|
// リプライ
|
||||||
const reply: MiNote | null = note.inReplyTo
|
const reply: MiNote | null = note.inReplyTo
|
||||||
|
|
|
@ -25,6 +25,7 @@ export interface IObject {
|
||||||
endTime?: Date;
|
endTime?: Date;
|
||||||
icon?: any;
|
icon?: any;
|
||||||
image?: any;
|
image?: any;
|
||||||
|
mediaType?: string;
|
||||||
url?: ApObject | string;
|
url?: ApObject | string;
|
||||||
href?: string;
|
href?: string;
|
||||||
tag?: IObject | IObject[];
|
tag?: IObject | IObject[];
|
||||||
|
@ -239,14 +240,14 @@ export interface IKey extends IObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IApDocument extends IObject {
|
export interface IApDocument extends IObject {
|
||||||
type: 'Document';
|
type: 'Audio' | 'Document' | 'Image' | 'Page' | 'Video';
|
||||||
name: string | null;
|
|
||||||
mediaType: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IApImage extends IObject {
|
export const isDocument = (object: IObject): object is IApDocument =>
|
||||||
|
['Audio', 'Document', 'Image', 'Page', 'Video'].includes(getApType(object));
|
||||||
|
|
||||||
|
export interface IApImage extends IApDocument {
|
||||||
type: 'Image';
|
type: 'Image';
|
||||||
name: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICreate extends IActivity {
|
export interface ICreate extends IActivity {
|
||||||
|
|
|
@ -459,13 +459,15 @@ export default abstract class Chart<T extends Schema> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// bake unique count
|
// bake cardinality
|
||||||
for (const [k, v] of Object.entries(finalDiffs)) {
|
for (const [k, v] of Object.entries(finalDiffs)) {
|
||||||
if (this.schema[k].uniqueIncrement) {
|
if (this.schema[k].uniqueIncrement) {
|
||||||
const name = COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as keyof Columns<T>;
|
const name = COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as keyof Columns<T>;
|
||||||
const tempColumnName = UNIQUE_TEMP_COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as keyof TempColumnsForUnique<T>;
|
const tempColumnName = UNIQUE_TEMP_COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as keyof TempColumnsForUnique<T>;
|
||||||
queryForHour[name] = new Set([...(v as string[]), ...(logHour[tempColumnName] as unknown as string[])]).size;
|
const cardinalityOfHour = new Set([...(v as string[]), ...(logHour[tempColumnName] as unknown as string[])]).size;
|
||||||
queryForDay[name] = new Set([...(v as string[]), ...(logDay[tempColumnName] as unknown as string[])]).size;
|
const cardinalityOfDay = new Set([...(v as string[]), ...(logDay[tempColumnName] as unknown as string[])]).size;
|
||||||
|
queryForHour[name] = cardinalityOfHour;
|
||||||
|
queryForDay[name] = cardinalityOfDay;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -637,7 +639,7 @@ export default abstract class Chart<T extends Schema> {
|
||||||
// 要求された範囲にログがひとつもなかったら
|
// 要求された範囲にログがひとつもなかったら
|
||||||
if (logs.length === 0) {
|
if (logs.length === 0) {
|
||||||
// もっとも新しいログを持ってくる
|
// もっとも新しいログを持ってくる
|
||||||
// (すくなくともひとつログが無いと隙間埋めできないため)
|
// (すくなくともひとつログが無いと補間できないため)
|
||||||
const recentLog = await repository.findOne({
|
const recentLog = await repository.findOne({
|
||||||
where: group ? {
|
where: group ? {
|
||||||
group: group,
|
group: group,
|
||||||
|
@ -654,7 +656,7 @@ export default abstract class Chart<T extends Schema> {
|
||||||
// 要求された範囲の最も古い箇所に位置するログが存在しなかったら
|
// 要求された範囲の最も古い箇所に位置するログが存在しなかったら
|
||||||
} else if (!isTimeSame(new Date(logs.at(-1)!.date * 1000), gt)) {
|
} else if (!isTimeSame(new Date(logs.at(-1)!.date * 1000), gt)) {
|
||||||
// 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する
|
// 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する
|
||||||
// (隙間埋めできないため)
|
// (補間できないため)
|
||||||
const outdatedLog = await repository.findOne({
|
const outdatedLog = await repository.findOne({
|
||||||
where: {
|
where: {
|
||||||
date: LessThan(Chart.dateToTimestamp(gt)),
|
date: LessThan(Chart.dateToTimestamp(gt)),
|
||||||
|
@ -683,7 +685,7 @@ export default abstract class Chart<T extends Schema> {
|
||||||
if (log) {
|
if (log) {
|
||||||
chart.unshift(this.convertRawRecord(log));
|
chart.unshift(this.convertRawRecord(log));
|
||||||
} else {
|
} else {
|
||||||
// 隙間埋め
|
// 補間
|
||||||
const latest = logs.find(l => isTimeBefore(new Date(l.date * 1000), current));
|
const latest = logs.find(l => isTimeBefore(new Date(l.date * 1000), current));
|
||||||
const data = latest ? this.convertRawRecord(latest) : null;
|
const data = latest ? this.convertRawRecord(latest) : null;
|
||||||
chart.unshift(this.getNewLog(data));
|
chart.unshift(this.getNewLog(data));
|
||||||
|
|
|
@ -39,6 +39,7 @@ export class AntennaEntityService {
|
||||||
caseSensitive: antenna.caseSensitive,
|
caseSensitive: antenna.caseSensitive,
|
||||||
localOnly: antenna.localOnly,
|
localOnly: antenna.localOnly,
|
||||||
notify: antenna.notify,
|
notify: antenna.notify,
|
||||||
|
excludeBots: antenna.excludeBots,
|
||||||
withReplies: antenna.withReplies,
|
withReplies: antenna.withReplies,
|
||||||
withFile: antenna.withFile,
|
withFile: antenna.withFile,
|
||||||
isActive: antenna.isActive,
|
isActive: antenna.isActive,
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { ClipFavoritesRepository, ClipsRepository, MiUser } from '@/models/_.js';
|
import type { ClipNotesRepository, ClipFavoritesRepository, ClipsRepository, MiUser } from '@/models/_.js';
|
||||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import type { } from '@/models/Blocking.js';
|
import type { } from '@/models/Blocking.js';
|
||||||
|
@ -20,6 +20,9 @@ export class ClipEntityService {
|
||||||
@Inject(DI.clipsRepository)
|
@Inject(DI.clipsRepository)
|
||||||
private clipsRepository: ClipsRepository,
|
private clipsRepository: ClipsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.clipNotesRepository)
|
||||||
|
private clipNotesRepository: ClipNotesRepository,
|
||||||
|
|
||||||
@Inject(DI.clipFavoritesRepository)
|
@Inject(DI.clipFavoritesRepository)
|
||||||
private clipFavoritesRepository: ClipFavoritesRepository,
|
private clipFavoritesRepository: ClipFavoritesRepository,
|
||||||
|
|
||||||
|
@ -47,6 +50,7 @@ export class ClipEntityService {
|
||||||
isPublic: clip.isPublic,
|
isPublic: clip.isPublic,
|
||||||
favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
|
favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
|
||||||
isFavorited: meId ? await this.clipFavoritesRepository.exists({ where: { clipId: clip.id, userId: meId } }) : undefined,
|
isFavorited: meId ? await this.clipFavoritesRepository.exists({ where: { clipId: clip.id, userId: meId } }) : undefined,
|
||||||
|
notesCount: meId ? await this.clipNotesRepository.countBy({ clipId: clip.id }) : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -111,6 +111,7 @@ export class MetaEntityService {
|
||||||
policies: { ...DEFAULT_POLICIES, ...instance.policies },
|
policies: { ...DEFAULT_POLICIES, ...instance.policies },
|
||||||
|
|
||||||
mediaProxy: this.config.mediaProxy,
|
mediaProxy: this.config.mediaProxy,
|
||||||
|
enableUrlPreview: instance.urlPreviewEnabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
return packed;
|
return packed;
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import _Ajv from 'ajv';
|
import _Ajv from 'ajv';
|
||||||
import { ModuleRef } from '@nestjs/core';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
|
import { In } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
|
@ -14,9 +15,30 @@ import type { Promiseable } from '@/misc/prelude/await-all.js';
|
||||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||||
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
|
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
|
||||||
import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
||||||
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/User.js';
|
import {
|
||||||
import { MiNotification } from '@/models/Notification.js';
|
birthdaySchema,
|
||||||
import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, UserNotePiningsRepository, UserProfilesRepository, AnnouncementReadsRepository, AnnouncementsRepository, MiUserProfile, RenoteMutingsRepository, UserMemoRepository } from '@/models/_.js';
|
descriptionSchema,
|
||||||
|
localUsernameSchema,
|
||||||
|
locationSchema,
|
||||||
|
nameSchema,
|
||||||
|
passwordSchema,
|
||||||
|
} from '@/models/User.js';
|
||||||
|
import type {
|
||||||
|
BlockingsRepository,
|
||||||
|
FollowingsRepository,
|
||||||
|
FollowRequestsRepository,
|
||||||
|
MiFollowing,
|
||||||
|
MiUserNotePining,
|
||||||
|
MiUserProfile,
|
||||||
|
MutingsRepository,
|
||||||
|
NoteUnreadsRepository,
|
||||||
|
RenoteMutingsRepository,
|
||||||
|
UserMemoRepository,
|
||||||
|
UserNotePiningsRepository,
|
||||||
|
UserProfilesRepository,
|
||||||
|
UserSecurityKeysRepository,
|
||||||
|
UsersRepository,
|
||||||
|
} from '@/models/_.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
||||||
|
@ -46,11 +68,23 @@ function isRemoteUser(user: MiUser | { host: MiUser['host'] }): boolean {
|
||||||
return !isLocalUser(user);
|
return !isLocalUser(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UserRelation = {
|
||||||
|
id: MiUser['id']
|
||||||
|
following: MiFollowing | null,
|
||||||
|
isFollowing: boolean
|
||||||
|
isFollowed: boolean
|
||||||
|
hasPendingFollowRequestFromYou: boolean
|
||||||
|
hasPendingFollowRequestToYou: boolean
|
||||||
|
isBlocking: boolean
|
||||||
|
isBlocked: boolean
|
||||||
|
isMuted: boolean
|
||||||
|
isRenoteMuted: boolean
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserEntityService implements OnModuleInit {
|
export class UserEntityService implements OnModuleInit {
|
||||||
private apPersonService: ApPersonService;
|
private apPersonService: ApPersonService;
|
||||||
private noteEntityService: NoteEntityService;
|
private noteEntityService: NoteEntityService;
|
||||||
private driveFileEntityService: DriveFileEntityService;
|
|
||||||
private pageEntityService: PageEntityService;
|
private pageEntityService: PageEntityService;
|
||||||
private customEmojiService: CustomEmojiService;
|
private customEmojiService: CustomEmojiService;
|
||||||
private announcementService: AnnouncementService;
|
private announcementService: AnnouncementService;
|
||||||
|
@ -89,9 +123,6 @@ export class UserEntityService implements OnModuleInit {
|
||||||
@Inject(DI.renoteMutingsRepository)
|
@Inject(DI.renoteMutingsRepository)
|
||||||
private renoteMutingsRepository: RenoteMutingsRepository,
|
private renoteMutingsRepository: RenoteMutingsRepository,
|
||||||
|
|
||||||
@Inject(DI.driveFilesRepository)
|
|
||||||
private driveFilesRepository: DriveFilesRepository,
|
|
||||||
|
|
||||||
@Inject(DI.noteUnreadsRepository)
|
@Inject(DI.noteUnreadsRepository)
|
||||||
private noteUnreadsRepository: NoteUnreadsRepository,
|
private noteUnreadsRepository: NoteUnreadsRepository,
|
||||||
|
|
||||||
|
@ -101,12 +132,6 @@ export class UserEntityService implements OnModuleInit {
|
||||||
@Inject(DI.userProfilesRepository)
|
@Inject(DI.userProfilesRepository)
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
|
||||||
@Inject(DI.announcementReadsRepository)
|
|
||||||
private announcementReadsRepository: AnnouncementReadsRepository,
|
|
||||||
|
|
||||||
@Inject(DI.announcementsRepository)
|
|
||||||
private announcementsRepository: AnnouncementsRepository,
|
|
||||||
|
|
||||||
@Inject(DI.userMemosRepository)
|
@Inject(DI.userMemosRepository)
|
||||||
private userMemosRepository: UserMemoRepository,
|
private userMemosRepository: UserMemoRepository,
|
||||||
) {
|
) {
|
||||||
|
@ -115,7 +140,6 @@ export class UserEntityService implements OnModuleInit {
|
||||||
onModuleInit() {
|
onModuleInit() {
|
||||||
this.apPersonService = this.moduleRef.get('ApPersonService');
|
this.apPersonService = this.moduleRef.get('ApPersonService');
|
||||||
this.noteEntityService = this.moduleRef.get('NoteEntityService');
|
this.noteEntityService = this.moduleRef.get('NoteEntityService');
|
||||||
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
|
|
||||||
this.pageEntityService = this.moduleRef.get('PageEntityService');
|
this.pageEntityService = this.moduleRef.get('PageEntityService');
|
||||||
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
|
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
|
||||||
this.announcementService = this.moduleRef.get('AnnouncementService');
|
this.announcementService = this.moduleRef.get('AnnouncementService');
|
||||||
|
@ -138,7 +162,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
public isRemoteUser = isRemoteUser;
|
public isRemoteUser = isRemoteUser;
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getRelation(me: MiUser['id'], target: MiUser['id']) {
|
public async getRelation(me: MiUser['id'], target: MiUser['id']): Promise<UserRelation> {
|
||||||
const [
|
const [
|
||||||
following,
|
following,
|
||||||
isFollowed,
|
isFollowed,
|
||||||
|
@ -211,6 +235,59 @@ export class UserEntityService implements OnModuleInit {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async getRelations(me: MiUser['id'], targets: MiUser['id'][]): Promise<Map<MiUser['id'], UserRelation>> {
|
||||||
|
const [
|
||||||
|
followers,
|
||||||
|
followees,
|
||||||
|
followersRequests,
|
||||||
|
followeesRequests,
|
||||||
|
blockers,
|
||||||
|
blockees,
|
||||||
|
muters,
|
||||||
|
renoteMuters,
|
||||||
|
] = await Promise.all([
|
||||||
|
this.followingsRepository.findBy({ followerId: me })
|
||||||
|
.then(f => new Map(f.map(it => [it.followeeId, it]))),
|
||||||
|
this.followingsRepository.findBy({ followeeId: me })
|
||||||
|
.then(it => it.map(it => it.followerId)),
|
||||||
|
this.followRequestsRepository.findBy({ followerId: me })
|
||||||
|
.then(it => it.map(it => it.followeeId)),
|
||||||
|
this.followRequestsRepository.findBy({ followeeId: me })
|
||||||
|
.then(it => it.map(it => it.followerId)),
|
||||||
|
this.blockingsRepository.findBy({ blockerId: me })
|
||||||
|
.then(it => it.map(it => it.blockeeId)),
|
||||||
|
this.blockingsRepository.findBy({ blockeeId: me })
|
||||||
|
.then(it => it.map(it => it.blockerId)),
|
||||||
|
this.mutingsRepository.findBy({ muterId: me })
|
||||||
|
.then(it => it.map(it => it.muteeId)),
|
||||||
|
this.renoteMutingsRepository.findBy({ muterId: me })
|
||||||
|
.then(it => it.map(it => it.muteeId)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return new Map(
|
||||||
|
targets.map(target => {
|
||||||
|
const following = followers.get(target) ?? null;
|
||||||
|
|
||||||
|
return [
|
||||||
|
target,
|
||||||
|
{
|
||||||
|
id: target,
|
||||||
|
following: following,
|
||||||
|
isFollowing: following != null,
|
||||||
|
isFollowed: followees.includes(target),
|
||||||
|
hasPendingFollowRequestFromYou: followersRequests.includes(target),
|
||||||
|
hasPendingFollowRequestToYou: followeesRequests.includes(target),
|
||||||
|
isBlocking: blockers.includes(target),
|
||||||
|
isBlocked: blockees.includes(target),
|
||||||
|
isMuted: muters.includes(target),
|
||||||
|
isRenoteMuted: renoteMuters.includes(target),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getHasUnreadAntenna(userId: MiUser['id']): Promise<boolean> {
|
public async getHasUnreadAntenna(userId: MiUser['id']): Promise<boolean> {
|
||||||
/*
|
/*
|
||||||
|
@ -303,6 +380,9 @@ export class UserEntityService implements OnModuleInit {
|
||||||
schema?: S,
|
schema?: S,
|
||||||
includeSecrets?: boolean,
|
includeSecrets?: boolean,
|
||||||
userProfile?: MiUserProfile,
|
userProfile?: MiUserProfile,
|
||||||
|
userRelations?: Map<MiUser['id'], UserRelation>,
|
||||||
|
userMemos?: Map<MiUser['id'], string | null>,
|
||||||
|
pinNotes?: Map<MiUser['id'], MiUserNotePining[]>,
|
||||||
},
|
},
|
||||||
): Promise<Packed<S>> {
|
): Promise<Packed<S>> {
|
||||||
const opts = Object.assign({
|
const opts = Object.assign({
|
||||||
|
@ -317,13 +397,41 @@ export class UserEntityService implements OnModuleInit {
|
||||||
const isMe = meId === user.id;
|
const isMe = meId === user.id;
|
||||||
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
|
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
|
||||||
|
|
||||||
const relation = meId && !isMe && isDetailed ? await this.getRelation(meId, user.id) : null;
|
const profile = isDetailed
|
||||||
const pins = isDetailed ? await this.userNotePiningsRepository.createQueryBuilder('pin')
|
? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }))
|
||||||
.where('pin.userId = :userId', { userId: user.id })
|
: null;
|
||||||
.innerJoinAndSelect('pin.note', 'note')
|
|
||||||
.orderBy('pin.id', 'DESC')
|
let relation: UserRelation | null = null;
|
||||||
.getMany() : [];
|
if (meId && !isMe && isDetailed) {
|
||||||
const profile = isDetailed ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null;
|
if (opts.userRelations) {
|
||||||
|
relation = opts.userRelations.get(user.id) ?? null;
|
||||||
|
} else {
|
||||||
|
relation = await this.getRelation(meId, user.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let memo: string | null = null;
|
||||||
|
if (isDetailed && meId) {
|
||||||
|
if (opts.userMemos) {
|
||||||
|
memo = opts.userMemos.get(user.id) ?? null;
|
||||||
|
} else {
|
||||||
|
memo = await this.userMemosRepository.findOneBy({ userId: meId, targetUserId: user.id })
|
||||||
|
.then(row => row?.memo ?? null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pins: MiUserNotePining[] = [];
|
||||||
|
if (isDetailed) {
|
||||||
|
if (opts.pinNotes) {
|
||||||
|
pins = opts.pinNotes.get(user.id) ?? [];
|
||||||
|
} else {
|
||||||
|
pins = await this.userNotePiningsRepository.createQueryBuilder('pin')
|
||||||
|
.where('pin.userId = :userId', { userId: user.id })
|
||||||
|
.innerJoinAndSelect('pin.note', 'note')
|
||||||
|
.orderBy('pin.id', 'DESC')
|
||||||
|
.getMany();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const followingCount = profile == null ? null :
|
const followingCount = profile == null ? null :
|
||||||
(profile.followingVisibility === 'public') || isMe ? user.followingCount :
|
(profile.followingVisibility === 'public') || isMe ? user.followingCount :
|
||||||
|
@ -416,9 +524,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
twoFactorEnabled: profile!.twoFactorEnabled,
|
twoFactorEnabled: profile!.twoFactorEnabled,
|
||||||
usePasswordLessLogin: profile!.usePasswordLessLogin,
|
usePasswordLessLogin: profile!.usePasswordLessLogin,
|
||||||
securityKeys: profile!.twoFactorEnabled
|
securityKeys: profile!.twoFactorEnabled
|
||||||
? this.userSecurityKeysRepository.countBy({
|
? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
|
||||||
userId: user.id,
|
|
||||||
}).then(result => result >= 1)
|
|
||||||
: false,
|
: false,
|
||||||
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
|
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
|
||||||
id: role.id,
|
id: role.id,
|
||||||
|
@ -430,10 +536,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
isAdministrator: role.isAdministrator,
|
isAdministrator: role.isAdministrator,
|
||||||
displayOrder: role.displayOrder,
|
displayOrder: role.displayOrder,
|
||||||
}))),
|
}))),
|
||||||
memo: meId == null ? null : await this.userMemosRepository.findOneBy({
|
memo: memo,
|
||||||
userId: meId,
|
|
||||||
targetUserId: user.id,
|
|
||||||
}).then(row => row?.memo ?? null),
|
|
||||||
moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
|
moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
|
||||||
} : {}),
|
} : {}),
|
||||||
|
|
||||||
|
@ -514,7 +617,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
return await awaitAll(packed);
|
return await awaitAll(packed);
|
||||||
}
|
}
|
||||||
|
|
||||||
public packMany<S extends 'MeDetailed' | 'UserDetailedNotMe' | 'UserDetailed' | 'UserLite' = 'UserLite'>(
|
public async packMany<S extends 'MeDetailed' | 'UserDetailedNotMe' | 'UserDetailed' | 'UserLite' = 'UserLite'>(
|
||||||
users: (MiUser['id'] | MiUser)[],
|
users: (MiUser['id'] | MiUser)[],
|
||||||
me?: { id: MiUser['id'] } | null | undefined,
|
me?: { id: MiUser['id'] } | null | undefined,
|
||||||
options?: {
|
options?: {
|
||||||
|
@ -522,6 +625,70 @@ export class UserEntityService implements OnModuleInit {
|
||||||
includeSecrets?: boolean,
|
includeSecrets?: boolean,
|
||||||
},
|
},
|
||||||
): Promise<Packed<S>[]> {
|
): Promise<Packed<S>[]> {
|
||||||
return Promise.all(users.map(u => this.pack(u, me, options)));
|
// -- IDのみの要素を補完して完全なエンティティ一覧を作る
|
||||||
|
|
||||||
|
const _users = users.filter((user): user is MiUser => typeof user !== 'string');
|
||||||
|
if (_users.length !== users.length) {
|
||||||
|
_users.push(
|
||||||
|
...await this.usersRepository.findBy({
|
||||||
|
id: In(users.filter((user): user is string => typeof user === 'string')),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const _userIds = _users.map(u => u.id);
|
||||||
|
|
||||||
|
// -- 特に前提条件のない値群を取得
|
||||||
|
|
||||||
|
const profilesMap = await this.userProfilesRepository.findBy({ userId: In(_userIds) })
|
||||||
|
.then(profiles => new Map(profiles.map(p => [p.userId, p])));
|
||||||
|
|
||||||
|
// -- 実行者の有無や指定スキーマの種別によって要否が異なる値群を取得
|
||||||
|
|
||||||
|
let userRelations: Map<MiUser['id'], UserRelation> = new Map();
|
||||||
|
let userMemos: Map<MiUser['id'], string | null> = new Map();
|
||||||
|
let pinNotes: Map<MiUser['id'], MiUserNotePining[]> = new Map();
|
||||||
|
|
||||||
|
if (options?.schema !== 'UserLite') {
|
||||||
|
const meId = me ? me.id : null;
|
||||||
|
if (meId) {
|
||||||
|
userMemos = await this.userMemosRepository.findBy({ userId: meId })
|
||||||
|
.then(memos => new Map(memos.map(memo => [memo.targetUserId, memo.memo])));
|
||||||
|
|
||||||
|
if (_userIds.length > 0) {
|
||||||
|
userRelations = await this.getRelations(meId, _userIds);
|
||||||
|
pinNotes = await this.userNotePiningsRepository.createQueryBuilder('pin')
|
||||||
|
.where('pin.userId IN (:...userIds)', { userIds: _userIds })
|
||||||
|
.innerJoinAndSelect('pin.note', 'note')
|
||||||
|
.getMany()
|
||||||
|
.then(pinsNotes => {
|
||||||
|
const map = new Map<MiUser['id'], MiUserNotePining[]>();
|
||||||
|
for (const note of pinsNotes) {
|
||||||
|
const notes = map.get(note.userId) ?? [];
|
||||||
|
notes.push(note);
|
||||||
|
map.set(note.userId, notes);
|
||||||
|
}
|
||||||
|
for (const [, notes] of map.entries()) {
|
||||||
|
// pack側ではDESCで取得しているので、それに合わせて降順に並び替えておく
|
||||||
|
notes.sort((a, b) => b.id.localeCompare(a.id));
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
_users.map(u => this.pack(
|
||||||
|
u,
|
||||||
|
me,
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
userProfile: profilesMap.get(u.id),
|
||||||
|
userRelations: userRelations,
|
||||||
|
userMemos: userMemos,
|
||||||
|
pinNotes: pinNotes,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
import type { onRequestHookHandler } from 'fastify';
|
import type { onRequestHookHandler } from 'fastify';
|
||||||
|
|
||||||
export const handleRequestRedirectToOmitSearch: onRequestHookHandler = (request, reply, done) => {
|
export const handleRequestRedirectToOmitSearch: onRequestHookHandler = (request, reply, done) => {
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
import type { MiNote } from '@/models/Note.js';
|
|
||||||
|
|
||||||
export function isPureRenote(note: MiNote): note is MiNote & { renoteId: NonNullable<MiNote['renoteId']> } {
|
|
||||||
if (!note.renoteId) return false;
|
|
||||||
|
|
||||||
if (note.text) return false; // it's quoted with text
|
|
||||||
if (note.fileIds.length !== 0) return false; // it's quoted with files
|
|
||||||
if (note.hasPoll) return false; // it's quoted with poll
|
|
||||||
return true;
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { MiNote } from '@/models/Note.js';
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
|
||||||
export default function(note: MiNote): boolean {
|
|
||||||
// sync with NoteCreateService.isQuote
|
|
||||||
return note.renoteId != null && (note.text != null || note.hasPoll || (note.fileIds != null && note.fileIds.length > 0));
|
|
||||||
}
|
|
67
packages/backend/src/misc/is-renote.ts
Normal file
67
packages/backend/src/misc/is-renote.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { MiNote } from '@/models/Note.js';
|
||||||
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
|
|
||||||
|
type Renote =
|
||||||
|
MiNote & {
|
||||||
|
renoteId: NonNullable<MiNote['renoteId']>
|
||||||
|
};
|
||||||
|
|
||||||
|
type Quote =
|
||||||
|
Renote & ({
|
||||||
|
text: NonNullable<MiNote['text']>
|
||||||
|
} | {
|
||||||
|
cw: NonNullable<MiNote['cw']>
|
||||||
|
} | {
|
||||||
|
replyId: NonNullable<MiNote['replyId']>
|
||||||
|
reply: NonNullable<MiNote['reply']>
|
||||||
|
} | {
|
||||||
|
hasPoll: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export function isRenote(note: MiNote): note is Renote {
|
||||||
|
return note.renoteId != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isQuote(note: Renote): note is Quote {
|
||||||
|
// NOTE: SYNC WITH NoteCreateService.isQuote
|
||||||
|
return note.text != null ||
|
||||||
|
note.cw != null ||
|
||||||
|
note.replyId != null ||
|
||||||
|
note.hasPoll ||
|
||||||
|
note.fileIds.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PackedRenote =
|
||||||
|
Packed<'Note'> & {
|
||||||
|
renoteId: NonNullable<Packed<'Note'>['renoteId']>
|
||||||
|
};
|
||||||
|
|
||||||
|
type PackedQuote =
|
||||||
|
PackedRenote & ({
|
||||||
|
text: NonNullable<Packed<'Note'>['text']>
|
||||||
|
} | {
|
||||||
|
cw: NonNullable<Packed<'Note'>['cw']>
|
||||||
|
} | {
|
||||||
|
replyId: NonNullable<Packed<'Note'>['replyId']>
|
||||||
|
} | {
|
||||||
|
poll: NonNullable<Packed<'Note'>['poll']>
|
||||||
|
} | {
|
||||||
|
fileIds: NonNullable<Packed<'Note'>['fileIds']>
|
||||||
|
});
|
||||||
|
|
||||||
|
export function isRenotePacked(note: Packed<'Note'>): note is PackedRenote {
|
||||||
|
return note.renoteId != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isQuotePacked(note: PackedRenote): note is PackedQuote {
|
||||||
|
return note.text != null ||
|
||||||
|
note.cw != null ||
|
||||||
|
note.replyId != null ||
|
||||||
|
note.poll != null ||
|
||||||
|
(note.fileIds != null && note.fileIds.length > 0);
|
||||||
|
}
|
|
@ -48,6 +48,7 @@ import {
|
||||||
packedRoleCondFormulaValueCreatedSchema,
|
packedRoleCondFormulaValueCreatedSchema,
|
||||||
packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
|
packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
|
||||||
packedRoleCondFormulaValueSchema,
|
packedRoleCondFormulaValueSchema,
|
||||||
|
packedRoleCondFormulaValueUserSettingBooleanSchema,
|
||||||
} from '@/models/json-schema/role.js';
|
} from '@/models/json-schema/role.js';
|
||||||
import { packedAdSchema } from '@/models/json-schema/ad.js';
|
import { packedAdSchema } from '@/models/json-schema/ad.js';
|
||||||
import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js';
|
import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js';
|
||||||
|
@ -97,6 +98,7 @@ export const refs = {
|
||||||
RoleCondFormulaLogics: packedRoleCondFormulaLogicsSchema,
|
RoleCondFormulaLogics: packedRoleCondFormulaLogicsSchema,
|
||||||
RoleCondFormulaValueNot: packedRoleCondFormulaValueNot,
|
RoleCondFormulaValueNot: packedRoleCondFormulaValueNot,
|
||||||
RoleCondFormulaValueIsLocalOrRemote: packedRoleCondFormulaValueIsLocalOrRemoteSchema,
|
RoleCondFormulaValueIsLocalOrRemote: packedRoleCondFormulaValueIsLocalOrRemoteSchema,
|
||||||
|
RoleCondFormulaValueUserSettingBooleanSchema: packedRoleCondFormulaValueUserSettingBooleanSchema,
|
||||||
RoleCondFormulaValueAssignedRole: packedRoleCondFormulaValueAssignedRoleSchema,
|
RoleCondFormulaValueAssignedRole: packedRoleCondFormulaValueAssignedRoleSchema,
|
||||||
RoleCondFormulaValueCreated: packedRoleCondFormulaValueCreatedSchema,
|
RoleCondFormulaValueCreated: packedRoleCondFormulaValueCreatedSchema,
|
||||||
RoleCondFormulaFollowersOrFollowingOrNotes: packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
|
RoleCondFormulaFollowersOrFollowingOrNotes: packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
export type FetchFunction<K, V> = (key: K) => Promise<V>;
|
export type FetchFunction<K, V> = (key: K) => Promise<V>;
|
||||||
|
|
||||||
type ResolveReject<V> = Parameters<ConstructorParameters<typeof Promise<V>>[0]>;
|
type ResolveReject<V> = Parameters<ConstructorParameters<typeof Promise<V>>[0]>;
|
||||||
|
|
|
@ -72,6 +72,11 @@ export class MiAntenna {
|
||||||
})
|
})
|
||||||
public caseSensitive: boolean;
|
public caseSensitive: boolean;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public excludeBots: boolean;
|
||||||
|
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: false,
|
default: false,
|
||||||
})
|
})
|
||||||
|
|
|
@ -277,12 +277,6 @@ export class MiMeta {
|
||||||
})
|
})
|
||||||
public enableSensitiveMediaDetectionForVideos: boolean;
|
public enableSensitiveMediaDetectionForVideos: boolean;
|
||||||
|
|
||||||
@Column('varchar', {
|
|
||||||
length: 1024,
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
public summalyProxy: string | null;
|
|
||||||
|
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: false,
|
default: false,
|
||||||
})
|
})
|
||||||
|
@ -588,4 +582,36 @@ export class MiMeta {
|
||||||
default: 0,
|
default: 0,
|
||||||
})
|
})
|
||||||
public notesPerOneAd: number;
|
public notesPerOneAd: number;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: true,
|
||||||
|
})
|
||||||
|
public urlPreviewEnabled: boolean;
|
||||||
|
|
||||||
|
@Column('integer', {
|
||||||
|
default: 10000,
|
||||||
|
})
|
||||||
|
public urlPreviewTimeout: number;
|
||||||
|
|
||||||
|
@Column('bigint', {
|
||||||
|
default: 1024 * 1024 * 10,
|
||||||
|
})
|
||||||
|
public urlPreviewMaximumContentLength: number;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: true,
|
||||||
|
})
|
||||||
|
public urlPreviewRequireContentLength: boolean;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 1024,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public urlPreviewSummaryProxyUrl: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 1024,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public urlPreviewUserAgent: string | null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||||
import { id } from './util/id.js';
|
import { id } from './util/id.js';
|
||||||
import { MiUser } from './User.js';
|
import { MiUser } from './User.js';
|
||||||
|
|
|
@ -6,69 +6,149 @@
|
||||||
import { Entity, Column, PrimaryColumn } from 'typeorm';
|
import { Entity, Column, PrimaryColumn } from 'typeorm';
|
||||||
import { id } from './util/id.js';
|
import { id } from './util/id.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ~かつ~
|
||||||
|
* 複数の条件を同時に満たす場合のみ成立とする
|
||||||
|
*/
|
||||||
type CondFormulaValueAnd = {
|
type CondFormulaValueAnd = {
|
||||||
type: 'and';
|
type: 'and';
|
||||||
values: RoleCondFormulaValue[];
|
values: RoleCondFormulaValue[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ~または~
|
||||||
|
* 複数の条件のうち、いずれかを満たす場合のみ成立とする
|
||||||
|
*/
|
||||||
type CondFormulaValueOr = {
|
type CondFormulaValueOr = {
|
||||||
type: 'or';
|
type: 'or';
|
||||||
values: RoleCondFormulaValue[];
|
values: RoleCondFormulaValue[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ~ではない
|
||||||
|
* 条件を満たさない場合のみ成立とする
|
||||||
|
*/
|
||||||
type CondFormulaValueNot = {
|
type CondFormulaValueNot = {
|
||||||
type: 'not';
|
type: 'not';
|
||||||
value: RoleCondFormulaValue;
|
value: RoleCondFormulaValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ローカルユーザーのみ成立とする
|
||||||
|
*/
|
||||||
type CondFormulaValueIsLocal = {
|
type CondFormulaValueIsLocal = {
|
||||||
type: 'isLocal';
|
type: 'isLocal';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* リモートユーザーのみ成立とする
|
||||||
|
*/
|
||||||
type CondFormulaValueIsRemote = {
|
type CondFormulaValueIsRemote = {
|
||||||
type: 'isRemote';
|
type: 'isRemote';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 既に指定のマニュアルロールにアサインされている場合のみ成立とする
|
||||||
|
*/
|
||||||
type CondFormulaValueRoleAssignedTo = {
|
type CondFormulaValueRoleAssignedTo = {
|
||||||
type: 'roleAssignedTo';
|
type: 'roleAssignedTo';
|
||||||
roleId: string;
|
roleId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* サスペンド済みアカウントの場合のみ成立とする
|
||||||
|
*/
|
||||||
|
type CondFormulaValueIsSuspended = {
|
||||||
|
type: 'isSuspended';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 鍵アカウントの場合のみ成立とする
|
||||||
|
*/
|
||||||
|
type CondFormulaValueIsLocked = {
|
||||||
|
type: 'isLocked';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* botアカウントの場合のみ成立とする
|
||||||
|
*/
|
||||||
|
type CondFormulaValueIsBot = {
|
||||||
|
type: 'isBot';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 猫アカウントの場合のみ成立とする
|
||||||
|
*/
|
||||||
|
type CondFormulaValueIsCat = {
|
||||||
|
type: 'isCat';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 「ユーザを見つけやすくする」が有効なアカウントの場合のみ成立とする
|
||||||
|
*/
|
||||||
|
type CondFormulaValueIsExplorable = {
|
||||||
|
type: 'isExplorable';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ユーザが作成されてから指定期間経過した場合のみ成立とする
|
||||||
|
*/
|
||||||
type CondFormulaValueCreatedLessThan = {
|
type CondFormulaValueCreatedLessThan = {
|
||||||
type: 'createdLessThan';
|
type: 'createdLessThan';
|
||||||
sec: number;
|
sec: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ユーザが作成されてから指定期間経っていない場合のみ成立とする
|
||||||
|
*/
|
||||||
type CondFormulaValueCreatedMoreThan = {
|
type CondFormulaValueCreatedMoreThan = {
|
||||||
type: 'createdMoreThan';
|
type: 'createdMoreThan';
|
||||||
sec: number;
|
sec: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* フォロワー数が指定値以下の場合のみ成立とする
|
||||||
|
*/
|
||||||
type CondFormulaValueFollowersLessThanOrEq = {
|
type CondFormulaValueFollowersLessThanOrEq = {
|
||||||
type: 'followersLessThanOrEq';
|
type: 'followersLessThanOrEq';
|
||||||
value: number;
|
value: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* フォロワー数が指定値以上の場合のみ成立とする
|
||||||
|
*/
|
||||||
type CondFormulaValueFollowersMoreThanOrEq = {
|
type CondFormulaValueFollowersMoreThanOrEq = {
|
||||||
type: 'followersMoreThanOrEq';
|
type: 'followersMoreThanOrEq';
|
||||||
value: number;
|
value: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* フォロー数が指定値以下の場合のみ成立とする
|
||||||
|
*/
|
||||||
type CondFormulaValueFollowingLessThanOrEq = {
|
type CondFormulaValueFollowingLessThanOrEq = {
|
||||||
type: 'followingLessThanOrEq';
|
type: 'followingLessThanOrEq';
|
||||||
value: number;
|
value: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* フォロー数が指定値以上の場合のみ成立とする
|
||||||
|
*/
|
||||||
type CondFormulaValueFollowingMoreThanOrEq = {
|
type CondFormulaValueFollowingMoreThanOrEq = {
|
||||||
type: 'followingMoreThanOrEq';
|
type: 'followingMoreThanOrEq';
|
||||||
value: number;
|
value: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 投稿数が指定値以下の場合のみ成立とする
|
||||||
|
*/
|
||||||
type CondFormulaValueNotesLessThanOrEq = {
|
type CondFormulaValueNotesLessThanOrEq = {
|
||||||
type: 'notesLessThanOrEq';
|
type: 'notesLessThanOrEq';
|
||||||
value: number;
|
value: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 投稿数が指定値以上の場合のみ成立とする
|
||||||
|
*/
|
||||||
type CondFormulaValueNotesMoreThanOrEq = {
|
type CondFormulaValueNotesMoreThanOrEq = {
|
||||||
type: 'notesMoreThanOrEq';
|
type: 'notesMoreThanOrEq';
|
||||||
value: number;
|
value: number;
|
||||||
|
@ -80,6 +160,11 @@ export type RoleCondFormulaValue = { id: string } & (
|
||||||
CondFormulaValueNot |
|
CondFormulaValueNot |
|
||||||
CondFormulaValueIsLocal |
|
CondFormulaValueIsLocal |
|
||||||
CondFormulaValueIsRemote |
|
CondFormulaValueIsRemote |
|
||||||
|
CondFormulaValueIsSuspended |
|
||||||
|
CondFormulaValueIsLocked |
|
||||||
|
CondFormulaValueIsBot |
|
||||||
|
CondFormulaValueIsCat |
|
||||||
|
CondFormulaValueIsExplorable |
|
||||||
CondFormulaValueRoleAssignedTo |
|
CondFormulaValueRoleAssignedTo |
|
||||||
CondFormulaValueCreatedLessThan |
|
CondFormulaValueCreatedLessThan |
|
||||||
CondFormulaValueCreatedMoreThan |
|
CondFormulaValueCreatedMoreThan |
|
||||||
|
|
|
@ -76,6 +76,11 @@ export const packedAntennaSchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
excludeBots: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
withReplies: {
|
withReplies: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
|
|
@ -52,5 +52,9 @@ export const packedClipSchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: true, nullable: false,
|
optional: true, nullable: false,
|
||||||
},
|
},
|
||||||
|
notesCount: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
@ -207,6 +207,10 @@ export const packedMetaLiteSchema = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
enableUrlPreview: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
backgroundImageUrl: {
|
backgroundImageUrl: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
|
|
|
@ -57,6 +57,20 @@ export const packedRoleCondFormulaValueIsLocalOrRemoteSchema = {
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const packedRoleCondFormulaValueUserSettingBooleanSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string', optional: false,
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
nullable: false, optional: false,
|
||||||
|
enum: ['isSuspended', 'isLocked', 'isBot', 'isCat', 'isExplorable'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const packedRoleCondFormulaValueAssignedRoleSchema = {
|
export const packedRoleCondFormulaValueAssignedRoleSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
@ -135,6 +149,9 @@ export const packedRoleCondFormulaValueSchema = {
|
||||||
{
|
{
|
||||||
ref: 'RoleCondFormulaValueIsLocalOrRemote',
|
ref: 'RoleCondFormulaValueIsLocalOrRemote',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ref: 'RoleCondFormulaValueUserSettingBooleanSchema',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ref: 'RoleCondFormulaValueAssignedRole',
|
ref: 'RoleCondFormulaValueAssignedRole',
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
export const packedSigninSchema = {
|
export const packedSigninSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
|
|
@ -63,7 +63,7 @@ export class CleanRemoteFilesProcessorService {
|
||||||
isLink: false,
|
isLink: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
job.updateProgress(deletedCount / total);
|
job.updateProgress(100 / total * deletedCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.succ('All cached remote files has been deleted.');
|
this.logger.succ('All cached remote files has been deleted.');
|
||||||
|
|
|
@ -81,6 +81,7 @@ export class ExportAntennasProcessorService {
|
||||||
}) : null,
|
}) : null,
|
||||||
caseSensitive: antenna.caseSensitive,
|
caseSensitive: antenna.caseSensitive,
|
||||||
localOnly: antenna.localOnly,
|
localOnly: antenna.localOnly,
|
||||||
|
excludeBots: antenna.excludeBots,
|
||||||
withReplies: antenna.withReplies,
|
withReplies: antenna.withReplies,
|
||||||
withFile: antenna.withFile,
|
withFile: antenna.withFile,
|
||||||
notify: antenna.notify,
|
notify: antenna.notify,
|
||||||
|
|
|
@ -44,6 +44,7 @@ const validate = new Ajv().compile({
|
||||||
} },
|
} },
|
||||||
caseSensitive: { type: 'boolean' },
|
caseSensitive: { type: 'boolean' },
|
||||||
localOnly: { type: 'boolean' },
|
localOnly: { type: 'boolean' },
|
||||||
|
excludeBots: { type: 'boolean' },
|
||||||
withReplies: { type: 'boolean' },
|
withReplies: { type: 'boolean' },
|
||||||
withFile: { type: 'boolean' },
|
withFile: { type: 'boolean' },
|
||||||
notify: { type: 'boolean' },
|
notify: { type: 'boolean' },
|
||||||
|
@ -88,6 +89,7 @@ export class ImportAntennasProcessorService {
|
||||||
users: (antenna.src === 'list' && antenna.userListAccts !== null ? antenna.userListAccts : antenna.users).filter(Boolean),
|
users: (antenna.src === 'list' && antenna.userListAccts !== null ? antenna.userListAccts : antenna.users).filter(Boolean),
|
||||||
caseSensitive: antenna.caseSensitive,
|
caseSensitive: antenna.caseSensitive,
|
||||||
localOnly: antenna.localOnly,
|
localOnly: antenna.localOnly,
|
||||||
|
excludeBots: antenna.excludeBots,
|
||||||
withReplies: antenna.withReplies,
|
withReplies: antenna.withReplies,
|
||||||
withFile: antenna.withFile,
|
withFile: antenna.withFile,
|
||||||
notify: antenna.notify,
|
notify: antenna.notify,
|
||||||
|
|
|
@ -15,6 +15,7 @@ import InstanceChart from '@/core/chart/charts/instance.js';
|
||||||
import ApRequestChart from '@/core/chart/charts/ap-request.js';
|
import ApRequestChart from '@/core/chart/charts/ap-request.js';
|
||||||
import FederationChart from '@/core/chart/charts/federation.js';
|
import FederationChart from '@/core/chart/charts/federation.js';
|
||||||
import { getApId } from '@/core/activitypub/type.js';
|
import { getApId } from '@/core/activitypub/type.js';
|
||||||
|
import type { IActivity } from '@/core/activitypub/type.js';
|
||||||
import type { MiRemoteUser } from '@/models/User.js';
|
import type { MiRemoteUser } from '@/models/User.js';
|
||||||
import type { MiUserPublickey } from '@/models/UserPublickey.js';
|
import type { MiUserPublickey } from '@/models/UserPublickey.js';
|
||||||
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
|
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
|
||||||
|
@ -22,7 +23,7 @@ import { StatusError } from '@/misc/status-error.js';
|
||||||
import * as Acct from '@/misc/acct.js';
|
import * as Acct from '@/misc/acct.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
||||||
import { LdSignatureService } from '@/core/activitypub/LdSignatureService.js';
|
import { JsonLdService } from '@/core/activitypub/JsonLdService.js';
|
||||||
import { ApInboxService } from '@/core/activitypub/ApInboxService.js';
|
import { ApInboxService } from '@/core/activitypub/ApInboxService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
|
@ -39,7 +40,7 @@ export class InboxProcessorService {
|
||||||
private apInboxService: ApInboxService,
|
private apInboxService: ApInboxService,
|
||||||
private federatedInstanceService: FederatedInstanceService,
|
private federatedInstanceService: FederatedInstanceService,
|
||||||
private fetchInstanceMetadataService: FetchInstanceMetadataService,
|
private fetchInstanceMetadataService: FetchInstanceMetadataService,
|
||||||
private ldSignatureService: LdSignatureService,
|
private jsonLdService: JsonLdService,
|
||||||
private apPersonService: ApPersonService,
|
private apPersonService: ApPersonService,
|
||||||
private apDbResolverService: ApDbResolverService,
|
private apDbResolverService: ApDbResolverService,
|
||||||
private instanceChart: InstanceChart,
|
private instanceChart: InstanceChart,
|
||||||
|
@ -59,8 +60,8 @@ export class InboxProcessorService {
|
||||||
// RFC 9401はsignatureが配列になるが、とりあえずエラーにする
|
// RFC 9401はsignatureが配列になるが、とりあえずエラーにする
|
||||||
throw new Error('signature is array');
|
throw new Error('signature is array');
|
||||||
}
|
}
|
||||||
const activity = job.data.activity;
|
let activity = job.data.activity;
|
||||||
const actorUri = getApId(activity.actor);
|
let actorUri = getApId(activity.actor);
|
||||||
|
|
||||||
//#region Log
|
//#region Log
|
||||||
const info = Object.assign({}, activity);
|
const info = Object.assign({}, activity);
|
||||||
|
@ -121,34 +122,37 @@ export class InboxProcessorService {
|
||||||
authUser.user.uri !== actorUri // 一応チェック
|
authUser.user.uri !== actorUri // 一応チェック
|
||||||
) {
|
) {
|
||||||
// 一致しなくても、でもLD-Signatureがありそうならそっちも見る
|
// 一致しなくても、でもLD-Signatureがありそうならそっちも見る
|
||||||
if (activity.signature?.creator) {
|
const ldSignature = activity.signature;
|
||||||
if (activity.signature.type !== 'RsaSignature2017') {
|
|
||||||
throw new Bull.UnrecoverableError(`skip: unsupported LD-signature type ${activity.signature.type}`);
|
if (ldSignature && ldSignature.creator) {
|
||||||
|
if (ldSignature.type !== 'RsaSignature2017') {
|
||||||
|
throw new Bull.UnrecoverableError(`skip: unsupported LD-signature type ${ldSignature.type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activity.signature.creator.toLowerCase().startsWith('acct:')) {
|
if (ldSignature.creator.toLowerCase().startsWith('acct:')) {
|
||||||
throw new Bull.UnrecoverableError(`old key not supported ${activity.signature.creator}`);
|
throw new Bull.UnrecoverableError(`old key not supported ${ldSignature.creator}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
authUser = await this.apDbResolverService.getAuthUserFromApId(actorUri, activity.signature.creator);
|
authUser = await this.apDbResolverService.getAuthUserFromApId(actorUri, ldSignature.creator);
|
||||||
|
|
||||||
if (authUser == null) {
|
if (authUser == null) {
|
||||||
throw new Bull.UnrecoverableError(`skip: LD-Signatureのactorとcreatorが一致しませんでした uri=${actorUri} creator=${activity.signature.creator}`);
|
throw new Bull.UnrecoverableError(`skip: LD-Signatureのactorとcreatorが一致しませんでした uri=${actorUri} creator=${ldSignature.creator}`);
|
||||||
}
|
}
|
||||||
if (authUser.user == null) {
|
if (authUser.user == null) {
|
||||||
throw new Bull.UnrecoverableError(`skip: LD-Signatureのユーザーが取得できませんでした uri=${actorUri} creator=${activity.signature.creator}`);
|
throw new Bull.UnrecoverableError(`skip: LD-Signatureのユーザーが取得できませんでした uri=${actorUri} creator=${ldSignature.creator}`);
|
||||||
}
|
}
|
||||||
// 一応actorチェック
|
// 一応actorチェック
|
||||||
if (authUser.user.uri !== actorUri) {
|
if (authUser.user.uri !== actorUri) {
|
||||||
throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${actorUri})`);
|
throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${actorUri})`);
|
||||||
}
|
}
|
||||||
if (authUser.key == null) {
|
if (authUser.key == null) {
|
||||||
throw new Bull.UnrecoverableError(`skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした uri=${actorUri} creator=${activity.signature.creator}`);
|
throw new Bull.UnrecoverableError(`skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした uri=${actorUri} creator=${ldSignature.creator}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const jsonLd = this.jsonLdService.use();
|
||||||
|
|
||||||
// LD-Signature検証
|
// LD-Signature検証
|
||||||
const ldSignature = this.ldSignatureService.use();
|
const verified = await jsonLd.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false);
|
||||||
const verified = await ldSignature.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false);
|
|
||||||
if (!verified) {
|
if (!verified) {
|
||||||
throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました');
|
throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました');
|
||||||
}
|
}
|
||||||
|
@ -158,6 +162,31 @@ export class InboxProcessorService {
|
||||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) {
|
if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) {
|
||||||
throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`);
|
throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// アクティビティを正規化
|
||||||
|
// GHSA-2vxv-pv3m-3wvj
|
||||||
|
delete activity.signature;
|
||||||
|
try {
|
||||||
|
activity = await jsonLd.compact(activity) as IActivity;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Bull.UnrecoverableError(`skip: failed to compact activity: ${e}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// actorが正規化前後で一致しているか確認
|
||||||
|
actorUri = getApId(activity.actor);
|
||||||
|
if (authUser.user.uri !== actorUri) {
|
||||||
|
throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity(after normalization).actor(${actorUri})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 元のアクティビティと非互換な形に正規化される場合は転送をスキップする
|
||||||
|
// https://github.com/mastodon/mastodon/blob/664b0ca/app/services/activitypub/process_collection_service.rb#L24-L29
|
||||||
|
activity.signature = ldSignature;
|
||||||
|
|
||||||
|
//#region Log
|
||||||
|
const compactedInfo = Object.assign({}, activity);
|
||||||
|
delete compactedInfo['@context'];
|
||||||
|
this.logger.debug(`compacted: ${JSON.stringify(compactedInfo, null, 2)}`);
|
||||||
|
//#endregion
|
||||||
} else {
|
} else {
|
||||||
throw new Bull.UnrecoverableError(`skip: http-signature verification failed and no LD-Signature. http_signature_keyId=${signature?.keyId}`);
|
throw new Bull.UnrecoverableError(`skip: http-signature verification failed and no LD-Signature. http_signature_keyId=${signature?.keyId}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { IActivity } from '@/core/activitypub/type.js';
|
import { IActivity } from '@/core/activitypub/type.js';
|
||||||
import { isPureRenote } from '@/misc/is-pure-renote.js';
|
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||||
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
|
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
|
||||||
import type { FindOptionsWhere } from 'typeorm';
|
import type { FindOptionsWhere } from 'typeorm';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
|
@ -98,7 +98,7 @@ export class ActivityPubServerService {
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
private async packActivity(note: MiNote): Promise<any> {
|
private async packActivity(note: MiNote): Promise<any> {
|
||||||
if (isPureRenote(note)) {
|
if (isRenote(note) && !isQuote(note)) {
|
||||||
const renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId });
|
const renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId });
|
||||||
return this.apRendererService.renderAnnounce(renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`, note);
|
return this.apRendererService.renderAnnounce(renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`, note);
|
||||||
}
|
}
|
||||||
|
|
|
@ -194,6 +194,7 @@ export class FileServerService {
|
||||||
reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
|
reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
|
||||||
reply.header('Accept-Ranges', 'bytes');
|
reply.header('Accept-Ranges', 'bytes');
|
||||||
reply.header('Content-Length', chunksize);
|
reply.header('Content-Length', chunksize);
|
||||||
|
reply.code(206);
|
||||||
} else {
|
} else {
|
||||||
image = {
|
image = {
|
||||||
data: fs.createReadStream(file.path),
|
data: fs.createReadStream(file.path),
|
||||||
|
@ -213,6 +214,8 @@ export class FileServerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream');
|
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream');
|
||||||
|
reply.header('Content-Length', file.file.size);
|
||||||
|
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||||
reply.header('Content-Disposition',
|
reply.header('Content-Disposition',
|
||||||
contentDisposition(
|
contentDisposition(
|
||||||
'inline',
|
'inline',
|
||||||
|
@ -255,6 +258,7 @@ export class FileServerService {
|
||||||
return fs.createReadStream(file.path);
|
return fs.createReadStream(file.path);
|
||||||
} else {
|
} else {
|
||||||
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.file.type) ? file.file.type : 'application/octet-stream');
|
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.file.type) ? file.file.type : 'application/octet-stream');
|
||||||
|
reply.header('Content-Length', file.file.size);
|
||||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||||
reply.header('Content-Disposition', contentDisposition('inline', file.filename));
|
reply.header('Content-Disposition', contentDisposition('inline', file.filename));
|
||||||
|
|
||||||
|
@ -263,7 +267,6 @@ export class FileServerService {
|
||||||
const parts = range.replace(/bytes=/, '').split('-');
|
const parts = range.replace(/bytes=/, '').split('-');
|
||||||
const start = parseInt(parts[0], 10);
|
const start = parseInt(parts[0], 10);
|
||||||
let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
|
let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
|
||||||
console.log(end);
|
|
||||||
if (end > file.file.size) {
|
if (end > file.file.size) {
|
||||||
end = file.file.size - 1;
|
end = file.file.size - 1;
|
||||||
}
|
}
|
||||||
|
@ -433,6 +436,7 @@ export class FileServerService {
|
||||||
reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
|
reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
|
||||||
reply.header('Accept-Ranges', 'bytes');
|
reply.header('Accept-Ranges', 'bytes');
|
||||||
reply.header('Content-Length', chunksize);
|
reply.header('Content-Length', chunksize);
|
||||||
|
reply.code(206);
|
||||||
} else {
|
} else {
|
||||||
image = {
|
image = {
|
||||||
data: fs.createReadStream(file.path),
|
data: fs.createReadStream(file.path),
|
||||||
|
@ -529,6 +533,7 @@ export class FileServerService {
|
||||||
if (!file.storedInternal) {
|
if (!file.storedInternal) {
|
||||||
if (!(file.isLink && file.uri)) return '204';
|
if (!(file.isLink && file.uri)) return '204';
|
||||||
const result = await this.downloadAndDetectTypeFromUrl(file.uri);
|
const result = await this.downloadAndDetectTypeFromUrl(file.uri);
|
||||||
|
file.size = (await fs.promises.stat(result.path)).size; // DB file.sizeは正確とは限らないので
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
url: file.uri,
|
url: file.uri,
|
||||||
|
|
|
@ -120,12 +120,20 @@ export class ServerService implements OnApplicationShutdown {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = path.split('@')[0].replace(/\.webp$/i, '');
|
const emojiPath = path.replace(/\.webp$/i, '');
|
||||||
const host = path.split('@')[1]?.replace(/\.webp$/i, '');
|
const pathChunks = emojiPath.split('@');
|
||||||
|
|
||||||
|
if (pathChunks.length > 2) {
|
||||||
|
reply.code(400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = pathChunks.shift();
|
||||||
|
const host = pathChunks.pop();
|
||||||
|
|
||||||
const emoji = await this.emojisRepository.findOneBy({
|
const emoji = await this.emojisRepository.findOneBy({
|
||||||
// `@.` is the spec of ReactionService.decodeReaction
|
// `@.` is the spec of ReactionService.decodeReaction
|
||||||
host: (host == null || host === '.') ? IsNull() : host,
|
host: (host === undefined || host === '.') ? IsNull() : host,
|
||||||
name: name,
|
name: name,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -434,6 +434,8 @@ export const meta = {
|
||||||
summalyProxy: {
|
summalyProxy: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
|
deprecated: true,
|
||||||
|
description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.',
|
||||||
},
|
},
|
||||||
themeColor: {
|
themeColor: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
@ -451,6 +453,30 @@ export const meta = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
urlPreviewEnabled: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
urlPreviewTimeout: {
|
||||||
|
type: 'number',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
urlPreviewMaximumContentLength: {
|
||||||
|
type: 'number',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
urlPreviewRequireContentLength: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
urlPreviewUserAgent: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
|
urlPreviewSummaryProxyUrl: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -533,7 +559,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically,
|
setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically,
|
||||||
enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos,
|
enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos,
|
||||||
proxyAccountId: instance.proxyAccountId,
|
proxyAccountId: instance.proxyAccountId,
|
||||||
summalyProxy: instance.summalyProxy,
|
|
||||||
email: instance.email,
|
email: instance.email,
|
||||||
smtpSecure: instance.smtpSecure,
|
smtpSecure: instance.smtpSecure,
|
||||||
smtpHost: instance.smtpHost,
|
smtpHost: instance.smtpHost,
|
||||||
|
@ -577,6 +602,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax,
|
perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax,
|
||||||
perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax,
|
perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax,
|
||||||
notesPerOneAd: instance.notesPerOneAd,
|
notesPerOneAd: instance.notesPerOneAd,
|
||||||
|
summalyProxy: instance.urlPreviewSummaryProxyUrl,
|
||||||
|
urlPreviewEnabled: instance.urlPreviewEnabled,
|
||||||
|
urlPreviewTimeout: instance.urlPreviewTimeout,
|
||||||
|
urlPreviewMaximumContentLength: instance.urlPreviewMaximumContentLength,
|
||||||
|
urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength,
|
||||||
|
urlPreviewUserAgent: instance.urlPreviewUserAgent,
|
||||||
|
urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,7 +90,6 @@ export const paramDef = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
summalyProxy: { type: 'string', nullable: true },
|
|
||||||
deeplAuthKey: { type: 'string', nullable: true },
|
deeplAuthKey: { type: 'string', nullable: true },
|
||||||
deeplIsPro: { type: 'boolean' },
|
deeplIsPro: { type: 'boolean' },
|
||||||
enableEmail: { type: 'boolean' },
|
enableEmail: { type: 'boolean' },
|
||||||
|
@ -150,6 +149,16 @@ export const paramDef = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
summalyProxy: {
|
||||||
|
type: 'string', nullable: true,
|
||||||
|
description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.',
|
||||||
|
},
|
||||||
|
urlPreviewEnabled: { type: 'boolean' },
|
||||||
|
urlPreviewTimeout: { type: 'integer' },
|
||||||
|
urlPreviewMaximumContentLength: { type: 'integer' },
|
||||||
|
urlPreviewRequireContentLength: { type: 'boolean' },
|
||||||
|
urlPreviewUserAgent: { type: 'string', nullable: true },
|
||||||
|
urlPreviewSummaryProxyUrl: { type: 'string', nullable: true },
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -353,10 +362,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
set.langs = ps.langs.filter(Boolean);
|
set.langs = ps.langs.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ps.summalyProxy !== undefined) {
|
|
||||||
set.summalyProxy = ps.summalyProxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.enableEmail !== undefined) {
|
if (ps.enableEmail !== undefined) {
|
||||||
set.enableEmail = ps.enableEmail;
|
set.enableEmail = ps.enableEmail;
|
||||||
}
|
}
|
||||||
|
@ -581,6 +586,32 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
set.bannedEmailDomains = ps.bannedEmailDomains;
|
set.bannedEmailDomains = ps.bannedEmailDomains;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.urlPreviewEnabled !== undefined) {
|
||||||
|
set.urlPreviewEnabled = ps.urlPreviewEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.urlPreviewTimeout !== undefined) {
|
||||||
|
set.urlPreviewTimeout = ps.urlPreviewTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.urlPreviewMaximumContentLength !== undefined) {
|
||||||
|
set.urlPreviewMaximumContentLength = ps.urlPreviewMaximumContentLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.urlPreviewRequireContentLength !== undefined) {
|
||||||
|
set.urlPreviewRequireContentLength = ps.urlPreviewRequireContentLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.urlPreviewUserAgent !== undefined) {
|
||||||
|
const value = (ps.urlPreviewUserAgent ?? '').trim();
|
||||||
|
set.urlPreviewUserAgent = value === '' ? null : ps.urlPreviewUserAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.summalyProxy !== undefined || ps.urlPreviewSummaryProxyUrl !== undefined) {
|
||||||
|
const value = ((ps.urlPreviewSummaryProxyUrl ?? ps.summalyProxy) ?? '').trim();
|
||||||
|
set.urlPreviewSummaryProxyUrl = value === '' ? null : value;
|
||||||
|
}
|
||||||
|
|
||||||
const before = await this.metaService.fetch(true);
|
const before = await this.metaService.fetch(true);
|
||||||
|
|
||||||
await this.metaService.update(set);
|
await this.metaService.update(set);
|
||||||
|
|
|
@ -64,6 +64,7 @@ export const paramDef = {
|
||||||
} },
|
} },
|
||||||
caseSensitive: { type: 'boolean' },
|
caseSensitive: { type: 'boolean' },
|
||||||
localOnly: { type: 'boolean' },
|
localOnly: { type: 'boolean' },
|
||||||
|
excludeBots: { type: 'boolean' },
|
||||||
withReplies: { type: 'boolean' },
|
withReplies: { type: 'boolean' },
|
||||||
withFile: { type: 'boolean' },
|
withFile: { type: 'boolean' },
|
||||||
notify: { type: 'boolean' },
|
notify: { type: 'boolean' },
|
||||||
|
@ -124,6 +125,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
users: ps.users,
|
users: ps.users,
|
||||||
caseSensitive: ps.caseSensitive,
|
caseSensitive: ps.caseSensitive,
|
||||||
localOnly: ps.localOnly,
|
localOnly: ps.localOnly,
|
||||||
|
excludeBots: ps.excludeBots,
|
||||||
withReplies: ps.withReplies,
|
withReplies: ps.withReplies,
|
||||||
withFile: ps.withFile,
|
withFile: ps.withFile,
|
||||||
notify: ps.notify,
|
notify: ps.notify,
|
||||||
|
|
|
@ -63,11 +63,12 @@ export const paramDef = {
|
||||||
} },
|
} },
|
||||||
caseSensitive: { type: 'boolean' },
|
caseSensitive: { type: 'boolean' },
|
||||||
localOnly: { type: 'boolean' },
|
localOnly: { type: 'boolean' },
|
||||||
|
excludeBots: { type: 'boolean' },
|
||||||
withReplies: { type: 'boolean' },
|
withReplies: { type: 'boolean' },
|
||||||
withFile: { type: 'boolean' },
|
withFile: { type: 'boolean' },
|
||||||
notify: { type: 'boolean' },
|
notify: { type: 'boolean' },
|
||||||
},
|
},
|
||||||
required: ['antennaId', 'name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'],
|
required: ['antennaId'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -83,8 +84,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
|
if (ps.keywords && ps.excludeKeywords) {
|
||||||
throw new Error('either keywords or excludeKeywords is required.');
|
if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
|
||||||
|
throw new Error('either keywords or excludeKeywords is required.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Fetch the antenna
|
// Fetch the antenna
|
||||||
const antenna = await this.antennasRepository.findOneBy({
|
const antenna = await this.antennasRepository.findOneBy({
|
||||||
|
@ -98,7 +101,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
let userList;
|
let userList;
|
||||||
|
|
||||||
if (ps.src === 'list' && ps.userListId) {
|
if ((ps.src === 'list' || antenna.src === 'list') && ps.userListId) {
|
||||||
userList = await this.userListsRepository.findOneBy({
|
userList = await this.userListsRepository.findOneBy({
|
||||||
id: ps.userListId,
|
id: ps.userListId,
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
|
@ -112,12 +115,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
await this.antennasRepository.update(antenna.id, {
|
await this.antennasRepository.update(antenna.id, {
|
||||||
name: ps.name,
|
name: ps.name,
|
||||||
src: ps.src,
|
src: ps.src,
|
||||||
userListId: userList ? userList.id : null,
|
userListId: ps.userListId !== undefined ? userList ? userList.id : null : undefined,
|
||||||
keywords: ps.keywords,
|
keywords: ps.keywords,
|
||||||
excludeKeywords: ps.excludeKeywords,
|
excludeKeywords: ps.excludeKeywords,
|
||||||
users: ps.users,
|
users: ps.users,
|
||||||
caseSensitive: ps.caseSensitive,
|
caseSensitive: ps.caseSensitive,
|
||||||
localOnly: ps.localOnly,
|
localOnly: ps.localOnly,
|
||||||
|
excludeBots: ps.excludeBots,
|
||||||
withReplies: ps.withReplies,
|
withReplies: ps.withReplies,
|
||||||
withFile: ps.withFile,
|
withFile: ps.withFile,
|
||||||
notify: ps.notify,
|
notify: ps.notify,
|
||||||
|
|
|
@ -20,13 +20,188 @@ export const meta = {
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
image: {
|
||||||
|
type: 'object',
|
||||||
|
optional: true,
|
||||||
|
properties: {
|
||||||
|
link: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
paginationLinks: {
|
||||||
|
type: 'object',
|
||||||
|
optional: true,
|
||||||
|
properties: {
|
||||||
|
self: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
first: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
next: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
last: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
prev: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
items: {
|
items: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
|
optional: false,
|
||||||
items: {
|
items: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
link: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
guid: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
pubDate: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
creator: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
isoDate: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
categories: {
|
||||||
|
type: 'array',
|
||||||
|
optional: true,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contentSnippet: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
enclosure: {
|
||||||
|
type: 'object',
|
||||||
|
optional: true,
|
||||||
|
properties: {
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false,
|
||||||
|
},
|
||||||
|
length: {
|
||||||
|
type: 'number',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
feedUrl: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
itunes: {
|
||||||
|
type: 'object',
|
||||||
|
optional: true,
|
||||||
|
additionalProperties: true,
|
||||||
|
properties: {
|
||||||
|
image: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
owner: {
|
||||||
|
type: 'object',
|
||||||
|
optional: true,
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
author: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
explicit: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
categories: {
|
||||||
|
type: 'array',
|
||||||
|
optional: true,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
keywords: {
|
||||||
|
type: 'array',
|
||||||
|
optional: true,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,7 @@ export const paramDef = {
|
||||||
permissions: { type: 'array', items: {
|
permissions: { type: 'array', items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
} },
|
} },
|
||||||
|
visibility: { type: 'string', enum: ['public', 'private'], default: 'public' },
|
||||||
},
|
},
|
||||||
required: ['title', 'summary', 'script', 'permissions'],
|
required: ['title', 'summary', 'script', 'permissions'],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -66,6 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
summary: ps.summary,
|
summary: ps.summary,
|
||||||
script: ps.script,
|
script: ps.script,
|
||||||
permissions: ps.permissions,
|
permissions: ps.permissions,
|
||||||
|
visibility: ps.visibility,
|
||||||
}).then(x => this.flashsRepository.findOneByOrFail(x.identifiers[0]));
|
}).then(x => this.flashsRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
return await this.flashEntityService.pack(flash);
|
return await this.flashEntityService.pack(flash);
|
||||||
|
|
|
@ -75,7 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
|
const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
|
||||||
me,
|
me,
|
||||||
(old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(),
|
(old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(),
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
|
if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
|
||||||
|
|
|
@ -75,7 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
|
const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
|
||||||
me,
|
me,
|
||||||
(old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(),
|
(old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(),
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
|
if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
|
||||||
|
|
|
@ -75,7 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
|
const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
|
||||||
me,
|
me,
|
||||||
(old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(),
|
(old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(),
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
|
if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue