diff --git a/.config/docker_example.env b/.config/docker_example.env
index 4fe8e76b784b..c61248da2e46 100644
--- a/.config/docker_example.env
+++ b/.config/docker_example.env
@@ -1,5 +1,11 @@
+# misskey settings
+# MISSKEY_URL=https://example.tld/
+
# db settings
POSTGRES_PASSWORD=example-misskey-pass
+# DATABASE_PASSWORD=${POSTGRES_PASSWORD}
POSTGRES_USER=example-misskey-user
+# DATABASE_USER=${POSTGRES_USER}
POSTGRES_DB=misskey
+# DATABASE_DB=${POSTGRES_DB}
DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}"
diff --git a/.config/docker_example.yml b/.config/docker_example.yml
index acd169bf436e..d347882d1a91 100644
--- a/.config/docker_example.yml
+++ b/.config/docker_example.yml
@@ -6,6 +6,7 @@
#───┘ URL └─────────────────────────────────────────────────────
# Final accessible URL seen by a user.
+# You can set url from an environment variable instead.
url: https://example.tld/
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
@@ -38,9 +39,11 @@ db:
port: 5432
# Database name
+ # You can set db from an environment variable instead.
db: misskey
# Auth
+ # You can set user and pass from environment variables instead.
user: example-misskey-user
pass: example-misskey-pass
@@ -106,7 +109,7 @@ redis:
# ┌───────────────────────────┐
#───┘ MeiliSearch configuration └─────────────────────────────
-# You can set scope to local (default value) or global
+# You can set scope to local (default value) or global
# (include notes from remote).
#meilisearch:
@@ -136,6 +139,21 @@ redis:
id: 'aidx'
+# ┌────────────────┐
+#───┘ Error tracking └──────────────────────────────────────────
+
+# Sentry is available for error tracking.
+# See the Sentry documentation for more details on options.
+
+#sentryForBackend:
+# enableNodeProfiling: true
+# options:
+# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
+
+#sentryForFrontend:
+# options:
+# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
+
# ┌─────────────────────┐
#───┘ Other configuration └─────────────────────────────────────
@@ -185,7 +203,7 @@ proxyRemoteFiles: true
signToActivityPubGet: true
# For security reasons, uploading attachments from the intranet is prohibited,
-# but exceptions can be made from the following settings. Default value is "undefined".
+# but exceptions can be made from the following settings. Default value is "undefined".
# Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)).
#allowedPrivateNetworks: [
# '127.0.0.1/32'
diff --git a/.config/example.yml b/.config/example.yml
index b0b7f140593d..b11cbd137328 100644
--- a/.config/example.yml
+++ b/.config/example.yml
@@ -205,6 +205,21 @@ redis:
id: 'aidx'
+# ┌────────────────┐
+#───┘ Error tracking └──────────────────────────────────────────
+
+# Sentry is available for error tracking.
+# See the Sentry documentation for more details on options.
+
+#sentryForBackend:
+# enableNodeProfiling: true
+# options:
+# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
+
+#sentryForFrontend:
+# options:
+# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
+
# ┌─────────────────────┐
#───┘ Other configuration └─────────────────────────────────────
diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/compose.yml
similarity index 93%
rename from .devcontainer/docker-compose.yml
rename to .devcontainer/compose.yml
index 2809cd2ca469..d02d2a8f4a89 100644
--- a/.devcontainer/docker-compose.yml
+++ b/.devcontainer/compose.yml
@@ -1,5 +1,3 @@
-version: '3.8'
-
services:
app:
build:
@@ -8,6 +6,7 @@ services:
volumes:
- ../:/workspace:cached
+ - node_modules:/workspace/node_modules
command: sleep infinity
@@ -46,6 +45,7 @@ services:
volumes:
postgres-data:
redis-data:
+ node_modules:
networks:
internal_network:
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 31b6212cb5c4..7ea23e314ea4 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,6 +1,6 @@
{
"name": "Misskey",
- "dockerComposeFile": "docker-compose.yml",
+ "dockerComposeFile": "compose.yml",
"service": "app",
"workspaceFolder": "/workspace",
"features": {
@@ -10,7 +10,7 @@
"ghcr.io/devcontainers-contrib/features/corepack:1": {}
},
"forwardPorts": [3000],
- "postCreateCommand": "sudo chmod 755 .devcontainer/init.sh && .devcontainer/init.sh",
+ "postCreateCommand": "/bin/bash .devcontainer/init.sh",
"customizations": {
"vscode": {
"extensions": [
diff --git a/.devcontainer/devcontainer.yml b/.devcontainer/devcontainer.yml
index 7ea09294692a..beefcfd0a2d5 100644
--- a/.devcontainer/devcontainer.yml
+++ b/.devcontainer/devcontainer.yml
@@ -132,6 +132,21 @@ redis:
id: 'aidx'
+# ┌────────────────┐
+#───┘ Error tracking └──────────────────────────────────────────
+
+# Sentry is available for error tracking.
+# See the Sentry documentation for more details on options.
+
+#sentryForBackend:
+# enableNodeProfiling: true
+# options:
+# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
+
+#sentryForFrontend:
+# options:
+# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
+
# ┌─────────────────────┐
#───┘ Other configuration └─────────────────────────────────────
diff --git a/.devcontainer/init.sh b/.devcontainer/init.sh
index 729e1a9d2d9d..55fb1e6fa687 100755
--- a/.devcontainer/init.sh
+++ b/.devcontainer/init.sh
@@ -2,7 +2,8 @@
set -xe
-sudo chown -R node /workspace
+sudo chown node node_modules
+git config --global --add safe.directory /workspace
git submodule update --init
corepack install
corepack enable
diff --git a/.dockerignore b/.dockerignore
index 1de0c7982bcd..7dbb06e1d0e6 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -7,7 +7,7 @@ Dockerfile
build/
built/
db/
-docker-compose.yml
+.devcontainer/compose.yml
node_modules/
packages/*/node_modules
redis/
@@ -28,4 +28,4 @@ fluent-emojis/
.idea/
packages/*/.vscode/
-packages/backend/test/docker-compose.yml
+packages/backend/test/compose.yml
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
deleted file mode 100644
index d42b58abc09c..000000000000
--- a/.github/FUNDING.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-# These are supported funding model platforms
-
-github: [misskey-dev]
-patreon: syuilo
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index e8b65dc3b9fc..5acad83336d3 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -2,3 +2,7 @@ contact_links:
- name: 💬 Misskey official Discord
url: https://discord.gg/Wp8gVStHW3
about: Chat freely about Misskey
+ # 仮
+ - name: 💬 Start discussion
+ url: https://github.com/misskey-dev/misskey/discussions
+ about: The official forum to join conversation and ask question
diff --git a/.github/workflows/api-misskey-js.yml b/.github/workflows/api-misskey-js.yml
index 1b7b68b14f05..e7db18316cb3 100644
--- a/.github/workflows/api-misskey-js.yml
+++ b/.github/workflows/api-misskey-js.yml
@@ -4,10 +4,11 @@ on:
push:
paths:
- packages/misskey-js/**
+ - .github/workflows/api-misskey-js.yml
pull_request:
paths:
- packages/misskey-js/**
-
+ - .github/workflows/api-misskey-js.yml
jobs:
report:
@@ -20,7 +21,7 @@ jobs:
- run: corepack enable
- name: Setup Node.js
- uses: actions/setup-node@v4.0.2
+ uses: actions/setup-node@v4.0.3
with:
node-version-file: '.node-version'
cache: 'pnpm'
diff --git a/.github/workflows/changelog-check.yml b/.github/workflows/changelog-check.yml
index f254af0d1fd0..d4e99f966ef0 100644
--- a/.github/workflows/changelog-check.yml
+++ b/.github/workflows/changelog-check.yml
@@ -14,7 +14,7 @@ jobs:
- name: Checkout head
uses: actions/checkout@v4.1.1
- name: Setup Node.js
- uses: actions/setup-node@v4.0.2
+ uses: actions/setup-node@v4.0.3
with:
node-version-file: '.node-version'
diff --git a/.github/workflows/check-misskey-js-autogen.yml b/.github/workflows/check-misskey-js-autogen.yml
index 39acad8bc348..3a2a2d5f8dd7 100644
--- a/.github/workflows/check-misskey-js-autogen.yml
+++ b/.github/workflows/check-misskey-js-autogen.yml
@@ -28,7 +28,7 @@ jobs:
- name: setup node
id: setup-node
- uses: actions/setup-node@v4.0.2
+ uses: actions/setup-node@v4.0.3
with:
node-version-file: '.node-version'
cache: pnpm
diff --git a/.github/workflows/check-misskey-js-version.yml b/.github/workflows/check-misskey-js-version.yml
index 325a893605b3..99c29ac974cf 100644
--- a/.github/workflows/check-misskey-js-version.yml
+++ b/.github/workflows/check-misskey-js-version.yml
@@ -6,12 +6,13 @@ on:
paths:
- packages/misskey-js/package.json
- package.json
+ - .github/workflows/check-misskey-js-version.yml
pull_request:
branches: [ develop ]
paths:
- packages/misskey-js/package.json
- package.json
-
+ - .github/workflows/check-misskey-js-version.yml
jobs:
check-version:
# ルートの package.json と packages/misskey-js/package.json のバージョンが一致しているかを確認する
diff --git a/.github/workflows/docker-develop.yml b/.github/workflows/docker-develop.yml
index cb84849580ce..ac2b1b4d358a 100644
--- a/.github/workflows/docker-develop.yml
+++ b/.github/workflows/docker-develop.yml
@@ -37,7 +37,7 @@ jobs:
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push by digest
id: build
- uses: docker/build-push-action@v5
+ uses: docker/build-push-action@v6
with:
context: .
push: true
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 23c1bdbc1686..db899ba386be 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -48,7 +48,7 @@ jobs:
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and Push to Docker Hub
id: build
- uses: docker/build-push-action@v5
+ uses: docker/build-push-action@v6
with:
context: .
push: true
diff --git a/.github/workflows/dockle.yml b/.github/workflows/dockle.yml
index eee7a78fedfa..c3dba4213d31 100644
--- a/.github/workflows/dockle.yml
+++ b/.github/workflows/dockle.yml
@@ -13,14 +13,16 @@ jobs:
runs-on: ubuntu-latest
env:
DOCKER_CONTENT_TRUST: 1
+ DOCKLE_VERSION: 0.4.14
steps:
- uses: actions/checkout@v4.1.1
- - run: |
- curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb"
+ - name: Download and install dockle v${{ env.DOCKLE_VERSION }}
+ run: |
+ curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v${DOCKLE_VERSION}/dockle_${DOCKLE_VERSION}_Linux-64bit.deb"
sudo dpkg -i dockle.deb
- run: |
cp .config/docker_example.env .config/docker.env
- cp ./docker-compose_example.yml ./docker-compose.yml
+ cp ./compose_example.yml ./compose.yml
- run: |
docker compose up -d web
docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" misskey-web:latest
diff --git a/.github/workflows/get-api-diff.yml b/.github/workflows/get-api-diff.yml
index 9b9c8f11c413..4afafabf2ead 100644
--- a/.github/workflows/get-api-diff.yml
+++ b/.github/workflows/get-api-diff.yml
@@ -9,7 +9,7 @@ on:
paths:
- packages/backend/**
- .github/workflows/get-api-diff.yml
-
+ - .github/workflows/get-api-diff.yml
jobs:
get-from-misskey:
runs-on: ubuntu-latest
@@ -34,7 +34,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v4.0.2
+ uses: actions/setup-node@v4.0.3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 76616ec5a71a..c21fc951239f 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -10,15 +10,16 @@ on:
- packages/frontend/**
- packages/sw/**
- packages/misskey-js/**
- - packages/shared/.eslintrc.js
+ - packages/shared/eslint.config.js
+ - .github/workflows/lint.yml
pull_request:
paths:
- packages/backend/**
- packages/frontend/**
- packages/sw/**
- packages/misskey-js/**
- - packages/shared/.eslintrc.js
-
+ - packages/shared/eslint.config.js
+ - .github/workflows/lint.yml
jobs:
pnpm_install:
runs-on: ubuntu-latest
@@ -28,7 +29,7 @@ jobs:
fetch-depth: 0
submodules: true
- uses: pnpm/action-setup@v4
- - uses: actions/setup-node@v4.0.2
+ - uses: actions/setup-node@v4.0.3
with:
node-version-file: '.node-version'
cache: 'pnpm'
@@ -39,6 +40,8 @@ jobs:
needs: [pnpm_install]
runs-on: ubuntu-latest
continue-on-error: true
+ env:
+ eslint-cache-version: v1
strategy:
matrix:
workspace:
@@ -52,13 +55,20 @@ jobs:
fetch-depth: 0
submodules: true
- uses: pnpm/action-setup@v4
- - uses: actions/setup-node@v4.0.2
+ - uses: actions/setup-node@v4.0.3
with:
node-version-file: '.node-version'
cache: 'pnpm'
- run: corepack enable
- run: pnpm i --frozen-lockfile
- - run: pnpm --filter ${{ matrix.workspace }} run eslint
+ - name: Restore eslint cache
+ uses: actions/cache@v4.0.2
+ with:
+ path: node_modules/.cache/eslint
+ key: eslint-${{ env.eslint-cache-version }}-${{ hashFiles('/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}
+ restore-keys: |
+ eslint-${{ env.eslint-cache-version }}-${{ hashFiles('/pnpm-lock.yaml') }}-
+ - run: pnpm --filter ${{ matrix.workspace }} run eslint --cache --cache-location node_modules/.cache/eslint --cache-strategy content
typecheck:
needs: [pnpm_install]
@@ -75,7 +85,7 @@ jobs:
fetch-depth: 0
submodules: true
- uses: pnpm/action-setup@v4
- - uses: actions/setup-node@v4.0.2
+ - uses: actions/setup-node@v4.0.3
with:
node-version-file: '.node-version'
cache: 'pnpm'
diff --git a/.github/workflows/locale.yml b/.github/workflows/locale.yml
index de2247e77277..95251bfe31e0 100644
--- a/.github/workflows/locale.yml
+++ b/.github/workflows/locale.yml
@@ -4,10 +4,11 @@ on:
push:
paths:
- locales/**
+ - .github/workflows/locale.yml
pull_request:
paths:
- locales/**
-
+ - .github/workflows/locale.yml
jobs:
locale_verify:
runs-on: ubuntu-latest
@@ -18,7 +19,7 @@ jobs:
fetch-depth: 0
submodules: true
- uses: pnpm/action-setup@v4
- - uses: actions/setup-node@v4.0.2
+ - uses: actions/setup-node@v4.0.3
with:
node-version-file: '.node-version'
cache: 'pnpm'
diff --git a/.github/workflows/on-release-created.yml b/.github/workflows/on-release-created.yml
index edfdab99e930..22c04ff29711 100644
--- a/.github/workflows/on-release-created.yml
+++ b/.github/workflows/on-release-created.yml
@@ -26,7 +26,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v4.0.2
+ uses: actions/setup-node@v4.0.3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
diff --git a/.github/workflows/release-edit-with-push.yml b/.github/workflows/release-edit-with-push.yml
index 944b98eb7cc2..f86c1948f8b0 100644
--- a/.github/workflows/release-edit-with-push.yml
+++ b/.github/workflows/release-edit-with-push.yml
@@ -3,10 +3,10 @@ name: "Release Manager: sync changelog with PR"
on:
push:
branches:
- - release/**
+ - develop
paths:
- 'CHANGELOG.md'
-
+ # - .github/workflows/release-edit-with-push.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -20,21 +20,29 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- # headがrelease/かつopenのPRを1つ取得
+ # headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得
- name: Get PR
run: |
- echo "pr_number=$(gh pr list --limit 1 --head "${{ github.ref_name }}" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
+ echo "pr_number=$(gh pr list --limit 1 --search "head:$GITHUB_REF_NAME base:$STABLE_BRANCH is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
id: get_pr
+ env:
+ STABLE_BRANCH: ${{ vars.STABLE_BRANCH }}
- name: Get target version
- uses: misskey-dev/release-manager-actions/.github/actions/get-target-version@v1
+ if: steps.get_pr.outputs.pr_number != ''
+ uses: misskey-dev/release-manager-actions/.github/actions/get-target-version@v2
id: v
# CHANGELOG.mdの内容を取得
- name: Get changelog
- uses: misskey-dev/release-manager-actions/.github/actions/get-changelog@v1
+ if: steps.get_pr.outputs.pr_number != ''
+ uses: misskey-dev/release-manager-actions/.github/actions/get-changelog@v2
with:
version: ${{ steps.v.outputs.target_version }}
id: changelog
# PRのnotesを更新
- name: Update PR
+ if: steps.get_pr.outputs.pr_number != ''
run: |
- gh pr edit ${{ steps.get_pr.outputs.pr_number }} --body "${{ steps.changelog.outputs.changelog }}"
+ gh pr edit "$PR_NUMBER" --body "$CHANGELOG"
+ env:
+ PR_NUMBER: ${{ steps.get_pr.outputs.pr_number }}
+ CHANGELOG: ${{ steps.changelog.outputs.changelog }}
diff --git a/.github/workflows/release-with-dispatch.yml b/.github/workflows/release-with-dispatch.yml
index bc6448cb37d8..0936bc0ae8b9 100644
--- a/.github/workflows/release-with-dispatch.yml
+++ b/.github/workflows/release-with-dispatch.yml
@@ -33,18 +33,21 @@ jobs:
pr_number: ${{ steps.get_pr.outputs.pr_number }}
steps:
- uses: actions/checkout@v4
- # headがrelease/かつopenのPRを1つ取得
+ # headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得
- name: Get PRs
run: |
- echo "pr_number=$(gh pr list --limit 1 --search "head:release/ is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
+ echo "pr_number=$(gh pr list --limit 1 --search "head:$GITHUB_REF_NAME base:$STABLE_BRANCH is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
id: get_pr
+ env:
+ STABLE_BRANCH: ${{ vars.STABLE_BRANCH }}
merge:
- uses: misskey-dev/release-manager-actions/.github/workflows/merge.yml@v1
+ uses: misskey-dev/release-manager-actions/.github/workflows/merge.yml@v2
needs: get-pr
if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge == true }}
with:
pr_number: ${{ needs.get-pr.outputs.pr_number }}
+ user: 'github-actions[bot]'
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
# Text to prepend to the changelog
# The first line must be `## Unreleased`
@@ -65,15 +68,14 @@ jobs:
secrets:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
- RULESET_EDIT_APP_ID: ${{ secrets.RULESET_EDIT_APP_ID }}
- RULESET_EDIT_APP_PRIVATE_KEY: ${{ secrets.RULESET_EDIT_APP_PRIVATE_KEY }}
create-prerelease:
- uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v1
+ uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v2
needs: get-pr
if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge != true }}
with:
pr_number: ${{ needs.get-pr.outputs.pr_number }}
+ user: 'github-actions[bot]'
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
indent: ${{ vars.INDENT }}
@@ -82,10 +84,11 @@ jobs:
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
create-target:
- uses: misskey-dev/release-manager-actions/.github/workflows/create-target.yml@v1
+ uses: misskey-dev/release-manager-actions/.github/workflows/create-target.yml@v2
needs: get-pr
if: ${{ needs.get-pr.outputs.pr_number == '' }}
with:
+ user: 'github-actions[bot]'
# The script for version increment.
# process.env.CURRENT_VERSION: The current version.
#
@@ -118,8 +121,7 @@ jobs:
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
indent: ${{ vars.INDENT }}
+ stable_branch: ${{ vars.STABLE_BRANCH }}
secrets:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
- RULESET_EDIT_APP_ID: ${{ secrets.RULESET_EDIT_APP_ID }}
- RULESET_EDIT_APP_PRIVATE_KEY: ${{ secrets.RULESET_EDIT_APP_PRIVATE_KEY }}
diff --git a/.github/workflows/release-with-ready.yml b/.github/workflows/release-with-ready.yml
index 139503e563e8..79b6ade01285 100644
--- a/.github/workflows/release-with-ready.yml
+++ b/.github/workflows/release-with-ready.yml
@@ -16,21 +16,26 @@ jobs:
check:
runs-on: ubuntu-latest
outputs:
- ref: ${{ steps.get_pr.outputs.ref }}
+ head: ${{ steps.get_pr.outputs.head }}
+ base: ${{ steps.get_pr.outputs.base }}
steps:
- uses: actions/checkout@v4
# PR情報を取得
- name: Get PR
run: |
- pr_json=$(gh pr view ${{ github.event.pull_request.number }} --json isDraft,headRefName)
- echo "ref=$(echo $pr_json | jq -r '.headRefName')" >> $GITHUB_OUTPUT
+ pr_json=$(gh pr view "$PR_NUMBER" --json isDraft,headRefName,baseRefName)
+ echo "head=$(echo $pr_json | jq -r '.headRefName')" >> $GITHUB_OUTPUT
+ echo "base=$(echo $pr_json | jq -r '.baseRefName')" >> $GITHUB_OUTPUT
id: get_pr
+ env:
+ PR_NUMBER: ${{ github.event.pull_request.number }}
release:
- uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v1
+ uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v2
needs: check
- if: startsWith(needs.check.outputs.ref, 'release/')
+ if: needs.check.outputs.head == github.event.repository.default_branch && needs.check.outputs.base == vars.STABLE_BRANCH
with:
pr_number: ${{ github.event.pull_request.number }}
+ user: 'github-actions[bot]'
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
indent: ${{ vars.INDENT }}
diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml
index c52883ffdde9..68452aacaf88 100644
--- a/.github/workflows/storybook.yml
+++ b/.github/workflows/storybook.yml
@@ -36,7 +36,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js 20.x
- uses: actions/setup-node@v4.0.2
+ uses: actions/setup-node@v4.0.3
with:
node-version-file: '.node-version'
cache: 'pnpm'
@@ -88,7 +88,7 @@ jobs:
if [ "$BRANCH" = "misskey-dev:$HEAD_REF" ]; then
BRANCH="$HEAD_REF"
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:
HEAD_REF: ${{ github.event.pull_request.head.ref }}
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml
index b1c54bb3e77e..bfb79ef09013 100644
--- a/.github/workflows/test-backend.yml
+++ b/.github/workflows/test-backend.yml
@@ -9,12 +9,13 @@ on:
- packages/backend/**
# for permissions
- packages/misskey-js/**
+ - .github/workflows/test-backend.yml
pull_request:
paths:
- packages/backend/**
# for permissions
- packages/misskey-js/**
-
+ - .github/workflows/test-backend.yml
jobs:
unit:
runs-on: ubuntu-latest
@@ -45,7 +46,7 @@ jobs:
- name: Install FFmpeg
uses: FedericoCarboni/setup-ffmpeg@v3
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v4.0.2
+ uses: actions/setup-node@v4.0.3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
@@ -92,7 +93,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v4.0.2
+ uses: actions/setup-node@v4.0.3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml
index 9d5053b82aa8..c17a9fd3877a 100644
--- a/.github/workflows/test-frontend.yml
+++ b/.github/workflows/test-frontend.yml
@@ -11,7 +11,7 @@ on:
- packages/misskey-js/**
# for e2e
- packages/backend/**
-
+ - .github/workflows/test-frontend.yml
pull_request:
paths:
- packages/frontend/**
@@ -19,7 +19,7 @@ on:
- packages/misskey-js/**
# for e2e
- packages/backend/**
-
+ - .github/workflows/test-frontend.yml
jobs:
vitest:
runs-on: ubuntu-latest
@@ -35,7 +35,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v4.0.2
+ uses: actions/setup-node@v4.0.3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
@@ -90,7 +90,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v4.0.2
+ uses: actions/setup-node@v4.0.3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
diff --git a/.github/workflows/test-misskey-js.yml b/.github/workflows/test-misskey-js.yml
index 2589d908b8d4..6ee67e8735bf 100644
--- a/.github/workflows/test-misskey-js.yml
+++ b/.github/workflows/test-misskey-js.yml
@@ -8,11 +8,12 @@ on:
branches: [ develop ]
paths:
- packages/misskey-js/**
+ - .github/workflows/test-misskey-js.yml
pull_request:
branches: [ develop ]
paths:
- packages/misskey-js/**
-
+ - .github/workflows/test-misskey-js.yml
jobs:
test:
@@ -30,7 +31,7 @@ jobs:
- run: corepack enable
- name: Setup Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v4.0.2
+ uses: actions/setup-node@v4.0.3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
diff --git a/.github/workflows/test-production.yml b/.github/workflows/test-production.yml
index 7f8db652938f..18d02ec030ee 100644
--- a/.github/workflows/test-production.yml
+++ b/.github/workflows/test-production.yml
@@ -25,7 +25,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v4.0.2
+ uses: actions/setup-node@v4.0.3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
diff --git a/.github/workflows/validate-api-json.yml b/.github/workflows/validate-api-json.yml
index 24340e7d81f2..90f2929a2566 100644
--- a/.github/workflows/validate-api-json.yml
+++ b/.github/workflows/validate-api-json.yml
@@ -7,10 +7,11 @@ on:
- develop
paths:
- packages/backend/**
+ - .github/workflows/validate-api-json.yml
pull_request:
paths:
- packages/backend/**
-
+ - .github/workflows/validate-api-json.yml
jobs:
validate-api-json:
runs-on: ubuntu-latest
@@ -26,7 +27,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v4.0.2
+ uses: actions/setup-node@v4.0.3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
diff --git a/.gitignore b/.gitignore
index bdc14fea0aba..3466984cf648 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,8 +35,8 @@ coverage
!/.config/example.yml
!/.config/docker_example.yml
!/.config/docker_example.env
-docker-compose.yml
-!/.devcontainer/docker-compose.yml
+.devcontainer/compose.yml
+!/.devcontainer/compose.yml
# misskey
/build
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 681903a1f2ba..14cae5d3de8f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,11 +1,75 @@
## Unreleased
+### Note
+- デッキUIの新着ノートをサウンドで通知する機能の追加(v2024.5.0)に伴い、以前から動作しなくなっていたクライアント設定内の「アンテナ受信」「チャンネル通知」サウンドを削除しました。
+
+### General
+- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
+- Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に
+ - 変更不可となっていても、設定済みのものを解除してデフォルト画像に戻すことは出来ます
+- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
+- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
+- Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正
+- Enhance: Allow negative delay for MFM animation elements (`tada`, `jelly`, `twitch`, `shake`, `spin`, `jump`, `bounce`, `rainbow`)
+
+### Client
+- Enhance: 内蔵APIドキュメントのデザイン・パフォーマンスを改善
+- Enhance: 非ログイン時に他サーバーに遷移するアクションを追加
+- Enhance: 非ログイン時のハイライトTLのデザインを改善
+- Enhance: フロントエンドのアクセシビリティ改善
+ (Based on https://github.com/taiyme/misskey/pull/226)
+- Enhance: サーバー情報ページ・お問い合わせページを改善
+ (Cherry-picked from https://github.com/taiyme/misskey/pull/238)
+- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
+- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
+- Fix: リバーシの対局を正しく共有できないことがある問題を修正
+- Fix: コントロールパネルでベースロールのポリシーを編集してもUI上では変更が反映されない問題を修正
+- Fix: アンテナの編集画面のボタンに隙間を追加
+- Fix: テーマプレビューが見れない問題を修正
+- Fix: ショートカットキーが連打できる問題を修正
+ (Cherry-picked from https://github.com/taiyme/misskey/pull/234)
+
+### Server
+- Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)
+- Enhance: エンドポイント`clips/update`の必須項目を`clipId`のみに
+- Enhance: エンドポイント`admin/roles/update`の必須項目を`roleId`のみに
+- Enhance: エンドポイント`pages/update`の必須項目を`pageId`のみに
+- Enhance: エンドポイント`gallery/posts/update`の必須項目を`postId`のみに
+- Enhance: エンドポイント`i/webhook/update`の必須項目を`webhookId`のみに
+- Enhance: エンドポイント`admin/ad/update`の必須項目を`id`のみに
+- Enhance: `default.yml`内の`url`, `db.db`, `db.user`, `db.pass`を環境変数から読み込めるように
+- Fix: チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正
+- Fix: ユーザーのフィードページのMFMをHTMLに展開するように (#14006)
+- Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036)
+- Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059)
+- Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正
+- Fix: 自分以外のクリップ内のノート個数が見えることがあるのを修正
+- Fix: 空文字列のリアクションはフォールバックされるように
+- Fix: リノートにリアクションできないように
+- Fix: ユーザー名の前後に空白文字列がある場合は省略するように
+- Fix: プロフィール編集時に名前を空白文字列のみにできる問題を修正
+- Fix: ユーザ名のサジェスト時に表示される内容と順番を調整(以下の順番になります) #14149
+ 1. フォロー中かつアクティブなユーザ
+ 2. フォロー中かつ非アクティブなユーザ
+ 3. フォローしていないアクティブなユーザ
+ 4. フォローしていない非アクティブなユーザ
+- Fix: 一般ユーザーから見たユーザーのバッジの一覧に公開されていないものが含まれることがある問題を修正
+ (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/652)
+- Fix: ユーザーのリアクション一覧でミュート/ブロックが機能していなかった問題を修正
+
+### Misskey.js
+- Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応)
+- Feat: `/admin/role/create` のロールポリシーの型を修正
+
+## 2024.5.0
+
### Note
- コントロールパネル内にあるサマリープロキシの設定個所がセキュリティから全般へ変更となります。
- 悪意のある第三者がリモートユーザーになりすましたアクティビティを受け取れてしまう問題を修正しました。詳しくは[GitHub security advisory](https://github.com/misskey-dev/misskey/security/advisories/GHSA-2vxv-pv3m-3wvj)をご覧ください。
- 管理者向け権限 `read:admin:show-users` は `read:admin:show-user` に統合されました。必要に応じてAPIトークンを再発行してください。
### General
+- Feat: エラートラッキングにSentryを使用できるようになりました
- Enhance: URLプレビューの有効化・無効化を設定できるように #13569
- Enhance: アンテナでBotによるノートを除外できるように
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/545)
@@ -19,12 +83,15 @@
- Enhance: Goneを出さずに終了したサーバーへの配信停止を自動的に行うように
- もしそのようなサーバーからから配信が届いた場合には自動的に配信を再開します
- Enhance: 配信停止の理由を表示するように
+- Enhance: サーバーのお問い合わせ先URLを設定できるようになりました
- Fix: Play作成時に設定した公開範囲が機能していない問題を修正
- Fix: 正規化されていない状態のhashtagが連合されてきたhtmlに含まれているとhashtagが正しくhashtagに復元されない問題を修正
- Fix: みつけるのアンケート欄にてチャンネルのアンケートが含まれてしまう問題を修正
### Client
- Feat: アップロードするファイルの名前をランダム文字列にできるように
+- Feat: 個別のお知らせにリンクで飛べるように
+ (Based on https://github.com/MisskeyIO/misskey/pull/639)
- Enhance: 自分のノートの添付ファイルから直接ファイルの詳細ページに飛べるように
- Enhance: 広告がMisskeyと同一ドメインの場合はRouterで遷移するように
- Enhance: リアクション・いいねの総数を表示するように
@@ -47,6 +114,9 @@
- Enhance: AiScriptを0.18.0にバージョンアップ
- Enhance: 通常のノートでも、お気に入りに登録したチャンネルにリノートできるように
- Enhance: 長いテキストをペーストした際にテキストファイルとして添付するかどうかを選択できるように
+- Enhance: 新着ノートをサウンドで通知する機能をdeck UIに追加しました
+- Enhance: コントロールパネルのクイックアクションからファイルを照会できるように
+- Enhance: コントロールパネルのクイックアクションから通常の照会を行えるように
- Fix: 一部のページ内リンクが正しく動作しない問題を修正
- Fix: 周年の実績が閏年を考慮しない問題を修正
- Fix: ローカルURLのプレビューポップアップが左上に表示される
@@ -67,6 +137,7 @@
- Fix: 通知をグループ化している際に、人数が正常に表示されないことがある問題を修正
- Fix: 連合なしの状態の読み書きができない問題を修正
- Fix: `/share` で日本語等を含むurlがurlエンコードされない問題を修正
+- Fix: ファイルを5つ以上添付してもテキストがないとノートが折りたたまれない問題を修正
### Server
- Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに
@@ -92,6 +163,8 @@
- Fix: `/i/notifications`に `includeTypes`か`excludeTypes`を指定しているとき、通知が存在するのに空配列を返すことがある問題を修正
- Fix: 複数idを指定する`users/show`が関係ないユーザを返すことがある問題を修正
- Fix: `/tags` と `/user-tags` が検索エンジンにインデックスされないように
+- Fix: もともとセンシティブではないと連合されていたファイルがセンシティブとして連合された場合にセンシティブとしてそのファイルを扱うように
+ - センシティブとして連合したファイルは非センシティブとして連合されてもセンシティブとして扱われます
## 2024.3.1
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index dcb625626d6f..b718f3703f87 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -165,7 +165,7 @@ cp .github/misskey/test.yml .config/
```
Prepare DB/Redis for testing.
```
-docker compose -f packages/backend/test/docker-compose.yml up
+docker compose -f packages/backend/test/compose.yml up
```
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.
diff --git a/Dockerfile b/Dockerfile
index 9fc2d611cd82..d6ca6b8cdffb 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -82,6 +82,10 @@ RUN apt-get update \
USER misskey
WORKDIR /misskey
+# add package.json to add pnpm
+COPY --chown=misskey:misskey ./package.json ./package.json
+RUN corepack install
+
COPY --chown=misskey:misskey --from=target-builder /misskey/node_modules ./node_modules
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/backend/node_modules ./packages/backend/node_modules
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules
diff --git a/README.md b/README.md
index 24013a7bd817..92e8fef6396e 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,10 @@
## Thanks
+
+
+Thanks to [Sentry](https://sentry.io/) for providing the error tracking platform that helps us catch unexpected errors.
+
Thanks to [Chromatic](https://www.chromatic.com/) for providing the visual testing platform that helps us review UI changes and catch visual regressions.
diff --git a/chart/files/default.yml b/chart/files/default.yml
index 4cc291e80a77..f98b8ebfee04 100644
--- a/chart/files/default.yml
+++ b/chart/files/default.yml
@@ -152,6 +152,22 @@ redis:
# ID SETTINGS AFTER THAT!
id: "aidx"
+
+# ┌────────────────┐
+#───┘ Error tracking └──────────────────────────────────────────
+
+# Sentry is available for error tracking.
+# See the Sentry documentation for more details on options.
+
+#sentryForBackend:
+# enableNodeProfiling: true
+# options:
+# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
+
+#sentryForFrontend:
+# options:
+# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
+
# ┌─────────────────────┐
#───┘ Other configuration └─────────────────────────────────────
diff --git a/docker-compose.local-db.yml b/compose.local-db.yml
similarity index 98%
rename from docker-compose.local-db.yml
rename to compose.local-db.yml
index 16ba4b49e179..3835cb23dbad 100644
--- a/docker-compose.local-db.yml
+++ b/compose.local-db.yml
@@ -1,5 +1,3 @@
-version: "3"
-
# このconfigは、 dockerでMisskey本体を起動せず、 redisとpostgresql などだけを起動します
services:
diff --git a/docker-compose_example.yml b/compose_example.yml
similarity index 97%
rename from docker-compose_example.yml
rename to compose_example.yml
index 5cebbe416467..336bd814a793 100644
--- a/docker-compose_example.yml
+++ b/compose_example.yml
@@ -1,5 +1,3 @@
-version: "3"
-
services:
web:
build: .
@@ -19,6 +17,8 @@ services:
networks:
- internal_network
- external_network
+ # env_file:
+ # - .config/docker.env
volumes:
- ./files:/misskey/files
- ./.config:/misskey/.config:ro
diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml
index 88707fe1118e..955d672c1d05 100644
--- a/locales/ar-SA.yml
+++ b/locales/ar-SA.yml
@@ -1016,6 +1016,8 @@ sourceCode: "الشفرة المصدرية"
flip: "اقلب"
lastNDays: "آخر {n} أيام"
surrender: "ألغِ"
+_delivery:
+ stop: "مُعلّق"
_initialAccountSetting:
accountCreated: "نجح إنشاء حسابك!"
letsStartAccountSetup: "إذا كنت جديدًا لنعدّ حسابك الشخصي."
diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml
index dc5d315aed9f..abcf07da831e 100644
--- a/locales/bn-BD.yml
+++ b/locales/bn-BD.yml
@@ -857,6 +857,10 @@ replies: "জবাব"
renotes: "রিনোট"
sourceCode: "সোর্স কোড"
flip: "উল্টান"
+_delivery:
+ stop: "স্থগিত করা হয়েছে"
+ _type:
+ none: "প্রকাশ করা হচ্ছে"
_role:
priority: "অগ্রাধিকার"
_priority:
diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml
index d035555c73af..0345ee0326e9 100644
--- a/locales/ca-ES.yml
+++ b/locales/ca-ES.yml
@@ -1224,6 +1224,10 @@ gameRetry: "Torna a provar"
notUsePleaseLeaveBlank: "Si no voleu usar-ho, deixeu-ho en blanc"
useTotp: "Usa una contrasenya d'un sol ús"
useBackupCode: "Usa un codi de recuperació"
+_delivery:
+ stop: "Suspés"
+ _type:
+ none: "S'està publicant"
_bubbleGame:
howToPlay: "Com es juga"
_howToPlay:
@@ -2001,7 +2005,6 @@ _permissions:
"read:admin:server-info": "Veure informació del servidor"
"read:admin:show-moderation-log": "Veure registre de moderació "
"read:admin:show-user": "Veure informació privada de l'usuari "
- "read:admin:show-users": "Veure informació privada de l'usuari "
"write:admin:suspend-user": "Suspendre usuari"
"write:admin:unset-user-avatar": "Esborrar avatar d'usuari "
"write:admin:unset-user-banner": "Esborrar bàner de l'usuari "
diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml
index cff533976ec6..c8a0b0cb2804 100644
--- a/locales/cs-CZ.yml
+++ b/locales/cs-CZ.yml
@@ -1099,6 +1099,10 @@ sourceCode: "Zdrojový kód"
flip: "Otočit"
lastNDays: "Posledních {n} dnů"
surrender: "Zrušit"
+_delivery:
+ stop: "Suspendováno"
+ _type:
+ none: "Publikuji"
_initialAccountSetting:
accountCreated: "Váš účet byl úspěšně vytvořen!"
letsStartAccountSetup: "Pro začátek si nastavte svůj profil."
diff --git a/locales/da-DK.yml b/locales/da-DK.yml
index 08c15ed092fc..5eb7a5a5f417 100644
--- a/locales/da-DK.yml
+++ b/locales/da-DK.yml
@@ -1,2 +1,4 @@
---
_lang_: "Dansk"
+headlineMisskey: ""
+introMisskey: "ようこそ!Misskeyは、オープンソースの分散型マイクロブログサービスです。\n「ノート」を作成して、いま起こっていることを共有したり、あなたについて皆に発信しよう📡\n「リアクション」機能で、皆のノートに素早く反応を追加することもできます👍\n新しい世界を探検しよう🚀"
diff --git a/locales/de-DE.yml b/locales/de-DE.yml
index 3e1c40512e77..9e42e0125257 100644
--- a/locales/de-DE.yml
+++ b/locales/de-DE.yml
@@ -1185,6 +1185,10 @@ addMfmFunction: "MFM hinzufügen"
sfx: "Soundeffekte"
lastNDays: "Letzten {n} Tage"
surrender: "Abbrechen"
+_delivery:
+ stop: "Gesperrt"
+ _type:
+ none: "Wird veröffentlicht"
_announcement:
forExistingUsers: "Nur für existierende Nutzer"
forExistingUsersDescription: "Ist diese Option aktiviert, wird diese Ankündigung nur Nutzern angezeigt, die zum Zeitpunkt der Ankündigung bereits registriert sind. Ist sie deaktiviert, wird sie auch Nutzern, die sich nach dessen Veröffentlichung registrieren, angezeigt."
diff --git a/locales/en-US.yml b/locales/en-US.yml
index 10e9fd778e70..c20a1ac7d8ef 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -108,11 +108,14 @@ enterEmoji: "Enter an emoji"
renote: "Renote"
unrenote: "Remove renote"
renoted: "Renoted."
+renotedToX: "Renote from {name} users。"
cantRenote: "This post can't be renoted."
cantReRenote: "A renote can't be renoted."
quote: "Quote"
inChannelRenote: "Channel-only Renote"
inChannelQuote: "Channel-only Quote"
+renoteToChannel: "Renote to channel"
+renoteToOtherChannel: "Renote to other channel"
pinnedNote: "Pinned note"
pinned: "Pin to profile"
you: "You"
@@ -468,6 +471,7 @@ retype: "Enter again"
noteOf: "Note by {user}"
quoteAttached: "Quote"
quoteQuestion: "Append as quote?"
+attachAsFileQuestion: "The text in clipboard is long. Would you want to attach it as text file?"
noMessagesYet: "No messages yet"
newMessageExists: "There are new messages"
onlyOneFileCanBeAttached: "You can only attach one file to a message"
@@ -1235,6 +1239,15 @@ keepOriginalFilenameDescription: "If you turn off this setting, files names will
noDescription: "There is not the explanation"
alwaysConfirmFollow: "Always confirm when following"
inquiry: "Contact"
+_delivery:
+ status: "Delivery status"
+ stop: "Suspended"
+ resume: "Delivery resume"
+ _type:
+ none: "Publishing"
+ manuallySuspended: "Manually suspended"
+ goneSuspended: "Server is suspended due to server deletion"
+ autoSuspendedForNotResponding: "Server is suspended due to no responding"
_bubbleGame:
howToPlay: "How to play"
hold: "Hold"
@@ -2032,7 +2045,6 @@ _permissions:
"read:admin:server-info": "View server info"
"read:admin:show-moderation-log": "View moderation log"
"read:admin:show-user": "View private user info"
- "read:admin:show-users": "View private user info"
"write:admin:suspend-user": "Suspend user"
"write:admin:unset-user-avatar": "Remove user avatar"
"write:admin:unset-user-banner": "Remove user banner"
diff --git a/locales/es-ES.yml b/locales/es-ES.yml
index 2e05364c312f..5c8249ded50f 100644
--- a/locales/es-ES.yml
+++ b/locales/es-ES.yml
@@ -1233,6 +1233,10 @@ useNativeUIForVideoAudioPlayer: "Usar la interfaz del navegador cuando se reprod
keepOriginalFilename: "Mantener el nombre original del archivo"
noDescription: "No hay descripción"
alwaysConfirmFollow: "Confirmar siempre cuando se sigue a alguien"
+_delivery:
+ stop: "Suspendido"
+ _type:
+ none: "Publicando"
_bubbleGame:
howToPlay: "Cómo jugar"
hold: "Mantener"
@@ -2029,7 +2033,6 @@ _permissions:
"read:admin:server-info": "Ver información del servidor"
"read:admin:show-moderation-log": "Ver log de moderación"
"read:admin:show-user": "Ver información privada de usuario"
- "read:admin:show-users": "Ver información privada de usuario"
"write:admin:suspend-user": "Suspender cuentas de usuario"
"write:admin:unset-user-avatar": "Quitar avatares de usuario"
"write:admin:unset-user-banner": "Quitar banner de usuarios"
diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml
index 58a11a5cc4ea..8d66c3d37572 100644
--- a/locales/fr-FR.yml
+++ b/locales/fr-FR.yml
@@ -1224,6 +1224,10 @@ enableHorizontalSwipe: "Glisser pour changer d'onglet"
loading: "Chargement en cours"
surrender: "Annuler"
gameRetry: "Réessayer"
+_delivery:
+ stop: "Suspendu·e"
+ _type:
+ none: "Publié"
_bubbleGame:
howToPlay: "Comment jouer"
hold: "Réserver"
diff --git a/locales/id-ID.yml b/locales/id-ID.yml
index f8e645d63b57..7f509afa501a 100644
--- a/locales/id-ID.yml
+++ b/locales/id-ID.yml
@@ -108,11 +108,14 @@ enterEmoji: "Masukkan emoji"
renote: "Renote"
unrenote: "Hapus renote"
renoted: "Telah direnote"
+renotedToX: "{name} telah merenote"
cantRenote: "Postingan ini tidak dapat direnote"
cantReRenote: "Renote tidak dapat direnote"
quote: "Kutip"
inChannelRenote: "Hanya renote dalam kanal"
inChannelQuote: "Hanya kutip dalam kanal"
+renoteToChannel: "Renote ke kanal"
+renoteToOtherChannel: "Renote ke kanal lainnya"
pinnedNote: "Catatan yang disematkan"
pinned: "Sematkan ke profil"
you: "Kamu"
@@ -468,6 +471,7 @@ retype: "Masukkan ulang"
noteOf: "Catatan milik {user}"
quoteAttached: "Dikutip"
quoteQuestion: "Apakah kamu ingin menambahkan kutipan?"
+attachAsFileQuestion: "Teks dalam papan klip terlalu panjang. Apakah kamu ingin melampirkannya sebagai berkas teks?"
noMessagesYet: "Tidak ada pesan"
newMessageExists: "Kamu mendapatkan pesan baru"
onlyOneFileCanBeAttached: "Kamu hanya dapat melampirkan satu berkas ke dalam pesan"
@@ -1235,6 +1239,15 @@ keepOriginalFilenameDescription: "Apabila pengaturan ini dimatikan, nama berkas
noDescription: "Tidak ada deskripsi"
alwaysConfirmFollow: "Selalu konfirmasi ketika mengikuti"
inquiry: "Hubungi kami"
+_delivery:
+ status: "Status pengiriman"
+ stop: "Ditangguhkan"
+ resume: "Lanjutkan pengiriman"
+ _type:
+ none: "Sedang menyiarkan langsung"
+ manuallySuspended: "Ditangguhkan manual"
+ goneSuspended: "Sedang ditangguhkan untuk penghapusan peladen"
+ autoSuspendedForNotResponding: "Sedang ditangguhkan karena peladen tidak menjawab"
_bubbleGame:
howToPlay: "Cara bermain"
hold: "Tahan"
@@ -2032,7 +2045,6 @@ _permissions:
"read:admin:server-info": "Lihat informasi peladen"
"read:admin:show-moderation-log": "Lihat log moderasi"
"read:admin:show-user": "Lihat informasi pengguna privat"
- "read:admin:show-users": "Lihat informasi pengguna privat"
"write:admin:suspend-user": "Tangguhkan pengguna"
"write:admin:unset-user-avatar": "Hapus avatar pengguna"
"write:admin:unset-user-banner": "Hapus banner pengguna"
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 4c8d4c5c96df..35aa07305b67 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -736,6 +736,22 @@ export interface Locale extends ILocale {
* リモートで表示
*/
"showOnRemote": string;
+ /**
+ * リモートで続行
+ */
+ "continueOnRemote": string;
+ /**
+ * Misskey Hubからサーバーを選択
+ */
+ "chooseServerOnMisskeyHub": string;
+ /**
+ * サーバーのドメインを直接指定
+ */
+ "specifyServerHost": string;
+ /**
+ * ドメインを入力してください
+ */
+ "inputHostName": string;
/**
* 全般
*/
@@ -1292,6 +1308,10 @@ export interface Locale extends ILocale {
* フォルダーを選択
*/
"selectFolders": string;
+ /**
+ * ファイルが選択されていません
+ */
+ "fileNotSelected": string;
/**
* ファイル名を変更
*/
@@ -1929,9 +1949,13 @@ export interface Locale extends ILocale {
*/
"onlyOneFileCanBeAttached": string;
/**
- * 続行する前に、サインアップまたはサインインが必要です
+ * 続行する前に、登録またはログインが必要です
*/
"signinRequired": string;
+ /**
+ * 続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります
+ */
+ "signinOrContinueOnRemote": string;
/**
* 招待
*/
@@ -3372,6 +3396,10 @@ export interface Locale extends ILocale {
* 管理者情報が設定されていません。
*/
"noMaintainerInformationWarning": string;
+ /**
+ * 問い合わせ先URLが設定されていません。
+ */
+ "noInquiryUrlWarning": string;
/**
* Botプロテクションが設定されていません。
*/
@@ -4125,9 +4153,13 @@ export interface Locale extends ILocale {
*/
"thisPostMayBeAnnoyingIgnore": string;
/**
- * 見たことのあるリノートを省略して表示
+ * リノートのスマート省略
*/
"collapseRenotes": string;
+ /**
+ * リアクションやリノートをしたことがあるノートをたたんで表示します。
+ */
+ "collapseRenotesDescription": string;
/**
* サーバー内部エラー
*/
@@ -4984,6 +5016,10 @@ export interface Locale extends ILocale {
* お問い合わせ
*/
"inquiry": string;
+ /**
+ * もう一度お試しください。
+ */
+ "tryAgain": string;
"_delivery": {
/**
* 配信状態
@@ -5475,6 +5511,14 @@ export interface Locale extends ILocale {
* 有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。
*/
"fanoutTimelineDbFallbackDescription": string;
+ /**
+ * 問い合わせ先URL
+ */
+ "inquiryUrl": string;
+ /**
+ * サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。
+ */
+ "inquiryUrlDescription": string;
};
"_accountMigration": {
/**
@@ -6586,6 +6630,10 @@ export interface Locale extends ILocale {
* ファイルにNSFWを常に付与
*/
"alwaysMarkNsfw": string;
+ /**
+ * アイコンとバナーの更新を許可
+ */
+ "canUpdateBioMedia": string;
/**
* ノートのピン留めの最大数
*/
@@ -7507,14 +7555,6 @@ export interface Locale extends ILocale {
* 通知
*/
"notification": string;
- /**
- * アンテナ受信
- */
- "antenna": string;
- /**
- * チャンネル通知
- */
- "channel": string;
/**
* リアクション選択時
*/
@@ -9151,6 +9191,10 @@ export interface Locale extends ILocale {
* カラムを追加
*/
"addColumn": string;
+ /**
+ * 新着ノート通知の設定
+ */
+ "newNoteNotificationSettings": string;
/**
* カラムの設定
*/
@@ -9293,6 +9337,10 @@ export interface Locale extends ILocale {
* Webhookを作成
*/
"createWebhook": string;
+ /**
+ * Webhookを編集
+ */
+ "modifyWebhook": string;
/**
* 名前
*/
@@ -9339,6 +9387,72 @@ export interface Locale extends ILocale {
*/
"mention": string;
};
+ "_systemEvents": {
+ /**
+ * ユーザーから通報があったとき
+ */
+ "abuseReport": string;
+ /**
+ * ユーザーからの通報を処理したとき
+ */
+ "abuseReportResolved": string;
+ };
+ /**
+ * Webhookを削除しますか?
+ */
+ "deleteConfirm": string;
+ };
+ "_abuseReport": {
+ "_notificationRecipient": {
+ /**
+ * 通報の通知先を追加
+ */
+ "createRecipient": string;
+ /**
+ * 通報の通知先を編集
+ */
+ "modifyRecipient": string;
+ /**
+ * 通知先の種類
+ */
+ "recipientType": string;
+ "_recipientType": {
+ /**
+ * メール
+ */
+ "mail": string;
+ /**
+ * Webhook
+ */
+ "webhook": string;
+ "_captions": {
+ /**
+ * モデレーター権限を持つユーザーのメールアドレスに通知を送ります(通報を受けた時のみ)
+ */
+ "mail": string;
+ /**
+ * 指定したSystemWebhookに通知を送ります(通報を受けた時と通報を解決した時にそれぞれ発信)
+ */
+ "webhook": string;
+ };
+ };
+ /**
+ * キーワード
+ */
+ "keywords": string;
+ /**
+ * 通知先ユーザー
+ */
+ "notifiedUser": string;
+ /**
+ * 使用するWebhook
+ */
+ "notifiedWebhook": string;
+ /**
+ * 通知先を削除しますか?
+ */
+ "deleteConfirm": string;
+ };
};
"_moderationLogTypes": {
/**
@@ -9485,6 +9599,30 @@ export interface Locale extends ILocale {
* ユーザーのバナーを解除
*/
"unsetUserBanner": string;
+ /**
+ * SystemWebhookを作成
+ */
+ "createSystemWebhook": string;
+ /**
+ * SystemWebhookを更新
+ */
+ "updateSystemWebhook": string;
+ /**
+ * SystemWebhookを削除
+ */
+ "deleteSystemWebhook": string;
+ /**
+ * 通報の通知先を作成
+ */
+ "createAbuseReportNotificationRecipient": string;
+ /**
+ * 通報の通知先を更新
+ */
+ "updateAbuseReportNotificationRecipient": string;
+ /**
+ * 通報の通知先を削除
+ */
+ "deleteAbuseReportNotificationRecipient": string;
};
"_fileViewer": {
/**
@@ -9655,7 +9793,7 @@ export interface Locale extends ILocale {
"_dataSaver": {
"_media": {
/**
- * メディアの読み込み
+ * メディアの読み込みを無効化
*/
"title": string;
/**
@@ -9665,7 +9803,7 @@ export interface Locale extends ILocale {
};
"_avatar": {
/**
- * アイコン画像
+ * アイコン画像のアニメーションを無効化
*/
"title": string;
/**
@@ -9675,7 +9813,7 @@ export interface Locale extends ILocale {
};
"_urlPreview": {
/**
- * URLプレビューのサムネイル
+ * URLプレビューのサムネイルを非表示
*/
"title": string;
/**
@@ -9685,7 +9823,7 @@ export interface Locale extends ILocale {
};
"_code": {
/**
- * コードハイライト
+ * コードハイライトを非表示
*/
"title": string;
/**
diff --git a/locales/index.js b/locales/index.js
index 650e55233776..c2738884eb34 100644
--- a/locales/index.js
+++ b/locales/index.js
@@ -52,7 +52,11 @@ const primaries = {
const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
export function build() {
- const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {});
+ // vitestの挙動を調整するため、一度ローカル変数化する必要がある
+ // https://github.com/vitest-dev/vitest/issues/3988#issuecomment-1686599577
+ // https://github.com/misskey-dev/misskey/pull/14057#issuecomment-2192833785
+ const metaUrl = import.meta.url;
+ const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, metaUrl), 'utf-8'))) || {}, a), {});
// 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す
const removeEmpty = (obj) => {
diff --git a/locales/it-IT.yml b/locales/it-IT.yml
index 0a250a2e289e..1d12a62ccadc 100644
--- a/locales/it-IT.yml
+++ b/locales/it-IT.yml
@@ -1233,6 +1233,10 @@ useNativeUIForVideoAudioPlayer: "Riprodurre audio/video usando le funzionalità
keepOriginalFilename: "Mantieni il nome file originale"
keepOriginalFilenameDescription: "Disattivandola, i file verranno caricati usando nomi casuali."
noDescription: "Manca la descrizione"
+_delivery:
+ stop: "Sospensione"
+ _type:
+ none: "Pubblicazione"
_bubbleGame:
howToPlay: "Come giocare"
hold: "Tieni"
@@ -2025,7 +2029,6 @@ _permissions:
"read:admin:server-info": "Vedere le informazioni sul server"
"read:admin:show-moderation-log": "Vedere lo storico di moderazione"
"read:admin:show-user": "Vedere le informazioni private degli account utente"
- "read:admin:show-users": "Vedere le informazioni private degli account utente"
"write:admin:suspend-user": "Sospendere i profili"
"write:admin:unset-user-avatar": "Rimuovere la foto profilo dai profili"
"write:admin:unset-user-banner": "Rimuovere l'immagine testata dai profili"
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 7a12abba5721..6902e21530b1 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -180,6 +180,10 @@ addAccount: "アカウントを追加"
reloadAccountsList: "アカウントリストの情報を更新"
loginFailed: "ログインに失敗しました"
showOnRemote: "リモートで表示"
+continueOnRemote: "リモートで続行"
+chooseServerOnMisskeyHub: "Misskey Hubからサーバーを選択"
+specifyServerHost: "サーバーのドメインを直接指定"
+inputHostName: "ドメインを入力してください"
general: "全般"
wallpaper: "壁紙"
setWallpaper: "壁紙を設定"
@@ -319,6 +323,7 @@ selectFile: "ファイルを選択"
selectFiles: "ファイルを選択"
selectFolder: "フォルダーを選択"
selectFolders: "フォルダーを選択"
+fileNotSelected: "ファイルが選択されていません"
renameFile: "ファイル名を変更"
folderName: "フォルダー名"
createFolder: "フォルダーを作成"
@@ -478,7 +483,8 @@ attachAsFileQuestion: "クリップボードのテキストが長いです。テ
noMessagesYet: "まだチャットはありません"
newMessageExists: "新しいメッセージがあります"
onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです"
-signinRequired: "続行する前に、サインアップまたはサインインが必要です"
+signinRequired: "続行する前に、登録またはログインが必要です"
+signinOrContinueOnRemote: "続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります"
invitations: "招待"
invitationCode: "招待コード"
checking: "確認しています"
@@ -839,6 +845,7 @@ administration: "管理"
accounts: "アカウント"
switch: "切り替え"
noMaintainerInformationWarning: "管理者情報が設定されていません。"
+noInquiryUrlWarning: "問い合わせ先URLが設定されていません。"
noBotProtectionWarning: "Botプロテクションが設定されていません。"
configure: "設定する"
postToGallery: "ギャラリーへ投稿"
@@ -1027,7 +1034,8 @@ thisPostMayBeAnnoying: "この投稿は迷惑になる可能性があります
thisPostMayBeAnnoyingHome: "ホームに投稿"
thisPostMayBeAnnoyingCancel: "やめる"
thisPostMayBeAnnoyingIgnore: "このまま投稿"
-collapseRenotes: "見たことのあるリノートを省略して表示"
+collapseRenotes: "リノートのスマート省略"
+collapseRenotesDescription: "リアクションやリノートをしたことがあるノートをたたんで表示します。"
internalServerError: "サーバー内部エラー"
internalServerErrorDescription: "サーバー内部で予期しないエラーが発生しました。"
copyErrorInfo: "エラー情報をコピー"
@@ -1242,6 +1250,7 @@ keepOriginalFilenameDescription: "この設定をオフにすると、アップ
noDescription: "説明文はありません"
alwaysConfirmFollow: "フォローの際常に確認する"
inquiry: "お問い合わせ"
+tryAgain: "もう一度お試しください。"
_delivery:
status: "配信状態"
@@ -1384,6 +1393,8 @@ _serverSettings:
fanoutTimelineDescription: "有効にすると、各種タイムラインを取得する際のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。サーバーのメモリ容量が少ない場合、または動作が不安定な場合は無効にすることができます。"
fanoutTimelineDbFallback: "データベースへのフォールバック"
fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。"
+ inquiryUrl: "問い合わせ先URL"
+ inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。"
_accountMigration:
moveFrom: "別のアカウントからこのアカウントに移行"
@@ -1703,6 +1714,7 @@ _role:
canManageAvatarDecorations: "アバターデコレーションの管理"
driveCapacity: "ドライブ容量"
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
+ canUpdateBioMedia: "アイコンとバナーの更新を許可"
pinMax: "ノートのピン留めの最大数"
antennaMax: "アンテナの作成可能数"
wordMuteMax: "ワードミュートの最大文字数"
@@ -1969,8 +1981,6 @@ _sfx:
note: "ノート"
noteMy: "ノート(自分)"
notification: "通知"
- antenna: "アンテナ受信"
- channel: "チャンネル通知"
reaction: "リアクション選択時"
_soundSettings:
@@ -2422,6 +2432,7 @@ _deck:
alwaysShowMainColumn: "常にメインカラムを表示"
columnAlign: "カラムの寄せ"
addColumn: "カラムを追加"
+ newNoteNotificationSettings: "新着ノート通知の設定"
configureColumn: "カラムの設定"
swapLeft: "左に移動"
swapRight: "右に移動"
@@ -2465,6 +2476,7 @@ _drivecleaner:
_webhookSettings:
createWebhook: "Webhookを作成"
+ modifyWebhook: "Webhookを編集"
name: "名前"
secret: "シークレット"
events: "Webhookを実行するタイミング"
@@ -2477,6 +2489,26 @@ _webhookSettings:
renote: "Renoteされたとき"
reaction: "リアクションがあったとき"
mention: "メンションされたとき"
+ _systemEvents:
+ abuseReport: "ユーザーから通報があったとき"
+ abuseReportResolved: "ユーザーからの通報を処理したとき"
+ deleteConfirm: "Webhookを削除しますか?"
+
+_abuseReport:
+ _notificationRecipient:
+ createRecipient: "通報の通知先を追加"
+ modifyRecipient: "通報の通知先を編集"
+ recipientType: "通知先の種類"
+ _recipientType:
+ mail: "メール"
+ webhook: "Webhook"
+ _captions:
+ mail: "モデレーター権限を持つユーザーのメールアドレスに通知を送ります(通報を受けた時のみ)"
+ webhook: "指定したSystemWebhookに通知を送ります(通報を受けた時と通報を解決した時にそれぞれ発信)"
+ keywords: "キーワード"
+ notifiedUser: "通知先ユーザー"
+ notifiedWebhook: "使用するWebhook"
+ deleteConfirm: "通知先を削除しますか?"
_moderationLogTypes:
createRole: "ロールを作成"
@@ -2515,6 +2547,12 @@ _moderationLogTypes:
deleteAvatarDecoration: "アイコンデコレーションを削除"
unsetUserAvatar: "ユーザーのアイコンを解除"
unsetUserBanner: "ユーザーのバナーを解除"
+ createSystemWebhook: "SystemWebhookを作成"
+ updateSystemWebhook: "SystemWebhookを更新"
+ deleteSystemWebhook: "SystemWebhookを削除"
+ createAbuseReportNotificationRecipient: "通報の通知先を作成"
+ updateAbuseReportNotificationRecipient: "通報の通知先を更新"
+ deleteAbuseReportNotificationRecipient: "通報の通知先を削除"
_fileViewer:
title: "ファイルの詳細"
@@ -2569,16 +2607,16 @@ _externalResourceInstaller:
_dataSaver:
_media:
- title: "メディアの読み込み"
+ title: "メディアの読み込みを無効化"
description: "画像・動画が自動で読み込まれるのを防止します。隠れている画像・動画はタップすると読み込まれます。"
_avatar:
- title: "アイコン画像"
+ title: "アイコン画像のアニメーションを無効化"
description: "アイコン画像のアニメーションが停止します。アニメーション画像は通常の画像よりファイルサイズが大きいことがあるので、データ通信量をさらに削減できます。"
_urlPreview:
- title: "URLプレビューのサムネイル"
+ title: "URLプレビューのサムネイルを非表示"
description: "URLプレビューのサムネイル画像が読み込まれなくなります。"
_code:
- title: "コードハイライト"
+ title: "コードハイライトを非表示"
description: "MFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。"
_hemisphere:
diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml
index e6a23a34d7e2..7a33968e9e37 100644
--- a/locales/ja-KS.yml
+++ b/locales/ja-KS.yml
@@ -1235,6 +1235,10 @@ keepOriginalFilenameDescription: "この設定をオフにすると、アップ
noDescription: "説明文はあらへんで"
alwaysConfirmFollow: "フォローの際常に確認する"
inquiry: "問い合わせ"
+_delivery:
+ stop: "配信せぇへん"
+ _type:
+ none: "配信しとる"
_bubbleGame:
howToPlay: "遊び方"
hold: "ホールド"
@@ -2032,7 +2036,6 @@ _permissions:
"read:admin:server-info": "サーバーの情報見る"
"read:admin:show-moderation-log": "モデレーションログ見る"
"read:admin:show-user": "ユーザーのプライベートな情報見る"
- "read:admin:show-users": "ユーザーのプライベートな情報見る"
"write:admin:suspend-user": "ユーザーを凍結"
"write:admin:unset-user-avatar": "ユーザーのアバターを削除"
"write:admin:unset-user-banner": "ユーザーのバナーを削除"
diff --git a/locales/ko-GS.yml b/locales/ko-GS.yml
index c80a4d399752..9466aff01f41 100644
--- a/locales/ko-GS.yml
+++ b/locales/ko-GS.yml
@@ -649,6 +649,10 @@ replies: "답하기"
renotes: "리노트"
attach: "옇기"
surrender: "아이예"
+_delivery:
+ stop: "고만 보내예"
+ _type:
+ none: "보내고 잇어예"
_initialAccountSetting:
startTutorial: "길라잡이 하기"
_initialTutorial:
diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml
index fc3a64acab1d..294a5a1520cd 100644
--- a/locales/ko-KR.yml
+++ b/locales/ko-KR.yml
@@ -1230,6 +1230,10 @@ useTotp: "일회용 비밀번호 사용"
useBackupCode: "백업 코드 사용"
launchApp: "앱 실행"
useNativeUIForVideoAudioPlayer: "브라우저 UI에서 미디어 재생"
+_delivery:
+ stop: "정지됨"
+ _type:
+ none: "배포 중"
_bubbleGame:
howToPlay: "설명"
hold: "홀드"
@@ -2021,7 +2025,6 @@ _permissions:
"read:admin:server-info": "서버 정보 보기"
"read:admin:show-moderation-log": "조정 기록 보기"
"read:admin:show-user": "사용자 개인정보 보기"
- "read:admin:show-users": "사용자 개인정보 보기"
"write:admin:suspend-user": "사용자 정지하기"
"write:admin:unset-user-avatar": "사용자 아바타 삭제하기"
"write:admin:unset-user-banner": "사용자 배너 삭제하기"
diff --git a/locales/lo-LA.yml b/locales/lo-LA.yml
index fa4b3b6f9a95..087bac374524 100644
--- a/locales/lo-LA.yml
+++ b/locales/lo-LA.yml
@@ -395,6 +395,10 @@ searchByGoogle: "ຄົ້ນຫາ"
file: "ໄຟລ໌"
replies: "ຕອບໄປທີ"
renotes: "Renote"
+_delivery:
+ stop: "ໂຈະ"
+ _type:
+ none: "ການພິມເຜີຍແຜ່"
_role:
_priority:
middle: "ປານກາງ"
diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml
index e33b978bc857..eb48cf72da02 100644
--- a/locales/nl-NL.yml
+++ b/locales/nl-NL.yml
@@ -429,6 +429,10 @@ loggedInAsBot: "Momenteel als bot ingelogd"
icon: "Avatar"
replies: "Antwoord"
renotes: "Herdelen"
+_delivery:
+ stop: "Opgeschort"
+ _type:
+ none: "Publiceren"
_email:
_follow:
title: "volgde jou"
diff --git a/locales/no-NO.yml b/locales/no-NO.yml
index 475f93267be8..2b4c9b77761f 100644
--- a/locales/no-NO.yml
+++ b/locales/no-NO.yml
@@ -464,6 +464,8 @@ icon: "Avatar"
replies: "Svar"
renotes: "Renote"
surrender: "Avbryt"
+_delivery:
+ stop: "Suspendert"
_initialAccountSetting:
theseSettingsCanEditLater: "Du kan endre disse innstillingene senere."
_achievements:
diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml
index 2183aa3022bc..9d75f7a9d76a 100644
--- a/locales/pl-PL.yml
+++ b/locales/pl-PL.yml
@@ -1023,6 +1023,10 @@ flip: "Odwróć"
lastNDays: "W ciągu ostatnich {n} dni"
surrender: "Odrzuć"
gameRetry: "Spróbuj ponownie"
+_delivery:
+ stop: "Zawieszono"
+ _type:
+ none: "Publikowanie"
_bubbleGame:
_score:
score: "Wynik"
diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml
index e00f5750ddb5..cfc576b6e119 100644
--- a/locales/pt-PT.yml
+++ b/locales/pt-PT.yml
@@ -1012,6 +1012,10 @@ keepScreenOn: "Manter a tela do dispositivo sempre ligada"
flip: "Inversão"
lastNDays: "Últimos {n} dias"
surrender: "Cancelar"
+_delivery:
+ stop: "Suspenso"
+ _type:
+ none: "Publicando"
_initialAccountSetting:
followUsers: "Siga usuários que lhe interessam para criar a sua linha do tempo."
_serverSettings:
diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml
index 695eb2501fe6..328d34405e30 100644
--- a/locales/ro-RO.yml
+++ b/locales/ro-RO.yml
@@ -651,6 +651,10 @@ show: "Arată"
icon: "Avatar"
replies: "Răspunde"
renotes: "Re-notează"
+_delivery:
+ stop: "Suspendat"
+ _type:
+ none: "Publicare"
_role:
_priority:
middle: "Mediu"
diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml
index 66e032f16f82..71f5cad601b5 100644
--- a/locales/ru-RU.yml
+++ b/locales/ru-RU.yml
@@ -1099,6 +1099,10 @@ flip: "Переворот"
code: "Код"
lastNDays: "Последние {n} сут"
surrender: "Этот пост не может быть отменен."
+_delivery:
+ stop: "Заморожено"
+ _type:
+ none: "Публикация"
_initialAccountSetting:
accountCreated: "Аккаунт успешно создан!"
letsStartAccountSetup: "Давайте настроим вашу учётную запись."
diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml
index 0978701e5557..52f6bf142cac 100644
--- a/locales/sk-SK.yml
+++ b/locales/sk-SK.yml
@@ -922,6 +922,10 @@ renotes: "Preposlať"
sourceCode: "Zdrojový kód"
flip: "Preklopiť"
lastNDays: "Posledných {n} dní"
+_delivery:
+ stop: "Zmrazené"
+ _type:
+ none: "Zverejňovanie"
_role:
priority: "Priorita"
_priority:
diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml
index 62bc71a13df2..089dc3949f8a 100644
--- a/locales/sv-SE.yml
+++ b/locales/sv-SE.yml
@@ -488,6 +488,10 @@ dataSaver: "Databesparing"
icon: "Profilbild"
replies: "Svara"
renotes: "Omnotera"
+_delivery:
+ stop: "Suspenderad"
+ _type:
+ none: "Publiceras"
_achievements:
_types:
_open3windows:
diff --git a/locales/th-TH.yml b/locales/th-TH.yml
index 020b95485475..ab09ac4d5a45 100644
--- a/locales/th-TH.yml
+++ b/locales/th-TH.yml
@@ -1235,6 +1235,10 @@ keepOriginalFilenameDescription: "หากปิดการตั้งค่
noDescription: "ไม่มีข้อความอธิบาย"
alwaysConfirmFollow: "แสดงข้อความยืนยันเมื่อกดติดตาม"
inquiry: "ติดต่อเรา"
+_delivery:
+ stop: "ถูกระงับ"
+ _type:
+ none: "กำลังเผยแพร่"
_bubbleGame:
howToPlay: "วิธีเล่น"
hold: "หยุดชั่วคราว"
@@ -2032,7 +2036,6 @@ _permissions:
"read:admin:server-info": "ดูข้อมูลเซิร์ฟเวอร์"
"read:admin:show-moderation-log": "ดูปูมการแก้ไข"
"read:admin:show-user": "ดูข้อมูลส่วนตัวของผู้ใช้"
- "read:admin:show-users": "ดูข้อมูลส่วนตัวของผู้ใช้"
"write:admin:suspend-user": "ระงับผู้ใช้"
"write:admin:unset-user-avatar": "ลบอวตารผู้ใช้"
"write:admin:unset-user-banner": "ลบแบนเนอร์ผู้ใช้"
diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml
index 0793592d3402..cf6729a81d8e 100644
--- a/locales/tr-TR.yml
+++ b/locales/tr-TR.yml
@@ -378,6 +378,10 @@ addMemo: "Kısa not ekle"
icon: "Avatar"
replies: "yanıt"
renotes: "vazgeçme"
+_delivery:
+ stop: "Askıya alınmış"
+ _type:
+ none: "Paylaşım"
_accountDelete:
started: "Silme işlemi başlatıldı"
_email:
diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml
index 0ce5dc12028d..661ecf19d7d9 100644
--- a/locales/uk-UA.yml
+++ b/locales/uk-UA.yml
@@ -914,6 +914,10 @@ renotes: "Поширити"
sourceCode: "Вихідний код"
flip: "Перевернути"
lastNDays: "Останні {n} днів"
+_delivery:
+ stop: "Призупинено"
+ _type:
+ none: "Публікація"
_achievements:
earnedAt: "Відкрито"
_types:
diff --git a/locales/uz-UZ.yml b/locales/uz-UZ.yml
index 809e78549295..4a930626f4f8 100644
--- a/locales/uz-UZ.yml
+++ b/locales/uz-UZ.yml
@@ -846,6 +846,10 @@ icon: "Avatar"
replies: "Javob berish"
renotes: "Qayta qayd etish"
flip: "Teskari"
+_delivery:
+ stop: "To'xtatilgan"
+ _type:
+ none: "Yuborilmoqda"
_achievements:
_types:
_viewInstanceChart:
diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml
index d9c21d29ad4c..acc2e0c6a99e 100644
--- a/locales/vi-VN.yml
+++ b/locales/vi-VN.yml
@@ -1118,6 +1118,10 @@ pullDownToRefresh: "Kéo xuống để làm mới"
cwNotationRequired: "Nếu \"Ẩn nội dung\" được bật thì cần phải có chú thích."
lastNDays: "{n} ngày trước"
surrender: "Từ chối"
+_delivery:
+ stop: "Đã vô hiệu hóa"
+ _type:
+ none: "Đang đăng"
_announcement:
forExistingUsers: "Chỉ những người dùng đã tồn tại"
forExistingUsersDescription: "Nếu được bật, thông báo này sẽ chỉ hiển thị với những người dùng đã tồn tại vào lúc thông báo được tạo. Nếu tắt đi, những tài khoản mới đăng ký sau khi thông báo được đăng lên cũng sẽ thấy nó."
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index 17164dfe988c..f92d997b5a3a 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -316,6 +316,7 @@ selectFile: "选择文件"
selectFiles: "选择文件"
selectFolder: "选择文件夹"
selectFolders: "选择多个文件夹"
+fileNotSelected: "未选择文件"
renameFile: "重命名文件"
folderName: "文件夹名称"
createFolder: "创建文件夹"
@@ -471,6 +472,7 @@ retype: "重新输入"
noteOf: "{user} 的帖子"
quoteAttached: "已引用"
quoteQuestion: "是否引用此链接内容?"
+attachAsFileQuestion: "剪贴板内的文字过长。要转换为文本文件并添加吗?"
noMessagesYet: "现在没有新的聊天"
newMessageExists: "新信息"
onlyOneFileCanBeAttached: "只能添加一个附件"
@@ -1024,6 +1026,7 @@ thisPostMayBeAnnoyingHome: "发到首页"
thisPostMayBeAnnoyingCancel: "取消"
thisPostMayBeAnnoyingIgnore: "就这样发布"
collapseRenotes: "省略显示已经看过的转发内容"
+collapseRenotesDescription: "将回应过或转贴过的贴子折叠表示。"
internalServerError: "内部服务器错误"
internalServerErrorDescription: "内部服务器发生了预期外的错误"
copyErrorInfo: "复制错误信息"
@@ -1238,6 +1241,15 @@ keepOriginalFilenameDescription: "若关闭此设置,上传文件时文件名
noDescription: "没有描述"
alwaysConfirmFollow: "总是确认关注"
inquiry: "联系我们"
+_delivery:
+ status: "投递状态"
+ stop: "停止投递"
+ resume: "继续投递"
+ _type:
+ none: "投递中"
+ manuallySuspended: "手动停止中"
+ goneSuspended: "因服务器被删除而停止"
+ autoSuspendedForNotResponding: "因服务器无应答而停止"
_bubbleGame:
howToPlay: "游戏说明"
hold: "抓住"
@@ -1696,8 +1708,10 @@ _role:
roleAssignedTo: "已分配给手动角色"
isLocal: "是本地用户"
isRemote: "是远程用户"
+ isCat: "猫猫用户"
isBot: "机器人用户"
isSuspended: "停用的用户"
+ isLocked: "锁推用户"
isExplorable: "启用“使账号可见”的用户"
createdLessThan: "账户创建时间少于"
createdMoreThan: "账户创建时间超过"
@@ -2032,7 +2046,6 @@ _permissions:
"read:admin:server-info": "查看服务器信息"
"read:admin:show-moderation-log": "查看管理日志"
"read:admin:show-user": "查看用户的非公开信息"
- "read:admin:show-users": "查看用户的非公开信息"
"write:admin:suspend-user": "冻结用户"
"write:admin:unset-user-avatar": "删除用户头像"
"write:admin:unset-user-banner": "删除用户横幅"
@@ -2346,6 +2359,7 @@ _deck:
alwaysShowMainColumn: "总是显示主列"
columnAlign: "列对齐"
addColumn: "添加列"
+ newNoteNotificationSettings: "新帖子通知设定"
configureColumn: "列设置"
swapLeft: "向左移动"
swapRight: "向右移动"
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index 8cde13052f35..aac3f7662c7e 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -108,11 +108,14 @@ enterEmoji: "輸入表情符號"
renote: "轉發"
unrenote: "取消轉發"
renoted: "轉發成功。"
+renotedToX: "轉發給 {name} 了。"
cantRenote: "無法轉發此貼文。"
cantReRenote: "無法轉發之前已經轉發過的內容。"
quote: "引用"
inChannelRenote: "在頻道內轉發"
inChannelQuote: "在頻道內引用"
+renoteToChannel: "轉發至頻道"
+renoteToOtherChannel: "轉發至其他頻道"
pinnedNote: "已置頂的貼文"
pinned: "置頂"
you: "您"
@@ -169,7 +172,7 @@ cacheRemoteSensitiveFilesDescription: "若停用這個設定,則不會快取
flagAsBot: "此使用者是機器人"
flagAsBotDescription: "如果本帳戶是由程式控制,請啟用此選項。啟用後,會作為標示幫助其他開發者防止機器人之間產生無限互動的行為,並會調整 Misskey 內部系統將本帳戶識別為機器人。"
flagAsCat: "此帳戶是一隻貓,喵~~~!!!"
-flagAsCatDescription: "如果想將本帳戶標示為一隻貓,請開啟此標示"
+flagAsCatDescription: "喵喵喵??"
flagShowTimelineReplies: "在時間軸上顯示貼文的回覆"
flagShowTimelineRepliesDescription: "啟用後,時間軸除了顯示使用者的貼文以外,還會顯示使用者對其他貼文的回覆。"
autoAcceptFollowed: "自動允許來自追隨中使用者的追隨請求"
@@ -313,6 +316,7 @@ selectFile: "選擇檔案"
selectFiles: "選擇檔案"
selectFolder: "選擇資料夾"
selectFolders: "選擇資料夾"
+fileNotSelected: "尚未選擇檔案"
renameFile: "重新命名檔案"
folderName: "資料夾名稱"
createFolder: "新增資料夾"
@@ -366,7 +370,7 @@ enableRegistration: "開放新使用者註冊"
invite: "邀請"
driveCapacityPerLocalAccount: "每個本地使用者的雲端硬碟容量"
driveCapacityPerRemoteAccount: "每個非本地用戶的雲端空間大小"
-inMb: "以Mbps為單位"
+inMb: "以 MB 為單位"
bannerUrl: "橫幅圖片URL"
backgroundImageUrl: "背景圖片的來源網址 "
basicInfo: "基本資訊"
@@ -378,12 +382,12 @@ pinnedClipId: "置頂的摘錄ID"
pinnedNotes: "已置頂的貼文"
hcaptcha: "hCaptcha"
enableHcaptcha: "啟用 hCaptcha"
-hcaptchaSiteKey: "網站金鑰"
-hcaptchaSecretKey: "金鑰"
+hcaptchaSiteKey: "hcaptchaSiteKey"
+hcaptchaSecretKey: "hcaptchaSecretKey"
mcaptcha: "mCaptcha"
enableMcaptcha: "啟用 mCaptcha"
mcaptchaSiteKey: "網站金鑰"
-mcaptchaSecretKey: "金鑰"
+mcaptchaSecretKey: "私密金鑰"
mcaptchaInstanceUrl: "mCaptcha 的實例網址"
recaptcha: "reCAPTCHA"
enableRecaptcha: "啟用 reCAPTCHA"
@@ -391,8 +395,8 @@ recaptchaSiteKey: "網站金鑰"
recaptchaSecretKey: "金鑰"
turnstile: "Turnstile"
enableTurnstile: "啟用 Turnstile"
-turnstileSiteKey: "網站金鑰"
-turnstileSecretKey: "金鑰"
+turnstileSiteKey: "turnstileSiteKey"
+turnstileSecretKey: "turnstileSecretKey"
avoidMultiCaptchaConfirm: "使用多種驗證方式可能會造成干擾,您要關閉其他驗證方式嗎?您可以按「取消」保留多種驗證方式。"
antennas: "天線"
manageAntennas: "管理天線"
@@ -464,10 +468,11 @@ title: "標題"
text: "文字"
enable: "啟用"
next: "下一步"
-retype: "再次輸入"
+retype: "重新輸入"
noteOf: "{user}的貼文"
quoteAttached: "引用"
quoteQuestion: "是否要引用?"
+attachAsFileQuestion: "剪貼簿的文字較長。請問是否要將其以文字檔的方式附加呢?"
noMessagesYet: "沒有訊息"
newMessageExists: "有新的訊息"
onlyOneFileCanBeAttached: "只能加入一個附件"
@@ -602,7 +607,7 @@ addItem: "新增項目"
rearrange: "排序方式"
relays: "中繼器"
addRelay: "新增中繼器"
-inboxUrl: "收件夾URL"
+inboxUrl: "收件夾 URL"
addedRelays: "已加入的中繼器"
serviceworkerInfo: "如要使用推播通知,需要啟用此選項並設定金鑰。"
deletedNote: "已刪除的貼文"
@@ -791,7 +796,7 @@ newVersionOfClientAvailable: "新版本的客戶端可用。"
usageAmount: "使用量"
capacity: "容量"
inUse: "已使用"
-editCode: "編輯代碼"
+editCode: "編輯程式碼"
apply: "套用"
receiveAnnouncementFromInstance: "接收來自伺服器的通知"
emailNotification: "郵件通知"
@@ -1021,6 +1026,7 @@ thisPostMayBeAnnoyingHome: "發佈到首頁"
thisPostMayBeAnnoyingCancel: "退出"
thisPostMayBeAnnoyingIgnore: "直接發佈貼文"
collapseRenotes: "省略顯示已看過的轉發貼文"
+collapseRenotesDescription: "將已做過反應和轉發的貼文折疊顯示。"
internalServerError: "內部伺服器錯誤"
internalServerErrorDescription: "內部伺服器出現意外錯誤。"
copyErrorInfo: "複製錯誤資訊"
@@ -1062,7 +1068,7 @@ enableChartsForFederatedInstances: "生成遠端伺服器的圖表"
showClipButtonInNoteFooter: "新增摘錄按鈕至貼文"
reactionsDisplaySize: "反應的顯示尺寸"
limitWidthOfReaction: "限制反應的最大寬度,並縮小顯示尺寸。"
-noteIdOrUrl: "貼文ID或URL"
+noteIdOrUrl: "貼文 ID 或 URL"
video: "影片"
videos: "影片"
audio: "音效"
@@ -1077,7 +1083,7 @@ addMemo: "新增備註"
editMemo: "編輯備註"
reactionsList: "反應列表"
renotesList: "轉發貼文列表"
-notificationDisplay: "通知的顯示"
+notificationDisplay: "通知"
leftTop: "左上"
rightTop: "右上"
leftBottom: "左下"
@@ -1179,15 +1185,15 @@ repositoryUrlOrTarballRequired: "如果儲存庫不是公開的,則必須提
feedback: "意見回饋"
feedbackUrl: "意見回饋 URL"
impressum: "營運者資訊"
-impressumUrl: "營運者資訊網址"
+impressumUrl: "營運者資訊 URL"
impressumDescription: "在德國與部份地區必須要明確顯示營運者資訊。"
privacyPolicy: "隱私政策"
-privacyPolicyUrl: "隱私政策網址"
+privacyPolicyUrl: "隱私政策 URL"
tosAndPrivacyPolicy: "服務條款和隱私政策"
avatarDecorations: "頭像裝飾"
attach: "裝上"
detach: "取下"
-detachAll: "移除所有裝飾"
+detachAll: "全部移除"
angle: "角度"
flip: "翻轉"
showAvatarDecorations: "顯示頭像裝飾"
@@ -1205,7 +1211,7 @@ remainingN: "剩餘:{n}"
overwriteContentConfirm: "確定要覆蓋目前的內容嗎?"
seasonalScreenEffect: "隨季節變換畫面的呈現"
decorate: "設置頭像裝飾"
-addMfmFunction: "插入MFM功能語法"
+addMfmFunction: "插入 MFM 功能語法"
enableQuickAddMfmFunction: "顯示高級 MFM 選擇器"
bubbleGame: "氣泡遊戲"
sfx: "音效"
@@ -1225,16 +1231,25 @@ enableHorizontalSwipe: "滑動切換時間軸"
loading: "載入中"
surrender: "退出"
gameRetry: "再試一次"
-notUsePleaseLeaveBlank: "如不使用,請留空"
+notUsePleaseLeaveBlank: "如果不使用的話請留白"
useTotp: "使用一次性密碼"
useBackupCode: "使用備用驗證碼"
-launchApp: "啟動 App"
+launchApp: "啟動 APP"
useNativeUIForVideoAudioPlayer: "使用瀏覽器的 UI 播放影片與音訊"
keepOriginalFilename: "保留原始檔名"
keepOriginalFilenameDescription: "如果關閉此設置,上傳時檔案名稱會自動替換為隨機字串。"
noDescription: "沒有說明文字"
alwaysConfirmFollow: "點擊追隨時總是顯示確認訊息"
inquiry: "聯絡我們"
+_delivery:
+ status: "傳送狀態"
+ stop: "停止傳送"
+ resume: "恢復傳送"
+ _type:
+ none: "直播中"
+ manuallySuspended: "手動暫停中"
+ goneSuspended: "因為伺服器刪除所以暫停中"
+ autoSuspendedForNotResponding: "因為伺服器沒有回應所以暫停中"
_bubbleGame:
howToPlay: "玩法說明"
hold: "保留"
@@ -1243,7 +1258,7 @@ _bubbleGame:
scoreYen: "賺取的金額"
highScore: "最高分"
maxChain: "最大結合數"
- yen: "{yen} 日圓"
+ yen: "{yen}円"
estimatedQty: "{qty}個"
scoreSweets: "飯糰 {onigiriQtyWithUnit}"
_howToPlay:
@@ -1271,7 +1286,7 @@ _initialAccountSetting:
privacySetting: "隱私設定"
theseSettingsCanEditLater: "這裡的設定可以在之後變更。"
youCanEditMoreSettingsInSettingsPageLater: "除此之外,還可以在「設定」頁面進行各種設定。之後請確認看看。"
- followUsers: "為了構築時間軸,試著追蹤您感興趣的使用者吧。"
+ followUsers: "為了構築時間軸,試著追隨您感興趣的使用者吧。"
pushNotificationDescription: "啟用推送通知,就可以在設備上接收{name}的通知。"
initialAccountSettingCompleted: "初始設定完成了!"
haveFun: "盡情享受{name}吧!"
@@ -1326,7 +1341,7 @@ _initialTutorial:
title: "隱藏內容(CW)"
description: "將顯示「註釋」中寫入的內容而不是本文。按一下「顯示內容」以顯示本文。"
_exampleNote:
- cw: "美食恐怖主義注意"
+ cw: "注意消夜文"
note: "我吃了一個巧克力甜甜圈🍩😋"
useCases: "伺服器的服務條款可能會規範特定的貼文需要使用隱藏內容,除此之外也會用在隱藏劇情洩漏與敏感內容的貼文。"
_howToMakeAttachmentsSensitive:
@@ -1351,7 +1366,7 @@ _serverRules:
_serverSettings:
iconUrl: "圖示的 URL"
appIconDescription: "指定顯示 {host} 為應用程式時的圖示。"
- appIconUsageExample: "例如:漸進式網路應用程式(PWA)、於手機桌面新增書籤"
+ appIconUsageExample: "例如:PWA 或是在手機桌面作為書籤等"
appIconStyleRecommendation: "因為可能會裁剪成圓形或圓角,所以建議用單色填滿邊框及背景。"
appIconResolutionMustBe: "解析度必須為 {resolution}。"
manifestJsonOverride: "覆寫 manifest.json"
@@ -1360,6 +1375,8 @@ _serverSettings:
fanoutTimelineDescription: "如果啟用的話,檢索各個時間軸的性能會顯著提昇,資料庫的負荷也會減少。不過,Redis 的記憶體使用量會增加。如果伺服器的記憶體容量比較少或者運行不穩定,可以停用。"
fanoutTimelineDbFallback: "資料庫的回退"
fanoutTimelineDbFallbackDescription: "若啟用,在時間軸沒有快取的情況下將執行回退處理以額外查詢資料庫。若停用,可以透過不執行回退處理來進一步減少伺服器的負荷,但會限制可取得的時間軸範圍。"
+ inquiryUrl: "聯絡表單網址"
+ inquiryUrlDescription: "指定伺服器運營者的聯絡表單網址或包含運營者聯絡資訊網頁的網址。"
_accountMigration:
moveFrom: "從其他帳戶遷移到這個帳戶"
moveFromSub: "為另一個帳戶建立別名"
@@ -1559,7 +1576,7 @@ _achievements:
_postedAt0min0sec:
title: "報時"
description: "在零分零秒發佈貼文"
- flavor: "啵、啵、啵、嗶ーー"
+ flavor: "啵.啵.啵.嗶ー"
_selfQuote:
title: "自我引用"
description: "引用了自己的貼文"
@@ -1694,8 +1711,8 @@ _role:
roleAssignedTo: "手動指派角色完成"
isLocal: "本地使用者"
isRemote: "遠端使用者"
- isCat: "使用者是貓"
- isBot: "使用者是機器人"
+ isCat: "貓使用者"
+ isBot: "機器人使用者"
isSuspended: "被停權的使用者"
isLocked: "上鎖的使用者"
isExplorable: "開啟了「使您的帳戶更容易被找到」功能的使用者"
@@ -1857,7 +1874,7 @@ _theme:
invalid: "佈景主題格式錯誤"
make: "製作佈景主題"
base: "基於"
- addConstant: "添加常數"
+ addConstant: "新增常數"
constant: "常數"
defaultValue: "預設值"
color: "顏色"
@@ -1932,22 +1949,22 @@ _soundSettings:
_ago:
future: "未來"
justNow: "剛剛"
- secondsAgo: "{n} 秒前"
- minutesAgo: "{n} 分鐘前 "
- hoursAgo: "{n} 小時前"
- daysAgo: "{n} 天前"
- weeksAgo: "{n} 週前"
- monthsAgo: "{n} 個月前"
- yearsAgo: "{n} 年前"
+ secondsAgo: "{n}秒前"
+ minutesAgo: "{n}分鐘前"
+ hoursAgo: "{n}小時前"
+ daysAgo: "{n}天前"
+ weeksAgo: "{n}周前"
+ monthsAgo: "{n}個月前"
+ yearsAgo: "{n}年前"
invalid: "無"
_timeIn:
- seconds: "{n} 秒後"
- minutes: "{n} 分後"
- hours: "{n} 小時後"
- days: "{n} 日後"
- weeks: "{n} 週後"
- months: "{n} 個月後"
- years: "{n} 年後"
+ seconds: "{n}秒後"
+ minutes: "{n}分鐘後"
+ hours: "{n}小時後"
+ days: "{n}天後"
+ weeks: "{n}周後"
+ months: "{n}個月後"
+ years: "{n}年後"
_time:
second: "秒"
minute: "分鐘"
@@ -2032,7 +2049,6 @@ _permissions:
"read:admin:server-info": "查看伺服器的資訊"
"read:admin:show-moderation-log": "查看審查紀錄"
"read:admin:show-user": "查看使用者的私密資訊"
- "read:admin:show-users": "查看使用者的私密資訊"
"write:admin:suspend-user": "凍結使用者"
"write:admin:unset-user-avatar": "刪除使用者的頭像"
"write:admin:unset-user-banner": "刪除使用者的橫幅"
@@ -2085,13 +2101,13 @@ _antennaSources:
userList: "來自特定清單中的貼文"
userBlacklist: "除指定使用者外的所有貼文"
_weekday:
- sunday: "週日"
- monday: "週一"
- tuesday: "週二"
- wednesday: "週三"
- thursday: "週四"
- friday: "週五"
- saturday: "週六"
+ sunday: "星期天"
+ monday: "星期一"
+ tuesday: "星期二"
+ wednesday: "星期三"
+ thursday: "星期四"
+ friday: "星期五"
+ saturday: "星期六"
_widgets:
profile: "個人檔案"
instanceInfo: "伺服器資訊"
@@ -2140,7 +2156,7 @@ _poll:
deadlineDate: "截止日期"
deadlineTime: "小時"
duration: "時長"
- votesCount: "{n} 票"
+ votesCount: "{n}票"
totalVotes: "合計 {n} 票"
vote: "投票"
showResult: "顯示結果"
@@ -2173,7 +2189,7 @@ _postForm:
e: "寫些什麼吧……"
f: "靜待發文……"
_profile:
- name: "名稱"
+ name: "名字"
username: "使用者名稱"
description: "關於我"
youCanIncludeHashtags: "你也可以在「關於我」中加上 #tag"
@@ -2231,10 +2247,10 @@ _timelines:
_play:
new: "新增 Play"
edit: "編輯 Play"
- created: "已新增Play "
- updated: "已更新Play "
+ created: "已新增 Play "
+ updated: "已更新 Play "
deleted: "已刪除 Play"
- pageSetting: "Play設定"
+ pageSetting: "Play 設定"
editThisPage: "編輯此 Play"
viewSource: "檢視原始碼"
my: "自己的 Play"
@@ -2247,7 +2263,7 @@ _play:
_pages:
newPage: "建立頁面"
editPage: "編輯頁面"
- readPage: "正檢視原始碼"
+ readPage: "正在檢視原始碼"
created: "頁面已建立"
updated: "頁面已更新"
deleted: "頁面已被刪除"
@@ -2274,7 +2290,7 @@ _pages:
hideTitleWhenPinned: "被置頂於個人資料時隱藏頁面標題"
font: "字型"
fontSerif: "襯線體"
- fontSansSerif: "無襯線體"
+ fontSansSerif: "黑體"
eyeCatchingImageSet: "設定封面影像"
eyeCatchingImageRemove: "刪除封面影像"
chooseBlock: "新增方塊"
@@ -2346,6 +2362,7 @@ _deck:
alwaysShowMainColumn: "總是顯示主欄"
columnAlign: "對齊欄位"
addColumn: "新增欄位"
+ newNoteNotificationSettings: "新貼文通知的設定"
configureColumn: "欄位的設定"
swapLeft: "向左移動"
swapRight: "向右移動"
@@ -2384,7 +2401,7 @@ _drivecleaner:
orderByCreatedAtAsc: "按新增日期降序排列"
_webhookSettings:
createWebhook: "建立 Webhook"
- name: "名稱"
+ name: "名字"
secret: "密鑰"
events: "何時運行 Webhook"
active: "已啟用"
diff --git a/package.json b/package.json
index 23e0ea0ee5b9..bf8415d21217 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "misskey",
- "version": "2024.3.1",
+ "version": "2024.5.0",
"codename": "nasubi",
"repository": {
"type": "git",
@@ -55,20 +55,22 @@
"js-yaml": "4.1.0",
"postcss": "8.4.38",
"tar": "6.2.1",
- "terser": "5.30.3",
- "typescript": "5.4.5",
- "esbuild": "0.20.2",
+ "terser": "5.31.1",
+ "typescript": "5.5.3",
+ "esbuild": "0.22.0",
"glob": "10.3.12"
},
"devDependencies": {
- "@types/node": "20.12.7",
- "@typescript-eslint/eslint-plugin": "7.7.1",
- "@typescript-eslint/parser": "7.7.1",
+ "@misskey-dev/eslint-plugin": "2.0.2",
+ "@types/node": "20.14.9",
+ "@typescript-eslint/eslint-plugin": "7.15.0",
+ "@typescript-eslint/parser": "7.15.0",
"cross-env": "7.0.3",
- "cypress": "13.7.3",
- "eslint": "8.57.0",
+ "cypress": "13.13.0",
+ "eslint": "9.6.0",
+ "globals": "15.7.0",
"ncp": "2.0.0",
- "start-server-and-test": "2.0.3"
+ "start-server-and-test": "2.0.4"
},
"optionalDependencies": {
"@tensorflow/tfjs-core": "4.4.0"
diff --git a/packages/backend/.eslintignore b/packages/backend/.eslintignore
deleted file mode 100644
index 790eb90145b4..000000000000
--- a/packages/backend/.eslintignore
+++ /dev/null
@@ -1,4 +0,0 @@
-node_modules
-/built
-/.eslintrc.js
-/@types/**/*
diff --git a/packages/backend/.eslintrc.cjs b/packages/backend/.eslintrc.cjs
deleted file mode 100644
index f9fe4814e671..000000000000
--- a/packages/backend/.eslintrc.cjs
+++ /dev/null
@@ -1,32 +0,0 @@
-module.exports = {
- parserOptions: {
- tsconfigRootDir: __dirname,
- project: ['./tsconfig.json', './test/tsconfig.json'],
- },
- extends: [
- '../shared/.eslintrc.js',
- ],
- rules: {
- 'import/order': ['warn', {
- 'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
- 'pathGroups': [
- {
- 'pattern': '@/**',
- 'group': 'external',
- 'position': 'after'
- }
- ],
- }],
- 'no-restricted-globals': [
- 'error',
- {
- 'name': '__dirname',
- 'message': 'Not in ESModule. Use `import.meta.url` instead.'
- },
- {
- 'name': '__filename',
- 'message': 'Not in ESModule. Use `import.meta.url` instead.'
- }
- ]
- },
-};
diff --git a/packages/backend/assets/api-doc.html b/packages/backend/assets/api-doc.html
new file mode 100644
index 000000000000..19e0349d47d9
--- /dev/null
+++ b/packages/backend/assets/api-doc.html
@@ -0,0 +1,20 @@
+
+
+
+ Misskey API
+
+
+
+
+
+
+
+
+
diff --git a/packages/backend/assets/redoc.html b/packages/backend/assets/redoc.html
deleted file mode 100644
index 2557b4532ec3..000000000000
--- a/packages/backend/assets/redoc.html
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
- Misskey API
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/backend/eslint.config.js b/packages/backend/eslint.config.js
new file mode 100644
index 000000000000..318b7fd340f3
--- /dev/null
+++ b/packages/backend/eslint.config.js
@@ -0,0 +1,46 @@
+import tsParser from '@typescript-eslint/parser';
+import sharedConfig from '../shared/eslint.config.js';
+
+export default [
+ ...sharedConfig,
+ {
+ ignores: ['**/node_modules', 'built', '@types/**/*'],
+ },
+ {
+ files: ['**/*.ts', '**/*.tsx'],
+ languageOptions: {
+ parserOptions: {
+ parser: tsParser,
+ project: ['./tsconfig.json', './test/tsconfig.json'],
+ sourceType: 'module',
+ tsconfigRootDir: import.meta.dirname,
+ },
+ },
+ rules: {
+ 'import/order': ['warn', {
+ groups: [
+ 'builtin',
+ 'external',
+ 'internal',
+ 'parent',
+ 'sibling',
+ 'index',
+ 'object',
+ 'type',
+ ],
+ pathGroups: [{
+ pattern: '@/**',
+ group: 'external',
+ position: 'after',
+ }],
+ }],
+ 'no-restricted-globals': ['error', {
+ name: '__dirname',
+ message: 'Not in ESModule. Use `import.meta.url` instead.',
+ }, {
+ name: '__filename',
+ message: 'Not in ESModule. Use `import.meta.url` instead.',
+ }],
+ },
+ },
+];
diff --git a/packages/backend/migration/1713656541000-abuse-report-notification.js b/packages/backend/migration/1713656541000-abuse-report-notification.js
new file mode 100644
index 000000000000..4a754f81e2f5
--- /dev/null
+++ b/packages/backend/migration/1713656541000-abuse-report-notification.js
@@ -0,0 +1,62 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class AbuseReportNotification1713656541000 {
+ name = 'AbuseReportNotification1713656541000'
+
+ async up(queryRunner) {
+ await queryRunner.query(`
+ CREATE TABLE "system_webhook" (
+ "id" varchar(32) NOT NULL,
+ "isActive" boolean NOT NULL DEFAULT true,
+ "updatedAt" timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "latestSentAt" timestamp with time zone NULL DEFAULT NULL,
+ "latestStatus" integer NULL DEFAULT NULL,
+ "name" varchar(255) NOT NULL,
+ "on" varchar(128) [] NOT NULL DEFAULT '{}'::character varying[],
+ "url" varchar(1024) NOT NULL,
+ "secret" varchar(1024) NOT NULL,
+ CONSTRAINT "PK_system_webhook_id" PRIMARY KEY ("id")
+ );
+ CREATE INDEX "IDX_system_webhook_isActive" ON "system_webhook" ("isActive");
+ CREATE INDEX "IDX_system_webhook_on" ON "system_webhook" USING gin ("on");
+
+ CREATE TABLE "abuse_report_notification_recipient" (
+ "id" varchar(32) NOT NULL,
+ "isActive" boolean NOT NULL DEFAULT true,
+ "updatedAt" timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "name" varchar(255) NOT NULL,
+ "method" varchar(64) NOT NULL,
+ "userId" varchar(32) NULL DEFAULT NULL,
+ "systemWebhookId" varchar(32) NULL DEFAULT NULL,
+ CONSTRAINT "PK_abuse_report_notification_recipient_id" PRIMARY KEY ("id"),
+ CONSTRAINT "FK_abuse_report_notification_recipient_userId1" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION,
+ CONSTRAINT "FK_abuse_report_notification_recipient_userId2" FOREIGN KEY ("userId") REFERENCES "user_profile"("userId") ON DELETE CASCADE ON UPDATE NO ACTION,
+ CONSTRAINT "FK_abuse_report_notification_recipient_systemWebhookId" FOREIGN KEY ("systemWebhookId") REFERENCES "system_webhook"("id") ON DELETE CASCADE ON UPDATE NO ACTION
+ );
+ CREATE INDEX "IDX_abuse_report_notification_recipient_isActive" ON "abuse_report_notification_recipient" ("isActive");
+ CREATE INDEX "IDX_abuse_report_notification_recipient_method" ON "abuse_report_notification_recipient" ("method");
+ CREATE INDEX "IDX_abuse_report_notification_recipient_userId" ON "abuse_report_notification_recipient" ("userId");
+ CREATE INDEX "IDX_abuse_report_notification_recipient_systemWebhookId" ON "abuse_report_notification_recipient" ("systemWebhookId");
+ `);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`
+ ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_userId1";
+ ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_userId2";
+ ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_systemWebhookId";
+ DROP INDEX "IDX_abuse_report_notification_recipient_isActive";
+ DROP INDEX "IDX_abuse_report_notification_recipient_method";
+ DROP INDEX "IDX_abuse_report_notification_recipient_userId";
+ DROP INDEX "IDX_abuse_report_notification_recipient_systemWebhookId";
+ DROP TABLE "abuse_report_notification_recipient";
+
+ DROP INDEX "IDX_system_webhook_isActive";
+ DROP INDEX "IDX_system_webhook_on";
+ DROP TABLE "system_webhook";
+ `);
+ }
+}
diff --git a/packages/backend/migration/1716450883149-RemoveAntennaNotify.js b/packages/backend/migration/1716450883149-RemoveAntennaNotify.js
new file mode 100644
index 000000000000..b5a2441855d2
--- /dev/null
+++ b/packages/backend/migration/1716450883149-RemoveAntennaNotify.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class RemoveAntennaNotify1716450883149 {
+ name = 'RemoveAntennaNotify1716450883149'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "notify"`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "antenna" ADD "notify" boolean NOT NULL`);
+ }
+}
diff --git a/packages/backend/migration/1717117195275-inquiryUrl.js b/packages/backend/migration/1717117195275-inquiryUrl.js
new file mode 100644
index 000000000000..29ca31af1403
--- /dev/null
+++ b/packages/backend/migration/1717117195275-inquiryUrl.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class InquiryUrl1717117195275 {
+ name = 'InquiryUrl1717117195275'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "meta" ADD "inquiryUrl" character varying(1024)`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "inquiryUrl"`);
+ }
+}
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 8e29252d759c..22fdc5cf1681 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"engines": {
- "node": ">=20.10.0"
+ "node": "^20.10.0 || ^22.0.0"
},
"scripts": {
"start": "node ./built/boot/entry.js",
@@ -65,41 +65,43 @@
"utf-8-validate": "6.0.3"
},
"dependencies": {
- "@aws-sdk/client-s3": "3.412.0",
- "@aws-sdk/lib-storage": "3.412.0",
- "@bull-board/api": "5.17.0",
- "@bull-board/fastify": "5.17.0",
- "@bull-board/ui": "5.17.0",
+ "@aws-sdk/client-s3": "3.600.0",
+ "@aws-sdk/lib-storage": "3.600.0",
+ "@bull-board/api": "5.20.5",
+ "@bull-board/fastify": "5.20.5",
+ "@bull-board/ui": "5.20.5",
"@discordapp/twemoji": "15.0.3",
"@fastify/accepts": "4.3.0",
"@fastify/cookie": "9.3.1",
"@fastify/cors": "9.0.1",
"@fastify/express": "3.0.0",
"@fastify/http-proxy": "9.5.0",
- "@fastify/multipart": "8.2.0",
- "@fastify/static": "7.0.3",
+ "@fastify/multipart": "8.3.0",
+ "@fastify/static": "7.0.4",
"@fastify/view": "9.1.0",
"@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.1.0",
- "@napi-rs/canvas": "^0.1.52",
- "@nestjs/common": "10.3.8",
- "@nestjs/core": "10.3.8",
- "@nestjs/testing": "10.3.8",
+ "@napi-rs/canvas": "^0.1.53",
+ "@nestjs/common": "10.3.10",
+ "@nestjs/core": "10.3.10",
+ "@nestjs/testing": "10.3.10",
"@peertube/http-signature": "1.7.0",
+ "@sentry/node": "8.13.0",
+ "@sentry/profiling-node": "8.13.0",
"@simplewebauthn/server": "10.0.0",
"@sinonjs/fake-timers": "11.2.2",
"@smithy/node-http-handler": "2.5.0",
"@swc/cli": "0.3.12",
- "@swc/core": "1.4.17",
+ "@swc/core": "1.6.6",
"@twemoji/parser": "15.1.1",
"accepts": "1.3.8",
- "ajv": "8.13.0",
+ "ajv": "8.16.0",
"archiver": "7.0.1",
"async-mutex": "0.5.0",
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.2",
- "bullmq": "5.7.8",
+ "bullmq": "5.8.3",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.2",
"chalk": "5.3.0",
@@ -110,27 +112,27 @@
"content-disposition": "0.5.4",
"date-fns": "2.30.0",
"deep-email-validator": "0.1.21",
- "fastify": "4.26.2",
+ "fastify": "4.28.1",
"fastify-raw-body": "4.3.0",
"feed": "4.2.2",
"file-type": "19.0.0",
- "fluent-ffmpeg": "2.1.2",
+ "fluent-ffmpeg": "2.1.3",
"form-data": "4.0.0",
- "got": "14.2.1",
+ "got": "14.4.1",
"happy-dom": "10.0.3",
"hpagent": "1.2.0",
"htmlescape": "1.1.1",
"http-link-header": "1.1.3",
"ioredis": "5.4.1",
- "ip-cidr": "3.1.0",
+ "ip-cidr": "4.0.1",
"ipaddr.js": "2.2.0",
- "is-svg": "5.0.0",
+ "is-svg": "5.0.1",
"js-yaml": "4.1.0",
- "jsdom": "24.0.0",
+ "jsdom": "24.1.0",
"json5": "2.2.3",
"jsonld": "8.3.2",
"jsrsasign": "11.1.0",
- "meilisearch": "0.38.0",
+ "meilisearch": "0.41.0",
"mfm-js": "0.24.0",
"microformats-parser": "2.0.2",
"mime-types": "2.1.35",
@@ -140,24 +142,24 @@
"nanoid": "5.0.7",
"nested-property": "4.0.0",
"node-fetch": "3.3.2",
- "nodemailer": "6.9.13",
+ "nodemailer": "6.9.14",
"nsfwjs": "2.4.2",
"oauth": "0.10.0",
"oauth2orize": "1.12.0",
"oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14",
- "otpauth": "9.2.3",
+ "otpauth": "9.3.1",
"parse5": "7.1.2",
- "pg": "8.11.5",
+ "pg": "8.12.0",
"pkce-challenge": "4.1.0",
"probe-image-size": "7.2.3",
"promise-limit": "2.7.0",
- "pug": "3.0.2",
+ "pug": "3.0.3",
"punycode": "2.3.1",
"qrcode": "1.5.3",
"random-seed": "0.3.0",
"ratelimiter": "3.4.1",
- "re2": "1.20.10",
+ "re2": "1.21.3",
"redis-lock": "0.1.4",
"reflect-metadata": "0.2.2",
"rename": "1.0.4",
@@ -165,27 +167,26 @@
"rxjs": "7.8.1",
"sanitize-html": "2.13.0",
"secure-json-parse": "2.7.0",
- "sharp": "0.33.3",
+ "sharp": "0.33.4",
"slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
- "systeminformation": "5.22.7",
+ "systeminformation": "5.22.11",
"tinycolor2": "1.6.0",
"tmp": "0.2.3",
- "tsc-alias": "1.8.8",
+ "tsc-alias": "1.8.10",
"tsconfig-paths": "4.2.0",
"typeorm": "0.3.20",
- "typescript": "5.4.5",
+ "typescript": "5.5.3",
"ulid": "2.3.0",
"vary": "1.1.2",
"web-push": "3.6.7",
- "ws": "8.17.0",
+ "ws": "8.17.1",
"xev": "3.0.2"
},
"devDependencies": {
"@jest/globals": "29.7.0",
- "@misskey-dev/eslint-plugin": "1.0.0",
- "@nestjs/platform-express": "10.3.8",
+ "@nestjs/platform-express": "10.3.10",
"@simplewebauthn/types": "10.0.0",
"@swc/jest": "0.2.36",
"@types/accepts": "1.3.7",
@@ -195,22 +196,21 @@
"@types/color-convert": "2.0.3",
"@types/content-disposition": "0.5.8",
"@types/fluent-ffmpeg": "2.1.24",
- "@types/htmlescape": "^1.1.3",
- "@types/http-link-header": "1.0.5",
+ "@types/htmlescape": "1.1.3",
+ "@types/http-link-header": "1.0.7",
"@types/jest": "29.5.12",
"@types/js-yaml": "4.0.9",
- "@types/jsdom": "21.1.6",
- "@types/jsonld": "1.5.13",
+ "@types/jsdom": "21.1.7",
+ "@types/jsonld": "1.5.14",
"@types/jsrsasign": "10.5.14",
"@types/mime-types": "2.1.4",
"@types/ms": "0.7.34",
- "@types/node": "20.12.7",
- "@types/node-fetch": "3.0.3",
+ "@types/node": "20.14.9",
"@types/nodemailer": "6.4.15",
- "@types/oauth": "0.9.4",
+ "@types/oauth": "0.9.5",
"@types/oauth2orize": "1.11.5",
"@types/oauth2orize-pkce": "0.1.2",
- "@types/pg": "8.11.5",
+ "@types/pg": "8.11.6",
"@types/pug": "2.0.10",
"@types/punycode": "2.1.4",
"@types/qrcode": "1.5.5",
@@ -226,18 +226,17 @@
"@types/vary": "1.1.3",
"@types/web-push": "3.6.3",
"@types/ws": "8.5.10",
- "@typescript-eslint/eslint-plugin": "7.7.1",
- "@typescript-eslint/parser": "7.7.1",
- "aws-sdk-client-mock": "3.0.1",
+ "@typescript-eslint/eslint-plugin": "7.15.0",
+ "@typescript-eslint/parser": "7.15.0",
+ "aws-sdk-client-mock": "4.0.1",
"cross-env": "7.0.3",
- "eslint": "8.57.0",
"eslint-plugin-import": "2.29.1",
- "execa": "8.0.1",
- "fkill": "^9.0.0",
+ "execa": "9.2.0",
+ "fkill": "9.0.0",
"jest": "29.7.0",
"jest-mock": "29.7.0",
- "nodemon": "3.1.0",
+ "nodemon": "3.1.4",
"pid-port": "1.0.0",
- "simple-oauth2": "5.0.0"
+ "simple-oauth2": "5.0.1"
}
}
diff --git a/packages/backend/scripts/dev.mjs b/packages/backend/scripts/dev.mjs
index 2d0de0f91666..a3e0558abd97 100644
--- a/packages/backend/scripts/dev.mjs
+++ b/packages/backend/scripts/dev.mjs
@@ -30,6 +30,7 @@ function execStart() {
async function killProc() {
if (backendProcess) {
+ backendProcess.catch(() => {}); // backendProcess.kill()によって発生する例外を無視するためにcatch()を呼び出す
backendProcess.kill();
await new Promise(resolve => backendProcess.on('exit', resolve));
backendProcess = undefined;
@@ -46,6 +47,7 @@ async function killProc() {
],
{
stdio: [process.stdin, process.stdout, process.stderr, 'ipc'],
+ serialization: "json",
})
.on('message', async (message) => {
if (message.type === 'exit') {
diff --git a/packages/backend/src/NestLogger.ts b/packages/backend/src/NestLogger.ts
index 80f1f7a024d6..d0be19664f31 100644
--- a/packages/backend/src/NestLogger.ts
+++ b/packages/backend/src/NestLogger.ts
@@ -7,7 +7,7 @@ import { LoggerService } from '@nestjs/common';
import Logger from '@/logger.js';
const logger = new Logger('core', 'cyan');
-const nestLogger = logger.createSubLogger('nest', 'green', false);
+const nestLogger = logger.createSubLogger('nest', 'green');
export class NestLogger implements LoggerService {
/**
diff --git a/packages/backend/src/boot/entry.ts b/packages/backend/src/boot/entry.ts
index 04c6ca9723cb..25375c3015f7 100644
--- a/packages/backend/src/boot/entry.ts
+++ b/packages/backend/src/boot/entry.ts
@@ -25,7 +25,7 @@ Error.stackTraceLimit = Infinity;
EventEmitter.defaultMaxListeners = 128;
const logger = new Logger('core', 'cyan');
-const clusterLogger = logger.createSubLogger('cluster', 'orange', false);
+const clusterLogger = logger.createSubLogger('cluster', 'orange');
const ev = new Xev();
//#region Events
diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts
index 30f9477ccf81..4bc5c799cfb9 100644
--- a/packages/backend/src/boot/master.ts
+++ b/packages/backend/src/boot/master.ts
@@ -10,6 +10,8 @@ import * as os from 'node:os';
import cluster from 'node:cluster';
import chalk from 'chalk';
import chalkTemplate from 'chalk-template';
+import * as Sentry from '@sentry/node';
+import { nodeProfilingIntegration } from '@sentry/profiling-node';
import Logger from '@/logger.js';
import { loadConfig } from '@/config.js';
import type { Config } from '@/config.js';
@@ -23,7 +25,7 @@ const _dirname = dirname(_filename);
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
const logger = new Logger('core', 'cyan');
-const bootLogger = logger.createSubLogger('boot', 'magenta', false);
+const bootLogger = logger.createSubLogger('boot', 'magenta');
const themeColor = chalk.hex('#86b300');
@@ -71,6 +73,24 @@ export async function masterMain() {
bootLogger.succ('Misskey initialized');
+ if (config.sentryForBackend) {
+ Sentry.init({
+ integrations: [
+ ...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []),
+ ],
+
+ // Performance Monitoring
+ tracesSampleRate: 1.0, // Capture 100% of the transactions
+
+ // Set sampling rate for profiling - this is relative to tracesSampleRate
+ profilesSampleRate: 1.0,
+
+ maxBreadcrumbs: 0,
+
+ ...config.sentryForBackend.options,
+ });
+ }
+
if (envOption.disableClustering) {
if (envOption.onlyServer) {
await server();
diff --git a/packages/backend/src/boot/worker.ts b/packages/backend/src/boot/worker.ts
index d4a7cd56e5ce..5d4a15b29f02 100644
--- a/packages/backend/src/boot/worker.ts
+++ b/packages/backend/src/boot/worker.ts
@@ -4,13 +4,36 @@
*/
import cluster from 'node:cluster';
+import * as Sentry from '@sentry/node';
+import { nodeProfilingIntegration } from '@sentry/profiling-node';
import { envOption } from '@/env.js';
+import { loadConfig } from '@/config.js';
import { jobQueue, server } from './common.js';
/**
* Init worker process
*/
export async function workerMain() {
+ const config = loadConfig();
+
+ if (config.sentryForBackend) {
+ Sentry.init({
+ integrations: [
+ ...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []),
+ ],
+
+ // Performance Monitoring
+ tracesSampleRate: 1.0, // Capture 100% of the transactions
+
+ // Set sampling rate for profiling - this is relative to tracesSampleRate
+ profilesSampleRate: 1.0,
+
+ maxBreadcrumbs: 0,
+
+ ...config.sentryForBackend.options,
+ });
+ }
+
if (envOption.onlyServer) {
await server();
} else if (envOption.onlyQueue) {
diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts
index 0ca1fa55c1f8..3e5a1e81cd70 100644
--- a/packages/backend/src/config.ts
+++ b/packages/backend/src/config.ts
@@ -7,6 +7,7 @@ import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
import * as yaml from 'js-yaml';
+import * as Sentry from '@sentry/node';
import type { RedisOptions } from 'ioredis';
type RedisOptionsSource = Partial & {
@@ -22,7 +23,7 @@ type RedisOptionsSource = Partial & {
* 設定ファイルの型
*/
type Source = {
- url: string;
+ url?: string;
port?: number;
socket?: string;
chmodSocket?: string;
@@ -30,9 +31,9 @@ type Source = {
db: {
host: string;
port: number;
- db: string;
- user: string;
- pass: string;
+ db?: string;
+ user?: string;
+ pass?: string;
disableCache?: boolean;
extra?: { [x: string]: string };
};
@@ -56,6 +57,8 @@ type Source = {
index: string;
scope?: 'local' | 'global' | string[];
};
+ sentryForBackend?: { options: Partial; enableNodeProfiling: boolean; };
+ sentryForFrontend?: { options: Partial };
publishTarballInsteadOfProvideRepositoryUrl?: boolean;
@@ -166,6 +169,8 @@ export type Config = {
redisForPubsub: RedisOptions & RedisOptionsSource;
redisForJobQueue: RedisOptions & RedisOptionsSource;
redisForTimelines: RedisOptions & RedisOptionsSource;
+ sentryForBackend: { options: Partial; enableNodeProfiling: boolean; } | undefined;
+ sentryForFrontend: { options: Partial } | undefined;
perChannelMaxNoteCacheCount: number;
perUserNotificationsMaxCount: number;
deactivateAntennaThreshold: number;
@@ -197,13 +202,17 @@ export function loadConfig(): Config {
: { 'src/_boot_.ts': { file: 'src/_boot_.ts' } };
const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
- const url = tryCreateUrl(config.url);
+ const url = tryCreateUrl(config.url ?? process.env.MISSKEY_URL ?? '');
const version = meta.version;
const host = url.host;
const hostname = url.hostname;
const scheme = url.protocol.replace(/:$/, '');
const wsScheme = scheme.replace('http', 'ws');
+ const dbDb = config.db.db ?? process.env.DATABASE_DB ?? '';
+ const dbUser = config.db.user ?? process.env.DATABASE_USER ?? '';
+ const dbPass = config.db.pass ?? process.env.DATABASE_PASSWORD ?? '';
+
const externalMediaProxy = config.mediaProxy ?
config.mediaProxy.endsWith('/') ? config.mediaProxy.substring(0, config.mediaProxy.length - 1) : config.mediaProxy
: null;
@@ -226,7 +235,7 @@ export function loadConfig(): Config {
apiUrl: `${scheme}://${host}/api`,
authUrl: `${scheme}://${host}/auth`,
driveUrl: `${scheme}://${host}/files`,
- db: config.db,
+ db: { ...config.db, db: dbDb, user: dbUser, pass: dbPass },
dbReplications: config.dbReplications,
dbSlaves: config.dbSlaves,
meilisearch: config.meilisearch,
@@ -234,6 +243,8 @@ export function loadConfig(): Config {
redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis,
redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis,
redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,
+ sentryForBackend: config.sentryForBackend,
+ sentryForFrontend: config.sentryForFrontend,
id: config.id,
proxy: config.proxy,
proxySmtp: config.proxySmtp,
@@ -252,7 +263,7 @@ export function loadConfig(): Config {
deliverJobMaxAttempts: config.deliverJobMaxAttempts,
inboxJobMaxAttempts: config.inboxJobMaxAttempts,
proxyRemoteFiles: config.proxyRemoteFiles,
- signToActivityPubGet: config.signToActivityPubGet,
+ signToActivityPubGet: config.signToActivityPubGet ?? true,
mediaProxy: externalMediaProxy ?? internalMediaProxy,
externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy,
videoThumbnailGenerator: config.videoThumbnailGenerator ?
diff --git a/packages/backend/src/const.ts b/packages/backend/src/const.ts
index a238f4973a95..4dc689238bf2 100644
--- a/packages/backend/src/const.ts
+++ b/packages/backend/src/const.ts
@@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+// dummy
export const MAX_NOTE_TEXT_LENGTH = 3000;
export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min
diff --git a/packages/backend/src/core/AbuseReportNotificationService.ts b/packages/backend/src/core/AbuseReportNotificationService.ts
new file mode 100644
index 000000000000..42e5931212f7
--- /dev/null
+++ b/packages/backend/src/core/AbuseReportNotificationService.ts
@@ -0,0 +1,405 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable, type OnApplicationShutdown } from '@nestjs/common';
+import { Brackets, In, IsNull, Not } from 'typeorm';
+import * as Redis from 'ioredis';
+import sanitizeHtml from 'sanitize-html';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
+import type {
+ AbuseReportNotificationRecipientRepository,
+ MiAbuseReportNotificationRecipient,
+ MiAbuseUserReport,
+ MiUser,
+} from '@/models/_.js';
+import { EmailService } from '@/core/EmailService.js';
+import { MetaService } from '@/core/MetaService.js';
+import { RoleService } from '@/core/RoleService.js';
+import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+import { IdService } from './IdService.js';
+
+@Injectable()
+export class AbuseReportNotificationService implements OnApplicationShutdown {
+ constructor(
+ @Inject(DI.abuseReportNotificationRecipientRepository)
+ private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository,
+ @Inject(DI.redisForSub)
+ private redisForSub: Redis.Redis,
+ private idService: IdService,
+ private roleService: RoleService,
+ private systemWebhookService: SystemWebhookService,
+ private emailService: EmailService,
+ private metaService: MetaService,
+ private moderationLogService: ModerationLogService,
+ private globalEventService: GlobalEventService,
+ ) {
+ this.redisForSub.on('message', this.onMessage);
+ }
+
+ /**
+ * 管理者用Redisイベントを用いて{@link abuseReports}の内容を管理者各位に通知する.
+ * 通知先ユーザは{@link RoleService.getModeratorIds}の取得結果に依る.
+ *
+ * @see RoleService.getModeratorIds
+ * @see GlobalEventService.publishAdminStream
+ */
+ @bindThis
+ public async notifyAdminStream(abuseReports: MiAbuseUserReport[]) {
+ if (abuseReports.length <= 0) {
+ return;
+ }
+
+ const moderatorIds = await this.roleService.getModeratorIds(true, true);
+
+ for (const moderatorId of moderatorIds) {
+ for (const abuseReport of abuseReports) {
+ this.globalEventService.publishAdminStream(
+ moderatorId,
+ 'newAbuseUserReport',
+ {
+ id: abuseReport.id,
+ targetUserId: abuseReport.targetUserId,
+ reporterId: abuseReport.reporterId,
+ comment: abuseReport.comment,
+ },
+ );
+ }
+ }
+ }
+
+ /**
+ * Mailを用いて{@link abuseReports}の内容を管理者各位に通知する.
+ * メールアドレスの送信先は以下の通り.
+ * - モデレータ権限所有者ユーザ(設定画面からメールアドレスの設定を行っているユーザに限る)
+ * - metaテーブルに設定されているメールアドレス
+ *
+ * @see EmailService.sendEmail
+ */
+ @bindThis
+ public async notifyMail(abuseReports: MiAbuseUserReport[]) {
+ if (abuseReports.length <= 0) {
+ return;
+ }
+
+ const recipientEMailAddresses = await this.fetchEMailRecipients().then(it => it
+ .filter(it => it.isActive && it.userProfile?.emailVerified)
+ .map(it => it.userProfile?.email)
+ .filter(x => x != null),
+ );
+
+ // 送信先の鮮度を保つため、毎回取得する
+ const meta = await this.metaService.fetch(true);
+ recipientEMailAddresses.push(
+ ...(meta.email ? [meta.email] : []),
+ );
+
+ if (recipientEMailAddresses.length <= 0) {
+ return;
+ }
+
+ for (const mailAddress of recipientEMailAddresses) {
+ await Promise.all(
+ abuseReports.map(it => {
+ // TODO: 送信処理はJobQueue化したい
+ return this.emailService.sendEmail(
+ mailAddress,
+ 'New Abuse Report',
+ sanitizeHtml(it.comment),
+ sanitizeHtml(it.comment),
+ );
+ }),
+ );
+ }
+ }
+
+ /**
+ * SystemWebhookを用いて{@link abuseReports}の内容を管理者各位に通知する.
+ * ここではJobQueueへのエンキューのみを行うため、即時実行されない.
+ *
+ * @see SystemWebhookService.enqueueSystemWebhook
+ */
+ @bindThis
+ public async notifySystemWebhook(
+ abuseReports: MiAbuseUserReport[],
+ type: 'abuseReport' | 'abuseReportResolved',
+ ) {
+ if (abuseReports.length <= 0) {
+ return;
+ }
+
+ const recipientWebhookIds = await this.fetchWebhookRecipients()
+ .then(it => it
+ .filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook')
+ .map(it => it.systemWebhookId)
+ .filter(x => x != null));
+ for (const webhookId of recipientWebhookIds) {
+ await Promise.all(
+ abuseReports.map(it => {
+ return this.systemWebhookService.enqueueSystemWebhook(
+ webhookId,
+ type,
+ it,
+ );
+ }),
+ );
+ }
+ }
+
+ /**
+ * 通報の通知先一覧を取得する.
+ *
+ * @param {Object} [params] クエリの取得条件
+ * @param {Object} [params.method] 取得する通知先の通知方法
+ * @param {Object} [opts] 動作時の詳細なオプション
+ * @param {boolean} [opts.removeUnauthorized] 副作用としてモデレータ権限を持たない送信先ユーザをDBから削除するかどうか(default: true)
+ * @param {boolean} [opts.joinUser] 通知先のユーザ情報をJOINするかどうか(default: false)
+ * @param {boolean} [opts.joinSystemWebhook] 通知先のSystemWebhook情報をJOINするかどうか(default: false)
+ * @see removeUnauthorizedRecipientUsers
+ */
+ @bindThis
+ public async fetchRecipients(
+ params?: {
+ ids?: MiAbuseReportNotificationRecipient['id'][],
+ method?: RecipientMethod[],
+ },
+ opts?: {
+ removeUnauthorized?: boolean,
+ joinUser?: boolean,
+ joinSystemWebhook?: boolean,
+ },
+ ): Promise {
+ const query = this.abuseReportNotificationRecipientRepository.createQueryBuilder('recipient');
+
+ if (opts?.joinUser) {
+ query.innerJoinAndSelect('user', 'user', 'recipient.userId = user.id');
+ query.innerJoinAndSelect('recipient.userProfile', 'userProfile');
+ }
+
+ if (opts?.joinSystemWebhook) {
+ query.innerJoinAndSelect('recipient.systemWebhook', 'systemWebhook');
+ }
+
+ if (params?.ids) {
+ query.andWhere({ id: In(params.ids) });
+ }
+
+ if (params?.method) {
+ query.andWhere(new Brackets(qb => {
+ if (params.method?.includes('email')) {
+ qb.orWhere({ method: 'email', userId: Not(IsNull()) });
+ }
+ if (params.method?.includes('webhook')) {
+ qb.orWhere({ method: 'webhook', userId: IsNull() });
+ }
+ }));
+ }
+
+ const recipients = await query.getMany();
+ if (recipients.length <= 0) {
+ return [];
+ }
+
+ // アサイン有効期限切れはイベントで拾えないので、このタイミングでチェック及び削除(オプション)
+ return (opts?.removeUnauthorized ?? true)
+ ? await this.removeUnauthorizedRecipientUsers(recipients)
+ : recipients;
+ }
+
+ /**
+ * EMailの通知先一覧を取得する.
+ * リレーション先の{@link MiUser}および{@link MiUserProfile}も同時に取得する.
+ *
+ * @param {Object} [opts]
+ * @param {boolean} [opts.removeUnauthorized] 副作用としてモデレータ権限を持たない送信先ユーザをDBから削除するかどうか(default: true)
+ * @see removeUnauthorizedRecipientUsers
+ */
+ @bindThis
+ public async fetchEMailRecipients(opts?: {
+ removeUnauthorized?: boolean
+ }): Promise {
+ return this.fetchRecipients({ method: ['email'] }, { joinUser: true, ...opts });
+ }
+
+ /**
+ * Webhookの通知先一覧を取得する.
+ * リレーション先の{@link MiSystemWebhook}も同時に取得する.
+ */
+ @bindThis
+ public fetchWebhookRecipients(): Promise {
+ return this.fetchRecipients({ method: ['webhook'] }, { joinSystemWebhook: true });
+ }
+
+ /**
+ * 通知先を作成する.
+ */
+ @bindThis
+ public async createRecipient(
+ params: {
+ isActive: MiAbuseReportNotificationRecipient['isActive'];
+ name: MiAbuseReportNotificationRecipient['name'];
+ method: MiAbuseReportNotificationRecipient['method'];
+ userId: MiAbuseReportNotificationRecipient['userId'];
+ systemWebhookId: MiAbuseReportNotificationRecipient['systemWebhookId'];
+ },
+ updater: MiUser,
+ ): Promise {
+ const id = this.idService.gen();
+ await this.abuseReportNotificationRecipientRepository.insert({
+ ...params,
+ id,
+ });
+
+ const created = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: id });
+
+ this.moderationLogService
+ .log(updater, 'createAbuseReportNotificationRecipient', {
+ recipientId: id,
+ recipient: created,
+ })
+ .then();
+
+ return created;
+ }
+
+ /**
+ * 通知先を更新する.
+ */
+ @bindThis
+ public async updateRecipient(
+ params: {
+ id: MiAbuseReportNotificationRecipient['id'];
+ isActive: MiAbuseReportNotificationRecipient['isActive'];
+ name: MiAbuseReportNotificationRecipient['name'];
+ method: MiAbuseReportNotificationRecipient['method'];
+ userId: MiAbuseReportNotificationRecipient['userId'];
+ systemWebhookId: MiAbuseReportNotificationRecipient['systemWebhookId'];
+ },
+ updater: MiUser,
+ ): Promise {
+ const beforeEntity = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: params.id });
+
+ await this.abuseReportNotificationRecipientRepository.update(params.id, {
+ isActive: params.isActive,
+ updatedAt: new Date(),
+ name: params.name,
+ method: params.method,
+ userId: params.userId,
+ systemWebhookId: params.systemWebhookId,
+ });
+
+ const afterEntity = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: params.id });
+
+ this.moderationLogService
+ .log(updater, 'updateAbuseReportNotificationRecipient', {
+ recipientId: params.id,
+ before: beforeEntity,
+ after: afterEntity,
+ })
+ .then();
+
+ return afterEntity;
+ }
+
+ /**
+ * 通知先を削除する.
+ */
+ @bindThis
+ public async deleteRecipient(
+ id: MiAbuseReportNotificationRecipient['id'],
+ updater: MiUser,
+ ) {
+ const entity = await this.abuseReportNotificationRecipientRepository.findBy({ id });
+
+ await this.abuseReportNotificationRecipientRepository.delete(id);
+
+ this.moderationLogService
+ .log(updater, 'deleteAbuseReportNotificationRecipient', {
+ recipientId: id,
+ recipient: entity,
+ })
+ .then();
+ }
+
+ /**
+ * モデレータ権限を持たない(*1)通知先ユーザを削除する.
+ *
+ * *1: 以下の両方を満たすものの事を言う
+ * - 通知先にユーザIDが設定されている
+ * - 付与ロールにモデレータ権限がない or アサインの有効期限が切れている
+ *
+ * @param recipients 通知先一覧の配列
+ * @returns {@lisk recipients}からモデレータ権限を持たない通知先を削除した配列
+ */
+ @bindThis
+ private async removeUnauthorizedRecipientUsers(recipients: MiAbuseReportNotificationRecipient[]): Promise {
+ const userRecipients = recipients.filter(it => it.userId !== null);
+ const recipientUserIds = new Set(userRecipients.map(it => it.userId).filter(x => x != null));
+ if (recipientUserIds.size <= 0) {
+ // ユーザが通知先として設定されていない場合、この関数での処理を行うべきレコードが無い
+ return recipients;
+ }
+
+ // モデレータ権限の有無で通知先設定を振り分ける
+ const authorizedUserIds = await this.roleService.getModeratorIds(true, true);
+ const authorizedUserRecipients = Array.of();
+ const unauthorizedUserRecipients = Array.of();
+ for (const recipient of userRecipients) {
+ // eslint-disable-next-line
+ if (authorizedUserIds.includes(recipient.userId!)) {
+ authorizedUserRecipients.push(recipient);
+ } else {
+ unauthorizedUserRecipients.push(recipient);
+ }
+ }
+
+ // モデレータ権限を持たない通知先をDBから削除する
+ if (unauthorizedUserRecipients.length > 0) {
+ await this.abuseReportNotificationRecipientRepository.delete(unauthorizedUserRecipients.map(it => it.id));
+ }
+ const nonUserRecipients = recipients.filter(it => it.userId === null);
+ return [...nonUserRecipients, ...authorizedUserRecipients].sort((a, b) => a.id.localeCompare(b.id));
+ }
+
+ @bindThis
+ private async onMessage(_: string, data: string): Promise {
+ const obj = JSON.parse(data);
+ if (obj.channel !== 'internal') {
+ return;
+ }
+
+ const { type } = obj.message as GlobalEvents['internal']['payload'];
+ switch (type) {
+ case 'roleUpdated':
+ case 'roleDeleted':
+ case 'userRoleUnassigned': {
+ // 場合によってはキャッシュ更新よりも先にここが呼ばれてしまう可能性があるのでnextTickで遅延実行
+ process.nextTick(async () => {
+ const recipients = await this.abuseReportNotificationRecipientRepository.findBy({
+ userId: Not(IsNull()),
+ });
+ await this.removeUnauthorizedRecipientUsers(recipients);
+ });
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ }
+
+ @bindThis
+ public dispose(): void {
+ this.redisForSub.off('message', this.onMessage);
+ }
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined): void {
+ this.dispose();
+ }
+}
diff --git a/packages/backend/src/core/AbuseReportService.ts b/packages/backend/src/core/AbuseReportService.ts
new file mode 100644
index 000000000000..69c51509ba40
--- /dev/null
+++ b/packages/backend/src/core/AbuseReportService.ts
@@ -0,0 +1,128 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { In } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import type { AbuseUserReportsRepository, MiAbuseUserReport, MiUser, UsersRepository } from '@/models/_.js';
+import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
+import { QueueService } from '@/core/QueueService.js';
+import { InstanceActorService } from '@/core/InstanceActorService.js';
+import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { IdService } from './IdService.js';
+
+@Injectable()
+export class AbuseReportService {
+ constructor(
+ @Inject(DI.abuseUserReportsRepository)
+ private abuseUserReportsRepository: AbuseUserReportsRepository,
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
+ private idService: IdService,
+ private abuseReportNotificationService: AbuseReportNotificationService,
+ private queueService: QueueService,
+ private instanceActorService: InstanceActorService,
+ private apRendererService: ApRendererService,
+ private moderationLogService: ModerationLogService,
+ ) {
+ }
+
+ /**
+ * ユーザからの通報をDBに記録し、その内容を下記の手段で管理者各位に通知する.
+ * - 管理者用Redisイベント
+ * - EMail(モデレータ権限所有者ユーザ+metaテーブルに設定されているメールアドレス)
+ * - SystemWebhook
+ *
+ * @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
+ * @see AbuseReportNotificationService.notify
+ */
+ @bindThis
+ public async report(params: {
+ targetUserId: MiAbuseUserReport['targetUserId'],
+ targetUserHost: MiAbuseUserReport['targetUserHost'],
+ reporterId: MiAbuseUserReport['reporterId'],
+ reporterHost: MiAbuseUserReport['reporterHost'],
+ comment: string,
+ }[]) {
+ const entities = params.map(param => {
+ return {
+ id: this.idService.gen(),
+ targetUserId: param.targetUserId,
+ targetUserHost: param.targetUserHost,
+ reporterId: param.reporterId,
+ reporterHost: param.reporterHost,
+ comment: param.comment,
+ };
+ });
+
+ const reports = Array.of();
+ for (const entity of entities) {
+ const report = await this.abuseUserReportsRepository.insertOne(entity);
+ reports.push(report);
+ }
+
+ return Promise.all([
+ this.abuseReportNotificationService.notifyAdminStream(reports),
+ this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReport'),
+ this.abuseReportNotificationService.notifyMail(reports),
+ ]);
+ }
+
+ /**
+ * 通報を解決し、その内容を下記の手段で管理者各位に通知する.
+ * - SystemWebhook
+ *
+ * @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
+ * @param operator 通報を処理したユーザ
+ * @see AbuseReportNotificationService.notify
+ */
+ @bindThis
+ public async resolve(
+ params: {
+ reportId: string;
+ forward: boolean;
+ }[],
+ operator: MiUser,
+ ) {
+ const paramsMap = new Map(params.map(it => [it.reportId, it]));
+ const reports = await this.abuseUserReportsRepository.findBy({
+ id: In(params.map(it => it.reportId)),
+ });
+
+ for (const report of reports) {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const ps = paramsMap.get(report.id)!;
+
+ await this.abuseUserReportsRepository.update(report.id, {
+ resolved: true,
+ assigneeId: operator.id,
+ forwarded: ps.forward && report.targetUserHost !== null,
+ });
+
+ if (ps.forward && report.targetUserHost != null) {
+ const actor = await this.instanceActorService.getInstanceActor();
+ const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
+
+ // eslint-disable-next-line
+ const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment);
+ const contextAssignedFlag = this.apRendererService.addContext(flag);
+ this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false);
+ }
+
+ this.moderationLogService
+ .log(operator, 'resolveAbuseReport', {
+ reportId: report.id,
+ report: report,
+ forwarded: ps.forward && report.targetUserHost !== null,
+ })
+ .then();
+ }
+
+ return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) })
+ .then(reports => this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReportResolved'));
+ }
+}
diff --git a/packages/backend/src/core/AnnouncementService.ts b/packages/backend/src/core/AnnouncementService.ts
index b298a7092983..40a9db01c057 100644
--- a/packages/backend/src/core/AnnouncementService.ts
+++ b/packages/backend/src/core/AnnouncementService.ts
@@ -4,13 +4,14 @@
*/
import { Inject, Injectable } from '@nestjs/common';
-import { Brackets } from 'typeorm';
+import { Brackets, EntityNotFoundError } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { MiUser } from '@/models/User.js';
import type { AnnouncementReadsRepository, AnnouncementsRepository, MiAnnouncement, MiAnnouncementRead, UsersRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { Packed } from '@/misc/json-schema.js';
import { IdService } from '@/core/IdService.js';
+import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
@@ -29,6 +30,7 @@ export class AnnouncementService {
private idService: IdService,
private globalEventService: GlobalEventService,
private moderationLogService: ModerationLogService,
+ private announcementEntityService: AnnouncementEntityService,
) {
}
@@ -65,7 +67,7 @@ export class AnnouncementService {
@bindThis
public async create(values: Partial, moderator?: MiUser): Promise<{ raw: MiAnnouncement; packed: Packed<'Announcement'> }> {
- const announcement = await this.announcementsRepository.insert({
+ const announcement = await this.announcementsRepository.insertOne({
id: this.idService.gen(),
updatedAt: null,
title: values.title,
@@ -77,9 +79,9 @@ export class AnnouncementService {
silence: values.silence,
needConfirmationToRead: values.needConfirmationToRead,
userId: values.userId,
- }).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0]));
+ });
- const packed = (await this.packMany([announcement]))[0];
+ const packed = await this.announcementEntityService.pack(announcement);
if (values.userId) {
this.globalEventService.publishMainStream(values.userId, 'announcementCreated', {
@@ -177,6 +179,24 @@ export class AnnouncementService {
}
}
+ @bindThis
+ public async getAnnouncement(announcementId: MiAnnouncement['id'], me: MiUser | null): Promise> {
+ const announcement = await this.announcementsRepository.findOneByOrFail({ id: announcementId });
+ if (me) {
+ if (announcement.userId && announcement.userId !== me.id) {
+ throw new EntityNotFoundError(this.announcementsRepository.metadata.target, { id: announcementId });
+ }
+
+ const read = await this.announcementReadsRepository.findOneBy({
+ announcementId: announcement.id,
+ userId: me.id,
+ });
+ return this.announcementEntityService.pack({ ...announcement, isRead: read !== null }, me);
+ } else {
+ return this.announcementEntityService.pack(announcement, null);
+ }
+ }
+
@bindThis
public async read(user: MiUser, announcementId: MiAnnouncement['id']): Promise {
try {
@@ -193,29 +213,4 @@ export class AnnouncementService {
this.globalEventService.publishMainStream(user.id, 'readAllAnnouncements');
}
}
-
- @bindThis
- public async packMany(
- announcements: MiAnnouncement[],
- me?: { id: MiUser['id'] } | null | undefined,
- options?: {
- reads?: MiAnnouncementRead[];
- },
- ): Promise[]> {
- const reads = me ? (options?.reads ?? await this.getReads(me.id)) : [];
- return announcements.map(announcement => ({
- id: announcement.id,
- createdAt: this.idService.parse(announcement.id).date.toISOString(),
- updatedAt: announcement.updatedAt?.toISOString() ?? null,
- text: announcement.text,
- title: announcement.title,
- imageUrl: announcement.imageUrl,
- icon: announcement.icon,
- display: announcement.display,
- needConfirmationToRead: announcement.needConfirmationToRead,
- silence: announcement.silence,
- forYou: announcement.userId === me?.id,
- isRead: reads.some(read => read.announcementId === announcement.id),
- }));
- }
}
diff --git a/packages/backend/src/core/AvatarDecorationService.ts b/packages/backend/src/core/AvatarDecorationService.ts
index 21e31d79a429..8b54bbe01241 100644
--- a/packages/backend/src/core/AvatarDecorationService.ts
+++ b/packages/backend/src/core/AvatarDecorationService.ts
@@ -55,10 +55,10 @@ export class AvatarDecorationService implements OnApplicationShutdown {
@bindThis
public async create(options: Partial, moderator?: MiUser): Promise {
- const created = await this.avatarDecorationsRepository.insert({
+ const created = await this.avatarDecorationsRepository.insertOne({
id: this.idService.gen(),
...options,
- }).then(x => this.avatarDecorationsRepository.findOneByOrFail(x.identifiers[0]));
+ });
this.globalEventService.publishInternalEvent('avatarDecorationCreated', created);
diff --git a/packages/backend/src/core/ClipService.ts b/packages/backend/src/core/ClipService.ts
index bb8be26ce6c5..929a9db0645a 100644
--- a/packages/backend/src/core/ClipService.ts
+++ b/packages/backend/src/core/ClipService.ts
@@ -41,17 +41,17 @@ export class ClipService {
const currentCount = await this.clipsRepository.countBy({
userId: me.id,
});
- if (currentCount > (await this.roleService.getUserPolicies(me.id)).clipLimit) {
+ if (currentCount >= (await this.roleService.getUserPolicies(me.id)).clipLimit) {
throw new ClipService.TooManyClipsError();
}
- const clip = await this.clipsRepository.insert({
+ const clip = await this.clipsRepository.insertOne({
id: this.idService.gen(),
userId: me.id,
name: name,
isPublic: isPublic,
description: description,
- }).then(x => this.clipsRepository.findOneByOrFail(x.identifiers[0]));
+ });
return clip;
}
@@ -102,7 +102,7 @@ export class ClipService {
const currentCount = await this.clipNotesRepository.countBy({
clipId: clip.id,
});
- if (currentCount > (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) {
+ if (currentCount >= (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) {
throw new ClipService.TooManyClipNotesError();
}
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index 595315587241..0208540afa7b 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -5,6 +5,14 @@
import { Module } from '@nestjs/common';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
+import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
+import {
+ AbuseReportNotificationRecipientEntityService,
+} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
+import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+import { UserSearchService } from '@/core/UserSearchService.js';
import { AccountMoveService } from './AccountMoveService.js';
import { AccountUpdateService } from './AccountUpdateService.js';
import { AiService } from './AiService.js';
@@ -56,7 +64,7 @@ import { UserMutingService } from './UserMutingService.js';
import { UserSuspendService } from './UserSuspendService.js';
import { UserAuthService } from './UserAuthService.js';
import { VideoProcessingService } from './VideoProcessingService.js';
-import { WebhookService } from './WebhookService.js';
+import { UserWebhookService } from './UserWebhookService.js';
import { ProxyAccountService } from './ProxyAccountService.js';
import { UtilityService } from './UtilityService.js';
import { FileInfoService } from './FileInfoService.js';
@@ -84,6 +92,7 @@ import ApRequestChart from './chart/charts/ap-request.js';
import { ChartManagementService } from './chart/ChartManagementService.js';
import { AbuseUserReportEntityService } from './entities/AbuseUserReportEntityService.js';
+import { AnnouncementEntityService } from './entities/AnnouncementEntityService.js';
import { AntennaEntityService } from './entities/AntennaEntityService.js';
import { AppEntityService } from './entities/AppEntityService.js';
import { AuthSessionEntityService } from './entities/AuthSessionEntityService.js';
@@ -143,6 +152,8 @@ import type { Provider } from '@nestjs/common';
//#region 文字列ベースでのinjection用(循環参照対応のため)
const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService };
+const $AbuseReportService: Provider = { provide: 'AbuseReportService', useExisting: AbuseReportService };
+const $AbuseReportNotificationService: Provider = { provide: 'AbuseReportNotificationService', useExisting: AbuseReportNotificationService };
const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService };
const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService };
const $AiService: Provider = { provide: 'AiService', useExisting: AiService };
@@ -192,10 +203,12 @@ const $UserFollowingService: Provider = { provide: 'UserFollowingService', useEx
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
+const $UserSearchService: Provider = { provide: 'UserSearchService', useExisting: UserSearchService };
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
-const $WebhookService: Provider = { provide: 'WebhookService', useExisting: WebhookService };
+const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
+const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
@@ -223,6 +236,8 @@ const $ApRequestChart: Provider = { provide: 'ApRequestChart', useExisting: ApRe
const $ChartManagementService: Provider = { provide: 'ChartManagementService', useExisting: ChartManagementService };
const $AbuseUserReportEntityService: Provider = { provide: 'AbuseUserReportEntityService', useExisting: AbuseUserReportEntityService };
+const $AnnouncementEntityService: Provider = { provide: 'AnnouncementEntityService', useExisting: AnnouncementEntityService };
+const $AbuseReportNotificationRecipientEntityService: Provider = { provide: 'AbuseReportNotificationRecipientEntityService', useExisting: AbuseReportNotificationRecipientEntityService };
const $AntennaEntityService: Provider = { provide: 'AntennaEntityService', useExisting: AntennaEntityService };
const $AppEntityService: Provider = { provide: 'AppEntityService', useExisting: AppEntityService };
const $AuthSessionEntityService: Provider = { provide: 'AuthSessionEntityService', useExisting: AuthSessionEntityService };
@@ -256,6 +271,7 @@ const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', u
const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService };
const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService };
const $MetaEntityService: Provider = { provide: 'MetaEntityService', useExisting: MetaEntityService };
+const $SystemWebhookEntityService: Provider = { provide: 'SystemWebhookEntityService', useExisting: SystemWebhookEntityService };
const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService };
const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService };
@@ -283,6 +299,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
],
providers: [
LoggerService,
+ AbuseReportService,
+ AbuseReportNotificationService,
AccountMoveService,
AccountUpdateService,
AiService,
@@ -332,10 +350,12 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
UserKeypairService,
UserListService,
UserMutingService,
+ UserSearchService,
UserSuspendService,
UserAuthService,
VideoProcessingService,
- WebhookService,
+ UserWebhookService,
+ SystemWebhookService,
UtilityService,
FileInfoService,
SearchService,
@@ -363,6 +383,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
ChartManagementService,
AbuseUserReportEntityService,
+ AnnouncementEntityService,
+ AbuseReportNotificationRecipientEntityService,
AntennaEntityService,
AppEntityService,
AuthSessionEntityService,
@@ -396,6 +418,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
RoleEntityService,
ReversiGameEntityService,
MetaEntityService,
+ SystemWebhookEntityService,
ApAudienceService,
ApDbResolverService,
@@ -419,6 +442,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
//#region 文字列ベースでのinjection用(循環参照対応のため)
$LoggerService,
+ $AbuseReportService,
+ $AbuseReportNotificationService,
$AccountMoveService,
$AccountUpdateService,
$AiService,
@@ -468,10 +493,12 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$UserKeypairService,
$UserListService,
$UserMutingService,
+ $UserSearchService,
$UserSuspendService,
$UserAuthService,
$VideoProcessingService,
- $WebhookService,
+ $UserWebhookService,
+ $SystemWebhookService,
$UtilityService,
$FileInfoService,
$SearchService,
@@ -499,6 +526,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$ChartManagementService,
$AbuseUserReportEntityService,
+ $AnnouncementEntityService,
+ $AbuseReportNotificationRecipientEntityService,
$AntennaEntityService,
$AppEntityService,
$AuthSessionEntityService,
@@ -532,6 +561,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$RoleEntityService,
$ReversiGameEntityService,
$MetaEntityService,
+ $SystemWebhookEntityService,
$ApAudienceService,
$ApDbResolverService,
@@ -556,6 +586,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
exports: [
QueueModule,
LoggerService,
+ AbuseReportService,
+ AbuseReportNotificationService,
AccountMoveService,
AccountUpdateService,
AiService,
@@ -605,10 +637,12 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
UserKeypairService,
UserListService,
UserMutingService,
+ UserSearchService,
UserSuspendService,
UserAuthService,
VideoProcessingService,
- WebhookService,
+ UserWebhookService,
+ SystemWebhookService,
UtilityService,
FileInfoService,
SearchService,
@@ -635,6 +669,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
ChartManagementService,
AbuseUserReportEntityService,
+ AnnouncementEntityService,
+ AbuseReportNotificationRecipientEntityService,
AntennaEntityService,
AppEntityService,
AuthSessionEntityService,
@@ -668,6 +704,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
RoleEntityService,
ReversiGameEntityService,
MetaEntityService,
+ SystemWebhookEntityService,
ApAudienceService,
ApDbResolverService,
@@ -691,6 +728,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
//#region 文字列ベースでのinjection用(循環参照対応のため)
$LoggerService,
+ $AbuseReportService,
+ $AbuseReportNotificationService,
$AccountMoveService,
$AccountUpdateService,
$AiService,
@@ -740,10 +779,12 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$UserKeypairService,
$UserListService,
$UserMutingService,
+ $UserSearchService,
$UserSuspendService,
$UserAuthService,
$VideoProcessingService,
- $WebhookService,
+ $UserWebhookService,
+ $SystemWebhookService,
$UtilityService,
$FileInfoService,
$SearchService,
@@ -770,6 +811,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$ChartManagementService,
$AbuseUserReportEntityService,
+ $AnnouncementEntityService,
+ $AbuseReportNotificationRecipientEntityService,
$AntennaEntityService,
$AppEntityService,
$AuthSessionEntityService,
@@ -803,6 +846,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$RoleEntityService,
$ReversiGameEntityService,
$MetaEntityService,
+ $SystemWebhookEntityService,
$ApAudienceService,
$ApDbResolverService,
diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts
index 1c75566755ad..7e11b9cdca15 100644
--- a/packages/backend/src/core/CustomEmojiService.ts
+++ b/packages/backend/src/core/CustomEmojiService.ts
@@ -68,7 +68,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
localOnly: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][];
}, moderator?: MiUser): Promise {
- const emoji = await this.emojisRepository.insert({
+ const emoji = await this.emojisRepository.insertOne({
id: this.idService.gen(),
updatedAt: new Date(),
name: data.name,
@@ -82,7 +82,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
isSensitive: data.isSensitive,
localOnly: data.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction,
- }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
+ });
if (data.host == null) {
this.localEmojisCache.refresh();
@@ -346,10 +346,11 @@ export class CustomEmojiService implements OnApplicationShutdown {
@bindThis
public async populateEmojis(emojiNames: string[], noteUserHost: string | null): Promise> {
const emojis = await Promise.all(emojiNames.map(x => this.populateEmoji(x, noteUserHost)));
- const res = {} as any;
+ const res = {} as Record;
for (let i = 0; i < emojiNames.length; i++) {
- if (emojis[i] != null) {
- res[emojiNames[i]] = emojis[i];
+ const resolvedEmoji = emojis[i];
+ if (resolvedEmoji != null) {
+ res[emojiNames[i]] = resolvedEmoji;
}
}
return res;
diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts
index cdd0f9536887..8aa04b4da733 100644
--- a/packages/backend/src/core/DriveService.ts
+++ b/packages/backend/src/core/DriveService.ts
@@ -222,7 +222,7 @@ export class DriveService {
file.size = size;
file.storedInternal = false;
- return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0]));
+ return await this.driveFilesRepository.insertOne(file);
} else { // use internal storage
const accessKey = randomUUID();
const thumbnailAccessKey = 'thumbnail-' + randomUUID();
@@ -256,7 +256,7 @@ export class DriveService {
file.md5 = hash;
file.size = size;
- return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0]));
+ return await this.driveFilesRepository.insertOne(file);
}
}
@@ -499,14 +499,20 @@ export class DriveService {
if (user && !force) {
// Check if there is a file with the same hash
- const much = await this.driveFilesRepository.findOneBy({
+ const matched = await this.driveFilesRepository.findOneBy({
md5: info.md5,
userId: user.id,
});
- if (much) {
- this.registerLogger.info(`file with same hash is found: ${much.id}`);
- return much;
+ if (matched) {
+ this.registerLogger.info(`file with same hash is found: ${matched.id}`);
+ if (sensitive && !matched.isSensitive) {
+ // The file is federated as sensitive for this time, but was federated as non-sensitive before.
+ // Therefore, update the file to sensitive.
+ await this.driveFilesRepository.update({ id: matched.id }, { isSensitive: true });
+ matched.isSensitive = true;
+ }
+ return matched;
}
}
@@ -612,7 +618,7 @@ export class DriveService {
file.type = info.type.mime;
file.storedInternal = false;
- file = await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0]));
+ file = await this.driveFilesRepository.insertOne(file);
} catch (err) {
// duplicate key error (when already registered)
if (isDuplicateKeyValueError(err)) {
diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts
index 08f8f80a6ed9..435dbbae28a9 100644
--- a/packages/backend/src/core/EmailService.ts
+++ b/packages/backend/src/core/EmailService.ts
@@ -16,6 +16,7 @@ import type { UserProfilesRepository } from '@/models/_.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
+import { QueueService } from '@/core/QueueService.js';
@Injectable()
export class EmailService {
@@ -32,6 +33,7 @@ export class EmailService {
private loggerService: LoggerService,
private utilityService: UtilityService,
private httpRequestService: HttpRequestService,
+ private queueService: QueueService,
) {
this.logger = this.loggerService.getLogger('email');
}
diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts
index d5058f37c2a5..b05af99c5e5e 100644
--- a/packages/backend/src/core/FanoutTimelineEndpointService.ts
+++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts
@@ -55,9 +55,6 @@ export class FanoutTimelineEndpointService {
@bindThis
private async getMiNotes(ps: TimelineOptions): Promise {
- let noteIds: string[];
- let shouldFallbackToDb = false;
-
// 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える
if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]);
@@ -67,12 +64,11 @@ export class FanoutTimelineEndpointService {
const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId);
// TODO: いい感じにgetMulti内でソート済だからuniqするときにredisResultが全てソート済なのを利用して再ソートを避けたい
- const redisResultIds = Array.from(new Set(redisResult.flat(1)));
-
- redisResultIds.sort(idCompare);
- noteIds = redisResultIds.slice(0, ps.limit);
+ const redisResultIds = Array.from(new Set(redisResult.flat(1))).sort(idCompare);
- shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
+ let noteIds = redisResultIds.slice(0, ps.limit);
+ const oldestNoteId = ascending ? redisResultIds[0] : redisResultIds[redisResultIds.length - 1];
+ const shouldFallbackToDb = noteIds.length === 0 || ps.sinceId != null && ps.sinceId < oldestNoteId;
if (!shouldFallbackToDb) {
let filter = ps.noteFilter ?? (_note => true);
diff --git a/packages/backend/src/core/FederatedInstanceService.ts b/packages/backend/src/core/FederatedInstanceService.ts
index 66db2067d9f8..7aeeb781786c 100644
--- a/packages/backend/src/core/FederatedInstanceService.ts
+++ b/packages/backend/src/core/FederatedInstanceService.ts
@@ -40,6 +40,7 @@ export class FederatedInstanceService implements OnApplicationShutdown {
firstRetrievedAt: new Date(parsed.firstRetrievedAt),
latestRequestReceivedAt: parsed.latestRequestReceivedAt ? new Date(parsed.latestRequestReceivedAt) : null,
infoUpdatedAt: parsed.infoUpdatedAt ? new Date(parsed.infoUpdatedAt) : null,
+ notRespondingSince: parsed.notRespondingSince ? new Date(parsed.notRespondingSince) : null,
};
},
});
@@ -55,11 +56,11 @@ export class FederatedInstanceService implements OnApplicationShutdown {
const index = await this.instancesRepository.findOneBy({ host });
if (index == null) {
- const i = await this.instancesRepository.insert({
+ const i = await this.instancesRepository.insertOne({
id: this.idService.gen(),
host,
firstRetrievedAt: new Date(),
- }).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0]));
+ });
this.federatedInstanceCache.set(host, i);
return i;
diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts
index 8d173855f33d..aa16468ecb65 100644
--- a/packages/backend/src/core/FetchInstanceMetadataService.ts
+++ b/packages/backend/src/core/FetchInstanceMetadataService.ts
@@ -154,7 +154,7 @@ export class FetchInstanceMetadataService {
throw new Error('No wellknown links');
}
- const links = wellknown.links as any[];
+ const links = wellknown.links as ({ rel: string, href: string; })[];
const link1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0');
const link2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0');
diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts
index 90efd63f3a05..a70743bed203 100644
--- a/packages/backend/src/core/GlobalEventService.ts
+++ b/packages/backend/src/core/GlobalEventService.ts
@@ -18,6 +18,7 @@ import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import type { MiSignin } from '@/models/Signin.js';
import type { MiPage } from '@/models/Page.js';
import type { MiWebhook } from '@/models/Webhook.js';
+import type { MiSystemWebhook } from '@/models/SystemWebhook.js';
import type { MiMeta } from '@/models/Meta.js';
import { MiAvatarDecoration, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js';
@@ -227,6 +228,9 @@ export interface InternalEventTypes {
webhookCreated: MiWebhook;
webhookDeleted: MiWebhook;
webhookUpdated: MiWebhook;
+ systemWebhookCreated: MiSystemWebhook;
+ systemWebhookDeleted: MiSystemWebhook;
+ systemWebhookUpdated: MiSystemWebhook;
antennaCreated: MiAntenna;
antennaDeleted: MiAntenna;
antennaUpdated: MiAntenna;
diff --git a/packages/backend/src/core/LoggerService.ts b/packages/backend/src/core/LoggerService.ts
index 96d9b099927b..f102461a5065 100644
--- a/packages/backend/src/core/LoggerService.ts
+++ b/packages/backend/src/core/LoggerService.ts
@@ -15,7 +15,7 @@ export class LoggerService {
}
@bindThis
- public getLogger(domain: string, color?: KEYWORD | undefined, store?: boolean) {
- return new Logger(domain, color, store);
+ public getLogger(domain: string, color?: KEYWORD | undefined) {
+ return new Logger(domain, color);
}
}
diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts
index 9786f8b8bb7a..74536c68f515 100644
--- a/packages/backend/src/core/MfmService.ts
+++ b/packages/backend/src/core/MfmService.ts
@@ -13,10 +13,12 @@ import { intersperse } from '@/misc/prelude/array.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import type { IMentionedRemoteUsers } from '@/models/Note.js';
import { bindThis } from '@/decorators.js';
-import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js';
+import type { DefaultTreeAdapterMap } from 'parse5';
import type * as mfm from 'mfm-js';
-const treeAdapter = TreeAdapter.defaultTreeAdapter;
+const treeAdapter = parse5.defaultTreeAdapter;
+type Node = DefaultTreeAdapterMap['node'];
+type ChildNode = DefaultTreeAdapterMap['childNode'];
const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
@@ -46,7 +48,7 @@ export class MfmService {
return text.trim();
- function getText(node: TreeAdapter.Node): string {
+ function getText(node: Node): string {
if (treeAdapter.isTextNode(node)) return node.value;
if (!treeAdapter.isElementNode(node)) return '';
if (node.nodeName === 'br') return '\n';
@@ -58,7 +60,7 @@ export class MfmService {
return '';
}
- function appendChildren(childNodes: TreeAdapter.ChildNode[]): void {
+ function appendChildren(childNodes: ChildNode[]): void {
if (childNodes) {
for (const n of childNodes) {
analyze(n);
@@ -66,14 +68,16 @@ export class MfmService {
}
}
- function analyze(node: TreeAdapter.Node) {
+ function analyze(node: Node) {
if (treeAdapter.isTextNode(node)) {
text += node.value;
return;
}
// Skip comment or document type node
- if (!treeAdapter.isElementNode(node)) return;
+ if (!treeAdapter.isElementNode(node)) {
+ return;
+ }
switch (node.nodeName) {
case 'br': {
@@ -81,8 +85,7 @@ export class MfmService {
break;
}
- case 'a':
- {
+ case 'a': {
const txt = getText(node);
const rel = node.attrs.find(x => x.name === 'rel');
const href = node.attrs.find(x => x.name === 'href');
@@ -90,7 +93,7 @@ export class MfmService {
// ハッシュタグ
if (normalizedHashtagNames && href && normalizedHashtagNames.has(normalizeForSearch(txt))) {
text += txt;
- // メンション
+ // メンション
} else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) {
const part = txt.split('@');
@@ -102,7 +105,7 @@ export class MfmService {
} else if (part.length === 3) {
text += txt;
}
- // その他
+ // その他
} else {
const generateLink = () => {
if (!href && !txt) {
@@ -130,8 +133,7 @@ export class MfmService {
break;
}
- case 'h1':
- {
+ case 'h1': {
text += '【';
appendChildren(node.childNodes);
text += '】\n';
@@ -139,16 +141,14 @@ export class MfmService {
}
case 'b':
- case 'strong':
- {
+ case 'strong': {
text += '**';
appendChildren(node.childNodes);
text += '**';
break;
}
- case 'small':
- {
+ case 'small': {
text += '';
appendChildren(node.childNodes);
text += '';
@@ -156,8 +156,7 @@ export class MfmService {
}
case 's':
- case 'del':
- {
+ case 'del': {
text += '~~';
appendChildren(node.childNodes);
text += '~~';
@@ -165,8 +164,7 @@ export class MfmService {
}
case 'i':
- case 'em':
- {
+ case 'em': {
text += '';
appendChildren(node.childNodes);
text += '';
@@ -207,8 +205,7 @@ export class MfmService {
case 'h3':
case 'h4':
case 'h5':
- case 'h6':
- {
+ case 'h6': {
text += '\n\n';
appendChildren(node.childNodes);
break;
@@ -221,8 +218,7 @@ export class MfmService {
case 'article':
case 'li':
case 'dt':
- case 'dd':
- {
+ case 'dd': {
text += '\n';
appendChildren(node.childNodes);
break;
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index f7ecb2844503..83ed1f2c7cde 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -38,7 +38,7 @@ import InstanceChart from '@/core/chart/charts/instance.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { NotificationService } from '@/core/NotificationService.js';
-import { WebhookService } from '@/core/WebhookService.js';
+import { UserWebhookService } from '@/core/UserWebhookService.js';
import { HashtagService } from '@/core/HashtagService.js';
import { AntennaService } from '@/core/AntennaService.js';
import { QueueService } from '@/core/QueueService.js';
@@ -59,7 +59,6 @@ import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import { isReply } from '@/misc/is-reply.js';
import { trackPromise } from '@/misc/promise-tracker.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@@ -205,7 +204,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private federatedInstanceService: FederatedInstanceService,
private hashtagService: HashtagService,
private antennaService: AntennaService,
- private webhookService: WebhookService,
+ private webhookService: UserWebhookService,
private featuredService: FeaturedService,
private remoteUserResolveService: RemoteUserResolveService,
private apDeliverManagerService: ApDeliverManagerService,
@@ -609,7 +608,7 @@ export class NoteCreateService implements OnApplicationShutdown {
this.webhookService.getActiveWebhooks().then(webhooks => {
webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'note', {
+ this.queueService.userWebhookDeliver(webhook, 'note', {
note: noteObj,
});
}
@@ -636,7 +635,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'reply', {
+ this.queueService.userWebhookDeliver(webhook, 'reply', {
note: noteObj,
});
}
@@ -659,7 +658,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'renote', {
+ this.queueService.userWebhookDeliver(webhook, 'renote', {
note: noteObj,
});
}
@@ -791,7 +790,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'mention', {
+ this.queueService.userWebhookDeliver(webhook, 'mention', {
note: detailPackedNote,
});
}
@@ -842,7 +841,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const mentions = extractMentions(tokens);
let mentionedUsers = (await Promise.all(mentions.map(m =>
this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null),
- ))).filter(isNotNull);
+ ))).filter(x => x != null);
// Drop duplicate users
mentionedUsers = mentionedUsers.filter((u, i, self) =>
diff --git a/packages/backend/src/core/QueueModule.ts b/packages/backend/src/core/QueueModule.ts
index 216734e9e5e4..b10b8e589955 100644
--- a/packages/backend/src/core/QueueModule.ts
+++ b/packages/backend/src/core/QueueModule.ts
@@ -7,10 +7,17 @@ import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
import * as Bull from 'bullmq';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
-import { QUEUE, baseQueueOptions } from '@/queue/const.js';
+import { baseQueueOptions, QUEUE } from '@/queue/const.js';
import { allSettled } from '@/misc/promise-tracker.js';
+import {
+ DeliverJobData,
+ EndedPollNotificationJobData,
+ InboxJobData,
+ RelationshipJobData,
+ UserWebhookDeliverJobData,
+ SystemWebhookDeliverJobData,
+} from '../queue/types.js';
import type { Provider } from '@nestjs/common';
-import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js';
export type SystemQueue = Bull.Queue>;
export type EndedPollNotificationQueue = Bull.Queue;
@@ -19,7 +26,8 @@ export type InboxQueue = Bull.Queue;
export type DbQueue = Bull.Queue;
export type RelationshipQueue = Bull.Queue;
export type ObjectStorageQueue = Bull.Queue;
-export type WebhookDeliverQueue = Bull.Queue;
+export type UserWebhookDeliverQueue = Bull.Queue;
+export type SystemWebhookDeliverQueue = Bull.Queue;
const $system: Provider = {
provide: 'queue:system',
@@ -63,9 +71,15 @@ const $objectStorage: Provider = {
inject: [DI.config],
};
-const $webhookDeliver: Provider = {
- provide: 'queue:webhookDeliver',
- useFactory: (config: Config) => new Bull.Queue(QUEUE.WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.WEBHOOK_DELIVER)),
+const $userWebhookDeliver: Provider = {
+ provide: 'queue:userWebhookDeliver',
+ useFactory: (config: Config) => new Bull.Queue(QUEUE.USER_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.USER_WEBHOOK_DELIVER)),
+ inject: [DI.config],
+};
+
+const $systemWebhookDeliver: Provider = {
+ provide: 'queue:systemWebhookDeliver',
+ useFactory: (config: Config) => new Bull.Queue(QUEUE.SYSTEM_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.SYSTEM_WEBHOOK_DELIVER)),
inject: [DI.config],
};
@@ -80,7 +94,8 @@ const $webhookDeliver: Provider = {
$db,
$relationship,
$objectStorage,
- $webhookDeliver,
+ $userWebhookDeliver,
+ $systemWebhookDeliver,
],
exports: [
$system,
@@ -90,7 +105,8 @@ const $webhookDeliver: Provider = {
$db,
$relationship,
$objectStorage,
- $webhookDeliver,
+ $userWebhookDeliver,
+ $systemWebhookDeliver,
],
})
export class QueueModule implements OnApplicationShutdown {
@@ -102,7 +118,8 @@ export class QueueModule implements OnApplicationShutdown {
@Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
- @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
+ @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
+ @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
) {}
public async dispose(): Promise {
@@ -117,7 +134,8 @@ export class QueueModule implements OnApplicationShutdown {
this.dbQueue.close(),
this.relationshipQueue.close(),
this.objectStorageQueue.close(),
- this.webhookDeliverQueue.close(),
+ this.userWebhookDeliverQueue.close(),
+ this.systemWebhookDeliverQueue.close(),
]);
}
diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts
index c258a22927d4..80827a500b56 100644
--- a/packages/backend/src/core/QueueService.ts
+++ b/packages/backend/src/core/QueueService.ts
@@ -8,15 +8,33 @@ import { Inject, Injectable } from '@nestjs/common';
import type { IActivity } from '@/core/activitypub/type.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js';
+import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
-import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
-import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
+import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
+import type {
+ DbJobData,
+ DeliverJobData,
+ RelationshipJobData,
+ SystemWebhookDeliverJobData,
+ ThinUser,
+ UserWebhookDeliverJobData,
+} from '../queue/types.js';
+import type {
+ DbQueue,
+ DeliverQueue,
+ EndedPollNotificationQueue,
+ InboxQueue,
+ ObjectStorageQueue,
+ RelationshipQueue,
+ SystemQueue,
+ UserWebhookDeliverQueue,
+ SystemWebhookDeliverQueue,
+} from './QueueModule.js';
import type httpSignature from '@peertube/http-signature';
import type * as Bull from 'bullmq';
-import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
@Injectable()
export class QueueService {
@@ -31,7 +49,8 @@ export class QueueService {
@Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
- @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
+ @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
+ @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
) {
this.systemQueue.add('tickCharts', {
}, {
@@ -431,9 +450,13 @@ export class QueueService {
});
}
+ /**
+ * @see UserWebhookDeliverJobData
+ * @see WebhookDeliverProcessorService
+ */
@bindThis
- public webhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
- const data = {
+ public userWebhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
+ const data: UserWebhookDeliverJobData = {
type,
content,
webhookId: webhook.id,
@@ -444,7 +467,33 @@ export class QueueService {
eventId: randomUUID(),
};
- return this.webhookDeliverQueue.add(webhook.id, data, {
+ return this.userWebhookDeliverQueue.add(webhook.id, data, {
+ attempts: 4,
+ backoff: {
+ type: 'custom',
+ },
+ removeOnComplete: true,
+ removeOnFail: true,
+ });
+ }
+
+ /**
+ * @see SystemWebhookDeliverJobData
+ * @see WebhookDeliverProcessorService
+ */
+ @bindThis
+ public systemWebhookDeliver(webhook: MiSystemWebhook, type: SystemWebhookEventType, content: unknown) {
+ const data: SystemWebhookDeliverJobData = {
+ type,
+ content,
+ webhookId: webhook.id,
+ to: webhook.url,
+ secret: webhook.secret,
+ createdAt: Date.now(),
+ eventId: randomUUID(),
+ };
+
+ return this.systemWebhookDeliverQueue.add(webhook.id, data, {
attempts: 4,
backoff: {
type: 'custom',
diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts
index d557e9ce86f7..371207c33a7d 100644
--- a/packages/backend/src/core/ReactionService.ts
+++ b/packages/backend/src/core/ReactionService.ts
@@ -29,6 +29,7 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { RoleService } from '@/core/RoleService.js';
import { FeaturedService } from '@/core/FeaturedService.js';
import { trackPromise } from '@/misc/promise-tracker.js';
+import { isQuote, isRenote } from '@/misc/is-renote.js';
const FALLBACK = '\u2764';
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
@@ -119,11 +120,16 @@ export class ReactionService {
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
}
+ // Check if note is Renote
+ if (isRenote(note) && !isQuote(note)) {
+ throw new IdentifiableError('12c35529-3c79-4327-b1cc-e2cf63a71925', 'You cannot react to Renote.');
+ }
+
let reaction = _reaction ?? FALLBACK;
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) {
reaction = '\u2764';
- } else if (_reaction) {
+ } else if (_reaction != null) {
const custom = reaction.match(isCustomEmojiRegexp);
if (custom) {
const reacterHost = this.utilityService.toPunyNullable(user.host);
diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts
index e9dc9b57afef..8dd3d64f5b29 100644
--- a/packages/backend/src/core/RelayService.ts
+++ b/packages/backend/src/core/RelayService.ts
@@ -53,11 +53,11 @@ export class RelayService {
@bindThis
public async addRelay(inbox: string): Promise {
- const relay = await this.relaysRepository.insert({
+ const relay = await this.relaysRepository.insertOne({
id: this.idService.gen(),
inbox,
status: 'requesting',
- }).then(x => this.relaysRepository.findOneByOrFail(x.identifiers[0]));
+ });
const relayActor = await this.getRelayActor();
const follow = await this.apRendererService.renderFollowRelay(relay, relayActor);
diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts
index 439bc0884585..7f939b99c7a6 100644
--- a/packages/backend/src/core/ReversiService.ts
+++ b/packages/backend/src/core/ReversiService.ts
@@ -281,7 +281,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
@bindThis
private async matched(parentId: MiUser['id'], childId: MiUser['id'], options: { noIrregularRules: boolean; }): Promise {
- const game = await this.reversiGamesRepository.insert({
+ const game = await this.reversiGamesRepository.insertOne({
id: this.idService.gen(),
user1Id: parentId,
user2Id: childId,
@@ -294,10 +294,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
bw: 'random',
isLlotheo: false,
noIrregularRules: options.noIrregularRules,
- }).then(x => this.reversiGamesRepository.findOneOrFail({
- where: { id: x.identifiers[0].id },
- relations: ['user1', 'user2'],
- }));
+ }, { relations: ['user1', 'user2'] });
this.cacheGame(game);
const packed = await this.reversiGameEntityService.packDetail(game);
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index 70c537f9abdc..94026fd5030e 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -47,6 +47,7 @@ export type RolePolicies = {
canHideAds: boolean;
driveCapacityMb: number;
alwaysMarkNsfw: boolean;
+ canUpdateBioMedia: boolean;
pinLimit: number;
antennaLimit: number;
wordMuteLimit: number;
@@ -75,6 +76,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
canHideAds: false,
driveCapacityMb: 100,
alwaysMarkNsfw: false,
+ canUpdateBioMedia: true,
pinLimit: 5,
antennaLimit: 5,
wordMuteLimit: 200,
@@ -376,6 +378,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
+ canUpdateBioMedia: calc('canUpdateBioMedia', vs => vs.some(v => v === true)),
pinLimit: calc('pinLimit', vs => Math.max(...vs)),
antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)),
@@ -410,14 +413,32 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
}
@bindThis
- public async getModeratorIds(includeAdmins = true): Promise {
+ public async getModeratorIds(includeAdmins = true, excludeExpire = false): Promise {
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
- const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator);
- const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
- roleId: In(moderatorRoles.map(r => r.id)),
- }) : [];
+ const moderatorRoles = includeAdmins
+ ? roles.filter(r => r.isModerator || r.isAdministrator)
+ : roles.filter(r => r.isModerator);
+
// TODO: isRootなアカウントも含める
- return assigns.map(a => a.userId);
+ const assigns = moderatorRoles.length > 0
+ ? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) })
+ : [];
+
+ const now = Date.now();
+ const result = [
+ // Setを経由して重複を除去(ユーザIDは重複する可能性があるので)
+ ...new Set(
+ assigns
+ .filter(it =>
+ (excludeExpire)
+ ? (it.expiresAt == null || it.expiresAt.getTime() > now)
+ : true,
+ )
+ .map(a => a.userId),
+ ),
+ ];
+
+ return result.sort((x, y) => x.localeCompare(y));
}
@bindThis
@@ -471,12 +492,12 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
}
}
- const created = await this.roleAssignmentsRepository.insert({
+ const created = await this.roleAssignmentsRepository.insertOne({
id: this.idService.gen(now),
expiresAt: expiresAt,
roleId: roleId,
userId: userId,
- }).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0]));
+ });
this.rolesRepository.update(roleId, {
lastUsedAt: new Date(),
@@ -558,7 +579,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
@bindThis
public async create(values: Partial, moderator?: MiUser): Promise {
const date = new Date();
- const created = await this.rolesRepository.insert({
+ const created = await this.rolesRepository.insertOne({
id: this.idService.gen(date.getTime()),
updatedAt: date,
lastUsedAt: date,
@@ -576,7 +597,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
canEditMembersByModerator: values.canEditMembersByModerator,
displayOrder: values.displayOrder,
policies: values.policies,
- }).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0]));
+ });
this.globalEventService.publishInternalEvent('roleCreated', created);
diff --git a/packages/backend/src/core/SystemWebhookService.ts b/packages/backend/src/core/SystemWebhookService.ts
new file mode 100644
index 000000000000..bc6851f788da
--- /dev/null
+++ b/packages/backend/src/core/SystemWebhookService.ts
@@ -0,0 +1,233 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import * as Redis from 'ioredis';
+import type { MiUser, SystemWebhooksRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
+import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
+import { IdService } from '@/core/IdService.js';
+import { QueueService } from '@/core/QueueService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { LoggerService } from '@/core/LoggerService.js';
+import Logger from '@/logger.js';
+import type { OnApplicationShutdown } from '@nestjs/common';
+
+@Injectable()
+export class SystemWebhookService implements OnApplicationShutdown {
+ private logger: Logger;
+ private activeSystemWebhooksFetched = false;
+ private activeSystemWebhooks: MiSystemWebhook[] = [];
+
+ constructor(
+ @Inject(DI.redisForSub)
+ private redisForSub: Redis.Redis,
+ @Inject(DI.systemWebhooksRepository)
+ private systemWebhooksRepository: SystemWebhooksRepository,
+ private idService: IdService,
+ private queueService: QueueService,
+ private moderationLogService: ModerationLogService,
+ private loggerService: LoggerService,
+ private globalEventService: GlobalEventService,
+ ) {
+ this.redisForSub.on('message', this.onMessage);
+ this.logger = this.loggerService.getLogger('webhook');
+ }
+
+ @bindThis
+ public async fetchActiveSystemWebhooks() {
+ if (!this.activeSystemWebhooksFetched) {
+ this.activeSystemWebhooks = await this.systemWebhooksRepository.findBy({
+ isActive: true,
+ });
+ this.activeSystemWebhooksFetched = true;
+ }
+
+ return this.activeSystemWebhooks;
+ }
+
+ /**
+ * SystemWebhook の一覧を取得する.
+ */
+ @bindThis
+ public async fetchSystemWebhooks(params?: {
+ ids?: MiSystemWebhook['id'][];
+ isActive?: MiSystemWebhook['isActive'];
+ on?: MiSystemWebhook['on'];
+ }): Promise {
+ const query = this.systemWebhooksRepository.createQueryBuilder('systemWebhook');
+ if (params) {
+ if (params.ids && params.ids.length > 0) {
+ query.andWhere('systemWebhook.id IN (:...ids)', { ids: params.ids });
+ }
+ if (params.isActive !== undefined) {
+ query.andWhere('systemWebhook.isActive = :isActive', { isActive: params.isActive });
+ }
+ if (params.on && params.on.length > 0) {
+ query.andWhere(':on <@ systemWebhook.on', { on: params.on });
+ }
+ }
+
+ return query.getMany();
+ }
+
+ /**
+ * SystemWebhook を作成する.
+ */
+ @bindThis
+ public async createSystemWebhook(
+ params: {
+ isActive: MiSystemWebhook['isActive'];
+ name: MiSystemWebhook['name'];
+ on: MiSystemWebhook['on'];
+ url: MiSystemWebhook['url'];
+ secret: MiSystemWebhook['secret'];
+ },
+ updater: MiUser,
+ ): Promise {
+ const id = this.idService.gen();
+ await this.systemWebhooksRepository.insert({
+ ...params,
+ id,
+ });
+
+ const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id });
+ this.globalEventService.publishInternalEvent('systemWebhookCreated', webhook);
+ this.moderationLogService
+ .log(updater, 'createSystemWebhook', {
+ systemWebhookId: webhook.id,
+ webhook: webhook,
+ })
+ .then();
+
+ return webhook;
+ }
+
+ /**
+ * SystemWebhook を更新する.
+ */
+ @bindThis
+ public async updateSystemWebhook(
+ params: {
+ id: MiSystemWebhook['id'];
+ isActive: MiSystemWebhook['isActive'];
+ name: MiSystemWebhook['name'];
+ on: MiSystemWebhook['on'];
+ url: MiSystemWebhook['url'];
+ secret: MiSystemWebhook['secret'];
+ },
+ updater: MiUser,
+ ): Promise {
+ const beforeEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: params.id });
+ await this.systemWebhooksRepository.update(beforeEntity.id, {
+ updatedAt: new Date(),
+ isActive: params.isActive,
+ name: params.name,
+ on: params.on,
+ url: params.url,
+ secret: params.secret,
+ });
+
+ const afterEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: beforeEntity.id });
+ this.globalEventService.publishInternalEvent('systemWebhookUpdated', afterEntity);
+ this.moderationLogService
+ .log(updater, 'updateSystemWebhook', {
+ systemWebhookId: beforeEntity.id,
+ before: beforeEntity,
+ after: afterEntity,
+ })
+ .then();
+
+ return afterEntity;
+ }
+
+ /**
+ * SystemWebhook を削除する.
+ */
+ @bindThis
+ public async deleteSystemWebhook(id: MiSystemWebhook['id'], updater: MiUser) {
+ const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id });
+ await this.systemWebhooksRepository.delete(id);
+
+ this.globalEventService.publishInternalEvent('systemWebhookDeleted', webhook);
+ this.moderationLogService
+ .log(updater, 'deleteSystemWebhook', {
+ systemWebhookId: webhook.id,
+ webhook,
+ })
+ .then();
+ }
+
+ /**
+ * SystemWebhook をWebhook配送キューに追加する
+ * @see QueueService.systemWebhookDeliver
+ */
+ @bindThis
+ public async enqueueSystemWebhook(webhook: MiSystemWebhook | MiSystemWebhook['id'], type: SystemWebhookEventType, content: unknown) {
+ const webhookEntity = typeof webhook === 'string'
+ ? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)
+ : webhook;
+ if (!webhookEntity || !webhookEntity.isActive) {
+ this.logger.info(`Webhook is not active or not found : ${webhook}`);
+ return;
+ }
+
+ if (!webhookEntity.on.includes(type)) {
+ this.logger.info(`Webhook ${webhookEntity.id} is not listening to ${type}`);
+ return;
+ }
+
+ return this.queueService.systemWebhookDeliver(webhookEntity, type, content);
+ }
+
+ @bindThis
+ private async onMessage(_: string, data: string): Promise {
+ const obj = JSON.parse(data);
+ if (obj.channel !== 'internal') {
+ return;
+ }
+
+ const { type, body } = obj.message as GlobalEvents['internal']['payload'];
+ switch (type) {
+ case 'systemWebhookCreated': {
+ if (body.isActive) {
+ this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body));
+ }
+ break;
+ }
+ case 'systemWebhookUpdated': {
+ if (body.isActive) {
+ const i = this.activeSystemWebhooks.findIndex(a => a.id === body.id);
+ if (i > -1) {
+ this.activeSystemWebhooks[i] = MiSystemWebhook.deserialize(body);
+ } else {
+ this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body));
+ }
+ } else {
+ this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id);
+ }
+ break;
+ }
+ case 'systemWebhookDeleted': {
+ this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id);
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ @bindThis
+ public dispose(): void {
+ this.redisForSub.off('message', this.onMessage);
+ }
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined): void {
+ this.dispose();
+ }
+}
diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts
index 96f389b54c55..2f1310b8efe7 100644
--- a/packages/backend/src/core/UserBlockingService.ts
+++ b/packages/backend/src/core/UserBlockingService.ts
@@ -16,7 +16,7 @@ import Logger from '@/logger.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { LoggerService } from '@/core/LoggerService.js';
-import { WebhookService } from '@/core/WebhookService.js';
+import { UserWebhookService } from '@/core/UserWebhookService.js';
import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
@@ -46,7 +46,7 @@ export class UserBlockingService implements OnModuleInit {
private idService: IdService,
private queueService: QueueService,
private globalEventService: GlobalEventService,
- private webhookService: WebhookService,
+ private webhookService: UserWebhookService,
private apRendererService: ApRendererService,
private loggerService: LoggerService,
) {
@@ -121,7 +121,7 @@ export class UserBlockingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'unfollow', {
+ this.queueService.userWebhookDeliver(webhook, 'unfollow', {
user: packed,
});
}
diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts
index deeecdeb1f12..267a6a3f1b78 100644
--- a/packages/backend/src/core/UserFollowingService.ts
+++ b/packages/backend/src/core/UserFollowingService.ts
@@ -16,7 +16,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js
import type { Packed } from '@/misc/json-schema.js';
import InstanceChart from '@/core/chart/charts/instance.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
-import { WebhookService } from '@/core/WebhookService.js';
+import { UserWebhookService } from '@/core/UserWebhookService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { DI } from '@/di-symbols.js';
import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
@@ -82,7 +82,7 @@ export class UserFollowingService implements OnModuleInit {
private metaService: MetaService,
private notificationService: NotificationService,
private federatedInstanceService: FederatedInstanceService,
- private webhookService: WebhookService,
+ private webhookService: UserWebhookService,
private apRendererService: ApRendererService,
private accountMoveService: AccountMoveService,
private fanoutTimelineService: FanoutTimelineService,
@@ -331,7 +331,7 @@ export class UserFollowingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'follow', {
+ this.queueService.userWebhookDeliver(webhook, 'follow', {
user: packed,
});
}
@@ -345,7 +345,7 @@ export class UserFollowingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'followed', {
+ this.queueService.userWebhookDeliver(webhook, 'followed', {
user: packed,
});
}
@@ -398,7 +398,7 @@ export class UserFollowingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'unfollow', {
+ this.queueService.userWebhookDeliver(webhook, 'unfollow', {
user: packed,
});
}
@@ -517,7 +517,7 @@ export class UserFollowingService implements OnModuleInit {
followerId: follower.id,
});
- const followRequest = await this.followRequestsRepository.insert({
+ const followRequest = await this.followRequestsRepository.insertOne({
id: this.idService.gen(),
followerId: follower.id,
followeeId: followee.id,
@@ -531,7 +531,7 @@ export class UserFollowingService implements OnModuleInit {
followeeHost: followee.host,
followeeInbox: this.userEntityService.isRemoteUser(followee) ? followee.inbox : undefined,
followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : undefined,
- }).then(x => this.followRequestsRepository.findOneByOrFail(x.identifiers[0]));
+ });
// Publish receiveRequest event
if (this.userEntityService.isLocalUser(followee)) {
@@ -740,7 +740,7 @@ export class UserFollowingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'unfollow', {
+ this.queueService.userWebhookDeliver(webhook, 'unfollow', {
user: packedFollowee,
});
}
diff --git a/packages/backend/src/core/UserListService.ts b/packages/backend/src/core/UserListService.ts
index bbdcfed73836..6333356fe9e8 100644
--- a/packages/backend/src/core/UserListService.ts
+++ b/packages/backend/src/core/UserListService.ts
@@ -95,7 +95,7 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit {
const currentCount = await this.userListMembershipsRepository.countBy({
userListId: list.id,
});
- if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
+ if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
throw new UserListService.TooManyUsersError();
}
diff --git a/packages/backend/src/core/UserSearchService.ts b/packages/backend/src/core/UserSearchService.ts
new file mode 100644
index 000000000000..0d03cf6ee00f
--- /dev/null
+++ b/packages/backend/src/core/UserSearchService.ts
@@ -0,0 +1,205 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Brackets, SelectQueryBuilder } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import { type FollowingsRepository, MiUser, type UsersRepository } from '@/models/_.js';
+import { bindThis } from '@/decorators.js';
+import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
+import type { Config } from '@/config.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { Packed } from '@/misc/json-schema.js';
+
+function defaultActiveThreshold() {
+ return new Date(Date.now() - 1000 * 60 * 60 * 24 * 30);
+}
+
+@Injectable()
+export class UserSearchService {
+ constructor(
+ @Inject(DI.config)
+ private config: Config,
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
+ @Inject(DI.followingsRepository)
+ private followingsRepository: FollowingsRepository,
+ private userEntityService: UserEntityService,
+ ) {
+ }
+
+ /**
+ * ユーザ名とホスト名によるユーザ検索を行う.
+ *
+ * - 検索結果には優先順位がつけられており、以下の順序で検索が行われる.
+ * 1. フォローしているユーザのうち、一定期間以内(※)に更新されたユーザ
+ * 2. フォローしているユーザのうち、一定期間以内に更新されていないユーザ
+ * 3. フォローしていないユーザのうち、一定期間以内に更新されたユーザ
+ * 4. フォローしていないユーザのうち、一定期間以内に更新されていないユーザ
+ * - ログインしていない場合は、以下の順序で検索が行われる.
+ * 1. 一定期間以内に更新されたユーザ
+ * 2. 一定期間以内に更新されていないユーザ
+ * - それぞれの検索結果はユーザ名の昇順でソートされる.
+ * - 動作的には先に登場した検索結果の登場位置が優先される(条件的にユーザIDが重複することはないが).
+ * (1で既にヒットしていた場合、2, 3, 4でヒットしても無視される)
+ * - ユーザ名とホスト名の検索条件はそれぞれ前方一致で検索される.
+ * - ユーザ名の検索は大文字小文字を区別しない.
+ * - ホスト名の検索は大文字小文字を区別しない.
+ * - 検索結果は最大で {@link opts.limit} 件までとなる.
+ *
+ * ※一定期間とは {@link params.activeThreshold} で指定された日時から現在までの期間を指す.
+ *
+ * @param params 検索条件.
+ * @param opts 関数の動作を制御するオプション.
+ * @param me 検索を実行するユーザの情報. 未ログインの場合は指定しない.
+ * @see {@link UserSearchService#buildSearchUserQueries}
+ * @see {@link UserSearchService#buildSearchUserNoLoginQueries}
+ */
+ @bindThis
+ public async search(
+ params: {
+ username?: string | null,
+ host?: string | null,
+ activeThreshold?: Date,
+ },
+ opts?: {
+ limit?: number,
+ detail?: boolean,
+ },
+ me?: MiUser | null,
+ ): Promise[]> {
+ const queries = me ? this.buildSearchUserQueries(me, params) : this.buildSearchUserNoLoginQueries(params);
+
+ let resultSet = new Set();
+ const limit = opts?.limit ?? 10;
+ for (const query of queries) {
+ const ids = await query
+ .select('user.id')
+ .limit(limit - resultSet.size)
+ .orderBy('user.usernameLower', 'ASC')
+ .getRawMany<{ user_id: MiUser['id'] }>()
+ .then(res => res.map(x => x.user_id));
+
+ resultSet = new Set([...resultSet, ...ids]);
+ if (resultSet.size >= limit) {
+ break;
+ }
+ }
+
+ return this.userEntityService.packMany<'UserLite' | 'UserDetailed'>(
+ [...resultSet].slice(0, limit),
+ me,
+ { schema: opts?.detail ? 'UserDetailed' : 'UserLite' },
+ );
+ }
+
+ /**
+ * ログイン済みユーザによる検索実行時のクエリ一覧を構築する.
+ * @param me
+ * @param params
+ * @private
+ */
+ @bindThis
+ private buildSearchUserQueries(
+ me: MiUser,
+ params: {
+ username?: string | null,
+ host?: string | null,
+ activeThreshold?: Date,
+ },
+ ) {
+ // デフォルト30日以内に更新されたユーザーをアクティブユーザーとする
+ const activeThreshold = params.activeThreshold ?? defaultActiveThreshold();
+
+ const followingUserQuery = this.followingsRepository.createQueryBuilder('following')
+ .select('following.followeeId')
+ .where('following.followerId = :followerId', { followerId: me.id });
+
+ const activeFollowingUsersQuery = this.generateUserQueryBuilder(params)
+ .andWhere(`user.id IN (${followingUserQuery.getQuery()})`)
+ .andWhere('user.updatedAt > :activeThreshold', { activeThreshold });
+ activeFollowingUsersQuery.setParameters(followingUserQuery.getParameters());
+
+ const inactiveFollowingUsersQuery = this.generateUserQueryBuilder(params)
+ .andWhere(`user.id IN (${followingUserQuery.getQuery()})`)
+ .andWhere(new Brackets(qb => {
+ qb
+ .where('user.updatedAt IS NULL')
+ .orWhere('user.updatedAt <= :activeThreshold', { activeThreshold });
+ }));
+ inactiveFollowingUsersQuery.setParameters(followingUserQuery.getParameters());
+
+ // 自分自身がヒットするとしたらここ
+ const activeUserQuery = this.generateUserQueryBuilder(params)
+ .andWhere(`user.id NOT IN (${followingUserQuery.getQuery()})`)
+ .andWhere('user.updatedAt > :activeThreshold', { activeThreshold });
+ activeUserQuery.setParameters(followingUserQuery.getParameters());
+
+ const inactiveUserQuery = this.generateUserQueryBuilder(params)
+ .andWhere(`user.id NOT IN (${followingUserQuery.getQuery()})`)
+ .andWhere('user.updatedAt <= :activeThreshold', { activeThreshold });
+ inactiveUserQuery.setParameters(followingUserQuery.getParameters());
+
+ return [activeFollowingUsersQuery, inactiveFollowingUsersQuery, activeUserQuery, inactiveUserQuery];
+ }
+
+ /**
+ * ログインしていないユーザによる検索実行時のクエリ一覧を構築する.
+ * @param params
+ * @private
+ */
+ @bindThis
+ private buildSearchUserNoLoginQueries(params: {
+ username?: string | null,
+ host?: string | null,
+ activeThreshold?: Date,
+ }) {
+ // デフォルト30日以内に更新されたユーザーをアクティブユーザーとする
+ const activeThreshold = params.activeThreshold ?? defaultActiveThreshold();
+
+ const activeUserQuery = this.generateUserQueryBuilder(params)
+ .andWhere(new Brackets(qb => {
+ qb
+ .where('user.updatedAt IS NULL')
+ .orWhere('user.updatedAt > :activeThreshold', { activeThreshold });
+ }));
+
+ const inactiveUserQuery = this.generateUserQueryBuilder(params)
+ .andWhere('user.updatedAt <= :activeThreshold', { activeThreshold });
+
+ return [activeUserQuery, inactiveUserQuery];
+ }
+
+ /**
+ * ユーザ検索クエリで共通する抽出条件をあらかじめ設定したクエリビルダを生成する.
+ * @param params
+ * @private
+ */
+ @bindThis
+ private generateUserQueryBuilder(params: {
+ username?: string | null,
+ host?: string | null,
+ }): SelectQueryBuilder {
+ const userQuery = this.usersRepository.createQueryBuilder('user');
+
+ if (params.username) {
+ userQuery.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(params.username.toLowerCase()) + '%' });
+ }
+
+ if (params.host) {
+ if (params.host === this.config.hostname || params.host === '.') {
+ userQuery.andWhere('user.host IS NULL');
+ } else {
+ userQuery.andWhere('user.host LIKE :host', {
+ host: sqlLikeEscape(params.host.toLowerCase()) + '%',
+ });
+ }
+ }
+
+ userQuery.andWhere('user.isSuspended = FALSE');
+
+ return userQuery;
+ }
+}
diff --git a/packages/backend/src/core/UserWebhookService.ts b/packages/backend/src/core/UserWebhookService.ts
new file mode 100644
index 000000000000..e96bfeea9581
--- /dev/null
+++ b/packages/backend/src/core/UserWebhookService.ts
@@ -0,0 +1,99 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import * as Redis from 'ioredis';
+import type { WebhooksRepository } from '@/models/_.js';
+import type { MiWebhook } from '@/models/Webhook.js';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import { GlobalEvents } from '@/core/GlobalEventService.js';
+import type { OnApplicationShutdown } from '@nestjs/common';
+
+@Injectable()
+export class UserWebhookService implements OnApplicationShutdown {
+ private activeWebhooksFetched = false;
+ private activeWebhooks: MiWebhook[] = [];
+
+ constructor(
+ @Inject(DI.redisForSub)
+ private redisForSub: Redis.Redis,
+ @Inject(DI.webhooksRepository)
+ private webhooksRepository: WebhooksRepository,
+ ) {
+ this.redisForSub.on('message', this.onMessage);
+ }
+
+ @bindThis
+ public async getActiveWebhooks() {
+ if (!this.activeWebhooksFetched) {
+ this.activeWebhooks = await this.webhooksRepository.findBy({
+ active: true,
+ });
+ this.activeWebhooksFetched = true;
+ }
+
+ return this.activeWebhooks;
+ }
+
+ @bindThis
+ private async onMessage(_: string, data: string): Promise {
+ const obj = JSON.parse(data);
+ if (obj.channel !== 'internal') {
+ return;
+ }
+
+ const { type, body } = obj.message as GlobalEvents['internal']['payload'];
+ switch (type) {
+ case 'webhookCreated': {
+ if (body.active) {
+ this.activeWebhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
+ ...body,
+ latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
+ user: null, // joinなカラムは通常取ってこないので
+ });
+ }
+ break;
+ }
+ case 'webhookUpdated': {
+ if (body.active) {
+ const i = this.activeWebhooks.findIndex(a => a.id === body.id);
+ if (i > -1) {
+ this.activeWebhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
+ ...body,
+ latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
+ user: null, // joinなカラムは通常取ってこないので
+ };
+ } else {
+ this.activeWebhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
+ ...body,
+ latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
+ user: null, // joinなカラムは通常取ってこないので
+ });
+ }
+ } else {
+ this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id);
+ }
+ break;
+ }
+ case 'webhookDeleted': {
+ this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id);
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ @bindThis
+ public dispose(): void {
+ this.redisForSub.off('message', this.onMessage);
+ }
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined): void {
+ this.dispose();
+ }
+}
diff --git a/packages/backend/src/core/WebhookService.ts b/packages/backend/src/core/WebhookService.ts
deleted file mode 100644
index 6be34977b064..000000000000
--- a/packages/backend/src/core/WebhookService.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { Inject, Injectable } from '@nestjs/common';
-import * as Redis from 'ioredis';
-import type { WebhooksRepository } from '@/models/_.js';
-import type { MiWebhook } from '@/models/Webhook.js';
-import { DI } from '@/di-symbols.js';
-import { bindThis } from '@/decorators.js';
-import type { GlobalEvents } from '@/core/GlobalEventService.js';
-import type { OnApplicationShutdown } from '@nestjs/common';
-
-@Injectable()
-export class WebhookService implements OnApplicationShutdown {
- private webhooksFetched = false;
- private webhooks: MiWebhook[] = [];
-
- constructor(
- @Inject(DI.redisForSub)
- private redisForSub: Redis.Redis,
-
- @Inject(DI.webhooksRepository)
- private webhooksRepository: WebhooksRepository,
- ) {
- //this.onMessage = this.onMessage.bind(this);
- this.redisForSub.on('message', this.onMessage);
- }
-
- @bindThis
- public async getActiveWebhooks() {
- if (!this.webhooksFetched) {
- this.webhooks = await this.webhooksRepository.findBy({
- active: true,
- });
- this.webhooksFetched = true;
- }
-
- return this.webhooks;
- }
-
- @bindThis
- private async onMessage(_: string, data: string): Promise {
- const obj = JSON.parse(data);
-
- if (obj.channel === 'internal') {
- const { type, body } = obj.message as GlobalEvents['internal']['payload'];
- switch (type) {
- case 'webhookCreated':
- if (body.active) {
- this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
- ...body,
- latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
- user: null, // joinなカラムは通常取ってこないので
- });
- }
- break;
- case 'webhookUpdated':
- if (body.active) {
- const i = this.webhooks.findIndex(a => a.id === body.id);
- if (i > -1) {
- this.webhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
- ...body,
- latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
- user: null, // joinなカラムは通常取ってこないので
- };
- } else {
- this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
- ...body,
- latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
- user: null, // joinなカラムは通常取ってこないので
- });
- }
- } else {
- this.webhooks = this.webhooks.filter(a => a.id !== body.id);
- }
- break;
- case 'webhookDeleted':
- this.webhooks = this.webhooks.filter(a => a.id !== body.id);
- break;
- default:
- break;
- }
- }
- }
-
- @bindThis
- public dispose(): void {
- this.redisForSub.off('message', this.onMessage);
- }
-
- @bindThis
- public onApplicationShutdown(signal?: string | undefined): void {
- this.dispose();
- }
-}
diff --git a/packages/backend/src/core/activitypub/ApAudienceService.ts b/packages/backend/src/core/activitypub/ApAudienceService.ts
index 0fccc7b95086..5a5a76f7d65f 100644
--- a/packages/backend/src/core/activitypub/ApAudienceService.ts
+++ b/packages/backend/src/core/activitypub/ApAudienceService.ts
@@ -8,7 +8,6 @@ import promiseLimit from 'promise-limit';
import type { MiRemoteUser, MiUser } from '@/models/User.js';
import { concat, unique } from '@/misc/prelude/array.js';
import { bindThis } from '@/decorators.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import { getApIds } from './type.js';
import { ApPersonService } from './models/ApPersonService.js';
import type { ApObject } from './type.js';
@@ -41,7 +40,7 @@ export class ApAudienceService {
const limit = promiseLimit(2);
const mentionedUsers = (await Promise.all(
others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))),
- )).filter(isNotNull);
+ )).filter(x => x != null);
if (toGroups.public.length > 0) {
return {
diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts
index 1621c41bcc54..e2164fec1d93 100644
--- a/packages/backend/src/core/activitypub/ApInboxService.ts
+++ b/packages/backend/src/core/activitypub/ApInboxService.ts
@@ -27,7 +27,8 @@ import { QueueService } from '@/core/QueueService.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import type { MiRemoteUser } from '@/models/User.js';
-import { isNotNull } from '@/misc/is-not-null.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
import { ApNoteService } from './models/ApNoteService.js';
import { ApLoggerService } from './ApLoggerService.js';
@@ -36,9 +37,8 @@ import { ApResolverService } from './ApResolverService.js';
import { ApAudienceService } from './ApAudienceService.js';
import { ApPersonService } from './models/ApPersonService.js';
import { ApQuestionService } from './models/ApQuestionService.js';
-import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { Resolver } from './ApResolverService.js';
-import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove } from './type.js';
+import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove, IPost } from './type.js';
@Injectable()
export class ApInboxService {
@@ -57,9 +57,6 @@ export class ApInboxService {
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
- @Inject(DI.abuseUserReportsRepository)
- private abuseUserReportsRepository: AbuseUserReportsRepository,
-
@Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository,
@@ -68,6 +65,7 @@ export class ApInboxService {
private utilityService: UtilityService,
private idService: IdService,
private metaService: MetaService,
+ private abuseReportService: AbuseReportService,
private userFollowingService: UserFollowingService,
private apAudienceService: ApAudienceService,
private reactionService: ReactionService,
@@ -90,13 +88,15 @@ export class ApInboxService {
}
@bindThis
- public async performActivity(actor: MiRemoteUser, activity: IObject): Promise {
+ public async performActivity(actor: MiRemoteUser, activity: IObject): Promise {
+ let result = undefined as string | void;
if (isCollectionOrOrderedCollection(activity)) {
+ const results = [] as [string, string | void][];
const resolver = this.apResolverService.createResolver();
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
const act = await resolver.resolve(item);
try {
- await this.performOneActivity(actor, act);
+ results.push([getApId(item), await this.performOneActivity(actor, act)]);
} catch (err) {
if (err instanceof Error || typeof err === 'string') {
this.logger.error(err);
@@ -105,8 +105,13 @@ export class ApInboxService {
}
}
}
+
+ const hasReason = results.some(([, reason]) => (reason != null && !reason.startsWith('ok')));
+ if (hasReason) {
+ result = results.map(([id, reason]) => `${id}: ${reason}`).join('\n');
+ }
} else {
- await this.performOneActivity(actor, activity);
+ result = await this.performOneActivity(actor, activity);
}
// ついでにリモートユーザーの情報が古かったら更新しておく
@@ -117,42 +122,43 @@ export class ApInboxService {
});
}
}
+ return result;
}
@bindThis
- public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise {
+ public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise {
if (actor.isSuspended) return;
if (isCreate(activity)) {
- await this.create(actor, activity);
+ return await this.create(actor, activity);
} else if (isDelete(activity)) {
- await this.delete(actor, activity);
+ return await this.delete(actor, activity);
} else if (isUpdate(activity)) {
- await this.update(actor, activity);
+ return await this.update(actor, activity);
} else if (isFollow(activity)) {
- await this.follow(actor, activity);
+ return await this.follow(actor, activity);
} else if (isAccept(activity)) {
- await this.accept(actor, activity);
+ return await this.accept(actor, activity);
} else if (isReject(activity)) {
- await this.reject(actor, activity);
+ return await this.reject(actor, activity);
} else if (isAdd(activity)) {
- await this.add(actor, activity).catch(err => this.logger.error(err));
+ return await this.add(actor, activity);
} else if (isRemove(activity)) {
- await this.remove(actor, activity).catch(err => this.logger.error(err));
+ return await this.remove(actor, activity);
} else if (isAnnounce(activity)) {
- await this.announce(actor, activity);
+ return await this.announce(actor, activity);
} else if (isLike(activity)) {
- await this.like(actor, activity);
+ return await this.like(actor, activity);
} else if (isUndo(activity)) {
- await this.undo(actor, activity);
+ return await this.undo(actor, activity);
} else if (isBlock(activity)) {
- await this.block(actor, activity);
+ return await this.block(actor, activity);
} else if (isFlag(activity)) {
- await this.flag(actor, activity);
+ return await this.flag(actor, activity);
} else if (isMove(activity)) {
- await this.move(actor, activity);
+ return await this.move(actor, activity);
} else {
- this.logger.warn(`unrecognized activity type: ${activity.type}`);
+ return `unrecognized activity type: ${activity.type}`;
}
}
@@ -234,38 +240,49 @@ export class ApInboxService {
}
@bindThis
- private async add(actor: MiRemoteUser, activity: IAdd): Promise {
+ private async add(actor: MiRemoteUser, activity: IAdd): Promise {
if (actor.uri !== activity.actor) {
- throw new Error('invalid actor');
+ return 'invalid actor';
}
if (activity.target == null) {
- throw new Error('target is null');
+ return 'target is null';
}
if (activity.target === actor.featured) {
const note = await this.apNoteService.resolveNote(activity.object);
- if (note == null) throw new Error('note not found');
+ if (note == null) return 'note not found';
await this.notePiningService.addPinned(actor, note.id);
return;
}
- throw new Error(`unknown target: ${activity.target}`);
+ return `unknown target: ${activity.target}`;
}
@bindThis
- private async announce(actor: MiRemoteUser, activity: IAnnounce): Promise {
+ private async announce(actor: MiRemoteUser, activity: IAnnounce): Promise {
const uri = getApId(activity);
this.logger.info(`Announce: ${uri}`);
+ const resolver = this.apResolverService.createResolver();
+
+ if (!activity.object) return 'skip: activity has no object property';
const targetUri = getApId(activity.object);
+ if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.';
+
+ const target = await resolver.resolve(activity.object).catch(e => {
+ this.logger.error(`Resolution failed: ${e}`);
+ return e;
+ });
+
+ if (isPost(target)) return await this.announceNote(actor, activity, target);
- await this.announceNote(actor, activity, targetUri);
+ return `skip: unknown object type ${getApType(target)}`;
}
@bindThis
- private async announceNote(actor: MiRemoteUser, activity: IAnnounce, targetUri: string): Promise {
+ private async announceNote(actor: MiRemoteUser, activity: IAnnounce, target: IPost): Promise {
const uri = getApId(activity);
if (actor.isSuspended) {
@@ -288,24 +305,21 @@ export class ApInboxService {
// Announce対象をresolve
let renote;
try {
- renote = await this.apNoteService.resolveNote(targetUri);
- if (renote == null) throw new Error('announce target is null');
+ renote = await this.apNoteService.resolveNote(target);
+ if (renote == null) return 'announce target is null';
} catch (err) {
// 対象が4xxならスキップ
if (err instanceof StatusError) {
if (!err.isRetryable) {
- this.logger.warn(`Ignored announce target ${targetUri} - ${err.statusCode}`);
- return;
+ return `Ignored announce target ${target.id} - ${err.statusCode}`;
}
-
- this.logger.warn(`Error in announce target ${targetUri} - ${err.statusCode}`);
+ return `Error in announce target ${target.id} - ${err.statusCode}`;
}
throw err;
}
if (!await this.noteEntityService.isVisibleForMe(renote, actor.id)) {
- this.logger.warn('skip: invalid actor for this activity');
- return;
+ return 'skip: invalid actor for this activity';
}
this.logger.info(`Creating the (Re)Note: ${uri}`);
@@ -314,8 +328,7 @@ export class ApInboxService {
const createdAt = activity.published ? new Date(activity.published) : null;
if (createdAt && createdAt < this.idService.parse(renote.id).date) {
- this.logger.warn('skip: malformed createdAt');
- return;
+ return 'skip: malformed createdAt';
}
await this.noteCreateService.create(actor, {
@@ -349,11 +362,15 @@ export class ApInboxService {
}
@bindThis
- private async create(actor: MiRemoteUser, activity: ICreate): Promise {
+ private async create(actor: MiRemoteUser, activity: ICreate): Promise {
const uri = getApId(activity);
this.logger.info(`Create: ${uri}`);
+ if (!activity.object) return 'skip: activity has no object property';
+ const targetUri = getApId(activity.object);
+ if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.';
+
// copy audiences between activity <=> object.
if (typeof activity.object === 'object') {
const to = unique(concat([toArray(activity.to), toArray(activity.object.to)]));
@@ -380,7 +397,7 @@ export class ApInboxService {
if (isPost(object)) {
await this.createNote(resolver, actor, object, false, activity);
} else {
- this.logger.warn(`Unknown type: ${getApType(object)}`);
+ return `Unknown type: ${getApType(object)}`;
}
}
@@ -422,7 +439,7 @@ export class ApInboxService {
@bindThis
private async delete(actor: MiRemoteUser, activity: IDelete): Promise {
if (actor.uri !== activity.actor) {
- throw new Error('invalid actor');
+ return 'invalid actor';
}
// 削除対象objectのtype
@@ -520,20 +537,19 @@ export class ApInboxService {
const userIds = uris
.filter(uri => uri.startsWith(this.config.url + '/users/'))
.map(uri => uri.split('/').at(-1))
- .filter(isNotNull);
+ .filter(x => x != null);
const users = await this.usersRepository.findBy({
id: In(userIds),
});
if (users.length < 1) return 'skip';
- await this.abuseUserReportsRepository.insert({
- id: this.idService.gen(),
+ await this.abuseReportService.report([{
targetUserId: users[0].id,
targetUserHost: users[0].host,
reporterId: actor.id,
reporterHost: actor.host,
comment: `${activity.content}\n${JSON.stringify(uris, null, 2)}`,
- });
+ }]);
return 'ok';
}
@@ -581,29 +597,29 @@ export class ApInboxService {
}
@bindThis
- private async remove(actor: MiRemoteUser, activity: IRemove): Promise {
+ private async remove(actor: MiRemoteUser, activity: IRemove): Promise {
if (actor.uri !== activity.actor) {
- throw new Error('invalid actor');
+ return 'invalid actor';
}
if (activity.target == null) {
- throw new Error('target is null');
+ return 'target is null';
}
if (activity.target === actor.featured) {
const note = await this.apNoteService.resolveNote(activity.object);
- if (note == null) throw new Error('note not found');
+ if (note == null) return 'note not found';
await this.notePiningService.removePinned(actor, note.id);
return;
}
- throw new Error(`unknown target: ${activity.target}`);
+ return `unknown target: ${activity.target}`;
}
@bindThis
private async undo(actor: MiRemoteUser, activity: IUndo): Promise {
if (actor.uri !== activity.actor) {
- throw new Error('invalid actor');
+ return 'invalid actor';
}
const uri = activity.id ?? activity;
@@ -614,7 +630,7 @@ export class ApInboxService {
const object = await resolver.resolve(activity.object).catch(e => {
this.logger.error(`Resolution failed: ${e}`);
- throw e;
+ return e;
});
// don't queue because the sender may attempt again when timeout
diff --git a/packages/backend/src/core/activitypub/ApMfmService.ts b/packages/backend/src/core/activitypub/ApMfmService.ts
index ab75b9abbd99..4036d2794a4c 100644
--- a/packages/backend/src/core/activitypub/ApMfmService.ts
+++ b/packages/backend/src/core/activitypub/ApMfmService.ts
@@ -25,7 +25,7 @@ export class ApMfmService {
}
@bindThis
- public getNoteHtml(note: MiNote, apAppend?: string) {
+ public getNoteHtml(note: Pick, apAppend?: string) {
let noMisskeyContent = false;
const srcMfm = (note.text ?? '') + (apAppend ?? '');
diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts
index 4fc724b5480e..98e944f347a5 100644
--- a/packages/backend/src/core/activitypub/ApRendererService.ts
+++ b/packages/backend/src/core/activitypub/ApRendererService.ts
@@ -26,7 +26,6 @@ import type { MiUserKeypair } from '@/models/UserKeypair.js';
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import { IdService } from '@/core/IdService.js';
import { JsonLdService } from './JsonLdService.js';
import { ApMfmService } from './ApMfmService.js';
@@ -317,7 +316,7 @@ export class ApRendererService {
const getPromisedFiles = async (ids: string[]): Promise => {
if (ids.length === 0) return [];
const items = await this.driveFilesRepository.findBy({ id: In(ids) });
- return ids.map(id => items.find(item => item.id === id)).filter(isNotNull);
+ return ids.map(id => items.find(item => item.id === id)).filter(x => x != null);
};
let inReplyTo;
@@ -686,7 +685,7 @@ export class ApRendererService {
if (names.length === 0) return [];
const allEmojis = await this.customEmojiService.localEmojisCache.fetch();
- const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull);
+ const emojis = names.map(name => allEmojis.get(name)).filter(x => x != null);
return emojis;
}
diff --git a/packages/backend/src/core/activitypub/models/ApMentionService.ts b/packages/backend/src/core/activitypub/models/ApMentionService.ts
index 0ced7e88aff2..2cd151fa048f 100644
--- a/packages/backend/src/core/activitypub/models/ApMentionService.ts
+++ b/packages/backend/src/core/activitypub/models/ApMentionService.ts
@@ -8,7 +8,6 @@ import promiseLimit from 'promise-limit';
import type { MiUser } from '@/models/_.js';
import { toArray, unique } from '@/misc/prelude/array.js';
import { bindThis } from '@/decorators.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import { isMention } from '../type.js';
import { Resolver } from '../ApResolverService.js';
import { ApPersonService } from './ApPersonService.js';
@@ -28,7 +27,7 @@ export class ApMentionService {
const limit = promiseLimit(2);
const mentionedUsers = (await Promise.all(
hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))),
- )).filter(isNotNull);
+ )).filter(x => x != null);
return mentionedUsers;
}
diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index 4e361b57bcf5..fc7aa1e0b972 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -24,7 +24,6 @@ import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { checkHttps } from '@/misc/check-https.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
import { ApLoggerService } from '../ApLoggerService.js';
import { ApMfmService } from '../ApMfmService.js';
@@ -81,20 +80,20 @@ export class ApNoteService {
const expectHost = this.utilityService.extractDbHost(uri);
if (!validPost.includes(getApType(object))) {
- return new Error(`invalid Note: invalid object type ${getApType(object)}`);
+ return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: invalid object type ${getApType(object)}`);
}
if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) {
- return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`);
+ return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`);
}
const actualHost = object.attributedTo && this.utilityService.extractDbHost(getOneApId(object.attributedTo));
if (object.attributedTo && actualHost !== expectHost) {
- return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
+ return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
}
if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) {
- return new Error('invalid Note: published timestamp is malformed');
+ return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note: published timestamp is malformed');
}
return null;
@@ -253,7 +252,7 @@ export class ApNoteService {
}
};
- const uris = unique([note._misskey_quote, note.quoteUrl].filter(isNotNull));
+ const uris = unique([note._misskey_quote, note.quoteUrl].filter(x => x != null));
const results = await Promise.all(uris.map(tryResolveNote));
quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);
@@ -407,7 +406,7 @@ export class ApNoteService {
this.logger.info(`register emoji host=${host}, name=${name}`);
- return await this.emojisRepository.insert({
+ return await this.emojisRepository.insertOne({
id: this.idService.gen(),
host,
name,
@@ -416,7 +415,7 @@ export class ApNoteService {
publicUrl: tag.icon.url,
updatedAt: new Date(),
aliases: [],
- }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
+ });
}));
}
}
diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index 744b1ea68376..457205e0238e 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -34,11 +34,11 @@ import { StatusError } from '@/misc/status-error.js';
import type { UtilityService } from '@/core/UtilityService.js';
import type { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
+import { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import type { AccountMoveService } from '@/core/AccountMoveService.js';
import { checkHttps } from '@/misc/check-https.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
import { extractApHashtags } from './tag.js';
import type { OnModuleInit } from '@nestjs/common';
@@ -101,6 +101,8 @@ export class ApPersonService implements OnModuleInit {
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
+
+ private roleService: RoleService,
) {
}
@@ -239,6 +241,11 @@ export class ApPersonService implements OnModuleInit {
return this.apImageService.resolveImage(user, img).catch(() => null);
}));
+ if (((avatar != null && avatar.id != null) || (banner != null && banner.id != null))
+ && !(await this.roleService.getUserPolicies(user.id)).canUpdateBioMedia) {
+ return {};
+ }
+
/*
we don't want to return nulls on errors! if the database fields
are already null, nothing changes; if the database has old
@@ -637,7 +644,7 @@ export class ApPersonService implements OnModuleInit {
// とりあえずidを別の時間で生成して順番を維持
let td = 0;
- for (const note of featuredNotes.filter(isNotNull)) {
+ for (const note of featuredNotes.filter(x => x != null)) {
td -= 1000;
transactionalEntityManager.insert(MiUserNotePining, {
id: this.idService.gen(Date.now() + td),
diff --git a/packages/backend/src/core/activitypub/models/ApQuestionService.ts b/packages/backend/src/core/activitypub/models/ApQuestionService.ts
index d1936cfe1dff..4fae1e897be9 100644
--- a/packages/backend/src/core/activitypub/models/ApQuestionService.ts
+++ b/packages/backend/src/core/activitypub/models/ApQuestionService.ts
@@ -10,7 +10,6 @@ import type { Config } from '@/config.js';
import type { IPoll } from '@/models/Poll.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import { isQuestion } from '../type.js';
import { ApLoggerService } from '../ApLoggerService.js';
import { ApResolverService } from '../ApResolverService.js';
@@ -52,7 +51,7 @@ export class ApQuestionService {
const choices = question[multiple ? 'anyOf' : 'oneOf']
?.map((x) => x.name)
- .filter(isNotNull)
+ .filter(x => x != null)
?? [];
const votes = question[multiple ? 'anyOf' : 'oneOf']?.map((x) => x.replies?.totalItems ?? x._misskey_votes ?? 0);
diff --git a/packages/backend/src/core/activitypub/models/tag.ts b/packages/backend/src/core/activitypub/models/tag.ts
index e7ceec3262a3..f75cc45f7e4f 100644
--- a/packages/backend/src/core/activitypub/models/tag.ts
+++ b/packages/backend/src/core/activitypub/models/tag.ts
@@ -4,7 +4,6 @@
*/
import { toArray } from '@/misc/prelude/array.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import { isHashtag } from '../type.js';
import type { IObject, IApHashtag } from '../type.js';
@@ -16,7 +15,7 @@ export function extractApHashtags(tags: IObject | IObject[] | null | undefined):
return hashtags.map(tag => {
const m = tag.name.match(/^#(.+)/);
return m ? m[1] : null;
- }).filter(isNotNull);
+ }).filter(x => x != null);
}
export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] {
diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts
index 09322888d518..5b6c6c8ca6cb 100644
--- a/packages/backend/src/core/activitypub/type.ts
+++ b/packages/backend/src/core/activitypub/type.ts
@@ -328,3 +328,4 @@ export const isAnnounce = (object: IObject): object is IAnnounce => getApType(ob
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
export const isMove = (object: IObject): object is IMove => getApType(object) === 'Move';
+export const isNote = (object: IObject): object is IPost => getApType(object) === 'Note';
diff --git a/packages/backend/src/core/chart/ChartLoggerService.ts b/packages/backend/src/core/chart/ChartLoggerService.ts
index afc728d56478..20815ea96869 100644
--- a/packages/backend/src/core/chart/ChartLoggerService.ts
+++ b/packages/backend/src/core/chart/ChartLoggerService.ts
@@ -14,6 +14,6 @@ export class ChartLoggerService {
constructor(
private loggerService: LoggerService,
) {
- this.logger = this.loggerService.getLogger('chart', 'white', process.env.NODE_ENV !== 'test');
+ this.logger = this.loggerService.getLogger('chart', 'white');
}
}
diff --git a/packages/backend/src/core/chart/charts/federation.ts b/packages/backend/src/core/chart/charts/federation.ts
index 5e4555ee96f5..c2329a2f7357 100644
--- a/packages/backend/src/core/chart/charts/federation.ts
+++ b/packages/backend/src/core/chart/charts/federation.ts
@@ -47,7 +47,7 @@ export default class FederationChart extends Chart { // eslint-di
const suspendedInstancesQuery = this.instancesRepository.createQueryBuilder('instance')
.select('instance.host')
- .where('instance.isSuspended = true');
+ .where('instance.suspensionState != \'none\'');
const pubsubSubQuery = this.followingsRepository.createQueryBuilder('f')
.select('f.followerHost')
@@ -89,7 +89,7 @@ export default class FederationChart extends Chart { // eslint-di
.select('COUNT(instance.id)')
.where(`instance.host IN (${ subInstancesQuery.getQuery() })`)
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
- .andWhere('instance.isSuspended = false')
+ .andWhere('instance.suspensionState = \'none\'')
.andWhere('instance.isNotResponding = false')
.getRawOne()
.then(x => parseInt(x.count, 10)),
@@ -97,7 +97,7 @@ export default class FederationChart extends Chart { // eslint-di
.select('COUNT(instance.id)')
.where(`instance.host IN (${ pubInstancesQuery.getQuery() })`)
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
- .andWhere('instance.isSuspended = false')
+ .andWhere('instance.suspensionState = \'none\'')
.andWhere('instance.isNotResponding = false')
.getRawOne()
.then(x => parseInt(x.count, 10)),
diff --git a/packages/backend/src/core/chart/core.ts b/packages/backend/src/core/chart/core.ts
index f10e30ef1048..af5485a46ee9 100644
--- a/packages/backend/src/core/chart/core.ts
+++ b/packages/backend/src/core/chart/core.ts
@@ -14,7 +14,8 @@ import { EntitySchema, LessThan, Between } from 'typeorm';
import { dateUTC, isTimeSame, isTimeBefore, subtractTime, addTime } from '@/misc/prelude/time.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
-import type { Repository, DataSource } from 'typeorm';
+import { MiRepository, miRepository } from '@/models/_.js';
+import type { DataSource, Repository } from 'typeorm';
const COLUMN_PREFIX = '___' as const;
const UNIQUE_TEMP_COLUMN_PREFIX = 'unique_temp___' as const;
@@ -145,10 +146,10 @@ export default abstract class Chart {
group: string | null;
}[] = [];
// ↓にしたいけどfindOneとかで型エラーになる
- //private repositoryForHour: Repository>;
- //private repositoryForDay: Repository>;
- private repositoryForHour: Repository<{ id: number; group?: string | null; date: number; }>;
- private repositoryForDay: Repository<{ id: number; group?: string | null; date: number; }>;
+ //private repositoryForHour: Repository> & MiRepository>;
+ //private repositoryForDay: Repository> & MiRepository>;
+ private repositoryForHour: Repository<{ id: number; group?: string | null; date: number; }> & MiRepository<{ id: number; group?: string | null; date: number; }>;
+ private repositoryForDay: Repository<{ id: number; group?: string | null; date: number; }> & MiRepository<{ id: number; group?: string | null; date: number; }>;
/**
* 1日に一回程度実行されれば良いような計算処理を入れる(主にCASCADE削除などアプリケーション側で感知できない変動によるズレの修正用)
@@ -211,6 +212,10 @@ export default abstract class Chart {
} {
const createEntity = (span: 'hour' | 'day'): EntitySchema => new EntitySchema({
name:
+ span === 'hour' ? `ChartX${name}` :
+ span === 'day' ? `ChartDayX${name}` :
+ new Error('not happen') as never,
+ tableName:
span === 'hour' ? `__chart__${camelToSnake(name)}` :
span === 'day' ? `__chart_day__${camelToSnake(name)}` :
new Error('not happen') as never,
@@ -271,8 +276,8 @@ export default abstract class Chart {
this.logger = logger;
const { hour, day } = Chart.schemaToEntity(name, schema, grouped);
- this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour);
- this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day);
+ this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour).extend(miRepository as MiRepository<{ id: number; group?: string | null; date: number; }>);
+ this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day).extend(miRepository as MiRepository<{ id: number; group?: string | null; date: number; }>);
}
@bindThis
@@ -387,11 +392,11 @@ export default abstract class Chart {
}
// 新規ログ挿入
- log = await repository.insert({
+ log = await repository.insertOne({
date: date,
...(group ? { group: group } : {}),
...columns,
- }).then(x => repository.findOneByOrFail(x.identifiers[0])) as RawRecord;
+ }) as RawRecord;
this.logger.info(`${this.name + (group ? `:${group}` : '')}(${span}): New commit created`);
diff --git a/packages/backend/src/core/entities/AbuseReportNotificationRecipientEntityService.ts b/packages/backend/src/core/entities/AbuseReportNotificationRecipientEntityService.ts
new file mode 100644
index 000000000000..1e23c194c58f
--- /dev/null
+++ b/packages/backend/src/core/entities/AbuseReportNotificationRecipientEntityService.ts
@@ -0,0 +1,87 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { In } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import type { AbuseReportNotificationRecipientRepository, MiAbuseReportNotificationRecipient } from '@/models/_.js';
+import { bindThis } from '@/decorators.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { Packed } from '@/misc/json-schema.js';
+import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
+
+@Injectable()
+export class AbuseReportNotificationRecipientEntityService {
+ constructor(
+ @Inject(DI.abuseReportNotificationRecipientRepository)
+ private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository,
+ private userEntityService: UserEntityService,
+ private systemWebhookEntityService: SystemWebhookEntityService,
+ ) {
+ }
+
+ @bindThis
+ public async pack(
+ src: MiAbuseReportNotificationRecipient['id'] | MiAbuseReportNotificationRecipient,
+ opts?: {
+ users: Map>,
+ webhooks: Map>,
+ },
+ ): Promise> {
+ const recipient = typeof src === 'object'
+ ? src
+ : await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: src });
+ const user = recipient.userId
+ ? (opts?.users.get(recipient.userId) ?? await this.userEntityService.pack<'UserLite'>(recipient.userId))
+ : undefined;
+ const webhook = recipient.systemWebhookId
+ ? (opts?.webhooks.get(recipient.systemWebhookId) ?? await this.systemWebhookEntityService.pack(recipient.systemWebhookId))
+ : undefined;
+
+ return {
+ id: recipient.id,
+ isActive: recipient.isActive,
+ updatedAt: recipient.updatedAt.toISOString(),
+ name: recipient.name,
+ method: recipient.method,
+ userId: recipient.userId ?? undefined,
+ user: user,
+ systemWebhookId: recipient.systemWebhookId ?? undefined,
+ systemWebhook: webhook,
+ };
+ }
+
+ @bindThis
+ public async packMany(
+ src: MiAbuseReportNotificationRecipient['id'][] | MiAbuseReportNotificationRecipient[],
+ ): Promise[]> {
+ const objs = src.filter((it): it is MiAbuseReportNotificationRecipient => typeof it === 'object');
+ const ids = src.filter((it): it is MiAbuseReportNotificationRecipient['id'] => typeof it === 'string');
+ if (ids.length > 0) {
+ objs.push(
+ ...await this.abuseReportNotificationRecipientRepository.findBy({ id: In(ids) }),
+ );
+ }
+
+ const userIds = objs.map(it => it.userId).filter(x => x != null);
+ const users: Map> = (userIds.length > 0)
+ ? await this.userEntityService.packMany(userIds)
+ .then(it => new Map(it.map(it => [it.id, it])))
+ : new Map();
+
+ const systemWebhookIds = objs.map(it => it.systemWebhookId).filter(x => x != null);
+ const systemWebhooks: Map> = (systemWebhookIds.length > 0)
+ ? await this.systemWebhookEntityService.packMany(systemWebhookIds)
+ .then(it => new Map(it.map(it => [it.id, it])))
+ : new Map();
+
+ return Promise
+ .all(
+ objs.map(it => this.pack(it, { users: users, webhooks: systemWebhooks })),
+ )
+ .then(it => it.sort((a, b) => a.id.localeCompare(b.id)));
+ }
+}
+
diff --git a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
index 49f256d870df..a13c244c19a1 100644
--- a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
+++ b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
@@ -10,6 +10,7 @@ import { awaitAll } from '@/misc/prelude/await-all.js';
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
+import type { Packed } from '@/misc/json-schema.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
@@ -26,6 +27,11 @@ export class AbuseUserReportEntityService {
@bindThis
public async pack(
src: MiAbuseUserReport['id'] | MiAbuseUserReport,
+ hint?: {
+ packedReporter?: Packed<'UserDetailedNotMe'>,
+ packedTargetUser?: Packed<'UserDetailedNotMe'>,
+ packedAssignee?: Packed<'UserDetailedNotMe'>,
+ },
) {
const report = typeof src === 'object' ? src : await this.abuseUserReportsRepository.findOneByOrFail({ id: src });
@@ -37,13 +43,13 @@ export class AbuseUserReportEntityService {
reporterId: report.reporterId,
targetUserId: report.targetUserId,
assigneeId: report.assigneeId,
- reporter: this.userEntityService.pack(report.reporter ?? report.reporterId, null, {
+ reporter: hint?.packedReporter ?? this.userEntityService.pack(report.reporter ?? report.reporterId, null, {
schema: 'UserDetailedNotMe',
}),
- targetUser: this.userEntityService.pack(report.targetUser ?? report.targetUserId, null, {
+ targetUser: hint?.packedTargetUser ?? this.userEntityService.pack(report.targetUser ?? report.targetUserId, null, {
schema: 'UserDetailedNotMe',
}),
- assignee: report.assigneeId ? this.userEntityService.pack(report.assignee ?? report.assigneeId, null, {
+ assignee: report.assigneeId ? hint?.packedAssignee ?? this.userEntityService.pack(report.assignee ?? report.assigneeId, null, {
schema: 'UserDetailedNotMe',
}) : null,
forwarded: report.forwarded,
@@ -51,9 +57,24 @@ export class AbuseUserReportEntityService {
}
@bindThis
- public packMany(
- reports: any[],
+ public async packMany(
+ reports: MiAbuseUserReport[],
) {
- return Promise.all(reports.map(x => this.pack(x)));
+ const _reporters = reports.map(({ reporter, reporterId }) => reporter ?? reporterId);
+ const _targetUsers = reports.map(({ targetUser, targetUserId }) => targetUser ?? targetUserId);
+ const _assignees = reports.map(({ assignee, assigneeId }) => assignee ?? assigneeId).filter(x => x != null);
+ const _userMap = await this.userEntityService.packMany(
+ [..._reporters, ..._targetUsers, ..._assignees],
+ null,
+ { schema: 'UserDetailedNotMe' },
+ ).then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(
+ reports.map(report => {
+ const packedReporter = _userMap.get(report.reporterId);
+ const packedTargetUser = _userMap.get(report.targetUserId);
+ const packedAssignee = report.assigneeId != null ? _userMap.get(report.assigneeId) : undefined;
+ return this.pack(report, { packedReporter, packedTargetUser, packedAssignee });
+ }),
+ );
}
}
diff --git a/packages/backend/src/core/entities/AnnouncementEntityService.ts b/packages/backend/src/core/entities/AnnouncementEntityService.ts
new file mode 100644
index 000000000000..90b04d0229a6
--- /dev/null
+++ b/packages/backend/src/core/entities/AnnouncementEntityService.ts
@@ -0,0 +1,71 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { DI } from '@/di-symbols.js';
+import type { AnnouncementsRepository, AnnouncementReadsRepository, MiAnnouncement, MiUser } from '@/models/_.js';
+import type { Packed } from '@/misc/json-schema.js';
+import { bindThis } from '@/decorators.js';
+import { IdService } from '@/core/IdService.js';
+
+@Injectable()
+export class AnnouncementEntityService {
+ constructor(
+ @Inject(DI.announcementsRepository)
+ private announcementsRepository: AnnouncementsRepository,
+
+ @Inject(DI.announcementReadsRepository)
+ private announcementReadsRepository: AnnouncementReadsRepository,
+
+ private idService: IdService,
+ ) {
+ }
+
+ @bindThis
+ public async pack(
+ src: MiAnnouncement['id'] | MiAnnouncement & { isRead?: boolean | null },
+ me?: { id: MiUser['id'] } | null | undefined,
+ ): Promise> {
+ const announcement = typeof src === 'object'
+ ? src
+ : await this.announcementsRepository.findOneByOrFail({
+ id: src,
+ }) as MiAnnouncement & { isRead?: boolean | null };
+
+ if (me && announcement.isRead === undefined) {
+ announcement.isRead = await this.announcementReadsRepository
+ .countBy({
+ announcementId: announcement.id,
+ userId: me.id,
+ })
+ .then((count: number) => count > 0);
+ }
+
+ return {
+ id: announcement.id,
+ createdAt: this.idService.parse(announcement.id).date.toISOString(),
+ updatedAt: announcement.updatedAt?.toISOString() ?? null,
+ title: announcement.title,
+ text: announcement.text,
+ imageUrl: announcement.imageUrl,
+ icon: announcement.icon,
+ display: announcement.display,
+ forYou: announcement.userId === me?.id,
+ needConfirmationToRead: announcement.needConfirmationToRead,
+ silence: announcement.silence,
+ isRead: announcement.isRead !== null ? announcement.isRead : undefined,
+ };
+ }
+
+ @bindThis
+ public async packMany(
+ announcements: (MiAnnouncement['id'] | MiAnnouncement & { isRead?: boolean | null } | MiAnnouncement)[],
+ me?: { id: MiUser['id'] } | null | undefined,
+ ) : Promise[]> {
+ return (await Promise.allSettled(announcements.map(x => this.pack(x, me))))
+ .filter(result => result.status === 'fulfilled')
+ .map(result => (result as PromiseFulfilledResult>).value);
+ }
+}
diff --git a/packages/backend/src/core/entities/AntennaEntityService.ts b/packages/backend/src/core/entities/AntennaEntityService.ts
index 3ec8efa6bfd0..e770028af3e9 100644
--- a/packages/backend/src/core/entities/AntennaEntityService.ts
+++ b/packages/backend/src/core/entities/AntennaEntityService.ts
@@ -38,12 +38,12 @@ export class AntennaEntityService {
users: antenna.users,
caseSensitive: antenna.caseSensitive,
localOnly: antenna.localOnly,
- notify: antenna.notify,
excludeBots: antenna.excludeBots,
withReplies: antenna.withReplies,
withFile: antenna.withFile,
isActive: antenna.isActive,
hasUnreadNote: false, // TODO
+ notify: false, // 後方互換性のため
};
}
}
diff --git a/packages/backend/src/core/entities/BlockingEntityService.ts b/packages/backend/src/core/entities/BlockingEntityService.ts
index c8c1520ceb50..1e699032e234 100644
--- a/packages/backend/src/core/entities/BlockingEntityService.ts
+++ b/packages/backend/src/core/entities/BlockingEntityService.ts
@@ -29,6 +29,9 @@ export class BlockingEntityService {
public async pack(
src: MiBlocking['id'] | MiBlocking,
me?: { id: MiUser['id'] } | null | undefined,
+ hint?: {
+ blockee?: Packed<'UserDetailedNotMe'>,
+ },
): Promise> {
const blocking = typeof src === 'object' ? src : await this.blockingsRepository.findOneByOrFail({ id: src });
@@ -36,17 +39,20 @@ export class BlockingEntityService {
id: blocking.id,
createdAt: this.idService.parse(blocking.id).date.toISOString(),
blockeeId: blocking.blockeeId,
- blockee: this.userEntityService.pack(blocking.blockeeId, me, {
+ blockee: hint?.blockee ?? this.userEntityService.pack(blocking.blockeeId, me, {
schema: 'UserDetailedNotMe',
}),
});
}
@bindThis
- public packMany(
- blockings: any[],
+ public async packMany(
+ blockings: MiBlocking[],
me: { id: MiUser['id'] },
) {
- return Promise.all(blockings.map(x => this.pack(x, me)));
+ const _blockees = blockings.map(({ blockee, blockeeId }) => blockee ?? blockeeId);
+ const _userMap = await this.userEntityService.packMany(_blockees, me, { schema: 'UserDetailedNotMe' })
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(blockings.map(blocking => this.pack(blocking, me, { blockee: _userMap.get(blocking.blockeeId) })));
}
}
diff --git a/packages/backend/src/core/entities/ClipEntityService.ts b/packages/backend/src/core/entities/ClipEntityService.ts
index ce49c3458c54..d91564590692 100644
--- a/packages/backend/src/core/entities/ClipEntityService.ts
+++ b/packages/backend/src/core/entities/ClipEntityService.ts
@@ -35,6 +35,9 @@ export class ClipEntityService {
public async pack(
src: MiClip['id'] | MiClip,
me?: { id: MiUser['id'] } | null | undefined,
+ hint?: {
+ packedUser?: Packed<'UserLite'>
+ },
): Promise> {
const meId = me ? me.id : null;
const clip = typeof src === 'object' ? src : await this.clipsRepository.findOneByOrFail({ id: src });
@@ -44,22 +47,25 @@ export class ClipEntityService {
createdAt: this.idService.parse(clip.id).date.toISOString(),
lastClippedAt: clip.lastClippedAt ? clip.lastClippedAt.toISOString() : null,
userId: clip.userId,
- user: this.userEntityService.pack(clip.user ?? clip.userId),
+ user: hint?.packedUser ?? this.userEntityService.pack(clip.user ?? clip.userId),
name: clip.name,
description: clip.description,
isPublic: clip.isPublic,
favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
isFavorited: meId ? await this.clipFavoritesRepository.exists({ where: { clipId: clip.id, userId: meId } }) : undefined,
- notesCount: meId ? await this.clipNotesRepository.countBy({ clipId: clip.id }) : undefined,
+ notesCount: (meId === clip.userId) ? await this.clipNotesRepository.countBy({ clipId: clip.id }) : undefined,
});
}
@bindThis
- public packMany(
+ public async packMany(
clips: MiClip[],
me?: { id: MiUser['id'] } | null | undefined,
) {
- return Promise.all(clips.map(x => this.pack(x, me)));
+ const _users = clips.map(({ user, userId }) => user ?? userId);
+ const _userMap = await this.userEntityService.packMany(_users, me)
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(clips.map(clip => this.pack(clip, me, { packedUser: _userMap.get(clip.userId) })));
}
}
diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts
index 26bf386cbc4b..c485555f90f9 100644
--- a/packages/backend/src/core/entities/DriveFileEntityService.ts
+++ b/packages/backend/src/core/entities/DriveFileEntityService.ts
@@ -16,7 +16,6 @@ import { appendQuery, query } from '@/misc/prelude/url.js';
import { deepClone } from '@/misc/clone.js';
import { bindThis } from '@/decorators.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import { IdService } from '@/core/IdService.js';
import { UtilityService } from '../UtilityService.js';
import { VideoProcessingService } from '../VideoProcessingService.js';
@@ -222,6 +221,9 @@ export class DriveFileEntityService {
public async packNullable(
src: MiDriveFile['id'] | MiDriveFile,
options?: PackOptions,
+ hint?: {
+ packedUser?: Packed<'UserLite'>
+ },
): Promise | null> {
const opts = Object.assign({
detail: false,
@@ -249,7 +251,7 @@ export class DriveFileEntityService {
detail: true,
}) : null,
userId: file.userId,
- user: (opts.withUser && file.userId) ? this.userEntityService.pack(file.userId) : null,
+ user: (opts.withUser && file.userId) ? hint?.packedUser ?? this.userEntityService.pack(file.userId) : null,
});
}
@@ -258,8 +260,11 @@ export class DriveFileEntityService {
files: MiDriveFile[],
options?: PackOptions,
): Promise[]> {
- const items = await Promise.all(files.map(f => this.packNullable(f, options)));
- return items.filter(isNotNull);
+ const _user = files.map(({ user, userId }) => user ?? userId).filter(x => x != null);
+ const _userMap = await this.userEntityService.packMany(_user)
+ .then(users => new Map(users.map(user => [user.id, user])));
+ const items = await Promise.all(files.map(f => this.packNullable(f, options, f.userId ? { packedUser: _userMap.get(f.userId) } : {})));
+ return items.filter(x => x != null);
}
@bindThis
@@ -284,6 +289,6 @@ export class DriveFileEntityService {
): Promise[]> {
if (fileIds.length === 0) return [];
const filesMap = await this.packManyByIdsMap(fileIds, options);
- return fileIds.map(id => filesMap.get(id)).filter(isNotNull);
+ return fileIds.map(id => filesMap.get(id)).filter(x => x != null);
}
}
diff --git a/packages/backend/src/core/entities/FlashEntityService.ts b/packages/backend/src/core/entities/FlashEntityService.ts
index db4cf6d360d6..d110f7afc632 100644
--- a/packages/backend/src/core/entities/FlashEntityService.ts
+++ b/packages/backend/src/core/entities/FlashEntityService.ts
@@ -33,6 +33,9 @@ export class FlashEntityService {
public async pack(
src: MiFlash['id'] | MiFlash,
me?: { id: MiUser['id'] } | null | undefined,
+ hint?: {
+ packedUser?: Packed<'UserLite'>
+ },
): Promise> {
const meId = me ? me.id : null;
const flash = typeof src === 'object' ? src : await this.flashsRepository.findOneByOrFail({ id: src });
@@ -42,7 +45,7 @@ export class FlashEntityService {
createdAt: this.idService.parse(flash.id).date.toISOString(),
updatedAt: flash.updatedAt.toISOString(),
userId: flash.userId,
- user: this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
+ user: hint?.packedUser ?? this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
title: flash.title,
summary: flash.summary,
script: flash.script,
@@ -52,11 +55,14 @@ export class FlashEntityService {
}
@bindThis
- public packMany(
- flashs: MiFlash[],
+ public async packMany(
+ flashes: MiFlash[],
me?: { id: MiUser['id'] } | null | undefined,
) {
- return Promise.all(flashs.map(x => this.pack(x, me)));
+ const _users = flashes.map(({ user, userId }) => user ?? userId);
+ const _userMap = await this.userEntityService.packMany(_users, me)
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(flashes.map(flash => this.pack(flash, me, { packedUser: _userMap.get(flash.userId) })));
}
}
diff --git a/packages/backend/src/core/entities/FollowRequestEntityService.ts b/packages/backend/src/core/entities/FollowRequestEntityService.ts
index 763b75101faa..0101ec8aa735 100644
--- a/packages/backend/src/core/entities/FollowRequestEntityService.ts
+++ b/packages/backend/src/core/entities/FollowRequestEntityService.ts
@@ -10,6 +10,7 @@ import type { } from '@/models/Blocking.js';
import type { MiUser } from '@/models/User.js';
import type { MiFollowRequest } from '@/models/FollowRequest.js';
import { bindThis } from '@/decorators.js';
+import type { Packed } from '@/misc/json-schema.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
@@ -26,14 +27,36 @@ export class FollowRequestEntityService {
public async pack(
src: MiFollowRequest['id'] | MiFollowRequest,
me?: { id: MiUser['id'] } | null | undefined,
+ hint?: {
+ packedFollower?: Packed<'UserLite'>,
+ packedFollowee?: Packed<'UserLite'>,
+ },
) {
const request = typeof src === 'object' ? src : await this.followRequestsRepository.findOneByOrFail({ id: src });
return {
id: request.id,
- follower: await this.userEntityService.pack(request.followerId, me),
- followee: await this.userEntityService.pack(request.followeeId, me),
+ follower: hint?.packedFollower ?? await this.userEntityService.pack(request.followerId, me),
+ followee: hint?.packedFollowee ?? await this.userEntityService.pack(request.followeeId, me),
};
}
+
+ @bindThis
+ public async packMany(
+ requests: MiFollowRequest[],
+ me?: { id: MiUser['id'] } | null | undefined,
+ ) {
+ const _followers = requests.map(({ follower, followerId }) => follower ?? followerId);
+ const _followees = requests.map(({ followee, followeeId }) => followee ?? followeeId);
+ const _userMap = await this.userEntityService.packMany([..._followers, ..._followees], me)
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(
+ requests.map(req => {
+ const packedFollower = _userMap.get(req.followerId);
+ const packedFollowee = _userMap.get(req.followeeId);
+ return this.pack(req, me, { packedFollower, packedFollowee });
+ }),
+ );
+ }
}
diff --git a/packages/backend/src/core/entities/FollowingEntityService.ts b/packages/backend/src/core/entities/FollowingEntityService.ts
index 24cd33e3f7d4..d2dbaf227030 100644
--- a/packages/backend/src/core/entities/FollowingEntityService.ts
+++ b/packages/backend/src/core/entities/FollowingEntityService.ts
@@ -78,6 +78,10 @@ export class FollowingEntityService {
populateFollowee?: boolean;
populateFollower?: boolean;
},
+ hint?: {
+ packedFollowee?: Packed<'UserDetailedNotMe'>,
+ packedFollower?: Packed<'UserDetailedNotMe'>,
+ },
): Promise> {
const following = typeof src === 'object' ? src : await this.followingsRepository.findOneByOrFail({ id: src });
@@ -88,25 +92,35 @@ export class FollowingEntityService {
createdAt: this.idService.parse(following.id).date.toISOString(),
followeeId: following.followeeId,
followerId: following.followerId,
- followee: opts.populateFollowee ? this.userEntityService.pack(following.followee ?? following.followeeId, me, {
+ followee: opts.populateFollowee ? hint?.packedFollowee ?? this.userEntityService.pack(following.followee ?? following.followeeId, me, {
schema: 'UserDetailedNotMe',
}) : undefined,
- follower: opts.populateFollower ? this.userEntityService.pack(following.follower ?? following.followerId, me, {
+ follower: opts.populateFollower ? hint?.packedFollower ?? this.userEntityService.pack(following.follower ?? following.followerId, me, {
schema: 'UserDetailedNotMe',
}) : undefined,
});
}
@bindThis
- public packMany(
- followings: any[],
+ public async packMany(
+ followings: MiFollowing[],
me?: { id: MiUser['id'] } | null | undefined,
opts?: {
populateFollowee?: boolean;
populateFollower?: boolean;
},
) {
- return Promise.all(followings.map(x => this.pack(x, me, opts)));
+ const _followees = opts?.populateFollowee ? followings.map(({ followee, followeeId }) => followee ?? followeeId) : [];
+ const _followers = opts?.populateFollower ? followings.map(({ follower, followerId }) => follower ?? followerId) : [];
+ const _userMap = await this.userEntityService.packMany([..._followees, ..._followers], me, { schema: 'UserDetailedNotMe' })
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(
+ followings.map(following => {
+ const packedFollowee = opts?.populateFollowee ? _userMap.get(following.followeeId) : undefined;
+ const packedFollower = opts?.populateFollower ? _userMap.get(following.followerId) : undefined;
+ return this.pack(following, me, opts, { packedFollowee, packedFollower });
+ }),
+ );
}
}
diff --git a/packages/backend/src/core/entities/GalleryPostEntityService.ts b/packages/backend/src/core/entities/GalleryPostEntityService.ts
index 101182a9e5aa..9746a4c1af3a 100644
--- a/packages/backend/src/core/entities/GalleryPostEntityService.ts
+++ b/packages/backend/src/core/entities/GalleryPostEntityService.ts
@@ -35,6 +35,9 @@ export class GalleryPostEntityService {
public async pack(
src: MiGalleryPost['id'] | MiGalleryPost,
me?: { id: MiUser['id'] } | null | undefined,
+ hint?: {
+ packedUser?: Packed<'UserLite'>
+ },
): Promise> {
const meId = me ? me.id : null;
const post = typeof src === 'object' ? src : await this.galleryPostsRepository.findOneByOrFail({ id: src });
@@ -44,7 +47,7 @@ export class GalleryPostEntityService {
createdAt: this.idService.parse(post.id).date.toISOString(),
updatedAt: post.updatedAt.toISOString(),
userId: post.userId,
- user: this.userEntityService.pack(post.user ?? post.userId, me),
+ user: hint?.packedUser ?? this.userEntityService.pack(post.user ?? post.userId, me),
title: post.title,
description: post.description,
fileIds: post.fileIds,
@@ -58,11 +61,14 @@ export class GalleryPostEntityService {
}
@bindThis
- public packMany(
+ public async packMany(
posts: MiGalleryPost[],
me?: { id: MiUser['id'] } | null | undefined,
) {
- return Promise.all(posts.map(x => this.pack(x, me)));
+ const _users = posts.map(({ user, userId }) => user ?? userId);
+ const _userMap = await this.userEntityService.packMany(_users, me)
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(posts.map(post => this.pack(post, me, { packedUser: _userMap.get(post.userId) })));
}
}
diff --git a/packages/backend/src/core/entities/InviteCodeEntityService.ts b/packages/backend/src/core/entities/InviteCodeEntityService.ts
index 891543bc0fec..5d3e823a2a9d 100644
--- a/packages/backend/src/core/entities/InviteCodeEntityService.ts
+++ b/packages/backend/src/core/entities/InviteCodeEntityService.ts
@@ -29,6 +29,10 @@ export class InviteCodeEntityService {
public async pack(
src: MiRegistrationTicket['id'] | MiRegistrationTicket,
me?: { id: MiUser['id'] } | null | undefined,
+ hints?: {
+ packedCreatedBy?: Packed<'UserLite'>,
+ packedUsedBy?: Packed<'UserLite'>,
+ },
): Promise> {
const target = typeof src === 'object' ? src : await this.registrationTicketsRepository.findOneOrFail({
where: {
@@ -42,18 +46,28 @@ export class InviteCodeEntityService {
code: target.code,
expiresAt: target.expiresAt ? target.expiresAt.toISOString() : null,
createdAt: this.idService.parse(target.id).date.toISOString(),
- createdBy: target.createdBy ? await this.userEntityService.pack(target.createdBy, me) : null,
- usedBy: target.usedBy ? await this.userEntityService.pack(target.usedBy, me) : null,
+ createdBy: target.createdBy ? hints?.packedCreatedBy ?? await this.userEntityService.pack(target.createdBy, me) : null,
+ usedBy: target.usedBy ? hints?.packedUsedBy ?? await this.userEntityService.pack(target.usedBy, me) : null,
usedAt: target.usedAt ? target.usedAt.toISOString() : null,
used: !!target.usedAt,
});
}
@bindThis
- public packMany(
- targets: any[],
+ public async packMany(
+ tickets: MiRegistrationTicket[],
me: { id: MiUser['id'] },
) {
- return Promise.all(targets.map(x => this.pack(x, me)));
+ const _createdBys = tickets.map(({ createdBy, createdById }) => createdBy ?? createdById).filter(x => x != null);
+ const _usedBys = tickets.map(({ usedBy, usedById }) => usedBy ?? usedById).filter(x => x != null);
+ const _userMap = await this.userEntityService.packMany([..._createdBys, ..._usedBys], me)
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(
+ tickets.map(ticket => {
+ const packedCreatedBy = ticket.createdById != null ? _userMap.get(ticket.createdById) : undefined;
+ const packedUsedBy = ticket.usedById != null ? _userMap.get(ticket.usedById) : undefined;
+ return this.pack(ticket, me, { packedCreatedBy, packedUsedBy });
+ }),
+ );
}
}
diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts
index 9d054ab6a156..09641ce48552 100644
--- a/packages/backend/src/core/entities/MetaEntityService.ts
+++ b/packages/backend/src/core/entities/MetaEntityService.ts
@@ -50,6 +50,22 @@ export class MetaEntityService {
}))
.getMany();
+ // クライアントの手間を減らすためあらかじめJSONに変換しておく
+ let defaultLightTheme = null;
+ let defaultDarkTheme = null;
+ if (instance.defaultLightTheme) {
+ try {
+ defaultLightTheme = JSON.stringify(JSON5.parse(instance.defaultLightTheme));
+ } catch (e) {
+ }
+ }
+ if (instance.defaultDarkTheme) {
+ try {
+ defaultDarkTheme = JSON.stringify(JSON5.parse(instance.defaultDarkTheme));
+ } catch (e) {
+ }
+ }
+
const packed: Packed<'MetaLite'> = {
maintainerName: instance.maintainerName,
maintainerEmail: instance.maintainerEmail,
@@ -67,6 +83,7 @@ export class MetaEntityService {
feedbackUrl: instance.feedbackUrl,
impressumUrl: instance.impressumUrl,
privacyPolicyUrl: instance.privacyPolicyUrl,
+ inquiryUrl: instance.inquiryUrl,
disableRegistration: instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha,
@@ -89,9 +106,8 @@ export class MetaEntityService {
backgroundImageUrl: instance.backgroundImageUrl,
logoImageUrl: instance.logoImageUrl,
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
- // クライアントの手間を減らすためあらかじめJSONに変換しておく
- defaultLightTheme: instance.defaultLightTheme ? JSON.stringify(JSON5.parse(instance.defaultLightTheme)) : null,
- defaultDarkTheme: instance.defaultDarkTheme ? JSON.stringify(JSON5.parse(instance.defaultDarkTheme)) : null,
+ defaultLightTheme,
+ defaultDarkTheme,
ads: ads.map(ad => ({
id: ad.id,
url: ad.url,
diff --git a/packages/backend/src/core/entities/ModerationLogEntityService.ts b/packages/backend/src/core/entities/ModerationLogEntityService.ts
index 205e147bd106..bf1b2a002cee 100644
--- a/packages/backend/src/core/entities/ModerationLogEntityService.ts
+++ b/packages/backend/src/core/entities/ModerationLogEntityService.ts
@@ -8,9 +8,10 @@ import { DI } from '@/di-symbols.js';
import type { ModerationLogsRepository } from '@/models/_.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { } from '@/models/Blocking.js';
-import type { MiModerationLog } from '@/models/ModerationLog.js';
+import { MiModerationLog } from '@/models/ModerationLog.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
+import type { Packed } from '@/misc/json-schema.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
@@ -27,6 +28,9 @@ export class ModerationLogEntityService {
@bindThis
public async pack(
src: MiModerationLog['id'] | MiModerationLog,
+ hint?: {
+ packedUser?: Packed<'UserDetailedNotMe'>,
+ },
) {
const log = typeof src === 'object' ? src : await this.moderationLogsRepository.findOneByOrFail({ id: src });
@@ -36,17 +40,20 @@ export class ModerationLogEntityService {
type: log.type,
info: log.info,
userId: log.userId,
- user: this.userEntityService.pack(log.user ?? log.userId, null, {
+ user: hint?.packedUser ?? this.userEntityService.pack(log.user ?? log.userId, null, {
schema: 'UserDetailedNotMe',
}),
});
}
@bindThis
- public packMany(
- reports: any[],
+ public async packMany(
+ reports: MiModerationLog[],
) {
- return Promise.all(reports.map(x => this.pack(x)));
+ const _users = reports.map(({ user, userId }) => user ?? userId);
+ const _userMap = await this.userEntityService.packMany(_users, null, { schema: 'UserDetailedNotMe' })
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(reports.map(report => this.pack(report, { packedUser: _userMap.get(report.userId) })));
}
}
diff --git a/packages/backend/src/core/entities/MutingEntityService.ts b/packages/backend/src/core/entities/MutingEntityService.ts
index 0a52f429a2b2..d361a20271b6 100644
--- a/packages/backend/src/core/entities/MutingEntityService.ts
+++ b/packages/backend/src/core/entities/MutingEntityService.ts
@@ -30,6 +30,9 @@ export class MutingEntityService {
public async pack(
src: MiMuting['id'] | MiMuting,
me?: { id: MiUser['id'] } | null | undefined,
+ hints?: {
+ packedMutee?: Packed<'UserDetailedNotMe'>,
+ },
): Promise> {
const muting = typeof src === 'object' ? src : await this.mutingsRepository.findOneByOrFail({ id: src });
@@ -38,18 +41,21 @@ export class MutingEntityService {
createdAt: this.idService.parse(muting.id).date.toISOString(),
expiresAt: muting.expiresAt ? muting.expiresAt.toISOString() : null,
muteeId: muting.muteeId,
- mutee: this.userEntityService.pack(muting.muteeId, me, {
+ mutee: hints?.packedMutee ?? this.userEntityService.pack(muting.muteeId, me, {
schema: 'UserDetailedNotMe',
}),
});
}
@bindThis
- public packMany(
- mutings: any[],
+ public async packMany(
+ mutings: MiMuting[],
me: { id: MiUser['id'] },
) {
- return Promise.all(mutings.map(x => this.pack(x, me)));
+ const _mutees = mutings.map(({ mutee, muteeId }) => mutee ?? muteeId);
+ const _userMap = await this.userEntityService.packMany(_mutees, me, { schema: 'UserDetailedNotMe' })
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(mutings.map(muting => this.pack(muting, me, { packedMutee: _userMap.get(muting.muteeId) })));
}
}
diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts
index 22d01462e773..2cd092231cf5 100644
--- a/packages/backend/src/core/entities/NoteEntityService.ts
+++ b/packages/backend/src/core/entities/NoteEntityService.ts
@@ -14,7 +14,6 @@ import type { MiNote } from '@/models/Note.js';
import type { MiNoteReaction } from '@/models/NoteReaction.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import { DebounceLoader } from '@/misc/loader.js';
import { IdService } from '@/core/IdService.js';
import type { OnModuleInit } from '@nestjs/common';
@@ -276,7 +275,7 @@ export class NoteEntityService implements OnModuleInit {
packedFiles.set(k, v);
}
}
- return fileIds.map(id => packedFiles.get(id)).filter(isNotNull);
+ return fileIds.map(id => packedFiles.get(id)).filter(x => x != null);
}
@bindThis
@@ -290,6 +289,7 @@ export class NoteEntityService implements OnModuleInit {
_hint_?: {
myReactions: Map;
packedFiles: Map | null>;
+ packedUsers: Map>
};
},
): Promise> {
@@ -319,12 +319,13 @@ export class NoteEntityService implements OnModuleInit {
.filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ
.map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', ''));
const packedFiles = options?._hint_?.packedFiles;
+ const packedUsers = options?._hint_?.packedUsers;
const packed: Packed<'Note'> = await awaitAll({
id: note.id,
createdAt: this.idService.parse(note.id).date.toISOString(),
userId: note.userId,
- user: this.userEntityService.pack(note.user ?? note.userId, me),
+ user: packedUsers?.get(note.userId) ?? this.userEntityService.pack(note.user ?? note.userId, me),
text: text,
cw: note.cw,
visibility: note.visibility,
@@ -447,14 +448,22 @@ export class NoteEntityService implements OnModuleInit {
await this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis(notes));
// TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく
- const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(isNotNull);
+ const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(x => x != null);
const packedFiles = fileIds.length > 0 ? await this.driveFileEntityService.packManyByIdsMap(fileIds) : new Map();
+ const users = [
+ ...notes.map(({ user, userId }) => user ?? userId),
+ ...notes.map(({ replyUserId }) => replyUserId).filter(x => x != null),
+ ...notes.map(({ renoteUserId }) => renoteUserId).filter(x => x != null),
+ ];
+ const packedUsers = await this.userEntityService.packMany(users, me)
+ .then(users => new Map(users.map(u => [u.id, u])));
return await Promise.all(notes.map(n => this.pack(n, me, {
...options,
_hint_: {
myReactions: myReactionsMap,
packedFiles,
+ packedUsers,
},
})));
}
diff --git a/packages/backend/src/core/entities/NoteReactionEntityService.ts b/packages/backend/src/core/entities/NoteReactionEntityService.ts
index 3f4fa3cf969d..46ec13704cfd 100644
--- a/packages/backend/src/core/entities/NoteReactionEntityService.ts
+++ b/packages/backend/src/core/entities/NoteReactionEntityService.ts
@@ -52,6 +52,9 @@ export class NoteReactionEntityService implements OnModuleInit {
options?: {
withNote: boolean;
},
+ hints?: {
+ packedUser?: Packed<'UserLite'>
+ },
): Promise> {
const opts = Object.assign({
withNote: false,
@@ -62,7 +65,7 @@ export class NoteReactionEntityService implements OnModuleInit {
return {
id: reaction.id,
createdAt: this.idService.parse(reaction.id).date.toISOString(),
- user: await this.userEntityService.pack(reaction.user ?? reaction.userId, me),
+ user: hints?.packedUser ?? await this.userEntityService.pack(reaction.user ?? reaction.userId, me),
type: this.reactionService.convertLegacyReaction(reaction.reaction),
...(opts.withNote ? {
note: await this.noteEntityService.pack(reaction.note ?? reaction.noteId, me),
@@ -81,7 +84,9 @@ export class NoteReactionEntityService implements OnModuleInit {
const opts = Object.assign({
withNote: false,
}, options);
-
- return Promise.all(reactions.map(reaction => this.pack(reaction, me, opts)));
+ const _users = reactions.map(({ user, userId }) => user ?? userId);
+ const _userMap = await this.userEntityService.packMany(_users, me)
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(reactions.map(reaction => this.pack(reaction, me, opts, { packedUser: _userMap.get(reaction.userId) })));
}
}
diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts
index 94d56c883bfb..f393513510e4 100644
--- a/packages/backend/src/core/entities/NotificationEntityService.ts
+++ b/packages/backend/src/core/entities/NotificationEntityService.ts
@@ -13,7 +13,6 @@ import type { MiGroupedNotification, MiNotification } from '@/models/Notificatio
import type { MiNote } from '@/models/Note.js';
import type { Packed } from '@/misc/json-schema.js';
import { bindThis } from '@/decorators.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import { FilterUnionByProperty, groupedNotificationTypes } from '@/types.js';
import { CacheService } from '@/core/CacheService.js';
import { RoleEntityService } from './RoleEntityService.js';
@@ -103,7 +102,7 @@ export class NotificationEntityService implements OnModuleInit {
user,
reaction: reaction.reaction,
};
- }))).filter(r => isNotNull(r.user));
+ }))).filter(r => r.user != null);
// if all users have been deleted, don't show this notification
if (reactions.length === 0) {
return null;
@@ -124,7 +123,7 @@ export class NotificationEntityService implements OnModuleInit {
}
return this.userEntityService.pack(userId, { id: meId });
- }))).filter(isNotNull);
+ }))).filter(x => x != null);
// if all users have been deleted, don't show this notification
if (users.length === 0) {
return null;
@@ -181,7 +180,7 @@ export class NotificationEntityService implements OnModuleInit {
validNotifications = await this.#filterValidNotifier(validNotifications, meId);
- const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(isNotNull);
+ const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(x => x != null);
const notes = noteIds.length > 0 ? await this.notesRepository.find({
where: { id: In(noteIds) },
relations: ['user', 'reply', 'reply.user', 'renote', 'renote.user'],
@@ -223,7 +222,7 @@ export class NotificationEntityService implements OnModuleInit {
);
});
- return (await Promise.all(packPromises)).filter(isNotNull);
+ return (await Promise.all(packPromises)).filter(x => x != null);
}
@bindThis
@@ -305,7 +304,7 @@ export class NotificationEntityService implements OnModuleInit {
this.cacheService.userProfileCache.fetch(meId).then(p => new Set(p.mutedInstances)),
]);
- const notifierIds = notifications.map(notification => 'notifierId' in notification ? notification.notifierId : null).filter(isNotNull);
+ const notifierIds = notifications.map(notification => 'notifierId' in notification ? notification.notifierId : null).filter(x => x != null);
const notifiers = notifierIds.length > 0 ? await this.usersRepository.find({
where: { id: In(notifierIds) },
}) : [];
@@ -313,7 +312,7 @@ export class NotificationEntityService implements OnModuleInit {
const filteredNotifications = ((await Promise.all(notifications.map(async (notification) => {
const isValid = this.#validateNotifier(notification, userIdsWhoMeMuting, userMutedInstances, notifiers);
return isValid ? notification : null;
- }))) as [T | null] ).filter(isNotNull);
+ }))) as [T | null] ).filter(x => x != null);
return filteredNotifications;
}
diff --git a/packages/backend/src/core/entities/PageEntityService.ts b/packages/backend/src/core/entities/PageEntityService.ts
index 65c69a49a7ec..46bf51bb6d28 100644
--- a/packages/backend/src/core/entities/PageEntityService.ts
+++ b/packages/backend/src/core/entities/PageEntityService.ts
@@ -14,7 +14,6 @@ import type { MiPage } from '@/models/Page.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import { UserEntityService } from './UserEntityService.js';
import { DriveFileEntityService } from './DriveFileEntityService.js';
@@ -40,6 +39,9 @@ export class PageEntityService {
public async pack(
src: MiPage['id'] | MiPage,
me?: { id: MiUser['id'] } | null | undefined,
+ hint?: {
+ packedUser?: Packed<'UserLite'>
+ },
): Promise> {
const meId = me ? me.id : null;
const page = typeof src === 'object' ? src : await this.pagesRepository.findOneByOrFail({ id: src });
@@ -91,7 +93,7 @@ export class PageEntityService {
createdAt: this.idService.parse(page.id).date.toISOString(),
updatedAt: page.updatedAt.toISOString(),
userId: page.userId,
- user: this.userEntityService.pack(page.user ?? page.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
+ user: hint?.packedUser ?? this.userEntityService.pack(page.user ?? page.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
content: page.content,
variables: page.variables,
title: page.title,
@@ -103,18 +105,21 @@ export class PageEntityService {
script: page.script,
eyeCatchingImageId: page.eyeCatchingImageId,
eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null,
- attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter(isNotNull)),
+ attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter(x => x != null)),
likedCount: page.likedCount,
isLiked: meId ? await this.pageLikesRepository.exists({ where: { pageId: page.id, userId: meId } }) : undefined,
});
}
@bindThis
- public packMany(
+ public async packMany(
pages: MiPage[],
me?: { id: MiUser['id'] } | null | undefined,
) {
- return Promise.all(pages.map(x => this.pack(x, me)));
+ const _users = pages.map(({ user, userId }) => user ?? userId);
+ const _userMap = await this.userEntityService.packMany(_users, me)
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(pages.map(page => this.pack(page, me, { packedUser: _userMap.get(page.userId) })));
}
}
diff --git a/packages/backend/src/core/entities/RenoteMutingEntityService.ts b/packages/backend/src/core/entities/RenoteMutingEntityService.ts
index 0b05a5db8090..e4e154109a32 100644
--- a/packages/backend/src/core/entities/RenoteMutingEntityService.ts
+++ b/packages/backend/src/core/entities/RenoteMutingEntityService.ts
@@ -30,6 +30,9 @@ export class RenoteMutingEntityService {
public async pack(
src: MiRenoteMuting['id'] | MiRenoteMuting,
me?: { id: MiUser['id'] } | null | undefined,
+ hints?: {
+ packedMutee?: Packed<'UserDetailedNotMe'>
+ },
): Promise> {
const muting = typeof src === 'object' ? src : await this.renoteMutingsRepository.findOneByOrFail({ id: src });
@@ -37,18 +40,21 @@ export class RenoteMutingEntityService {
id: muting.id,
createdAt: this.idService.parse(muting.id).date.toISOString(),
muteeId: muting.muteeId,
- mutee: this.userEntityService.pack(muting.muteeId, me, {
+ mutee: hints?.packedMutee ?? this.userEntityService.pack(muting.muteeId, me, {
schema: 'UserDetailedNotMe',
}),
});
}
@bindThis
- public packMany(
- mutings: any[],
+ public async packMany(
+ mutings: MiRenoteMuting[],
me: { id: MiUser['id'] },
) {
- return Promise.all(mutings.map(x => this.pack(x, me)));
+ const _users = mutings.map(({ mutee, muteeId }) => mutee ?? muteeId);
+ const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailedNotMe' })
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(mutings.map(muting => this.pack(muting, me, { packedMutee: _userMap.get(muting.muteeId) })));
}
}
diff --git a/packages/backend/src/core/entities/ReversiGameEntityService.ts b/packages/backend/src/core/entities/ReversiGameEntityService.ts
index 32cbe631e4e2..df042e75c177 100644
--- a/packages/backend/src/core/entities/ReversiGameEntityService.ts
+++ b/packages/backend/src/core/entities/ReversiGameEntityService.ts
@@ -28,13 +28,15 @@ export class ReversiGameEntityService {
@bindThis
public async packDetail(
src: MiReversiGame['id'] | MiReversiGame,
+ hint?: {
+ packedUser1?: Packed<'UserLite'>,
+ packedUser2?: Packed<'UserLite'>,
+ },
): Promise> {
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
- const users = await Promise.all([
- this.userEntityService.pack(game.user1 ?? game.user1Id),
- this.userEntityService.pack(game.user2 ?? game.user2Id),
- ]);
+ const user1 = hint?.packedUser1 ?? await this.userEntityService.pack(game.user1 ?? game.user1Id);
+ const user2 = hint?.packedUser2 ?? await this.userEntityService.pack(game.user2 ?? game.user2Id);
return await awaitAll({
id: game.id,
@@ -49,10 +51,10 @@ export class ReversiGameEntityService {
user2Ready: game.user2Ready,
user1Id: game.user1Id,
user2Id: game.user2Id,
- user1: users[0],
- user2: users[1],
+ user1,
+ user2,
winnerId: game.winnerId,
- winner: game.winnerId ? users.find(u => u.id === game.winnerId)! : null,
+ winner: game.winnerId ? [user1, user2].find(u => u.id === game.winnerId)! : null,
surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId,
black: game.black,
@@ -68,22 +70,35 @@ export class ReversiGameEntityService {
}
@bindThis
- public packDetailMany(
- xs: MiReversiGame[],
+ public async packDetailMany(
+ games: MiReversiGame[],
) {
- return Promise.all(xs.map(x => this.packDetail(x)));
+ const _user1s = games.map(({ user1, user1Id }) => user1 ?? user1Id);
+ const _user2s = games.map(({ user2, user2Id }) => user2 ?? user2Id);
+ const _userMap = await this.userEntityService.packMany([..._user1s, ..._user2s])
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(
+ games.map(game => {
+ return this.packDetail(game, {
+ packedUser1: _userMap.get(game.user1Id),
+ packedUser2: _userMap.get(game.user2Id),
+ });
+ }),
+ );
}
@bindThis
public async packLite(
src: MiReversiGame['id'] | MiReversiGame,
+ hint?: {
+ packedUser1?: Packed<'UserLite'>,
+ packedUser2?: Packed<'UserLite'>,
+ },
): Promise> {
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
- const users = await Promise.all([
- this.userEntityService.pack(game.user1 ?? game.user1Id),
- this.userEntityService.pack(game.user2 ?? game.user2Id),
- ]);
+ const user1 = hint?.packedUser1 ?? await this.userEntityService.pack(game.user1 ?? game.user1Id);
+ const user2 = hint?.packedUser2 ?? await this.userEntityService.pack(game.user2 ?? game.user2Id);
return await awaitAll({
id: game.id,
@@ -94,10 +109,10 @@ export class ReversiGameEntityService {
isEnded: game.isEnded,
user1Id: game.user1Id,
user2Id: game.user2Id,
- user1: users[0],
- user2: users[1],
+ user1,
+ user2,
winnerId: game.winnerId,
- winner: game.winnerId ? users.find(u => u.id === game.winnerId)! : null,
+ winner: game.winnerId ? [user1, user2].find(u => u.id === game.winnerId)! : null,
surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId,
black: game.black,
@@ -111,10 +126,21 @@ export class ReversiGameEntityService {
}
@bindThis
- public packLiteMany(
- xs: MiReversiGame[],
+ public async packLiteMany(
+ games: MiReversiGame[],
) {
- return Promise.all(xs.map(x => this.packLite(x)));
+ const _user1s = games.map(({ user1, user1Id }) => user1 ?? user1Id);
+ const _user2s = games.map(({ user2, user2Id }) => user2 ?? user2Id);
+ const _userMap = await this.userEntityService.packMany([..._user1s, ..._user2s])
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(
+ games.map(game => {
+ return this.packLite(game, {
+ packedUser1: _userMap.get(game.user1Id),
+ packedUser2: _userMap.get(game.user2Id),
+ });
+ }),
+ );
}
}
diff --git a/packages/backend/src/core/entities/SystemWebhookEntityService.ts b/packages/backend/src/core/entities/SystemWebhookEntityService.ts
new file mode 100644
index 000000000000..e18734091c62
--- /dev/null
+++ b/packages/backend/src/core/entities/SystemWebhookEntityService.ts
@@ -0,0 +1,74 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { In } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import type { MiSystemWebhook, SystemWebhooksRepository } from '@/models/_.js';
+import { bindThis } from '@/decorators.js';
+import { Packed } from '@/misc/json-schema.js';
+
+@Injectable()
+export class SystemWebhookEntityService {
+ constructor(
+ @Inject(DI.systemWebhooksRepository)
+ private systemWebhooksRepository: SystemWebhooksRepository,
+ ) {
+ }
+
+ @bindThis
+ public async pack(
+ src: MiSystemWebhook['id'] | MiSystemWebhook,
+ opts?: {
+ webhooks: Map
+ },
+ ): Promise> {
+ const webhook = typeof src === 'object'
+ ? src
+ : opts?.webhooks.get(src) ?? await this.systemWebhooksRepository.findOneByOrFail({ id: src });
+
+ return {
+ id: webhook.id,
+ isActive: webhook.isActive,
+ updatedAt: webhook.updatedAt.toISOString(),
+ latestSentAt: webhook.latestSentAt?.toISOString() ?? null,
+ latestStatus: webhook.latestStatus,
+ name: webhook.name,
+ on: webhook.on,
+ url: webhook.url,
+ secret: webhook.secret,
+ };
+ }
+
+ @bindThis
+ public async packMany(src: MiSystemWebhook['id'][] | MiSystemWebhook[]): Promise[]> {
+ if (src.length === 0) {
+ return [];
+ }
+
+ const webhooks = Array.of();
+ webhooks.push(
+ ...src.filter((it): it is MiSystemWebhook => typeof it === 'object'),
+ );
+
+ const ids = src.filter((it): it is MiSystemWebhook['id'] => typeof it === 'string');
+ if (ids.length > 0) {
+ webhooks.push(
+ ...await this.systemWebhooksRepository.findBy({ id: In(ids) }),
+ );
+ }
+
+ return Promise
+ .all(
+ webhooks.map(x =>
+ this.pack(x, {
+ webhooks: new Map(webhooks.map(x => [x.id, x])),
+ }),
+ ),
+ )
+ .then(it => it.sort((a, b) => a.id.localeCompare(b.id)));
+ }
+}
+
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index b80a1ec206b4..7fd093c1913a 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -47,7 +47,6 @@ import { IdService } from '@/core/IdService.js';
import type { AnnouncementService } from '@/core/AnnouncementService.js';
import type { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import type { OnModuleInit } from '@nestjs/common';
import type { NoteEntityService } from './NoteEntityService.js';
import type { DriveFileEntityService } from './DriveFileEntityService.js';
@@ -502,11 +501,15 @@ export class UserEntityService implements OnModuleInit {
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
onlineStatus: this.getOnlineStatus(user),
// パフォーマンス上の理由でローカルユーザーのみ
- badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then(rs => rs.sort((a, b) => b.displayOrder - a.displayOrder).map(r => ({
- name: r.name,
- iconUrl: r.iconUrl,
- displayOrder: r.displayOrder,
- }))) : undefined,
+ badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then((rs) => rs
+ .filter((r) => r.isPublic || iAmModerator)
+ .sort((a, b) => b.displayOrder - a.displayOrder)
+ .map((r) => ({
+ name: r.name,
+ iconUrl: r.iconUrl,
+ displayOrder: r.displayOrder,
+ }))
+ ) : undefined,
...(isDetailed ? {
url: profile!.url,
@@ -514,7 +517,7 @@ export class UserEntityService implements OnModuleInit {
movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null,
alsoKnownAs: user.alsoKnownAs
? Promise.all(user.alsoKnownAs.map(uri => this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null)))
- .then(xs => xs.length === 0 ? null : xs.filter(isNotNull))
+ .then(xs => xs.length === 0 ? null : xs.filter(x => x != null))
: null,
createdAt: this.idService.parse(user.id).date.toISOString(),
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
diff --git a/packages/backend/src/core/entities/UserListEntityService.ts b/packages/backend/src/core/entities/UserListEntityService.ts
index 09cab245212e..b77249c5cb72 100644
--- a/packages/backend/src/core/entities/UserListEntityService.ts
+++ b/packages/backend/src/core/entities/UserListEntityService.ts
@@ -50,11 +50,14 @@ export class UserListEntityService {
public async packMembershipsMany(
memberships: MiUserListMembership[],
) {
+ const _users = memberships.map(({ user, userId }) => user ?? userId);
+ const _userMap = await this.userEntityService.packMany(_users)
+ .then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(memberships.map(async x => ({
id: x.id,
createdAt: this.idService.parse(x.id).date.toISOString(),
userId: x.userId,
- user: await this.userEntityService.pack(x.userId),
+ user: _userMap.get(x.userId) ?? await this.userEntityService.pack(x.userId),
withReplies: x.withReplies,
})));
}
diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts
index 919f4794a325..271082b4ff35 100644
--- a/packages/backend/src/di-symbols.ts
+++ b/packages/backend/src/di-symbols.ts
@@ -49,6 +49,7 @@ export const DI = {
swSubscriptionsRepository: Symbol('swSubscriptionsRepository'),
hashtagsRepository: Symbol('hashtagsRepository'),
abuseUserReportsRepository: Symbol('abuseUserReportsRepository'),
+ abuseReportNotificationRecipientRepository: Symbol('abuseReportNotificationRecipientRepository'),
registrationTicketsRepository: Symbol('registrationTicketsRepository'),
authSessionsRepository: Symbol('authSessionsRepository'),
accessTokensRepository: Symbol('accessTokensRepository'),
@@ -70,6 +71,7 @@ export const DI = {
channelFavoritesRepository: Symbol('channelFavoritesRepository'),
registryItemsRepository: Symbol('registryItemsRepository'),
webhooksRepository: Symbol('webhooksRepository'),
+ systemWebhooksRepository: Symbol('systemWebhooksRepository'),
adsRepository: Symbol('adsRepository'),
passwordResetRequestsRepository: Symbol('passwordResetRequestsRepository'),
retentionAggregationsRepository: Symbol('retentionAggregationsRepository'),
diff --git a/packages/backend/src/logger.ts b/packages/backend/src/logger.ts
index d4705af60114..ff5363a425d7 100644
--- a/packages/backend/src/logger.ts
+++ b/packages/backend/src/logger.ts
@@ -22,31 +22,27 @@ type Level = 'error' | 'success' | 'warning' | 'debug' | 'info';
export default class Logger {
private context: Context;
private parentLogger: Logger | null = null;
- private store: boolean;
- constructor(context: string, color?: KEYWORD, store = true) {
+ constructor(context: string, color?: KEYWORD) {
this.context = {
name: context,
color: color,
};
- this.store = store;
}
@bindThis
- public createSubLogger(context: string, color?: KEYWORD, store = true): Logger {
- const logger = new Logger(context, color, store);
+ public createSubLogger(context: string, color?: KEYWORD): Logger {
+ const logger = new Logger(context, color);
logger.parentLogger = this;
return logger;
}
@bindThis
- private log(level: Level, message: string, data?: Record | null, important = false, subContexts: Context[] = [], store = true): void {
+ private log(level: Level, message: string, data?: Record | null, important = false, subContexts: Context[] = []): void {
if (envOption.quiet) return;
- if (!this.store) store = false;
- if (level === 'debug') store = false;
if (this.parentLogger) {
- this.parentLogger.log(level, message, data, important, [this.context].concat(subContexts), store);
+ this.parentLogger.log(level, message, data, important, [this.context].concat(subContexts));
return;
}
diff --git a/packages/backend/src/misc/is-not-null.ts b/packages/backend/src/misc/is-not-null.ts
deleted file mode 100644
index 8d9dc8bb396e..000000000000
--- a/packages/backend/src/misc/is-not-null.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export function isNotNull>(input: T | undefined | null): input is T {
- return input != null;
-}
diff --git a/packages/backend/src/misc/is-user-related.ts b/packages/backend/src/misc/is-user-related.ts
index 93c9b2b814bf..862d6e6a3839 100644
--- a/packages/backend/src/misc/is-user-related.ts
+++ b/packages/backend/src/misc/is-user-related.ts
@@ -4,6 +4,10 @@
*/
export function isUserRelated(note: any, userIds: Set, ignoreAuthor = false): boolean {
+ if (!note) {
+ return false;
+ }
+
if (userIds.has(note.userId) && !ignoreAuthor) {
return true;
}
diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts
index 41e5bfe9e4ad..a721b8663c3c 100644
--- a/packages/backend/src/misc/json-schema.ts
+++ b/packages/backend/src/misc/json-schema.ts
@@ -4,12 +4,12 @@
*/
import {
- packedUserLiteSchema,
- packedUserDetailedNotMeOnlySchema,
packedMeDetailedOnlySchema,
- packedUserDetailedNotMeSchema,
packedMeDetailedSchema,
+ packedUserDetailedNotMeOnlySchema,
+ packedUserDetailedNotMeSchema,
packedUserDetailedSchema,
+ packedUserLiteSchema,
packedUserSchema,
} from '@/models/json-schema/user.js';
import { packedNoteSchema } from '@/models/json-schema/note.js';
@@ -25,7 +25,7 @@ import { packedBlockingSchema } from '@/models/json-schema/blocking.js';
import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js';
import { packedHashtagSchema } from '@/models/json-schema/hashtag.js';
import { packedInviteCodeSchema } from '@/models/json-schema/invite-code.js';
-import { packedPageSchema, packedPageBlockSchema } from '@/models/json-schema/page.js';
+import { packedPageBlockSchema, packedPageSchema } from '@/models/json-schema/page.js';
import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js';
import { packedChannelSchema } from '@/models/json-schema/channel.js';
import { packedAntennaSchema } from '@/models/json-schema/antenna.js';
@@ -38,25 +38,27 @@ import { packedFlashSchema } from '@/models/json-schema/flash.js';
import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
import { packedSigninSchema } from '@/models/json-schema/signin.js';
import {
- packedRoleLiteSchema,
- packedRoleSchema,
- packedRolePoliciesSchema,
+ packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
packedRoleCondFormulaLogicsSchema,
- packedRoleCondFormulaValueNot,
- packedRoleCondFormulaValueIsLocalOrRemoteSchema,
packedRoleCondFormulaValueAssignedRoleSchema,
packedRoleCondFormulaValueCreatedSchema,
- packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
+ packedRoleCondFormulaValueIsLocalOrRemoteSchema,
+ packedRoleCondFormulaValueNot,
packedRoleCondFormulaValueSchema,
packedRoleCondFormulaValueUserSettingBooleanSchema,
+ packedRoleLiteSchema,
+ packedRolePoliciesSchema,
+ packedRoleSchema,
} from '@/models/json-schema/role.js';
import { packedAdSchema } from '@/models/json-schema/ad.js';
-import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js';
+import { packedReversiGameDetailedSchema, packedReversiGameLiteSchema } from '@/models/json-schema/reversi-game.js';
import {
- packedMetaLiteSchema,
packedMetaDetailedOnlySchema,
packedMetaDetailedSchema,
+ packedMetaLiteSchema,
} from '@/models/json-schema/meta.js';
+import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
+import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js';
export const refs = {
UserLite: packedUserLiteSchema,
@@ -111,6 +113,8 @@ export const refs = {
MetaLite: packedMetaLiteSchema,
MetaDetailedOnly: packedMetaDetailedOnlySchema,
MetaDetailed: packedMetaDetailedSchema,
+ SystemWebhook: packedSystemWebhookSchema,
+ AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,
};
export type Packed = SchemaType;
diff --git a/packages/backend/src/misc/prelude/array.ts b/packages/backend/src/misc/prelude/array.ts
index dbfe1fff18c6..f741a0c91339 100644
--- a/packages/backend/src/misc/prelude/array.ts
+++ b/packages/backend/src/misc/prelude/array.ts
@@ -65,44 +65,6 @@ export function maximum(xs: number[]): number {
return Math.max(...xs);
}
-/**
- * Splits an array based on the equivalence relation.
- * The concatenation of the result is equal to the argument.
- */
-export function groupBy(f: EndoRelation, xs: T[]): T[][] {
- const groups = [] as T[][];
- for (const x of xs) {
- const lastGroup = groups.at(-1);
- if (lastGroup !== undefined && f(lastGroup[0], x)) {
- lastGroup.push(x);
- } else {
- groups.push([x]);
- }
- }
- return groups;
-}
-
-/**
- * Splits an array based on the equivalence relation induced by the function.
- * The concatenation of the result is equal to the argument.
- */
-export function groupOn(f: (x: T) => S, xs: T[]): T[][] {
- return groupBy((a, b) => f(a) === f(b), xs);
-}
-
-export function groupByX(collections: T[], keySelector: (x: T) => string) {
- return collections.reduce((obj: Record, item: T) => {
- const key = keySelector(item);
- if (!Object.prototype.hasOwnProperty.call(obj, key)) {
- obj[key] = [];
- }
-
- obj[key].push(item);
-
- return obj;
- }, {});
-}
-
/**
* Compare two arrays by lexicographical order
*/
diff --git a/packages/backend/src/misc/prelude/maybe.ts b/packages/backend/src/misc/prelude/maybe.ts
deleted file mode 100644
index 1c58ccb9c778..000000000000
--- a/packages/backend/src/misc/prelude/maybe.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export interface IMaybe {
- isJust(): this is IJust;
-}
-
-export interface IJust extends IMaybe {
- get(): T;
-}
-
-export function just(value: T): IJust {
- return {
- isJust: () => true,
- get: () => value,
- };
-}
-
-export function nothing(): IMaybe {
- return {
- isJust: () => false,
- };
-}
diff --git a/packages/backend/src/misc/prelude/string.ts b/packages/backend/src/misc/prelude/string.ts
deleted file mode 100644
index 67ea5299619c..000000000000
--- a/packages/backend/src/misc/prelude/string.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export function concat(xs: string[]): string {
- return xs.join('');
-}
-
-export function capitalize(s: string): string {
- return toUpperCase(s.charAt(0)) + toLowerCase(s.slice(1));
-}
-
-export function toUpperCase(s: string): string {
- return s.toUpperCase();
-}
-
-export function toLowerCase(s: string): string {
- return s.toLowerCase();
-}
diff --git a/packages/backend/src/models/AbuseReportNotificationRecipient.ts b/packages/backend/src/models/AbuseReportNotificationRecipient.ts
new file mode 100644
index 000000000000..fbff880afca4
--- /dev/null
+++ b/packages/backend/src/models/AbuseReportNotificationRecipient.ts
@@ -0,0 +1,100 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
+import { MiSystemWebhook } from '@/models/SystemWebhook.js';
+import { MiUserProfile } from '@/models/UserProfile.js';
+import { id } from './util/id.js';
+import { MiUser } from './User.js';
+
+/**
+ * 通報受信時に通知を送信する方法.
+ */
+export type RecipientMethod = 'email' | 'webhook';
+
+@Entity('abuse_report_notification_recipient')
+export class MiAbuseReportNotificationRecipient {
+ @PrimaryColumn(id())
+ public id: string;
+
+ /**
+ * 有効かどうか.
+ */
+ @Index()
+ @Column('boolean', {
+ default: true,
+ })
+ public isActive: boolean;
+
+ /**
+ * 更新日時.
+ */
+ @Column('timestamp with time zone', {
+ default: () => 'CURRENT_TIMESTAMP',
+ })
+ public updatedAt: Date;
+
+ /**
+ * 通知設定名.
+ */
+ @Column('varchar', {
+ length: 255,
+ })
+ public name: string;
+
+ /**
+ * 通知方法.
+ */
+ @Index()
+ @Column('varchar', {
+ length: 64,
+ })
+ public method: RecipientMethod;
+
+ /**
+ * 通知先のユーザID.
+ */
+ @Index()
+ @Column({
+ ...id(),
+ nullable: true,
+ })
+ public userId: MiUser['id'] | null;
+
+ /**
+ * 通知先のユーザ.
+ */
+ @ManyToOne(type => MiUser, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn({ name: 'userId', referencedColumnName: 'id', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_userId1' })
+ public user: MiUser | null;
+
+ /**
+ * 通知先のユーザプロフィール.
+ */
+ @ManyToOne(type => MiUserProfile, {})
+ @JoinColumn({ name: 'userId', referencedColumnName: 'userId', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_userId2' })
+ public userProfile: MiUserProfile | null;
+
+ /**
+ * 通知先のシステムWebhookId.
+ */
+ @Index()
+ @Column({
+ ...id(),
+ nullable: true,
+ })
+ public systemWebhookId: string | null;
+
+ /**
+ * 通知先のシステムWebhook.
+ */
+ @ManyToOne(type => MiSystemWebhook, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public systemWebhook: MiSystemWebhook | null;
+}
diff --git a/packages/backend/src/models/Antenna.ts b/packages/backend/src/models/Antenna.ts
index f5e819059e03..33e6f4818952 100644
--- a/packages/backend/src/models/Antenna.ts
+++ b/packages/backend/src/models/Antenna.ts
@@ -90,9 +90,6 @@ export class MiAntenna {
})
public expression: string | null;
- @Column('boolean')
- public notify: boolean;
-
@Index()
@Column('boolean', {
default: true,
diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts
index ef4f97ceae9d..70d41801b5ee 100644
--- a/packages/backend/src/models/Meta.ts
+++ b/packages/backend/src/models/Meta.ts
@@ -381,6 +381,12 @@ export class MiMeta {
})
public privacyPolicyUrl: string | null;
+ @Column('varchar', {
+ length: 1024,
+ nullable: true,
+ })
+ public inquiryUrl: string | null;
+
@Column('varchar', {
length: 8192,
nullable: true,
diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts
index bd447570ddf0..ea0f88babaa7 100644
--- a/packages/backend/src/models/RepositoryModule.ts
+++ b/packages/backend/src/models/RepositoryModule.ts
@@ -3,417 +3,500 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import type { Provider } from '@nestjs/common';
import { Module } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
-import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, MiBubbleGameRecord, MiReversiGame } from './_.js';
+import {
+ MiAbuseReportNotificationRecipient,
+ MiAbuseUserReport,
+ MiAccessToken,
+ MiAd,
+ MiAnnouncement,
+ MiAnnouncementRead,
+ MiAntenna,
+ MiApp,
+ MiAuthSession,
+ MiAvatarDecoration,
+ MiBlocking,
+ MiBubbleGameRecord,
+ MiChannel,
+ MiChannelFavorite,
+ MiChannelFollowing,
+ MiClip,
+ MiClipFavorite,
+ MiClipNote,
+ MiDriveFile,
+ MiDriveFolder,
+ MiEmoji,
+ MiFlash,
+ MiFlashLike,
+ MiFollowing,
+ MiFollowRequest,
+ MiGalleryLike,
+ MiGalleryPost,
+ MiHashtag,
+ MiInstance,
+ MiMeta,
+ MiModerationLog,
+ MiMuting,
+ MiNote,
+ MiNoteFavorite,
+ MiNoteReaction,
+ MiNoteThreadMuting,
+ MiNoteUnread,
+ MiPage,
+ MiPageLike,
+ MiPasswordResetRequest,
+ MiPoll,
+ MiPollVote,
+ MiPromoNote,
+ MiPromoRead,
+ MiRegistrationTicket,
+ MiRegistryItem,
+ MiRelay,
+ MiRenoteMuting,
+ MiRepository,
+ miRepository,
+ MiRetentionAggregation,
+ MiReversiGame,
+ MiRole,
+ MiRoleAssignment,
+ MiSignin,
+ MiSwSubscription,
+ MiSystemWebhook,
+ MiUsedUsername,
+ MiUser,
+ MiUserIp,
+ MiUserKeypair,
+ MiUserList,
+ MiUserListFavorite,
+ MiUserListMembership,
+ MiUserMemo,
+ MiUserNotePining,
+ MiUserPending,
+ MiUserProfile,
+ MiUserPublickey,
+ MiUserSecurityKey,
+ MiWebhook
+} from './_.js';
import type { DataSource } from 'typeorm';
-import type { Provider } from '@nestjs/common';
const $usersRepository: Provider = {
provide: DI.usersRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUser),
+ useFactory: (db: DataSource) => db.getRepository(MiUser).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $notesRepository: Provider = {
provide: DI.notesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiNote),
+ useFactory: (db: DataSource) => db.getRepository(MiNote).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $announcementsRepository: Provider = {
provide: DI.announcementsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiAnnouncement),
+ useFactory: (db: DataSource) => db.getRepository(MiAnnouncement).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $announcementReadsRepository: Provider = {
provide: DI.announcementReadsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiAnnouncementRead),
+ useFactory: (db: DataSource) => db.getRepository(MiAnnouncementRead).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $appsRepository: Provider = {
provide: DI.appsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiApp),
+ useFactory: (db: DataSource) => db.getRepository(MiApp).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $avatarDecorationsRepository: Provider = {
provide: DI.avatarDecorationsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiAvatarDecoration),
+ useFactory: (db: DataSource) => db.getRepository(MiAvatarDecoration).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $noteFavoritesRepository: Provider = {
provide: DI.noteFavoritesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite),
+ useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $noteThreadMutingsRepository: Provider = {
provide: DI.noteThreadMutingsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiNoteThreadMuting),
+ useFactory: (db: DataSource) => db.getRepository(MiNoteThreadMuting).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $noteReactionsRepository: Provider = {
provide: DI.noteReactionsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiNoteReaction),
+ useFactory: (db: DataSource) => db.getRepository(MiNoteReaction).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $noteUnreadsRepository: Provider = {
provide: DI.noteUnreadsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiNoteUnread),
+ useFactory: (db: DataSource) => db.getRepository(MiNoteUnread).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $pollsRepository: Provider = {
provide: DI.pollsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiPoll),
+ useFactory: (db: DataSource) => db.getRepository(MiPoll).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $pollVotesRepository: Provider = {
provide: DI.pollVotesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiPollVote),
+ useFactory: (db: DataSource) => db.getRepository(MiPollVote).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $userProfilesRepository: Provider = {
provide: DI.userProfilesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserProfile),
+ useFactory: (db: DataSource) => db.getRepository(MiUserProfile).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $userKeypairsRepository: Provider = {
provide: DI.userKeypairsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserKeypair),
+ useFactory: (db: DataSource) => db.getRepository(MiUserKeypair).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $userPendingsRepository: Provider = {
provide: DI.userPendingsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserPending),
+ useFactory: (db: DataSource) => db.getRepository(MiUserPending).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $userSecurityKeysRepository: Provider = {
provide: DI.userSecurityKeysRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserSecurityKey),
+ useFactory: (db: DataSource) => db.getRepository(MiUserSecurityKey).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $userPublickeysRepository: Provider = {
provide: DI.userPublickeysRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserPublickey),
+ useFactory: (db: DataSource) => db.getRepository(MiUserPublickey).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $userListsRepository: Provider = {
provide: DI.userListsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserList),
+ useFactory: (db: DataSource) => db.getRepository(MiUserList).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $userListFavoritesRepository: Provider = {
provide: DI.userListFavoritesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserListFavorite),
+ useFactory: (db: DataSource) => db.getRepository(MiUserListFavorite).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $userListMembershipsRepository: Provider = {
provide: DI.userListMembershipsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserListMembership),
+ useFactory: (db: DataSource) => db.getRepository(MiUserListMembership).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $userNotePiningsRepository: Provider = {
provide: DI.userNotePiningsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserNotePining),
+ useFactory: (db: DataSource) => db.getRepository(MiUserNotePining).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $userIpsRepository: Provider = {
provide: DI.userIpsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserIp),
+ useFactory: (db: DataSource) => db.getRepository(MiUserIp).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $usedUsernamesRepository: Provider = {
provide: DI.usedUsernamesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUsedUsername),
+ useFactory: (db: DataSource) => db.getRepository(MiUsedUsername).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $followingsRepository: Provider = {
provide: DI.followingsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiFollowing),
+ useFactory: (db: DataSource) => db.getRepository(MiFollowing).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $followRequestsRepository: Provider = {
provide: DI.followRequestsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiFollowRequest),
+ useFactory: (db: DataSource) => db.getRepository(MiFollowRequest).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $instancesRepository: Provider = {
provide: DI.instancesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiInstance),
+ useFactory: (db: DataSource) => db.getRepository(MiInstance).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $emojisRepository: Provider = {
provide: DI.emojisRepository,
- useFactory: (db: DataSource) => db.getRepository(MiEmoji),
+ useFactory: (db: DataSource) => db.getRepository(MiEmoji).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $driveFilesRepository: Provider = {
provide: DI.driveFilesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiDriveFile),
+ useFactory: (db: DataSource) => db.getRepository(MiDriveFile).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $driveFoldersRepository: Provider = {
provide: DI.driveFoldersRepository,
- useFactory: (db: DataSource) => db.getRepository(MiDriveFolder),
+ useFactory: (db: DataSource) => db.getRepository(MiDriveFolder).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $metasRepository: Provider = {
provide: DI.metasRepository,
- useFactory: (db: DataSource) => db.getRepository(MiMeta),
+ useFactory: (db: DataSource) => db.getRepository(MiMeta).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $mutingsRepository: Provider = {
provide: DI.mutingsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiMuting),
+ useFactory: (db: DataSource) => db.getRepository(MiMuting).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $renoteMutingsRepository: Provider = {
provide: DI.renoteMutingsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiRenoteMuting),
+ useFactory: (db: DataSource) => db.getRepository(MiRenoteMuting).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $blockingsRepository: Provider = {
provide: DI.blockingsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiBlocking),
+ useFactory: (db: DataSource) => db.getRepository(MiBlocking).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $swSubscriptionsRepository: Provider = {
provide: DI.swSubscriptionsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiSwSubscription),
+ useFactory: (db: DataSource) => db.getRepository(MiSwSubscription).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $hashtagsRepository: Provider = {
provide: DI.hashtagsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiHashtag),
+ useFactory: (db: DataSource) => db.getRepository(MiHashtag).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $abuseUserReportsRepository: Provider = {
provide: DI.abuseUserReportsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiAbuseUserReport),
+ useFactory: (db: DataSource) => db.getRepository(MiAbuseUserReport).extend(miRepository as MiRepository),
+ inject: [DI.db],
+};
+
+const $abuseReportNotificationRecipientRepository: Provider = {
+ provide: DI.abuseReportNotificationRecipientRepository,
+ useFactory: (db: DataSource) => db.getRepository(MiAbuseReportNotificationRecipient),
inject: [DI.db],
};
const $registrationTicketsRepository: Provider = {
provide: DI.registrationTicketsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiRegistrationTicket),
+ useFactory: (db: DataSource) => db.getRepository(MiRegistrationTicket).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $authSessionsRepository: Provider = {
provide: DI.authSessionsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiAuthSession),
+ useFactory: (db: DataSource) => db.getRepository(MiAuthSession).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $accessTokensRepository: Provider = {
provide: DI.accessTokensRepository,
- useFactory: (db: DataSource) => db.getRepository(MiAccessToken),
+ useFactory: (db: DataSource) => db.getRepository(MiAccessToken).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $signinsRepository: Provider = {
provide: DI.signinsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiSignin),
+ useFactory: (db: DataSource) => db.getRepository(MiSignin).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $pagesRepository: Provider = {
provide: DI.pagesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiPage),
+ useFactory: (db: DataSource) => db.getRepository(MiPage).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $pageLikesRepository: Provider = {
provide: DI.pageLikesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiPageLike),
+ useFactory: (db: DataSource) => db.getRepository(MiPageLike).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $galleryPostsRepository: Provider = {
provide: DI.galleryPostsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiGalleryPost),
+ useFactory: (db: DataSource) => db.getRepository(MiGalleryPost).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $galleryLikesRepository: Provider = {
provide: DI.galleryLikesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiGalleryLike),
+ useFactory: (db: DataSource) => db.getRepository(MiGalleryLike).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $moderationLogsRepository: Provider = {
provide: DI.moderationLogsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiModerationLog),
+ useFactory: (db: DataSource) => db.getRepository(MiModerationLog).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $clipsRepository: Provider = {
provide: DI.clipsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiClip),
+ useFactory: (db: DataSource) => db.getRepository(MiClip).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $clipNotesRepository: Provider = {
provide: DI.clipNotesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiClipNote),
+ useFactory: (db: DataSource) => db.getRepository(MiClipNote).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $clipFavoritesRepository: Provider = {
provide: DI.clipFavoritesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiClipFavorite),
+ useFactory: (db: DataSource) => db.getRepository(MiClipFavorite).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $antennasRepository: Provider = {
provide: DI.antennasRepository,
- useFactory: (db: DataSource) => db.getRepository(MiAntenna),
+ useFactory: (db: DataSource) => db.getRepository(MiAntenna).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $promoNotesRepository: Provider = {
provide: DI.promoNotesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiPromoNote),
+ useFactory: (db: DataSource) => db.getRepository(MiPromoNote).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $promoReadsRepository: Provider = {
provide: DI.promoReadsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiPromoRead),
+ useFactory: (db: DataSource) => db.getRepository(MiPromoRead).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $relaysRepository: Provider = {
provide: DI.relaysRepository,
- useFactory: (db: DataSource) => db.getRepository(MiRelay),
+ useFactory: (db: DataSource) => db.getRepository(MiRelay).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $channelsRepository: Provider = {
provide: DI.channelsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiChannel),
+ useFactory: (db: DataSource) => db.getRepository(MiChannel).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $channelFollowingsRepository: Provider = {
provide: DI.channelFollowingsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiChannelFollowing),
+ useFactory: (db: DataSource) => db.getRepository(MiChannelFollowing).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $channelFavoritesRepository: Provider = {
provide: DI.channelFavoritesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiChannelFavorite),
+ useFactory: (db: DataSource) => db.getRepository(MiChannelFavorite).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $registryItemsRepository: Provider = {
provide: DI.registryItemsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiRegistryItem),
+ useFactory: (db: DataSource) => db.getRepository(MiRegistryItem).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $webhooksRepository: Provider = {
provide: DI.webhooksRepository,
- useFactory: (db: DataSource) => db.getRepository(MiWebhook),
+ useFactory: (db: DataSource) => db.getRepository(MiWebhook).extend(miRepository as MiRepository),
+ inject: [DI.db],
+};
+
+const $systemWebhooksRepository: Provider = {
+ provide: DI.systemWebhooksRepository,
+ useFactory: (db: DataSource) => db.getRepository(MiSystemWebhook),
inject: [DI.db],
};
const $adsRepository: Provider = {
provide: DI.adsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiAd),
+ useFactory: (db: DataSource) => db.getRepository(MiAd).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $passwordResetRequestsRepository: Provider = {
provide: DI.passwordResetRequestsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiPasswordResetRequest),
+ useFactory: (db: DataSource) => db.getRepository(MiPasswordResetRequest).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $retentionAggregationsRepository: Provider = {
provide: DI.retentionAggregationsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiRetentionAggregation),
+ useFactory: (db: DataSource) => db.getRepository(MiRetentionAggregation).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $flashsRepository: Provider = {
provide: DI.flashsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiFlash),
+ useFactory: (db: DataSource) => db.getRepository(MiFlash).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $flashLikesRepository: Provider = {
provide: DI.flashLikesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiFlashLike),
+ useFactory: (db: DataSource) => db.getRepository(MiFlashLike).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $rolesRepository: Provider = {
provide: DI.rolesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiRole),
+ useFactory: (db: DataSource) => db.getRepository(MiRole).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $roleAssignmentsRepository: Provider = {
provide: DI.roleAssignmentsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiRoleAssignment),
+ useFactory: (db: DataSource) => db.getRepository(MiRoleAssignment).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $userMemosRepository: Provider = {
provide: DI.userMemosRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserMemo),
+ useFactory: (db: DataSource) => db.getRepository(MiUserMemo).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $bubbleGameRecordsRepository: Provider = {
provide: DI.bubbleGameRecordsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord),
+ useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $reversiGamesRepository: Provider = {
provide: DI.reversiGamesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiReversiGame),
+ useFactory: (db: DataSource) => db.getRepository(MiReversiGame).extend(miRepository as MiRepository),
inject: [DI.db],
};
@Module({
- imports: [
- ],
+ imports: [],
providers: [
$usersRepository,
$notesRepository,
@@ -451,6 +534,7 @@ const $reversiGamesRepository: Provider = {
$swSubscriptionsRepository,
$hashtagsRepository,
$abuseUserReportsRepository,
+ $abuseReportNotificationRecipientRepository,
$registrationTicketsRepository,
$authSessionsRepository,
$accessTokensRepository,
@@ -472,6 +556,7 @@ const $reversiGamesRepository: Provider = {
$channelFavoritesRepository,
$registryItemsRepository,
$webhooksRepository,
+ $systemWebhooksRepository,
$adsRepository,
$passwordResetRequestsRepository,
$retentionAggregationsRepository,
@@ -520,6 +605,7 @@ const $reversiGamesRepository: Provider = {
$swSubscriptionsRepository,
$hashtagsRepository,
$abuseUserReportsRepository,
+ $abuseReportNotificationRecipientRepository,
$registrationTicketsRepository,
$authSessionsRepository,
$accessTokensRepository,
@@ -541,6 +627,7 @@ const $reversiGamesRepository: Provider = {
$channelFavoritesRepository,
$registryItemsRepository,
$webhooksRepository,
+ $systemWebhooksRepository,
$adsRepository,
$passwordResetRequestsRepository,
$retentionAggregationsRepository,
@@ -553,4 +640,5 @@ const $reversiGamesRepository: Provider = {
$reversiGamesRepository,
],
})
-export class RepositoryModule {}
+export class RepositoryModule {
+}
diff --git a/packages/backend/src/models/SystemWebhook.ts b/packages/backend/src/models/SystemWebhook.ts
new file mode 100644
index 000000000000..86fb323d1daf
--- /dev/null
+++ b/packages/backend/src/models/SystemWebhook.ts
@@ -0,0 +1,98 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
+import { Serialized } from '@/types.js';
+import { id } from './util/id.js';
+
+export const systemWebhookEventTypes = [
+ // ユーザからの通報を受けたとき
+ 'abuseReport',
+ // 通報を処理したとき
+ 'abuseReportResolved',
+] as const;
+export type SystemWebhookEventType = typeof systemWebhookEventTypes[number];
+
+@Entity('system_webhook')
+export class MiSystemWebhook {
+ @PrimaryColumn(id())
+ public id: string;
+
+ /**
+ * 有効かどうか.
+ */
+ @Index('IDX_system_webhook_isActive', { synchronize: false })
+ @Column('boolean', {
+ default: true,
+ })
+ public isActive: boolean;
+
+ /**
+ * 更新日時.
+ */
+ @Column('timestamp with time zone', {
+ default: () => 'CURRENT_TIMESTAMP',
+ })
+ public updatedAt: Date;
+
+ /**
+ * 最後に送信された日時.
+ */
+ @Column('timestamp with time zone', {
+ nullable: true,
+ })
+ public latestSentAt: Date | null;
+
+ /**
+ * 最後に送信されたステータスコード
+ */
+ @Column('integer', {
+ nullable: true,
+ })
+ public latestStatus: number | null;
+
+ /**
+ * 通知設定名.
+ */
+ @Column('varchar', {
+ length: 255,
+ })
+ public name: string;
+
+ /**
+ * イベント種別.
+ */
+ @Index('IDX_system_webhook_on', { synchronize: false })
+ @Column('varchar', {
+ length: 128,
+ array: true,
+ default: '{}',
+ })
+ public on: SystemWebhookEventType[];
+
+ /**
+ * Webhook送信先のURL.
+ */
+ @Column('varchar', {
+ length: 1024,
+ })
+ public url: string;
+
+ /**
+ * Webhook検証用の値.
+ */
+ @Column('varchar', {
+ length: 1024,
+ })
+ public secret: string;
+
+ static deserialize(obj: Serialized): MiSystemWebhook {
+ return {
+ ...obj,
+ updatedAt: new Date(obj.updatedAt),
+ latestSentAt: obj.latestSentAt ? new Date(obj.latestSentAt) : null,
+ };
+ }
+}
diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts
index 43d42d80dd32..c72bdaa72726 100644
--- a/packages/backend/src/models/_.ts
+++ b/packages/backend/src/models/_.ts
@@ -3,7 +3,15 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { FindOneOptions, InsertQueryBuilder, ObjectLiteral, Repository, SelectQueryBuilder, TypeORMError } from 'typeorm';
+import { DriverUtils } from 'typeorm/driver/DriverUtils.js';
+import { RelationCountLoader } from 'typeorm/query-builder/relation-count/RelationCountLoader.js';
+import { RelationIdLoader } from 'typeorm/query-builder/relation-id/RelationIdLoader.js';
+import { RawSqlResultsToEntityTransformer } from 'typeorm/query-builder/transformer/RawSqlResultsToEntityTransformer.js';
+import { ObjectUtils } from 'typeorm/util/ObjectUtils.js';
+import { OrmUtils } from 'typeorm/util/OrmUtils.js';
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
+import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js';
import { MiAccessToken } from '@/models/AccessToken.js';
import { MiAd } from '@/models/Ad.js';
import { MiAnnouncement } from '@/models/Announcement.js';
@@ -61,6 +69,7 @@ import { MiUserPublickey } from '@/models/UserPublickey.js';
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
import { MiUserMemo } from '@/models/UserMemo.js';
import { MiWebhook } from '@/models/Webhook.js';
+import { MiSystemWebhook } from '@/models/SystemWebhook.js';
import { MiChannel } from '@/models/Channel.js';
import { MiRetentionAggregation } from '@/models/RetentionAggregation.js';
import { MiRole } from '@/models/Role.js';
@@ -70,11 +79,54 @@ import { MiFlashLike } from '@/models/FlashLike.js';
import { MiUserListFavorite } from '@/models/UserListFavorite.js';
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
import { MiReversiGame } from '@/models/ReversiGame.js';
+import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
-import type { Repository } from 'typeorm';
+export interface MiRepository {
+ createTableColumnNames(this: Repository & MiRepository): string[];
+ insertOne(this: Repository & MiRepository, entity: QueryDeepPartialEntity, findOptions?: Pick, 'relations'>): Promise;
+ selectAliasColumnNames(this: Repository & MiRepository, queryBuilder: InsertQueryBuilder, builder: SelectQueryBuilder): void;
+}
+
+export const miRepository = {
+ createTableColumnNames() {
+ return this.metadata.columns.filter(column => column.isSelect && !column.isVirtual).map(column => column.databaseName);
+ },
+ async insertOne(entity, findOptions?) {
+ const queryBuilder = this.createQueryBuilder().insert().values(entity);
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const mainAlias = queryBuilder.expressionMap.mainAlias!;
+ const name = mainAlias.name;
+ mainAlias.name = 't';
+ const columnNames = this.createTableColumnNames();
+ queryBuilder.returning(columnNames.reduce((a, c) => `${a}, ${queryBuilder.escape(c)}`, '').slice(2));
+ const builder = this.createQueryBuilder().addCommonTableExpression(queryBuilder, 'cte', { columnNames });
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ builder.expressionMap.mainAlias!.tablePath = 'cte';
+ this.selectAliasColumnNames(queryBuilder, builder);
+ if (findOptions) {
+ builder.setFindOptions(findOptions);
+ }
+ const raw = await builder.execute();
+ mainAlias.name = name;
+ const relationId = await new RelationIdLoader(builder.connection, this.queryRunner, builder.expressionMap.relationIdAttributes).load(raw);
+ const relationCount = await new RelationCountLoader(builder.connection, this.queryRunner, builder.expressionMap.relationCountAttributes).load(raw);
+ const result = new RawSqlResultsToEntityTransformer(builder.expressionMap, builder.connection.driver, relationId, relationCount, this.queryRunner).transform(raw, mainAlias);
+ return result[0];
+ },
+ selectAliasColumnNames(queryBuilder, builder) {
+ let selectOrAddSelect = (selection: string, selectionAliasName?: string) => {
+ selectOrAddSelect = (selection, selectionAliasName) => builder.addSelect(selection, selectionAliasName);
+ return builder.select(selection, selectionAliasName);
+ };
+ for (const columnName of this.createTableColumnNames()) {
+ selectOrAddSelect(`${builder.alias}.${columnName}`, `${builder.alias}_${columnName}`);
+ }
+ },
+} satisfies MiRepository;
export {
MiAbuseUserReport,
+ MiAbuseReportNotificationRecipient,
MiAccessToken,
MiAd,
MiAnnouncement,
@@ -132,6 +184,7 @@ export {
MiUserPublickey,
MiUserSecurityKey,
MiWebhook,
+ MiSystemWebhook,
MiChannel,
MiRetentionAggregation,
MiRole,
@@ -143,70 +196,72 @@ export {
MiReversiGame,
};
-export type AbuseUserReportsRepository = Repository;
-export type AccessTokensRepository = Repository;
-export type AdsRepository = Repository;
-export type AnnouncementsRepository = Repository;
-export type AnnouncementReadsRepository = Repository;
-export type AntennasRepository = Repository;
-export type AppsRepository = Repository;
-export type AvatarDecorationsRepository = Repository;
-export type AuthSessionsRepository = Repository;
-export type BlockingsRepository = Repository;
-export type ChannelFollowingsRepository = Repository;
-export type ChannelFavoritesRepository = Repository;
-export type ClipsRepository = Repository;
-export type ClipNotesRepository = Repository;
-export type ClipFavoritesRepository = Repository;
-export type DriveFilesRepository = Repository;
-export type DriveFoldersRepository = Repository;
-export type EmojisRepository = Repository;
-export type FollowingsRepository = Repository;
-export type FollowRequestsRepository = Repository;
-export type GalleryLikesRepository = Repository;
-export type GalleryPostsRepository = Repository;
-export type HashtagsRepository = Repository;
-export type InstancesRepository = Repository;
-export type MetasRepository = Repository;
-export type ModerationLogsRepository = Repository;
-export type MutingsRepository = Repository;
-export type RenoteMutingsRepository = Repository;
-export type NotesRepository = Repository;
-export type NoteFavoritesRepository = Repository;
-export type NoteReactionsRepository = Repository;
-export type NoteThreadMutingsRepository = Repository;
-export type NoteUnreadsRepository = Repository;
-export type PagesRepository = Repository;
-export type PageLikesRepository = Repository;
-export type PasswordResetRequestsRepository = Repository;
-export type PollsRepository = Repository;
-export type PollVotesRepository = Repository