diff --git a/.config/example.yml b/.config/example.yml
index c127eaae22..c7884a3687 100644
--- a/.config/example.yml
+++ b/.config/example.yml
@@ -105,6 +105,54 @@ port: 3000
# socket: /path/to/misskey.sock
# chmodSocket: '777'
+# Proxy trust settings
+#
+# Specifies the IP addresses that Misskey will use as trusted
+# reverse proxies (e.g., nginx, Cloudflare). This affects how
+# Misskey determines the source IP for each request and is used
+# for important rate limiting and security features. If the value
+# is not set correctly, Misskey may use the IP address of the
+# reverse proxy instead of the actual source IP, which may lead to
+# unintended rate limiting or security vulnerabilities.
+# By default, the loopback network and private network address
+# ranges shown below are trusted.
+# If you are using a single reverse proxy and it is on the same
+# machine or the same private network as Misskey, it is unlikely you
+# need to change this setting, and the default setting is fine.
+# Also, if you are using multiple reverse proxy servers and they are
+# all on the same private network as Misskey, the default setting
+# is fine.
+# However, if you are using a reverse proxy server that accesses
+# Misskey web servers and streaming servers via public IP addresses
+# (for example, Cloudflare), you must set this variable.
+# When changing this setting, you can use one of the following values:
+#
+# - true: Trust all proxies
+# - false: Do not trust any proxies
+# - IP address, IP address range, or array of them: Trust hops that
+# match the specified criteria.
+# - Integer: Trust the nth hop from the front-facing proxy server as
+# the client.
+# For more information on how to configure this setting, please refer
+# to the Fastify documentation:
+# https://fastify.dev/docs/latest/Reference/Server/#trustproxy
+#
+# Note that if this variable is set, it overrides the default range,
+# so if you have both an external reverse proxy and a proxy on the
+# local host, you must include both IPs (or IP ranges).
+#
+#trustProxy:
+# - '10.0.0.0/8'
+# - '172.16.0.0/12'
+# - '192.168.0.0/16'
+# - '127.0.0.1/32'
+# - '::1/128'
+# - 'fc00::/7'
+# # Example: If you are using some external reverse proxies like CDNs,
+# # you may need to add the CDN IP ranges here.
+# # If you're using Cloudflare, you can find IP Ranges at:
+# # https://www.cloudflare.com/ips/
+
# ┌──────────────────────────┐
#───┘ PostgreSQL configuration └────────────────────────────────
@@ -273,6 +321,10 @@ id: 'aidx'
# Whether disable HSTS
#disableHsts: true
+# Enable internal IP-based rate limiting (default: true)
+# To configure them in reverse proxy instead, set this to false.
+#enableIpRateLimit: true
+
# Number of worker processes
#clusterLimit: 1
diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index b6ebcf6ad3..d208ad6ecf 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -1 +1 @@
-FROM mcr.microsoft.com/devcontainers/javascript-node:0-18
+FROM mcr.microsoft.com/devcontainers/javascript-node:4.0.3-24-trixie
diff --git a/.devcontainer/compose.yml b/.devcontainer/compose.yml
index d02d2a8f4a..501f78c814 100644
--- a/.devcontainer/compose.yml
+++ b/.devcontainer/compose.yml
@@ -28,7 +28,7 @@ services:
db:
restart: unless-stopped
- image: postgres:15-alpine
+ image: postgres:18-alpine
networks:
- internal_network
environment:
diff --git a/.dockerignore b/.dockerignore
index f204349160..39cbe2726f 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -6,6 +6,7 @@
Dockerfile
build/
built/
+src-js/
db/
.devcontainer/compose.yml
node_modules/
diff --git a/.github/ISSUE_TEMPLATE/01_bug-report.yml b/.github/ISSUE_TEMPLATE/01_bug-report.yml
index 077855b5bf..00da7e9a2a 100644
--- a/.github/ISSUE_TEMPLATE/01_bug-report.yml
+++ b/.github/ISSUE_TEMPLATE/01_bug-report.yml
@@ -54,7 +54,7 @@ body:
* Model and OS of the device(s): MacBook Pro (14inch, 2021), macOS Ventura 13.4
* Browser: Chrome 113.0.5672.126
* Server URL: misskey.example.com
- * Misskey: 2025.x.x
+ * Misskey: 2026.x.x
value: |
* Model and OS of the device(s):
* Browser:
@@ -74,9 +74,9 @@ body:
Examples:
* Installation Method or Hosting Service: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment
- * Misskey: 2025.x.x
+ * Misskey: 2026.x.x
* Node: 20.x.x
- * PostgreSQL: 15.x.x
+ * PostgreSQL: 18.x.x
* Redis: 7.x.x
* OS and Architecture: Ubuntu 24.04.2 LTS aarch64
value: |
diff --git a/.github/workflows/api-misskey-js.yml b/.github/workflows/api-misskey-js.yml
index 6117e69c03..49ca3058f3 100644
--- a/.github/workflows/api-misskey-js.yml
+++ b/.github/workflows/api-misskey-js.yml
@@ -16,13 +16,13 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v6.0.1
- name: Setup pnpm
- uses: pnpm/action-setup@v4.1.0
+ uses: pnpm/action-setup@v4.2.0
- name: Setup Node.js
- uses: actions/setup-node@v4.4.0
+ uses: actions/setup-node@v6.1.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
diff --git a/.github/workflows/changelog-check.yml b/.github/workflows/changelog-check.yml
index 5ca27749bb..d17999a271 100644
--- a/.github/workflows/changelog-check.yml
+++ b/.github/workflows/changelog-check.yml
@@ -12,9 +12,9 @@ jobs:
steps:
- name: Checkout head
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v6.0.1
- name: Setup Node.js
- uses: actions/setup-node@v4.4.0
+ uses: actions/setup-node@v6.1.0
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 22d500c306..8a81e85521 100644
--- a/.github/workflows/check-misskey-js-autogen.yml
+++ b/.github/workflows/check-misskey-js-autogen.yml
@@ -18,7 +18,7 @@ jobs:
if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }}
steps:
- name: checkout
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v6.0.1
with:
submodules: true
persist-credentials: false
@@ -29,7 +29,7 @@ jobs:
- name: setup node
id: setup-node
- uses: actions/setup-node@v4.4.0
+ uses: actions/setup-node@v6.1.0
with:
node-version-file: '.node-version'
cache: pnpm
@@ -53,7 +53,7 @@ jobs:
# packages/misskey-js/generator/built/autogen
- name: Upload Generated
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v6
with:
name: generated-misskey-js
path: packages/misskey-js/generator/built/autogen
@@ -66,14 +66,14 @@ jobs:
if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }}
steps:
- name: checkout
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v6.0.1
with:
submodules: true
persist-credentials: false
ref: refs/pull/${{ github.event.pull_request.number }}/merge
- name: Upload From Merged
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v6
with:
name: actual-misskey-js
path: packages/misskey-js/src/autogen
@@ -86,13 +86,13 @@ jobs:
pull-requests: write
steps:
- name: download generated-misskey-js
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v7
with:
name: generated-misskey-js
path: misskey-js-generated
- name: download actual-misskey-js
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v7
with:
name: actual-misskey-js
path: misskey-js-actual
@@ -113,9 +113,9 @@ jobs:
- name: send message
if: steps.check-changes.outputs.changes == 'true'
- uses: thollander/actions-comment-pull-request@v2
+ uses: thollander/actions-comment-pull-request@v3
with:
- comment_tag: check-misskey-js-autogen
+ comment-tag: check-misskey-js-autogen
message: |-
Thank you for sending us a great Pull Request! 👍
Please regenerate misskey-js type definitions! 🙏
@@ -127,9 +127,9 @@ jobs:
- name: send message
if: steps.check-changes.outputs.changes == 'false'
- uses: thollander/actions-comment-pull-request@v2
+ uses: thollander/actions-comment-pull-request@v3
with:
- comment_tag: check-misskey-js-autogen
+ comment-tag: check-misskey-js-autogen
mode: delete
message: "Thank you!"
create_if_not_exists: false
diff --git a/.github/workflows/check-misskey-js-version.yml b/.github/workflows/check-misskey-js-version.yml
index 2b15cbee53..ad07d47b65 100644
--- a/.github/workflows/check-misskey-js-version.yml
+++ b/.github/workflows/check-misskey-js-version.yml
@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v6.0.1
- name: Check version
run: |
if [ "$(jq -r '.version' package.json)" != "$(jq -r '.version' packages/misskey-js/package.json)" ]; then
diff --git a/.github/workflows/check-spdx-license-id.yml b/.github/workflows/check-spdx-license-id.yml
index cf1fd6007d..fe71473ea3 100644
--- a/.github/workflows/check-spdx-license-id.yml
+++ b/.github/workflows/check-spdx-license-id.yml
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v6.0.1
- name: Check
run: |
counter=0
diff --git a/.github/workflows/check_copyright_year.yml b/.github/workflows/check_copyright_year.yml
index eaf922d4bc..40016d39c5 100644
--- a/.github/workflows/check_copyright_year.yml
+++ b/.github/workflows/check_copyright_year.yml
@@ -10,7 +10,7 @@ jobs:
check_copyright_year:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4.2.2
+ - uses: actions/checkout@v6.0.1
- run: |
if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then
echo "Please change copyright year!"
diff --git a/.github/workflows/deploy-test-environment.yml b/.github/workflows/deploy-test-environment.yml
index 46baf7421b..32c7c6b6ea 100644
--- a/.github/workflows/deploy-test-environment.yml
+++ b/.github/workflows/deploy-test-environment.yml
@@ -28,7 +28,7 @@ jobs:
wait_time: ${{ steps.get-wait-time.outputs.wait_time }}
steps:
- name: Checkout
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v6.0.1
- name: Check allowed users
id: check-allowed-users
diff --git a/.github/workflows/docker-develop.yml b/.github/workflows/docker-develop.yml
index 56dedf273d..8a97959907 100644
--- a/.github/workflows/docker-develop.yml
+++ b/.github/workflows/docker-develop.yml
@@ -27,7 +27,7 @@ jobs:
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Check out the repo
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v6.0.1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
@@ -53,7 +53,7 @@ jobs:
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v6
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
@@ -66,7 +66,7 @@ jobs:
- build
steps:
- name: Download digests
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v7
with:
path: /tmp/digests
pattern: digests-*
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index eb98273ba0..37f6aca588 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -32,7 +32,7 @@ jobs:
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Check out the repo
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v6.0.1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
@@ -64,7 +64,7 @@ jobs:
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v6
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
@@ -77,7 +77,7 @@ jobs:
- build
steps:
- name: Download digests
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v7
with:
path: /tmp/digests
pattern: digests-*
diff --git a/.github/workflows/dockle.yml b/.github/workflows/dockle.yml
index f006a45ea4..ec7073c9fd 100644
--- a/.github/workflows/dockle.yml
+++ b/.github/workflows/dockle.yml
@@ -11,22 +11,43 @@ on:
jobs:
dockle:
runs-on: ubuntu-latest
+
env:
DOCKER_CONTENT_TRUST: 1
- DOCKLE_VERSION: 0.4.14
+ DOCKLE_VERSION: 0.4.15
+
steps:
- - uses: actions/checkout@v4.2.2
+ - uses: actions/checkout@v6.0.1
+
- name: Download and install dockle v${{ env.DOCKLE_VERSION }}
run: |
+ set -eux
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 ./compose_example.yml ./compose.yml
- - run: |
- docker compose up -d web
- docker tag "$(docker compose images --format json web | jq -r '.[] | .ID')" misskey-web:latest
- - run: |
- cmd="dockle --exit-code 1 misskey-web:latest ${image_name}"
- echo "> ${cmd}"
- eval "${cmd}"
+
+ - name: Build web image (docker build)
+ run: |
+ set -eux
+ docker build -t "misskey-web:ci" .
+ docker image ls
+
+ - name: Mount tmpfs for Dockle tar
+ env:
+ TMPFS_SIZE: 8G
+ run: |
+ set -eux
+ sudo mkdir -p /mnt/dockle-tmp
+ sudo mount -t tmpfs -o size=${{ env.TMPFS_SIZE }} tmpfs /mnt/dockle-tmp
+ free -h
+ df -h
+
+ - name: Save image tar into tmpfs
+ run: |
+ set -eux
+ docker save misskey-web:ci -o /mnt/dockle-tmp/misskey-web.tar
+ ls -lh /mnt/dockle-tmp/misskey-web.tar
+
+ - name: Run Dockle Scan (tar input)
+ run: |
+ set -eux
+ dockle --exit-code 1 --input /mnt/dockle-tmp/misskey-web.tar
diff --git a/.github/workflows/get-api-diff.yml b/.github/workflows/get-api-diff.yml
index 933404dfa5..f8a0c4aaa4 100644
--- a/.github/workflows/get-api-diff.yml
+++ b/.github/workflows/get-api-diff.yml
@@ -25,14 +25,14 @@ jobs:
ref: refs/pull/${{ github.event.number }}/merge
steps:
- - uses: actions/checkout@v4.2.2
+ - uses: actions/checkout@v6.0.1
with:
ref: ${{ matrix.ref }}
submodules: true
- name: Setup pnpm
- uses: pnpm/action-setup@v4.1.0
+ uses: pnpm/action-setup@v4.2.0
- name: Use Node.js
- uses: actions/setup-node@v4.4.0
+ uses: actions/setup-node@v6.1.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
@@ -48,7 +48,7 @@ jobs:
- name: Copy API.json
run: cp packages/backend/built/api.json ${{ matrix.api-json-name }}
- name: Upload Artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v6
with:
name: api-artifact-${{ matrix.api-json-name }}
path: ${{ matrix.api-json-name }}
@@ -61,7 +61,7 @@ jobs:
PR_NUMBER: ${{ github.event.number }}
run: |
echo "$PR_NUMBER" > ./pr_number
- - uses: actions/upload-artifact@v4
+ - uses: actions/upload-artifact@v6
with:
name: api-artifact-pr-number
path: pr_number
diff --git a/.github/workflows/get-backend-memory.yml b/.github/workflows/get-backend-memory.yml
new file mode 100644
index 0000000000..99f89631bb
--- /dev/null
+++ b/.github/workflows/get-backend-memory.yml
@@ -0,0 +1,87 @@
+# this name is used in report-backend-memory.yml so be careful when change name
+name: Get backend memory usage
+
+on:
+ pull_request:
+ branches:
+ - master
+ - develop
+ paths:
+ - packages/backend/**
+ - packages/misskey-js/**
+ - .github/workflows/get-backend-memory.yml
+
+jobs:
+ get-memory-usage:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+
+ strategy:
+ matrix:
+ memory-json-name: [memory-base.json, memory-head.json]
+ include:
+ - memory-json-name: memory-base.json
+ ref: ${{ github.base_ref }}
+ - memory-json-name: memory-head.json
+ ref: refs/pull/${{ github.event.number }}/merge
+
+ services:
+ postgres:
+ image: postgres:18
+ ports:
+ - 54312:5432
+ env:
+ POSTGRES_DB: test-misskey
+ POSTGRES_HOST_AUTH_METHOD: trust
+ redis:
+ image: redis:7
+ ports:
+ - 56312:6379
+
+ steps:
+ - uses: actions/checkout@v6.0.1
+ with:
+ ref: ${{ matrix.ref }}
+ submodules: true
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4.2.0
+ - name: Use Node.js
+ uses: actions/setup-node@v6.1.0
+ with:
+ node-version-file: '.node-version'
+ cache: 'pnpm'
+ - run: pnpm i --frozen-lockfile
+ - name: Check pnpm-lock.yaml
+ run: git diff --exit-code pnpm-lock.yaml
+ - name: Copy Configure
+ run: cp .github/misskey/test.yml .config/default.yml
+ - name: Compile Configure
+ run: pnpm compile-config
+ - name: Build
+ run: pnpm build
+ - name: Run migrations
+ run: pnpm --filter backend migrate
+ - name: Measure memory usage
+ run: |
+ # Start the server and measure memory usage
+ node packages/backend/scripts/measure-memory.mjs > ${{ matrix.memory-json-name }}
+ - name: Upload Artifact
+ uses: actions/upload-artifact@v6
+ with:
+ name: memory-artifact-${{ matrix.memory-json-name }}
+ path: ${{ matrix.memory-json-name }}
+
+ save-pr-number:
+ runs-on: ubuntu-latest
+ permissions: {}
+ steps:
+ - name: Save PR number
+ env:
+ PR_NUMBER: ${{ github.event.number }}
+ run: |
+ echo "$PR_NUMBER" > ./pr_number
+ - uses: actions/upload-artifact@v6
+ with:
+ name: memory-artifact-pr-number
+ path: pr_number
diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml
index 88e2aceaed..5787572dd5 100644
--- a/.github/workflows/labeler.yml
+++ b/.github/workflows/labeler.yml
@@ -11,6 +11,6 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- - uses: actions/labeler@v5
+ - uses: actions/labeler@v6
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 550438e308..91cbe52c38 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -36,13 +36,13 @@ jobs:
pnpm_install:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4.2.2
+ - uses: actions/checkout@v6.0.1
with:
fetch-depth: 0
submodules: true
- name: Setup pnpm
- uses: pnpm/action-setup@v4.1.0
- - uses: actions/setup-node@v4.4.0
+ uses: pnpm/action-setup@v4.2.0
+ - uses: actions/setup-node@v6.1.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
@@ -69,19 +69,19 @@ jobs:
eslint-cache-version: v1
eslint-cache-path: ${{ github.workspace }}/node_modules/.cache/eslint-${{ matrix.workspace }}
steps:
- - uses: actions/checkout@v4.2.2
+ - uses: actions/checkout@v6.0.1
with:
fetch-depth: 0
submodules: true
- name: Setup pnpm
- uses: pnpm/action-setup@v4.1.0
- - uses: actions/setup-node@v4.4.0
+ uses: pnpm/action-setup@v4.2.0
+ - uses: actions/setup-node@v6.1.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
- run: pnpm i --frozen-lockfile
- name: Restore eslint cache
- uses: actions/cache@v4.2.3
+ uses: actions/cache@v4.3.0
with:
path: ${{ env.eslint-cache-path }}
key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}
@@ -96,22 +96,20 @@ jobs:
matrix:
workspace:
- backend
+ - frontend
- sw
- misskey-js
steps:
- - uses: actions/checkout@v4.2.2
+ - uses: actions/checkout@v6.0.1
with:
fetch-depth: 0
submodules: true
- name: Setup pnpm
- uses: pnpm/action-setup@v4.1.0
- - uses: actions/setup-node@v4.4.0
+ uses: pnpm/action-setup@v4.2.0
+ - uses: actions/setup-node@v6.1.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
- run: pnpm i --frozen-lockfile
- - run: pnpm --filter misskey-js run build
- if: ${{ matrix.workspace == 'backend' || matrix.workspace == 'sw' }}
- - run: pnpm --filter misskey-reversi run build
- if: ${{ matrix.workspace == 'backend' }}
+ - run: pnpm --filter "${{ matrix.workspace }}^..." run build
- run: pnpm --filter ${{ matrix.workspace }} run typecheck
diff --git a/.github/workflows/locale.yml b/.github/workflows/locale.yml
index 68e45fdf61..15cc9153f6 100644
--- a/.github/workflows/locale.yml
+++ b/.github/workflows/locale.yml
@@ -3,10 +3,12 @@ name: Lint
on:
push:
paths:
+ - packages/i18n/**
- locales/**
- .github/workflows/locale.yml
pull_request:
paths:
+ - packages/i18n/**
- locales/**
- .github/workflows/locale.yml
jobs:
@@ -14,15 +16,18 @@ jobs:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- - uses: actions/checkout@v4.2.2
- with:
- fetch-depth: 0
- submodules: true
- - name: Setup pnpm
- uses: pnpm/action-setup@v4.1.0
- - uses: actions/setup-node@v4.4.0
- with:
- node-version-file: '.node-version'
- cache: 'pnpm'
- - run: pnpm i --frozen-lockfile
- - run: cd locales && node verify.js
+ - uses: actions/checkout@v6.0.1
+ with:
+ fetch-depth: 0
+ submodules: true
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4.2.0
+ - uses: actions/setup-node@v6.1.0
+ with:
+ node-version-file: ".node-version"
+ cache: "pnpm"
+ - run: pnpm i --frozen-lockfile
+ - run: pnpm --filter i18n build
+ - name: Verify Locales
+ working-directory: ./packages/i18n
+ run: pnpm run verify
diff --git a/.github/workflows/on-release-created.yml b/.github/workflows/on-release-created.yml
index 7787d6055b..c9a47385a0 100644
--- a/.github/workflows/on-release-created.yml
+++ b/.github/workflows/on-release-created.yml
@@ -16,13 +16,13 @@ jobs:
id-token: write
steps:
- - uses: actions/checkout@v4.2.2
+ - uses: actions/checkout@v6.0.1
with:
submodules: true
- name: Setup pnpm
- uses: pnpm/action-setup@v4.1.0
+ uses: pnpm/action-setup@v4.2.0
- name: Use Node.js
- uses: actions/setup-node@v4.4.0
+ uses: actions/setup-node@v6.1.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
diff --git a/.github/workflows/release-edit-with-push.yml b/.github/workflows/release-edit-with-push.yml
index 57657a4ba7..bc16dbcef2 100644
--- a/.github/workflows/release-edit-with-push.yml
+++ b/.github/workflows/release-edit-with-push.yml
@@ -19,7 +19,7 @@ jobs:
edit:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
# headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得
- name: Get PR
run: |
diff --git a/.github/workflows/release-with-dispatch.yml b/.github/workflows/release-with-dispatch.yml
index d750001b71..f318200584 100644
--- a/.github/workflows/release-with-dispatch.yml
+++ b/.github/workflows/release-with-dispatch.yml
@@ -36,7 +36,7 @@ jobs:
outputs:
pr_number: ${{ steps.get_pr.outputs.pr_number }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
# headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得
- name: Get PRs
run: |
diff --git a/.github/workflows/report-api-diff.yml b/.github/workflows/report-api-diff.yml
index 1170f898ce..59b92d022e 100644
--- a/.github/workflows/report-api-diff.yml
+++ b/.github/workflows/report-api-diff.yml
@@ -16,7 +16,7 @@ jobs:
# api-artifact
steps:
- name: Download artifact
- uses: actions/github-script@v7.0.1
+ uses: actions/github-script@v8.0.0
with:
script: |
const fs = require('fs');
@@ -60,7 +60,7 @@ jobs:
- name: Echo full diff
run: cat ./api-full.json.diff
- name: Upload full diff to Artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v6
with:
name: api-artifact
path: |
@@ -73,9 +73,9 @@ jobs:
HEADER="このPRによるapi.jsonの差分"
FOOTER="[Get diff files from Workflow Page](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})"
DIFF_BYTES="$(stat ./api.json.diff -c '%s' | tr -d '\n')"
-
+
echo "$HEADER" > ./output.md
-
+
if (( "$DIFF_BYTES" <= 1 )); then
echo '差分はありません。' >> ./output.md
else
@@ -87,18 +87,18 @@ jobs:
echo '```' >> ./output.md
echo '' >> .output.md
fi
-
+
echo "$FOOTER" >> ./output.md
- - uses: thollander/actions-comment-pull-request@v2
+ - uses: thollander/actions-comment-pull-request@v3
with:
- pr_number: ${{ steps.load-pr-num.outputs.pr-number }}
- comment_tag: show_diff
- filePath: ./output.md
+ pr-number: ${{ steps.load-pr-num.outputs.pr-number }}
+ comment-tag: show_diff
+ file-path: ./output.md
- name: Tell error to PR
- uses: thollander/actions-comment-pull-request@v2
+ uses: thollander/actions-comment-pull-request@v3
if: failure() && steps.load-pr-num.outputs.pr-number
with:
- pr_number: ${{ steps.load-pr-num.outputs.pr-number }}
- comment_tag: show_diff_error
+ pr-number: ${{ steps.load-pr-num.outputs.pr-number }}
+ comment-tag: show_diff_error
message: |
api.jsonの差分作成中にエラーが発生しました。詳細は[Workflowのログ](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})を確認してください。
diff --git a/.github/workflows/report-backend-memory.yml b/.github/workflows/report-backend-memory.yml
new file mode 100644
index 0000000000..bf2e311c83
--- /dev/null
+++ b/.github/workflows/report-backend-memory.yml
@@ -0,0 +1,177 @@
+name: Report backend memory
+
+on:
+ workflow_run:
+ types: [completed]
+ workflows:
+ - Get backend memory usage # get-backend-memory.yml
+
+jobs:
+ compare-memory:
+ runs-on: ubuntu-latest
+ if: ${{ github.event.workflow_run.conclusion == 'success' }}
+ permissions:
+ pull-requests: write
+
+ steps:
+ - name: Download artifact
+ uses: actions/github-script@v8.0.0
+ with:
+ script: |
+ const fs = require('fs');
+ let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ run_id: context.payload.workflow_run.id,
+ });
+ let matchArtifacts = allArtifacts.data.artifacts.filter((artifact) => {
+ return artifact.name.startsWith("memory-artifact-") || artifact.name == "memory-artifact"
+ });
+ await Promise.all(matchArtifacts.map(async (artifact) => {
+ let download = await github.rest.actions.downloadArtifact({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ artifact_id: artifact.id,
+ archive_format: 'zip',
+ });
+ await fs.promises.writeFile(`${process.env.GITHUB_WORKSPACE}/${artifact.name}.zip`, Buffer.from(download.data));
+ }));
+ - name: Extract all artifacts
+ run: |
+ find . -mindepth 1 -maxdepth 1 -type f -name '*.zip' -exec unzip {} -d artifacts ';'
+ ls -la artifacts/
+ - name: Load PR Number
+ id: load-pr-num
+ run: echo "pr-number=$(cat artifacts/pr_number)" >> "$GITHUB_OUTPUT"
+
+ - name: Output base
+ run: cat ./artifacts/memory-base.json
+ - name: Output head
+ run: cat ./artifacts/memory-head.json
+ - name: Compare memory usage
+ id: compare
+ run: |
+ BASE_MEMORY=$(cat ./artifacts/memory-base.json)
+ HEAD_MEMORY=$(cat ./artifacts/memory-head.json)
+
+ variation() {
+ calc() {
+ BASE=$(echo "$BASE_MEMORY" | jq -r ".${1}.${2} // 0")
+ HEAD=$(echo "$HEAD_MEMORY" | jq -r ".${1}.${2} // 0")
+
+ DIFF=$((HEAD - BASE))
+ if [ "$BASE" -gt 0 ]; then
+ DIFF_PERCENT=$(echo "scale=2; ($DIFF * 100) / $BASE" | bc)
+ else
+ DIFF_PERCENT=0
+ fi
+
+ # Convert KB to MB for readability
+ BASE_MB=$(echo "scale=2; $BASE / 1024" | bc)
+ HEAD_MB=$(echo "scale=2; $HEAD / 1024" | bc)
+ DIFF_MB=$(echo "scale=2; $DIFF / 1024" | bc)
+
+ JSON=$(jq -c -n \
+ --argjson base "$BASE_MB" \
+ --argjson head "$HEAD_MB" \
+ --argjson diff "$DIFF_MB" \
+ --argjson diff_percent "$DIFF_PERCENT" \
+ '{base: $base, head: $head, diff: $diff, diff_percent: $diff_percent}')
+
+ echo "$JSON"
+ }
+
+ JSON=$(jq -c -n \
+ --argjson VmRSS "$(calc $1 VmRSS)" \
+ --argjson VmHWM "$(calc $1 VmHWM)" \
+ --argjson VmSize "$(calc $1 VmSize)" \
+ --argjson VmData "$(calc $1 VmData)" \
+ '{VmRSS: $VmRSS, VmHWM: $VmHWM, VmSize: $VmSize, VmData: $VmData}')
+
+ echo "$JSON"
+ }
+
+ JSON=$(jq -c -n \
+ --argjson beforeGc "$(variation beforeGc)" \
+ --argjson afterGc "$(variation afterGc)" \
+ --argjson afterRequest "$(variation afterRequest)" \
+ '{beforeGc: $beforeGc, afterGc: $afterGc, afterRequest: $afterRequest}')
+
+ echo "res=$JSON" >> "$GITHUB_OUTPUT"
+ - id: build-comment
+ name: Build memory comment
+ env:
+ RES: ${{ steps.compare.outputs.res }}
+ run: |
+ HEADER="## Backend memory usage comparison"
+ FOOTER="[See workflow logs for details](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})"
+
+ echo "$HEADER" > ./output.md
+ echo >> ./output.md
+
+ table() {
+ echo "| Metric | base (MB) | head (MB) | Diff (MB) | Diff (%) |" >> ./output.md
+ echo "|--------|------:|------:|------:|------:|" >> ./output.md
+
+ line() {
+ METRIC=$2
+ BASE=$(echo "$RES" | jq -r ".${1}.${2}.base")
+ HEAD=$(echo "$RES" | jq -r ".${1}.${2}.head")
+ DIFF=$(echo "$RES" | jq -r ".${1}.${2}.diff")
+ DIFF_PERCENT=$(echo "$RES" | jq -r ".${1}.${2}.diff_percent")
+
+ if (( $(echo "$DIFF_PERCENT > 0" | bc -l) )); then
+ DIFF="+$DIFF"
+ DIFF_PERCENT="+$DIFF_PERCENT"
+ fi
+
+ # highlight VmRSS
+ if [ "$2" = "VmRSS" ]; then
+ METRIC="**${METRIC}**"
+ BASE="**${BASE}**"
+ HEAD="**${HEAD}**"
+ DIFF="**${DIFF}**"
+ DIFF_PERCENT="**${DIFF_PERCENT}**"
+ fi
+
+ echo "| ${METRIC} | ${BASE} MB | ${HEAD} MB | ${DIFF} MB | ${DIFF_PERCENT}% |" >> ./output.md
+ }
+
+ line $1 VmRSS
+ line $1 VmHWM
+ line $1 VmSize
+ line $1 VmData
+ }
+
+ echo "### Before GC" >> ./output.md
+ table beforeGc
+ echo >> ./output.md
+
+ echo "### After GC" >> ./output.md
+ table afterGc
+ echo >> ./output.md
+
+ echo "### After Request" >> ./output.md
+ table afterRequest
+ echo >> ./output.md
+
+ # Determine if this is a significant change (more than 5% increase)
+ if [ "$(echo "$RES" | jq -r '.afterGc.VmRSS.diff_percent | tonumber > 5')" = "true" ]; then
+ echo "⚠️ **Warning**: Memory usage has increased by more than 5%. Please verify this is not an unintended change." >> ./output.md
+ echo >> ./output.md
+ fi
+
+ echo "$FOOTER" >> ./output.md
+ - uses: thollander/actions-comment-pull-request@v3
+ with:
+ pr-number: ${{ steps.load-pr-num.outputs.pr-number }}
+ comment-tag: show_memory_diff
+ file-path: ./output.md
+ - name: Tell error to PR
+ uses: thollander/actions-comment-pull-request@v3
+ if: failure() && steps.load-pr-num.outputs.pr-number
+ with:
+ pr-number: ${{ steps.load-pr-num.outputs.pr-number }}
+ comment-tag: show_memory_diff_error
+ message: |
+ An error occurred while comparing backend memory usage. See [workflow logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.
diff --git a/.github/workflows/request-release-review.yml b/.github/workflows/request-release-review.yml
new file mode 100644
index 0000000000..9b6768149b
--- /dev/null
+++ b/.github/workflows/request-release-review.yml
@@ -0,0 +1,51 @@
+name: Request release review
+
+on:
+ issue_comment:
+ types: [created]
+
+jobs:
+ reply:
+ if: github.event.comment.body == '/request-release-review'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ issues: write
+ pull-requests: write
+ steps:
+ - name: Reply
+ uses: actions/github-script@v8
+ with:
+ script: |
+ const body = `To dev team (@misskey-dev/dev):
+
+ リリースが提案されています :rocket:
+
+ GOの場合はapprove、NO GOの場合はその旨コメントをお願いいたします。
+
+ 判断にあたって考慮すべき観点は、
+
+ - やり残したことはないか?
+ - CHANGELOGは過不足ないか?
+ - バージョンに問題はないか?(月跨いでいるのに更新忘れているなど)
+ - 再考すべき仕様・実装はないか?
+ - ベータ版を検証したサーバーから不具合の報告等は上がってないか?
+ - (セキュリティの修正や重要なバグ修正などのため)リリースを急いだ方が良いか?そうではないか?
+ - Actionsが落ちていないか?
+
+ などが挙げられます。
+
+ ご協力ありがとうございます :sparkles:
+ `
+
+ const issue_number = context.payload.issue ? context.payload.issue.number : (context.payload.pull_request && context.payload.pull_request.number)
+ if (!issue_number) {
+ console.log('No issue or PR number found in payload; skipping')
+ } else {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number,
+ body,
+ })
+ }
diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml
index b1d95c1b33..c28b1f6e93 100644
--- a/.github/workflows/storybook.yml
+++ b/.github/workflows/storybook.yml
@@ -22,12 +22,12 @@ jobs:
NODE_OPTIONS: "--max_old_space_size=7168"
steps:
- - uses: actions/checkout@v4.2.2
+ - uses: actions/checkout@v6.0.1
if: github.event_name != 'pull_request_target'
with:
fetch-depth: 0
submodules: true
- - uses: actions/checkout@v4.2.2
+ - uses: actions/checkout@v6.0.1
if: github.event_name == 'pull_request_target'
with:
fetch-depth: 0
@@ -37,9 +37,9 @@ jobs:
if: github.event_name == 'pull_request_target'
run: git checkout "$(git rev-list --parents -n1 HEAD | cut -d" " -f3)"
- name: Setup pnpm
- uses: pnpm/action-setup@v4.1.0
+ uses: pnpm/action-setup@v4.2.0
- name: Use Node.js
- uses: actions/setup-node@v4.4.0
+ uses: actions/setup-node@v6.1.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
@@ -90,7 +90,7 @@ jobs:
env:
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
- name: Notify that Chromatic detects changes
- uses: actions/github-script@v7.0.1
+ uses: actions/github-script@v8.0.0
if: github.event_name != 'pull_request_target' && steps.chromatic_push.outputs.success == 'false'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -102,7 +102,7 @@ jobs:
body: 'Chromatic detects changes. Please [review the changes on Chromatic](https://www.chromatic.com/builds?appId=6428f7d7b962f0b79f97d6e4).'
})
- name: Upload Artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v6
with:
name: storybook
path: packages/frontend/storybook-static
diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml
index 5358df3dc4..77bbdb2b0a 100644
--- a/.github/workflows/test-backend.yml
+++ b/.github/workflows/test-backend.yml
@@ -38,7 +38,7 @@ jobs:
services:
postgres:
- image: postgres:15
+ image: postgres:18
ports:
- 54312:5432
env:
@@ -48,13 +48,20 @@ jobs:
image: redis:7
ports:
- 56312:6379
+ meilisearch:
+ image: getmeili/meilisearch:v1.3.4
+ ports:
+ - 57712:7700
+ env:
+ MEILI_NO_ANALYTICS: true
+ MEILI_ENV: development
steps:
- - uses: actions/checkout@v4.2.2
+ - uses: actions/checkout@v6.0.1
with:
submodules: true
- name: Setup pnpm
- uses: pnpm/action-setup@v4.1.0
+ uses: pnpm/action-setup@v4.2.0
- name: Get current date
id: current-date
run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
@@ -86,7 +93,7 @@ jobs:
fi
done
- name: Use Node.js
- uses: actions/setup-node@v4.4.0
+ uses: actions/setup-node@v6.1.0
with:
node-version-file: ${{ matrix.node-version-file }}
cache: 'pnpm'
@@ -117,7 +124,7 @@ jobs:
services:
postgres:
- image: postgres:15
+ image: postgres:18
ports:
- 54312:5432
env:
@@ -129,13 +136,13 @@ jobs:
- 56312:6379
steps:
- - uses: actions/checkout@v4.2.2
+ - uses: actions/checkout@v6.0.1
with:
submodules: true
- name: Setup pnpm
- uses: pnpm/action-setup@v4.1.0
+ uses: pnpm/action-setup@v4.2.0
- name: Use Node.js
- uses: actions/setup-node@v4.4.0
+ uses: actions/setup-node@v6.1.0
with:
node-version-file: ${{ matrix.node-version-file }}
cache: 'pnpm'
@@ -165,7 +172,7 @@ jobs:
services:
postgres:
- image: postgres:15
+ image: postgres:18
ports:
- 54312:5432
env:
@@ -173,16 +180,16 @@ jobs:
POSTGRES_HOST_AUTH_METHOD: trust
steps:
- - uses: actions/checkout@v4.2.2
+ - uses: actions/checkout@v6.0.1
with:
submodules: true
- name: Setup pnpm
- uses: pnpm/action-setup@v4.1.0
+ uses: pnpm/action-setup@v4.2.0
- name: Get current date
id: current-date
run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Use Node.js
- uses: actions/setup-node@v4.4.0
+ uses: actions/setup-node@v6.1.0
with:
node-version-file: ${{ matrix.node-version-file }}
cache: 'pnpm'
diff --git a/.github/workflows/test-federation.yml b/.github/workflows/test-federation.yml
index 873396f622..7f8fe547e1 100644
--- a/.github/workflows/test-federation.yml
+++ b/.github/workflows/test-federation.yml
@@ -32,11 +32,11 @@ jobs:
- .node-version
- .github/min.node-version
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
with:
submodules: true
- name: Setup pnpm
- uses: pnpm/action-setup@v4.1.0
+ uses: pnpm/action-setup@v4.2.0
- name: Get current date
id: current-date
run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
@@ -68,7 +68,7 @@ jobs:
fi
done
- name: Use Node.js
- uses: actions/setup-node@v4.4.0
+ uses: actions/setup-node@v6.1.0
with:
node-version-file: ${{ matrix.node-version-file }}
cache: 'pnpm'
diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml
index 94e43cf91e..52723e894c 100644
--- a/.github/workflows/test-frontend.yml
+++ b/.github/workflows/test-frontend.yml
@@ -28,13 +28,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4.2.2
+ - uses: actions/checkout@v6.0.1
with:
submodules: true
- name: Setup pnpm
- uses: pnpm/action-setup@v4.1.0
+ uses: pnpm/action-setup@v4.2.0
- name: Use Node.js
- uses: actions/setup-node@v4.4.0
+ uses: actions/setup-node@v6.1.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
@@ -64,7 +64,7 @@ jobs:
services:
postgres:
- image: postgres:15
+ image: postgres:18
ports:
- 54312:5432
env:
@@ -76,7 +76,7 @@ jobs:
- 56312:6379
steps:
- - uses: actions/checkout@v4.2.2
+ - uses: actions/checkout@v6.0.1
with:
submodules: true
# https://github.com/cypress-io/cypress-docker-images/issues/150
@@ -86,9 +86,9 @@ jobs:
#- uses: browser-actions/setup-firefox@latest
# if: ${{ matrix.browser == 'firefox' }}
- name: Setup pnpm
- uses: pnpm/action-setup@v4.1.0
+ uses: pnpm/action-setup@v4.2.0
- name: Use Node.js
- uses: actions/setup-node@v4.4.0
+ uses: actions/setup-node@v6.1.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
@@ -113,12 +113,12 @@ jobs:
wait-on: 'http://localhost:61812'
headed: true
browser: ${{ matrix.browser }}
- - uses: actions/upload-artifact@v4
+ - uses: actions/upload-artifact@v6
if: failure()
with:
name: ${{ matrix.browser }}-cypress-screenshots
path: cypress/screenshots
- - uses: actions/upload-artifact@v4
+ - uses: actions/upload-artifact@v6
if: always()
with:
name: ${{ matrix.browser }}-cypress-videos
diff --git a/.github/workflows/test-misskey-js.yml b/.github/workflows/test-misskey-js.yml
index f6d16bbd76..428cbce3b8 100644
--- a/.github/workflows/test-misskey-js.yml
+++ b/.github/workflows/test-misskey-js.yml
@@ -22,13 +22,13 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v6.0.1
- name: Setup pnpm
- uses: pnpm/action-setup@v4.1.0
+ uses: pnpm/action-setup@v4.2.0
- name: Setup Node.js
- uses: actions/setup-node@v4.4.0
+ uses: actions/setup-node@v6.1.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
diff --git a/.github/workflows/test-production.yml b/.github/workflows/test-production.yml
index 751c374608..9c0ea4d738 100644
--- a/.github/workflows/test-production.yml
+++ b/.github/workflows/test-production.yml
@@ -16,13 +16,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4.2.2
+ - uses: actions/checkout@v6.0.1
with:
submodules: true
- name: Setup pnpm
- uses: pnpm/action-setup@v4.1.0
+ uses: pnpm/action-setup@v4.2.0
- name: Use Node.js
- uses: actions/setup-node@v4.4.0
+ uses: actions/setup-node@v6.1.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
diff --git a/.github/workflows/validate-api-json.yml b/.github/workflows/validate-api-json.yml
index edff7dbecb..8ffc60fc6e 100644
--- a/.github/workflows/validate-api-json.yml
+++ b/.github/workflows/validate-api-json.yml
@@ -17,13 +17,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4.2.2
+ - uses: actions/checkout@v6.0.1
with:
submodules: true
- name: Setup pnpm
- uses: pnpm/action-setup@v4.1.0
+ uses: pnpm/action-setup@v4.2.0
- name: Use Node.js
- uses: actions/setup-node@v4.4.0
+ uses: actions/setup-node@v6.1.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
diff --git a/.gitignore b/.gitignore
index ac7502f384..7839e4de66 100644
--- a/.gitignore
+++ b/.gitignore
@@ -46,6 +46,7 @@ docker-compose.yml
built
built-test
js-built
+src-js
/data
/.cache-loader
/db
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 5f36a32af4..2d11d24db2 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -3,6 +3,7 @@
"**/node_modules": true
},
"typescript.tsdk": "node_modules/typescript/lib",
+ "typescript.enablePromptUseWorkspaceTsdk": true,
"files.associations": {
"*.test.ts": "typescript"
},
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c860ed44eb..376c17d818 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,236 @@
+## 2026.1.0
+
+### Note
+- `users/following` の `birthday` プロパティは非推奨になりました。代わりに `users/get-following-birthday-users` をご利用ください。
+
+### General
+- Enhance: 「もうすぐ誕生日のユーザー」ウィジェットで、誕生日が至近のユーザーも表示できるように
+ (Cherry-picked from https://github.com/MisskeyIO/misskey)
+ - 「今日誕生日のユーザー」は「もうすぐ誕生日のユーザー」に名称変更されました
+- 依存関係の更新
+
+### Client
+- Enhance: ドライブのファイル一覧で自動でもっと見るを利用可能に
+- Enhance: ウィジェットの表示設定をプレビューを見ながら行えるように
+- Enhance: ウィジェットの設定項目のラベルの多言語対応
+- Enhance: 画面幅が広いときにメディアを横並びで表示できるようにするオプションを追加
+- Enhance: パフォーマンスの向上
+- Fix: ドライブクリーナーでファイルを削除しても画面に反映されない問題を修正 #16061
+- Fix: 非ログイン時にログインを求めるダイアログが表示された後にダイアログのぼかしが解除されず操作不能になることがある問題を修正
+- Fix: ドライブのソートが「登録日(昇順)」の場合に正しく動作しない問題を修正
+- Fix: 高度なMFMのピッカーを使用する際の挙動を改善
+- Fix: 管理画面でアーカイブ済のお知らせを表示した際にアクティブなお知らせが多い旨の警告が出る問題を修正
+- Fix: ファイルタブのセンシティブメディアを開く際に確認ダイアログを出す設定が適用されない問題を修正
+- Fix: 2月29日を誕生日に設定している場合、閏年以外は3月1日を誕生日として扱うように修正
+- Fix: `Mk:C:container` の `borderWidth` が正しく反映されない問題を修正
+
+### Server
+- Enhance: OAuthのクライアント情報取得(Client Information Discovery)において、IndieWeb Living Standard 11 July 2024で定義されているJSONドキュメント形式に対応しました
+ - JSONによるClient Information Discoveryを行うには、レスポンスの`Content-Type`ヘッダーが`application/json`である必要があります
+ - 従来の実装(12 February 2022版・HTML Microformat形式)も引き続きサポートされます
+- Enhance: メモリ使用量を削減
+
+## 2025.12.2
+
+### Note
+v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`に変更」について、正しく環境に応じた設定を行わないとサインインが困難になるといった状態を緩和するために、以下の対応を行いました。
+
+**正しく設定しないと、上記のような不具合の原因となったり、セキュリティリスクが高まったりする可能性があります。必ず現在のconfigをご確認の上、必要に応じて値を変更してください。**
+
+- `trustProxy`について、デフォルト(configに値が設定されていない状態)ではループバックアドレスとローカルIPアドレス空間を信頼するようにしました。
+- `trustProxy`の設定方法について、より詳細に記述しました。
+- リバースプロキシやCDNなどのより上流のレイヤでレートリミットを設定したい場合や、緊急時の一時的な緩和策として、Misskey内部でのIPアドレスペースでのレートリミットを無効化できるようにしました。
+
+### General
+- 依存関係の更新
+
+### Client
+- Enhance: デッキのUI説明を追加
+- Enhance: 設定がブラウザによって消去されないようにするオプションを追加
+- Fix: バージョン表記のないPlayが正しく動作しない問題を修正
+ バージョン表記のないものは v0.x 系として実行されます。v1.x 系で動作させたい場合は必ずバージョン表記を含めてください。
+- Fix: デッキUIでメニュー位置を下にしているとプロファイル削除ボタンが表示されないのを修正
+- Fix: 一部のUnicode絵文字のリアクションがボタンにならない問題を修正
+
+### Server
+- Enhance: Misskey内部でのIPアドレスペースでのレートリミットを無効化できるように
+ - リバースプロキシやCDNなど別のレイヤで別途レートリミットを設定する場合や、ローカルでのテスト用途等として利用することを想定しています。
+ - デフォルトは `enableIpRateLimit: true`(Misskey内部でのIPアドレスペースでのレートリミットは有効)です。
+- Fix: コントロールパネルのジョブキューページで使用される一部APIの応答速度を改善
+
+## 2025.12.1
+
+### Client
+- Fix: 特定の条件下でMisskeyが起動せず空白のページが表示されることがある問題を軽減
+- Fix: 初回読み込み時などに、言語設定で不整合が発生することがある問題を修正
+- Fix: 削除されたノートのリノートが正しく動作されない問題を修正
+- Fix: チャンネルオーナーが削除済みの時にチャンネルのヘッダーメニューが表示されない不具合を修正
+- Fix: ドライブで登録日以外でソートする場合は月でグループ化して表示しないように
+- Fix: `null` を返す note_view_intrruptor プラグインが動作しない問題を修正
+
+### Server
+- Fix: ジョブキューでSentryが有効にならない問題を修正
+
+
+## 2025.12.0
+
+### Note
+- configの`trustProxy`のデフォルト値を`false`に変更しました。アップデート前に現在のconfigをご確認の上、必要に応じて値を変更してください。
+
+### Client
+- Fix: stacking router viewで連続して戻る操作を行うと何も表示されなくなる問題を修正
+
+### Server
+- Enhance: メモリ使用量を削減しました
+- Enhance: ActivityPubアクティビティを送信する際のパフォーマンス向上
+- Enhance: 依存関係の更新
+- Fix: セキュリティに関する修正
+
+## 2025.11.1
+
+### Client
+
+- Enhance: リアクションの受け入れ設定にキャプションを追加 #15921
+- Fix: ページの内容がはみ出ることがある問題を修正
+- Fix: ナビゲーションバーを下に表示しているときに、項目数が多いと表示が崩れる問題を修正
+- Fix: ヘッダーメニューのチャンネルの新規作成の項目でチャンネル作成ページに飛べない問題を修正 #16816
+- Fix: ラジオボタンに空白の選択肢が表示される問題を修正
+ (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/1105)
+- Fix: 一部のシチュエーションで投稿フォームのツアーが正しく表示されない問題を修正
+- Fix: 投稿フォームのリセットボタンで注釈がリセットされない問題を修正
+- Fix: PlayのAiScriptバージョン判定(v0.x系・v1.x系の判定)が正しく動作しない問題を修正
+ (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/1129)
+- Fix: フォロー申請をキャンセルする際の確認ダイアログの文言が不正確な問題を修正
+- Fix: 初回読み込み時にエラーになることがある問題を修正
+- Fix: お気に入りクリップの一覧表示が正しく動作しない問題を修正
+- Fix: AiScript Misskey 拡張APIにおいて、各種関数の引数で明示的に `null` が指定されている場合のハンドリングを修正
+
+### Server
+- Enhance: メモリ使用量を削減しました
+- Enhance: 依存関係の更新
+- Fix: ワードミュートの文字数計算を修正
+- Fix: チャンネルのリアルタイム更新時に、ロックダウン設定にて非ログイン時にノートを表示しない設定にしている場合でもノートが表示されてしまう問題を修正
+- Fix: DeepL APIのAPIキー指定方式変更に対応
+ (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/1096)
+ - 内部実装の変更にて対応可能な更新です。Misskey側の設定方法に変更はありません。
+- Fix: DBレプリケーションを利用する環境でクエリーが失敗する問題を修正
+ (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/1123)
+
+## 2025.11.0
+
+### General
+- Feat: チャンネルミュート機能の実装 #10649
+ - チャンネルの概要画面の右上からミュートできます(リンクコピー、共有、設定と同列)
+- Enhance: Node.js 24.10.0をサポートするようになりました
+- Enhance: DockerのNode.jsが24.10.0に更新されました
+- 依存関係の更新
+
+### Client
+- Feat: 画像にメタデータを含むフレームをつけられる機能
+- Enhance: プリセットを作成しなくても画像にウォーターマークを付与できるように
+- Enhance: 管理しているチャンネルの見分けがつきやすくなるように
+- Enhance: プロフィールへのリンクをユーザーポップアップのアバターに追加
+- Enhance: ユーザーのノート、フォロー、フォロワーページへのリンクをユーザーポップアップに追加
+- Enhance: プッシュ通知を行うための権限確認をより確実に行うように
+- Enhance: 投稿フォームのチュートリアルを追加
+- Enhance: 「自動でもっと見る」をほとんどの箇所で利用可能に
+- Enhance: アンテナ・リスト設定画面とタイムラインの動線を改善
+ - アンテナ・リスト一覧画面の項目を選択すると、設定画面ではなくタイムラインに移動するようになりました
+ - アンテナ・リストの設定画面の右上にタイムラインに移動するボタンを追加しました
+- Fix: 紙吹雪エフェクトがアニメーション設定を考慮せず常に表示される問題を修正
+- Fix: ナビゲーションバーのリアルタイムモード切替ボタンの状態をよりわかりやすく表示するように
+- Fix: ページのタイトルが長いとき、はみ出る問題を修正
+- Fix: 投稿フォームのアバターが正しく表示されない問題を修正 #16789
+- FIx: カスタム絵文字(β)画面で変更行が正しくハイライトされない問題を修正 #16626
+
+### Server
+- Enhance: Remote Notes Cleaningが複雑度が高いノートの処理を中断せずに次のノートから再開するように
+- Fix: チャンネルの説明欄の最小文字数制約を除去
+
+## 2025.10.2
+
+### Client
+- Fix: アプリ内からキャッシュをクリアするとテーマ再適用するまでレンダリングが正しく行われない問題を修正
+- Fix: 期限が無期限のアンケートに投票できない問題を修正
+
+## 2025.10.1
+
+### General
+- Enhance: リモートユーザーに付与したロールバッジを表示できるように(オプトイン)
+ パフォーマンス上の問題からデフォルトで無効化されています。「コントロールパネル > パフォーマンス」から有効化できます。
+- 依存関係の更新
+
+### Client
+- Enhance: デッキのメインカラムのヘッダをクリックしてページ上部/下部にスクロールできるように
+- Enhance: 下書き/予約投稿一覧は投稿フォームのアカウントメニュー内に移動し、下書き保存は「...」メニュー内に移動されました
+- Fix: カスタム絵文字画面(beta)のaliasesで使用される区切り文字が一致していないのを修正 #15614
+- Fix: バナー画像の幅が表示領域と一致していない問題を修正
+- Fix: 一部のブラウザでバナー画像が上下中央に表示されない問題を修正
+- Fix: ナビゲーションバーの設定で削除した項目をその場で再追加できない問題を修正
+- Fix: ロールポリシーによりダイレクトメッセージが無効化されている際のデッキのダイレクトメッセージカラムの挙動を改善
+- Fix: 画像のマスクでタッチ操作が不安定な問題を修正
+- Fix: ウォーターマークの各種挙動修正
+ - ウォーターマークを回転させると歪む問題を修正
+ - ウォーターマークを敷き詰めると上下左右反転した画像/文字が表示される問題を修正
+ - ウォーターマークを回転させた際に画面からはみ出た部分を考慮できるように
+- Fix: 投票が終了した後に投票結果が正しく表示されない問題を修正
+- Fix: ダークモードの同期が機能しない場合がある問題を修正
+- Fix: iOSで動画の圧縮を行うと音声トラックが失われる問題を修正
+
+### Server
+- Enhance: 管理者/モデレーターはファイルのアップロード制限をバイパスするように
+- Enhance: セキュリティの向上
+
+## 2025.10.0
+
+### NOTE
+- pnpm 10.16.0 が必要です
+- ロールのインポート機能の利用可否ポリシーのデフォルト値が「いいえ」に変わったため、デフォルトから変更していないサーバーでは適宜設定を変更してください。
+- ロールのアップロード可能なファイル種別ポリシーのデフォルト値に「text/*」が追加されたため、デフォルトから変更していないサーバーでは適宜設定を変更してください。
+
+### General
+- Feat: 予約投稿ができるようになりました
+ - デフォルトで作成可能数は1になっています。適宜ロールのポリシーで設定を行ってください。
+- Enhance: 広告ごとにセンシティブフラグを設定できるようになりました
+- Enhance: 依存関係の更新
+- Enhance: 翻訳の更新
+
+### Client
+- Feat: アカウントのQRコードを表示・読み取りできるようになりました
+- Feat: 動画を圧縮してアップロードできるようになりました
+- Feat: (実験的) ブラウザ上でノートの翻訳を行えるように
+- Enhance: チャットの日本語名称がダイレクトメッセージに戻るとともに、ベータ版機能ではなくなりました
+- Enhance: 画像編集にマスクエフェクト(塗りつぶし、ぼかし、モザイク)を追加
+- Enhance: 画像編集の集中線エフェクトを強化
+- Enhance: ウォーターマークにアカウントのQRコードを追加できるように
+- Enhance: テーマをドラッグ&ドロップできるように
+- Enhance: 絵文字ピッカーのサイズをより大きくできるように
+- Enhance: カスタム絵文字が多い場合にサーバーの絵文字一覧ページがフリーズしないように
+- Enhance: 時刻計算のための基準値を一か所で管理するようにし、パフォーマンスを向上
+- Enhance: 「お問い合わせ」ページから、バグの調査等に役立つ情報(OSやブラウザのバージョン等)を取得・コピーできるように
+- Fix: iOSで、デバイスがダークモードだと初回読み込み時にエラーになる問題を修正
+- Fix: アクティビティウィジェットのグラフモードが動作しない問題を修正
+- Fix: ユニコード絵文字の追加辞書をインストールするとユニコード絵文字が絵文字ピッカーで検索できなくなる絵文字があるバグを修正
+
+### Server
+- Enhance: ユーザーIPを確実に取得できるために設定ファイルにFastifyOptions.trustProxyを追加しました
+
+## 2025.9.0
+
+### Client
+- Enhance: AiScriptAppウィジェットで構文エラーを検知してもダイアログではなくウィジェット内にエラーを表示するように
+- Enhance: /flushページでサイトキャッシュをクリアできるようになりました
+- Enhance: クリップ/リスト/アンテナ/ロール追加系メニュー項目において、表示件数を拡張
+- Enhance: 「キャッシュを削除」ボタンでブラウザの内部キャッシュの削除も行えるように
+- Enhance: Ctrlキー(Commandキー)を押下しながらリンクをクリックすると新しいタブで開くように
+- Fix: プッシュ通知を有効にできない問題を修正
+- Fix: RSSティッカーウィジェットが正しく動作しない問題を修正
+- Fix: プロファイルを復元後アカウントの切り替えができない問題を修正
+- Fix: エラー画像が横に引き伸ばされてしまう問題に対応
+
+### Server
+- Fix: webpなどの画像に対してセンシティブなメディアの検出が適用されていなかった問題を修正
+
## 2025.8.0
### Note
@@ -6,8 +239,8 @@
### General
- ノートを削除した際、関連するノートが同時に削除されないようになりました
- APIで、「replyIdが存在しているのにreplyがnull」や「renoteIdが存在しているのにrenoteがnull」であるという、今までにはなかったパターンが表れることになります
-- 定期的に参照されていない古いリモートの投稿を削除する機能が実装されました(コントロールパネル→パフォーマンス→Remote Notes Cleaning)
- - 既存のサーバーでは**デフォルトでオフ**、新規サーバーでは**デフォルトでオン**になります
+- 定期的に古いリモートの投稿を削除する機能が実装されました
+ - コントロールパネル→パフォーマンス→Remote Notes Cleaning で有効化できます
- データベースの肥大化を防止することが可能です
- 既存のサーバーで当機能を有効化した場合は、処理量が多くなるため、一時的にストレージ使用量が増加する可能性があります。
- 増加量を抑えるには、最大処理継続時間をデフォルトより短くしてください。
@@ -15,7 +248,8 @@
- サーバーの初期設定が完了するまでは連合がオンにならないようになりました
- 日本語における公開範囲名称の「ダイレクト」が「指名」に改称されました
- 実際の動作に即した名称になり、馴染みのない人でも理解しやすくなりました
- - 他サービスにおける「ダイレクトメッセージ」に相当するMisskeyの機能は「チャット」ですが、「ダイレクト投稿」という名称の機能が存在するとそちらがダイレクトメッセージ機能であるような誤解を生んでいました
+ - 他サービスにおける「ダイレクトメッセージ」に相当するMisskeyの機能は「チャット」ですが(過去のバージョンのMisskeyでも、当該機能は「チャット」ではなく「ダイレクトメッセージ」でした)、「ダイレクト投稿」という名称の機能が存在するとそちらがダイレクトメッセージ機能であるような誤解を生んでいました
+ - 今後、「チャット」の名称を「ダイレクトメッセージ」に戻す可能性があります
- mfm.jsをアップデートしました
- Enhance: Unicode 15.1 および 16.0 に収録されている絵文字に対応
- Enhance: acctに `.` が入っているユーザーのメンションに対応
@@ -33,6 +267,7 @@
- プラグインは1.xに対応したものが必要です
- Playはそのまま動作しますが、新規に作られるプリセットは1.xになります
- 以前のバージョンから無効化されていた note_view_interruptor が有効になりました
+ - ハンドラは同期的である必要があります
- Feat: セーフモード
- プラグイン・テーマ・カスタムCSSの使用でクライアントの起動に問題が発生した際に、これらを無効にして起動できます
- 以下の方法でセーフモードを起動できます
@@ -43,13 +278,16 @@
- コントロールパネル→ブランディング→エントランスページのスタイル
- Feat: ページのタブバーを下部に表示できるように
- Feat: (実験的)iOSでの触覚フィードバックを有効にできるように
+- Feat: コントロールパネルを検索できるように
- Enhance: 「自動でもっと見る」オプションが有効になり、安定性が向上しました
-- Enhance: コントロールパネルを検索できるように
- Enhance: トルコ語 (tr-TR) に対応
- Enhance: 不必要な翻訳データを読み込まなくなり、パフォーマンスが向上しました
- Enhance: 画像エフェクトのパラメータ名の多言語対応
-- Enhance: 依存ソフトウェアの更新
- Enhance: ノートを非表示にする相対期間を1ヶ月単位で自由に指定できるように
+- Enhance: メールアドレス確認画面のUIを改善
+- Enhance: アイコンのスクロール追従を無効化する際の適用範囲を強化
+- Enhance: レンダリングパフォーマンスの向上
+- Enhance: 依存ソフトウェアの更新
- Fix: 投稿フォームでファイルのアップロードが中止または失敗した際のハンドリングを修正
- Fix: 一部の設定検索結果が存在しないパスになる問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1171)
@@ -58,12 +296,27 @@
- Fix: カラムの名前が正しくリスト/チャンネルの名前にならない問題を修正
- Fix: 複数のメンションを1行に記述した場合に、サジェストが正しく表示されない問題を修正
- Fix: メンションとしての条件を満たしていても、特定の条件(`-`が含まれる場合など)で正しくサジェストされない問題を一部修正
+- Fix: ユーザーの前後ノートを閲覧する機能が動作しない問題を修正
+- Fix: 照会ダイアログでap/showでローカルユーザーを解決した際@username@nullに飛ばされる問題を修正
+- Fix: アイコンのデコレーションを付ける際にデコレーションが表示されなくなる問題を修正
+- Fix: タッチ操作時にマウスホバー時のユーザープレビューが開くことがある問題を修正
+- Fix: 管理中アカウント一覧で正しい表示が行われない問題を修正
+- Fix: lookupページでリモートURLを指定した際に正しく動作しない問題を修正
### Server
+- Feat: サーバー管理コマンド
+ - `pnpm cli foo` の形式で実行可能です
+ - 現在以下のコマンドが利用可能です
+ - `reset-captcha` - CAPTCHA設定をリセットします
- Enhance: ノートの削除処理の効率化
- Enhance: 全体的なパフォーマンスの向上
- Enhance: 依存ソフトウェアの更新
+- Enhance: `clips/list` APIがページネーションに対応しました
+- Fix: `notes/mentions` で場合によっては並び順が正しく返されない問題を修正
- Fix: SystemWebhook設定でsecretを空に出来ない問題を修正
+- Fix: 削除されたユーザーがチャットメッセージにリアクションしている場合`chat/history`などでエラーになる問題を修正
+- Fix: Pageのアイキャッチ画像をドライブから消してもPageごと消えないように
+- Fix: タイムラインAPIの withRenotes: false 時のレスポンスを修正
## 2025.7.0
diff --git a/COPYING b/COPYING
index 7635bfc913..a17c82c002 100644
--- a/COPYING
+++ b/COPYING
@@ -1,5 +1,5 @@
Unless otherwise stated this repository is
-Copyright © 2014-2025 syuilo and contributors
+Copyright © 2014-2026 syuilo and contributors
And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE.
diff --git a/Dockerfile b/Dockerfile
index 370bed5751..c1ac81f09e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,6 @@
-# syntax = docker/dockerfile:1.4
+# syntax = docker/dockerfile:1.20
-ARG NODE_VERSION=22.15.0-bookworm
+ARG NODE_VERSION=22.21.1-bookworm
# build assets & compile TypeScript
@@ -24,6 +24,7 @@ COPY --link ["packages/frontend-shared/package.json", "./packages/frontend-share
COPY --link ["packages/frontend/package.json", "./packages/frontend/"]
COPY --link ["packages/frontend-embed/package.json", "./packages/frontend-embed/"]
COPY --link ["packages/frontend-builder/package.json", "./packages/frontend-builder/"]
+COPY --link ["packages/i18n/package.json", "./packages/i18n/"]
COPY --link ["packages/icons-subsetter/package.json", "./packages/icons-subsetter/"]
COPY --link ["packages/sw/package.json", "./packages/sw/"]
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
@@ -101,6 +102,8 @@ COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-js/
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-reversi/built ./packages/misskey-reversi/built
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-bubble-game/built ./packages/misskey-bubble-game/built
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/built ./packages/backend/built
+COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/src-js ./packages/backend/src-js
+COPY --chown=misskey:misskey --from=native-builder /misskey/packages/i18n/built ./packages/i18n/built
COPY --chown=misskey:misskey --from=native-builder /misskey/fluent-emojis /misskey/fluent-emojis
COPY --chown=misskey:misskey . ./
diff --git a/README.md b/README.md
index 92e8fef639..e3261d13c2 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,10 @@
+[](https://deepwiki.com/misskey-dev/misskey)
+
+
+
## Thanks
@@ -47,3 +51,13 @@ Thanks to [Crowdin](https://crowdin.com/) for providing the localization platfor
Thanks to [Docker](https://hub.docker.com/) for providing the container platform that helps us run Misskey in production.
+
+---
+
+
[${(new Date()).toString()}] ${text.replace(/\n/g,'
')}
)
- case 'pre': {
- if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') {
+ case 'PRE': {
+ if (node.childNodes.length === 1 && (node.childNodes[0] instanceof htmlParser.HTMLElement) && node.childNodes[0].tagName === 'CODE') {
text += '\n```\n';
text += getText(node.childNodes[0]);
text += '\n```\n';
+ } else if (node.childNodes.length === 1 && (node.childNodes[0] instanceof htmlParser.TextNode) && node.childNodes[0].textContent.startsWith('') && node.childNodes[0].textContent.endsWith('')) {
+ text += '\n```\n';
+ text += node.childNodes[0].textContent.slice(6, -7);
+ text += '\n```\n';
} else {
- appendChildren(node.childNodes);
+ analyzeChildren(node.childNodes);
}
break;
}
// inline code ()
- case 'code': {
+ case 'CODE': {
text += '`';
- appendChildren(node.childNodes);
+ analyzeChildren(node.childNodes);
text += '`';
break;
}
- case 'blockquote': {
+ case 'BLOCKQUOTE': {
const t = getText(node);
if (t) {
text += '\n> ';
@@ -235,33 +236,33 @@ export class MfmService {
break;
}
- case 'p':
- case 'h2':
- case 'h3':
- case 'h4':
- case 'h5':
- case 'h6': {
+ case 'P':
+ case 'H2':
+ case 'H3':
+ case 'H4':
+ case 'H5':
+ case 'H6': {
text += '\n\n';
- appendChildren(node.childNodes);
+ analyzeChildren(node.childNodes);
break;
}
// other block elements
- case 'div':
- case 'header':
- case 'footer':
- case 'article':
- case 'li':
- case 'dt':
- case 'dd': {
+ case 'DIV':
+ case 'HEADER':
+ case 'FOOTER':
+ case 'ARTICLE':
+ case 'LI':
+ case 'DT':
+ case 'DD': {
text += '\n';
- appendChildren(node.childNodes);
+ analyzeChildren(node.childNodes);
break;
}
default: // includes inline elements
{
- appendChildren(node.childNodes);
+ analyzeChildren(node.childNodes);
break;
}
}
@@ -269,52 +270,35 @@ export class MfmService {
}
@bindThis
- public toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = [], additionalAppenders: Appender[] = []) {
+ public toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = [], extraHtml: string | null = null) {
if (nodes == null) {
return null;
}
- const { happyDOM, window } = new Window();
-
- const doc = window.document;
-
- const body = doc.createElement('p');
-
- function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
- if (children) {
- for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child);
- }
+ function toHtml(children?: mfm.MfmNode[]): string {
+ if (children == null) return '';
+ return children.map(x => handlers[x.type](x)).join('');
}
function fnDefault(node: mfm.MfmFn) {
- const el = doc.createElement('i');
- appendChildren(node.children, el);
- return el;
+ return `${toHtml(node.children)}`;
}
- const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType) => any } = {
+ const handlers = {
bold: (node) => {
- const el = doc.createElement('b');
- appendChildren(node.children, el);
- return el;
+ return `${toHtml(node.children)}`;
},
small: (node) => {
- const el = doc.createElement('small');
- appendChildren(node.children, el);
- return el;
+ return `${toHtml(node.children)}`;
},
strike: (node) => {
- const el = doc.createElement('del');
- appendChildren(node.children, el);
- return el;
+ return `${toHtml(node.children)}`;
},
italic: (node) => {
- const el = doc.createElement('i');
- appendChildren(node.children, el);
- return el;
+ return `${toHtml(node.children)}`;
},
fn: (node) => {
@@ -323,11 +307,8 @@ export class MfmService {
const text = node.children[0].type === 'text' ? node.children[0].props.text : '';
try {
const date = new Date(parseInt(text, 10) * 1000);
- const el = doc.createElement('time');
- el.setAttribute('datetime', date.toISOString());
- el.textContent = date.toISOString();
- return el;
- } catch (err) {
+ return ``;
+ } catch (_) {
return fnDefault(node);
}
}
@@ -336,21 +317,9 @@ export class MfmService {
if (node.children.length === 1) {
const child = node.children[0];
const text = child.type === 'text' ? child.props.text : '';
- const rubyEl = doc.createElement('ruby');
- const rtEl = doc.createElement('rt');
- // ruby未対応のHTMLサニタイザーを通したときにルビが「劉備(りゅうび)」となるようにする
- const rpStartEl = doc.createElement('rp');
- rpStartEl.appendChild(doc.createTextNode('('));
- const rpEndEl = doc.createElement('rp');
- rpEndEl.appendChild(doc.createTextNode(')'));
-
- rubyEl.appendChild(doc.createTextNode(text.split(' ')[0]));
- rtEl.appendChild(doc.createTextNode(text.split(' ')[1]));
- rubyEl.appendChild(rpStartEl);
- rubyEl.appendChild(rtEl);
- rubyEl.appendChild(rpEndEl);
- return rubyEl;
+ // ruby未対応のHTMLサニタイザーを通したときにルビが「対象テキスト(ルビテキスト)」にフォールバックするようにする
+ return `${escapeHtml(text.split(' ')[0])}`;
} else {
const rt = node.children.at(-1);
@@ -359,21 +328,9 @@ export class MfmService {
}
const text = rt.type === 'text' ? rt.props.text : '';
- const rubyEl = doc.createElement('ruby');
- const rtEl = doc.createElement('rt');
- // ruby未対応のHTMLサニタイザーを通したときにルビが「劉備(りゅうび)」となるようにする
- const rpStartEl = doc.createElement('rp');
- rpStartEl.appendChild(doc.createTextNode('('));
- const rpEndEl = doc.createElement('rp');
- rpEndEl.appendChild(doc.createTextNode(')'));
-
- appendChildren(node.children.slice(0, node.children.length - 1), rubyEl);
- rtEl.appendChild(doc.createTextNode(text.trim()));
- rubyEl.appendChild(rpStartEl);
- rubyEl.appendChild(rtEl);
- rubyEl.appendChild(rpEndEl);
- return rubyEl;
+ // ruby未対応のHTMLサニタイザーを通したときにルビが「対象テキスト(ルビテキスト)」にフォールバックするようにする
+ return `${toHtml(node.children.slice(0, node.children.length - 1))}`;
}
}
@@ -384,125 +341,98 @@ export class MfmService {
},
blockCode: (node) => {
- const pre = doc.createElement('pre');
- const inner = doc.createElement('code');
- inner.textContent = node.props.code;
- pre.appendChild(inner);
- return pre;
+ return `${escapeHtml(node.props.code)}
`;
},
center: (node) => {
- const el = doc.createElement('div');
- appendChildren(node.children, el);
- return el;
+ return `${toHtml(node.children)}`;
},
emojiCode: (node) => {
- return doc.createTextNode(`\u200B:${node.props.name}:\u200B`);
+ return `\u200B:${escapeHtml(node.props.name)}:\u200B`;
},
unicodeEmoji: (node) => {
- return doc.createTextNode(node.props.emoji);
+ return node.props.emoji;
},
hashtag: (node) => {
- const a = doc.createElement('a');
- a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`);
- a.textContent = `#${node.props.hashtag}`;
- a.setAttribute('rel', 'tag');
- return a;
+ return `#${escapeHtml(node.props.hashtag)}`;
},
inlineCode: (node) => {
- const el = doc.createElement('code');
- el.textContent = node.props.code;
- return el;
+ return `${escapeHtml(node.props.code)}`;
},
mathInline: (node) => {
- const el = doc.createElement('code');
- el.textContent = node.props.formula;
- return el;
+ return `${escapeHtml(node.props.formula)}`;
},
mathBlock: (node) => {
- const el = doc.createElement('code');
- el.textContent = node.props.formula;
- return el;
+ return `${escapeHtml(node.props.formula)}
`;
},
link: (node) => {
- const a = doc.createElement('a');
- a.setAttribute('href', node.props.url);
- appendChildren(node.children, a);
- return a;
+ try {
+ const url = new URL(node.props.url);
+ return `${toHtml(node.children)}`;
+ } catch (_) {
+ return `[${toHtml(node.children)}](${escapeHtml(node.props.url)})`;
+ }
},
mention: (node) => {
- const a = doc.createElement('a');
const { username, host, acct } = node.props;
const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username.toLowerCase() === username.toLowerCase() && remoteUser.host?.toLowerCase() === host?.toLowerCase());
- a.setAttribute('href', remoteUserInfo
+ const href = remoteUserInfo
? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri)
- : `${this.config.url}/${acct.endsWith(`@${this.config.url}`) ? acct.substring(0, acct.length - this.config.url.length - 1) : acct}`);
- a.className = 'u-url mention';
- a.textContent = acct;
- return a;
+ : `${this.config.url}/${acct.endsWith(`@${this.config.url}`) ? acct.substring(0, acct.length - this.config.url.length - 1) : acct}`;
+ try {
+ const url = new URL(href);
+ return `${escapeHtml(acct)}`;
+ } catch (_) {
+ return escapeHtml(acct);
+ }
},
quote: (node) => {
- const el = doc.createElement('blockquote');
- appendChildren(node.children, el);
- return el;
+ return `${toHtml(node.children)}
`;
},
text: (node) => {
if (!node.props.text.match(/[\r\n]/)) {
- return doc.createTextNode(node.props.text);
+ return escapeHtml(node.props.text);
}
- const el = doc.createElement('span');
- const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x));
+ let html = '';
- for (const x of intersperse('br', nodes)) {
- el.appendChild(x === 'br' ? doc.createElement('br') : x);
+ const lines = node.props.text.split(/\r\n|\r|\n/).map(x => escapeHtml(x));
+
+ for (const x of intersperse('br', lines)) {
+ html += x === 'br' ? '
' : x;
}
- return el;
+ return html;
},
url: (node) => {
- const a = doc.createElement('a');
- a.setAttribute('href', node.props.url);
- a.textContent = node.props.url;
- return a;
+ try {
+ const url = new URL(node.props.url);
+ return `${escapeHtml(node.props.url)}`;
+ } catch (_) {
+ return escapeHtml(node.props.url);
+ }
},
search: (node) => {
- const a = doc.createElement('a');
- a.setAttribute('href', `https://www.google.com/search?q=${node.props.query}`);
- a.textContent = node.props.content;
- return a;
+ return `${escapeHtml(node.props.content)}`;
},
plain: (node) => {
- const el = doc.createElement('span');
- appendChildren(node.children, el);
- return el;
+ return `${toHtml(node.children)}`;
},
- };
+ } satisfies { [K in mfm.MfmNode['type']]: (node: mfm.NodeType) => string } as { [K in mfm.MfmNode['type']]: (node: mfm.MfmNode) => string };
- appendChildren(nodes, body);
-
- for (const additionalAppender of additionalAppenders) {
- additionalAppender(doc, body);
- }
-
- // Remove the unnecessary namespace
- const serialized = new XMLSerializer().serializeToString(body).replace(/^\s*/, '
');
-
- happyDOM.close().catch(err => {});
-
- return serialized;
+ return `${toHtml(nodes)}${extraHtml ?? ''}`;
}
}
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index cd5b148ed7..6115542655 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -13,7 +13,7 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf
import { extractHashtags } from '@/misc/extract-hashtags.js';
import type { IMentionedRemoteUsers } from '@/models/Note.js';
import { MiNote } from '@/models/Note.js';
-import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import type { BlockingsRepository, ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, FollowingsRepository, InstancesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiApp } from '@/models/App.js';
import { concat } from '@/misc/prelude/array.js';
@@ -56,6 +56,7 @@ import { trackPromise } from '@/misc/promise-tracker.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { CollapsedQueue } from '@/misc/collapsed-queue.js';
import { CacheService } from '@/core/CacheService.js';
+import { isQuote, isRenote } from '@/misc/is-renote.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@@ -192,6 +193,12 @@ export class NoteCreateService implements OnApplicationShutdown {
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
+ @Inject(DI.blockingsRepository)
+ private blockingsRepository: BlockingsRepository,
+
+ @Inject(DI.driveFilesRepository)
+ private driveFilesRepository: DriveFilesRepository,
+
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
private idService: IdService,
@@ -221,6 +228,167 @@ export class NoteCreateService implements OnApplicationShutdown {
this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount);
}
+ @bindThis
+ public async fetchAndCreate(user: {
+ id: MiUser['id'];
+ username: MiUser['username'];
+ host: MiUser['host'];
+ isBot: MiUser['isBot'];
+ isCat: MiUser['isCat'];
+ }, data: {
+ createdAt: Date;
+ replyId: MiNote['id'] | null;
+ renoteId: MiNote['id'] | null;
+ fileIds: MiDriveFile['id'][];
+ text: string | null;
+ cw: string | null;
+ visibility: string;
+ visibleUserIds: MiUser['id'][];
+ channelId: MiChannel['id'] | null;
+ localOnly: boolean;
+ reactionAcceptance: MiNote['reactionAcceptance'];
+ poll: IPoll | null;
+ apMentions?: MinimumUser[] | null;
+ apHashtags?: string[] | null;
+ apEmojis?: string[] | null;
+ }): Promise {
+ const visibleUsers = data.visibleUserIds.length > 0 ? await this.usersRepository.findBy({
+ id: In(data.visibleUserIds),
+ }) : [];
+
+ let files: MiDriveFile[] = [];
+ if (data.fileIds.length > 0) {
+ files = await this.driveFilesRepository.createQueryBuilder('file')
+ .where('file.userId = :userId AND file.id IN (:...fileIds)', {
+ userId: user.id,
+ fileIds: data.fileIds,
+ })
+ .orderBy('array_position(ARRAY[:...fileIds], "id"::text)')
+ .setParameters({ fileIds: data.fileIds })
+ .getMany();
+
+ if (files.length !== data.fileIds.length) {
+ throw new IdentifiableError('801c046c-5bf5-4234-ad2b-e78fc20a2ac7', 'No such file');
+ }
+ }
+
+ let renote: MiNote | null = null;
+ if (data.renoteId != null) {
+ // Fetch renote to note
+ renote = await this.notesRepository.findOne({
+ where: { id: data.renoteId },
+ relations: ['user', 'renote', 'reply'],
+ });
+
+ if (renote == null) {
+ throw new IdentifiableError('53983c56-e163-45a6-942f-4ddc485d4290', 'No such renote target');
+ } else if (isRenote(renote) && !isQuote(renote)) {
+ throw new IdentifiableError('bde24c37-121f-4e7d-980d-cec52f599f02', 'Cannot renote pure renote');
+ }
+
+ // Check blocking
+ if (renote.userId !== user.id) {
+ const blockExist = await this.blockingsRepository.exists({
+ where: {
+ blockerId: renote.userId,
+ blockeeId: user.id,
+ },
+ });
+ if (blockExist) {
+ throw new IdentifiableError('2b4fe776-4414-4a2d-ae39-f3418b8fd4d3', 'You have been blocked by the user');
+ }
+ }
+
+ if (renote.visibility === 'followers' && renote.userId !== user.id) {
+ // 他人のfollowers noteはreject
+ throw new IdentifiableError('90b9d6f0-893a-4fef-b0f1-e9a33989f71a', 'Renote target visibility');
+ } else if (renote.visibility === 'specified') {
+ // specified / direct noteはreject
+ throw new IdentifiableError('48d7a997-da5c-4716-b3c3-92db3f37bf7d', 'Renote target visibility');
+ }
+
+ if (renote.channelId && renote.channelId !== data.channelId) {
+ // チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック
+ // リノートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する
+ const renoteChannel = await this.channelsRepository.findOneBy({ id: renote.channelId });
+ if (renoteChannel == null) {
+ // リノートしたいノートが書き込まれているチャンネルが無い
+ throw new IdentifiableError('b060f9a6-8909-4080-9e0b-94d9fa6f6a77', 'No such channel');
+ } else if (!renoteChannel.allowRenoteToExternal) {
+ // リノート作成のリクエストだが、対象チャンネルがリノート禁止だった場合
+ throw new IdentifiableError('7e435f4a-780d-4cfc-a15a-42519bd6fb67', 'Channel does not allow renote to external');
+ }
+ }
+ }
+
+ let reply: MiNote | null = null;
+ if (data.replyId != null) {
+ // Fetch reply
+ reply = await this.notesRepository.findOne({
+ where: { id: data.replyId },
+ relations: ['user'],
+ });
+
+ if (reply == null) {
+ throw new IdentifiableError('60142edb-1519-408e-926d-4f108d27bee0', 'No such reply target');
+ } else if (isRenote(reply) && !isQuote(reply)) {
+ throw new IdentifiableError('f089e4e2-c0e7-4f60-8a23-e5a6bf786b36', 'Cannot reply to pure renote');
+ } else if (!await this.noteEntityService.isVisibleForMe(reply, user.id)) {
+ throw new IdentifiableError('11cd37b3-a411-4f77-8633-c580ce6a8dce', 'No such reply target');
+ } else if (reply.visibility === 'specified' && data.visibility !== 'specified') {
+ throw new IdentifiableError('ced780a1-2012-4caf-bc7e-a95a291294cb', 'Cannot reply to specified note with different visibility');
+ }
+
+ // Check blocking
+ if (reply.userId !== user.id) {
+ const blockExist = await this.blockingsRepository.exists({
+ where: {
+ blockerId: reply.userId,
+ blockeeId: user.id,
+ },
+ });
+ if (blockExist) {
+ throw new IdentifiableError('b0df6025-f2e8-44b4-a26a-17ad99104612', 'You have been blocked by the user');
+ }
+ }
+ }
+
+ if (data.poll) {
+ if (data.poll.expiresAt != null) {
+ if (data.poll.expiresAt.getTime() < Date.now()) {
+ throw new IdentifiableError('0c11c11e-0c8d-48e7-822c-76ccef660068', 'Poll expiration must be future time');
+ }
+ }
+ }
+
+ let channel: MiChannel | null = null;
+ if (data.channelId != null) {
+ channel = await this.channelsRepository.findOneBy({ id: data.channelId, isArchived: false });
+
+ if (channel == null) {
+ throw new IdentifiableError('bfa3905b-25f5-4894-b430-da331a490e4b', 'No such channel');
+ }
+ }
+
+ return this.create(user, {
+ createdAt: data.createdAt,
+ files: files,
+ poll: data.poll,
+ text: data.text,
+ reply,
+ renote,
+ cw: data.cw,
+ localOnly: data.localOnly,
+ reactionAcceptance: data.reactionAcceptance,
+ visibility: data.visibility,
+ visibleUsers,
+ channel,
+ apMentions: data.apMentions,
+ apHashtags: data.apHashtags,
+ apEmojis: data.apEmojis,
+ });
+ }
+
@bindThis
public async create(user: {
id: MiUser['id'];
@@ -464,6 +632,7 @@ export class NoteCreateService implements OnApplicationShutdown {
replyUserHost: data.reply ? data.reply.userHost : null,
renoteUserId: data.renote ? data.renote.userId : null,
renoteUserHost: data.renote ? data.renote.userHost : null,
+ renoteChannelId: data.renote ? data.renote.channelId : null,
userHost: user.host,
});
diff --git a/packages/backend/src/core/NoteDraftService.ts b/packages/backend/src/core/NoteDraftService.ts
index c43be96efa..e144138c2c 100644
--- a/packages/backend/src/core/NoteDraftService.ts
+++ b/packages/backend/src/core/NoteDraftService.ts
@@ -5,32 +5,18 @@
import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
-import type { noteVisibilities, noteReactionAcceptances } from '@/types.js';
import { DI } from '@/di-symbols.js';
import type { MiNoteDraft, NoteDraftsRepository, MiNote, MiDriveFile, MiChannel, UsersRepository, DriveFilesRepository, NotesRepository, BlockingsRepository, ChannelsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { IdService } from '@/core/IdService.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
-import { IPoll } from '@/models/Poll.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { isRenote, isQuote } from '@/misc/is-renote.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
+import { QueueService } from '@/core/QueueService.js';
-export type NoteDraftOptions = {
- replyId?: MiNote['id'] | null;
- renoteId?: MiNote['id'] | null;
- text?: string | null;
- cw?: string | null;
- localOnly?: boolean | null;
- reactionAcceptance?: typeof noteReactionAcceptances[number];
- visibility?: typeof noteVisibilities[number];
- fileIds?: MiDriveFile['id'][];
- visibleUserIds?: MiUser['id'][];
- hashtag?: string;
- channelId?: MiChannel['id'] | null;
- poll?: (IPoll & { expiredAfter?: number | null }) | null;
-};
+export type NoteDraftOptions = Omit;
@Injectable()
export class NoteDraftService {
@@ -56,6 +42,7 @@ export class NoteDraftService {
private roleService: RoleService,
private idService: IdService,
private noteEntityService: NoteEntityService,
+ private queueService: QueueService,
) {
}
@@ -72,36 +59,43 @@ export class NoteDraftService {
@bindThis
public async create(me: MiLocalUser, data: NoteDraftOptions): Promise {
//#region check draft limit
+ const policies = await this.roleService.getUserPolicies(me.id);
const currentCount = await this.noteDraftsRepository.countBy({
userId: me.id,
});
- if (currentCount >= (await this.roleService.getUserPolicies(me.id)).noteDraftLimit) {
+ if (currentCount >= policies.noteDraftLimit) {
throw new IdentifiableError('9ee33bbe-fde3-4c71-9b51-e50492c6b9c8', 'Too many drafts');
}
+
+ if (data.isActuallyScheduled) {
+ const currentScheduledCount = await this.noteDraftsRepository.countBy({
+ userId: me.id,
+ isActuallyScheduled: true,
+ });
+ if (currentScheduledCount >= policies.scheduledNoteLimit) {
+ throw new IdentifiableError('c3275f19-4558-4c59-83e1-4f684b5fab66', 'Too many scheduled notes');
+ }
+ }
//#endregion
- if (data.poll) {
- if (typeof data.poll.expiresAt === 'number') {
- if (data.poll.expiresAt < Date.now()) {
- throw new IdentifiableError('04da457d-b083-4055-9082-955525eda5a5', 'Cannot create expired poll');
- }
- } else if (typeof data.poll.expiredAfter === 'number') {
- data.poll.expiresAt = new Date(Date.now() + data.poll.expiredAfter);
- }
+ await this.validate(me, data);
+
+ const draft = await this.noteDraftsRepository.insertOne({
+ ...data,
+ id: this.idService.gen(),
+ userId: me.id,
+ });
+
+ if (draft.scheduledAt && draft.isActuallyScheduled) {
+ this.schedule(draft);
}
- const appliedDraft = await this.checkAndSetDraftNoteOptions(me, this.noteDraftsRepository.create(), data);
-
- appliedDraft.id = this.idService.gen();
- appliedDraft.userId = me.id;
- const draft = this.noteDraftsRepository.save(appliedDraft);
-
return draft;
}
@bindThis
- public async update(me: MiLocalUser, draftId: MiNoteDraft['id'], data: NoteDraftOptions): Promise {
+ public async update(me: MiLocalUser, draftId: MiNoteDraft['id'], data: Partial): Promise {
const draft = await this.noteDraftsRepository.findOneBy({
id: draftId,
userId: me.id,
@@ -111,19 +105,36 @@ export class NoteDraftService {
throw new IdentifiableError('49cd6b9d-848e-41ee-b0b9-adaca711a6b1', 'No such note draft');
}
- if (data.poll) {
- if (typeof data.poll.expiresAt === 'number') {
- if (data.poll.expiresAt < Date.now()) {
- throw new IdentifiableError('04da457d-b083-4055-9082-955525eda5a5', 'Cannot create expired poll');
- }
- } else if (typeof data.poll.expiredAfter === 'number') {
- data.poll.expiresAt = new Date(Date.now() + data.poll.expiredAfter);
+ //#region check draft limit
+ const policies = await this.roleService.getUserPolicies(me.id);
+
+ if (!draft.isActuallyScheduled && data.isActuallyScheduled) {
+ const currentScheduledCount = await this.noteDraftsRepository.countBy({
+ userId: me.id,
+ isActuallyScheduled: true,
+ });
+ if (currentScheduledCount >= policies.scheduledNoteLimit) {
+ throw new IdentifiableError('bacdf856-5c51-4159-b88a-804fa5103be5', 'Too many scheduled notes');
}
}
+ //#endregion
- const appliedDraft = await this.checkAndSetDraftNoteOptions(me, draft, data);
+ await this.validate(me, data);
- return await this.noteDraftsRepository.save(appliedDraft);
+ const updatedDraft = await this.noteDraftsRepository.createQueryBuilder().update()
+ .set(data)
+ .where('id = :id', { id: draftId })
+ .returning('*')
+ .execute()
+ .then((response) => response.raw[0]);
+
+ this.clearSchedule(draftId).then(() => {
+ if (updatedDraft.scheduledAt != null && updatedDraft.isActuallyScheduled) {
+ this.schedule(updatedDraft);
+ }
+ });
+
+ return updatedDraft;
}
@bindThis
@@ -138,6 +149,8 @@ export class NoteDraftService {
}
await this.noteDraftsRepository.delete(draft.id);
+
+ this.clearSchedule(draftId);
}
@bindThis
@@ -154,28 +167,29 @@ export class NoteDraftService {
return draft;
}
- // 関連エンティティを取得し紐づける部分を共通化する
@bindThis
- public async checkAndSetDraftNoteOptions(
+ public async validate(
me: MiLocalUser,
- draft: MiNoteDraft,
- data: NoteDraftOptions,
- ): Promise {
- data.visibility ??= 'public';
- data.localOnly ??= false;
- if (data.reactionAcceptance === undefined) data.reactionAcceptance = null;
- if (data.channelId != null) {
- data.visibility = 'public';
- data.visibleUserIds = [];
- data.localOnly = true;
+ data: Partial,
+ ): Promise {
+ if (data.isActuallyScheduled) {
+ if (data.scheduledAt == null) {
+ throw new IdentifiableError('94a89a43-3591-400a-9c17-dd166e71fdfa', 'scheduledAt is required when isActuallyScheduled is true');
+ } else if (data.scheduledAt.getTime() < Date.now()) {
+ throw new IdentifiableError('b34d0c1b-996f-4e34-a428-c636d98df457', 'scheduledAt must be in the future');
+ }
}
- let appliedDraft = draft;
+ if (data.pollExpiresAt != null) {
+ if (data.pollExpiresAt.getTime() < Date.now()) {
+ throw new IdentifiableError('04da457d-b083-4055-9082-955525eda5a5', 'Cannot create expired poll');
+ }
+ }
//#region visibleUsers
- let visibleUsers: MiUser[] = [];
- if (data.visibleUserIds != null) {
- visibleUsers = await this.usersRepository.findBy({
+ let _visibleUsers: MiUser[] = [];
+ if (data.visibleUserIds != null && data.visibleUserIds.length > 0) {
+ _visibleUsers = await this.usersRepository.findBy({
id: In(data.visibleUserIds),
});
}
@@ -184,7 +198,7 @@ export class NoteDraftService {
//#region files
let files: MiDriveFile[] = [];
const fileIds = data.fileIds ?? null;
- if (fileIds != null) {
+ if (fileIds != null && fileIds.length > 0) {
files = await this.driveFilesRepository.createQueryBuilder('file')
.where('file.userId = :userId AND file.id IN (:...fileIds)', {
userId: me.id,
@@ -288,27 +302,38 @@ export class NoteDraftService {
}
}
//#endregion
+ }
- appliedDraft = {
- ...appliedDraft,
- visibility: data.visibility,
- cw: data.cw ?? null,
- fileIds: fileIds ?? [],
- replyId: data.replyId ?? null,
- renoteId: data.renoteId ?? null,
- channelId: data.channelId ?? null,
- text: data.text ?? null,
- hashtag: data.hashtag ?? null,
- hasPoll: data.poll != null,
- pollChoices: data.poll ? data.poll.choices : [],
- pollMultiple: data.poll ? data.poll.multiple : false,
- pollExpiresAt: data.poll ? data.poll.expiresAt : null,
- pollExpiredAfter: data.poll ? data.poll.expiredAfter ?? null : null,
- visibleUserIds: data.visibleUserIds ?? [],
- localOnly: data.localOnly,
- reactionAcceptance: data.reactionAcceptance,
- } satisfies MiNoteDraft;
+ @bindThis
+ public async schedule(draft: MiNoteDraft): Promise {
+ if (!draft.isActuallyScheduled) return;
+ if (draft.scheduledAt == null) return;
+ if (draft.scheduledAt.getTime() <= Date.now()) return;
- return appliedDraft;
+ const delay = draft.scheduledAt.getTime() - Date.now();
+ this.queueService.postScheduledNoteQueue.add(draft.id, {
+ noteDraftId: draft.id,
+ }, {
+ delay,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
+ });
+ }
+
+ @bindThis
+ public async clearSchedule(draftId: MiNoteDraft['id']): Promise {
+ // TODO: 線形探索なのをどうにかする
+ const jobs = await this.queueService.postScheduledNoteQueue.getJobs(['delayed', 'waiting', 'active']);
+ for (const job of jobs) {
+ if (job.data.noteDraftId === draftId) {
+ await job.remove();
+ }
+ }
}
}
diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts
index eeade4569b..310ffec7ce 100644
--- a/packages/backend/src/core/NotificationService.ts
+++ b/packages/backend/src/core/NotificationService.ts
@@ -202,7 +202,7 @@ export class NotificationService implements OnApplicationShutdown {
}
// TODO
- //const locales = await import('../../../../locales/index.js');
+ //const locales = await import('i18n');
// TODO: locale ファイルをクライアント用とサーバー用で分けたい
@@ -271,7 +271,7 @@ export class NotificationService implements OnApplicationShutdown {
let untilTime = untilId ? this.toXListId(untilId) : null;
let notifications: MiNotification[];
- for (;;) {
+ for (; ;) {
let notificationsRes: [id: string, fields: string[]][];
// sinceidのみの場合は古い順、そうでない場合は新しい順。 QueryService.makePaginationQueryも参照
diff --git a/packages/backend/src/core/PageService.ts b/packages/backend/src/core/PageService.ts
index 7f0e5c7ccc..4abeb30fce 100644
--- a/packages/backend/src/core/PageService.ts
+++ b/packages/backend/src/core/PageService.ts
@@ -29,7 +29,7 @@ export interface PageBody {
variables: Array>;
script: string;
eyeCatchingImage?: MiDriveFile | null;
- font: string;
+ font: 'serif' | 'sans-serif';
alignCenter: boolean;
hideTitleWhenPinned: boolean;
}
@@ -141,7 +141,7 @@ export class PageService {
eyeCatchingImageId: body.eyeCatchingImage === undefined ? undefined : (body.eyeCatchingImage?.id ?? null),
});
- console.log("page.content", page.content);
+ console.log('page.content', page.content);
if (body.content != null) {
const beforeReferencedNotes = this.collectReferencedNotes(page.content);
diff --git a/packages/backend/src/core/QueueModule.ts b/packages/backend/src/core/QueueModule.ts
index b10b8e5899..ecd96261e0 100644
--- a/packages/backend/src/core/QueueModule.ts
+++ b/packages/backend/src/core/QueueModule.ts
@@ -16,11 +16,13 @@ import {
RelationshipJobData,
UserWebhookDeliverJobData,
SystemWebhookDeliverJobData,
+ PostScheduledNoteJobData,
} from '../queue/types.js';
import type { Provider } from '@nestjs/common';
export type SystemQueue = Bull.Queue>;
export type EndedPollNotificationQueue = Bull.Queue;
+export type PostScheduledNoteQueue = Bull.Queue;
export type DeliverQueue = Bull.Queue;
export type InboxQueue = Bull.Queue;
export type DbQueue = Bull.Queue;
@@ -41,6 +43,12 @@ const $endedPollNotification: Provider = {
inject: [DI.config],
};
+const $postScheduledNote: Provider = {
+ provide: 'queue:postScheduledNote',
+ useFactory: (config: Config) => new Bull.Queue(QUEUE.POST_SCHEDULED_NOTE, baseQueueOptions(config, QUEUE.POST_SCHEDULED_NOTE)),
+ inject: [DI.config],
+};
+
const $deliver: Provider = {
provide: 'queue:deliver',
useFactory: (config: Config) => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config, QUEUE.DELIVER)),
@@ -89,6 +97,7 @@ const $systemWebhookDeliver: Provider = {
providers: [
$system,
$endedPollNotification,
+ $postScheduledNote,
$deliver,
$inbox,
$db,
@@ -100,6 +109,7 @@ const $systemWebhookDeliver: Provider = {
exports: [
$system,
$endedPollNotification,
+ $postScheduledNote,
$deliver,
$inbox,
$db,
@@ -113,6 +123,7 @@ export class QueueModule implements OnApplicationShutdown {
constructor(
@Inject('queue:system') public systemQueue: SystemQueue,
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
+ @Inject('queue:postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue,
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,
@@ -129,6 +140,7 @@ export class QueueModule implements OnApplicationShutdown {
await Promise.all([
this.systemQueue.close(),
this.endedPollNotificationQueue.close(),
+ this.postScheduledNoteQueue.close(),
this.deliverQueue.close(),
this.inboxQueue.close(),
this.dbQueue.close(),
diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts
index 0f225a8242..f90ae80731 100644
--- a/packages/backend/src/core/QueueService.ts
+++ b/packages/backend/src/core/QueueService.ts
@@ -6,7 +6,6 @@
import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
import { MetricsTime, type JobType } from 'bullmq';
-import { parse as parseRedisInfo } from 'redis-info';
import type { IActivity } from '@/core/activitypub/type.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js';
@@ -31,6 +30,7 @@ import type {
DbQueue,
DeliverQueue,
EndedPollNotificationQueue,
+ PostScheduledNoteQueue,
InboxQueue,
ObjectStorageQueue,
RelationshipQueue,
@@ -44,6 +44,7 @@ import type * as Bull from 'bullmq';
export const QUEUE_TYPES = [
'system',
'endedPollNotification',
+ 'postScheduledNote',
'deliver',
'inbox',
'db',
@@ -84,6 +85,19 @@ const REPEATABLE_SYSTEM_JOB_DEF = [{
pattern: '0 4 * * *',
}];
+function parseRedisInfo(infoText: string): Record {
+ const fields = infoText
+ .split('\n')
+ .filter(line => line.length > 0 && !line.startsWith('#'))
+ .map(line => line.trim().split(':'));
+
+ const result: Record = {};
+ for (const [key, value] of fields) {
+ result[key] = value;
+ }
+ return result;
+}
+
@Injectable()
export class QueueService {
constructor(
@@ -92,6 +106,7 @@ export class QueueService {
@Inject('queue:system') public systemQueue: SystemQueue,
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
+ @Inject('queue:postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue,
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,
@@ -717,6 +732,7 @@ export class QueueService {
switch (type) {
case 'system': return this.systemQueue;
case 'endedPollNotification': return this.endedPollNotificationQueue;
+ case 'postScheduledNote': return this.postScheduledNoteQueue;
case 'deliver': return this.deliverQueue;
case 'inbox': return this.inboxQueue;
case 'db': return this.dbQueue;
@@ -756,8 +772,8 @@ export class QueueService {
@bindThis
public async queueRetryJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
const queue = this.getQueue(queueType);
- const job: Bull.Job | null = await queue.getJob(jobId);
- if (job) {
+ const job = await queue.getJob(jobId);
+ if (job != null) {
if (job.finishedOn != null) {
await job.retry();
} else {
@@ -769,8 +785,8 @@ export class QueueService {
@bindThis
public async queueRemoveJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
const queue = this.getQueue(queueType);
- const job: Bull.Job | null = await queue.getJob(jobId);
- if (job) {
+ const job = await queue.getJob(jobId);
+ if (job != null) {
await job.remove();
}
}
@@ -803,8 +819,8 @@ export class QueueService {
@bindThis
public async queueGetJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
const queue = this.getQueue(queueType);
- const job: Bull.Job | null = await queue.getJob(jobId);
- if (job) {
+ const job = await queue.getJob(jobId);
+ if (job != null) {
return this.packJobData(job);
} else {
throw new Error(`Job not found: ${jobId}`);
@@ -886,7 +902,7 @@ export class QueueService {
},
db: {
version: db.redis_version,
- mode: db.redis_mode,
+ mode: db.redis_mode as 'cluster' | 'standalone' | 'sentinel',
runId: db.run_id,
processId: db.process_id,
port: parseInt(db.tcp_port),
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index b47972008c..203cd54498 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -32,6 +32,7 @@ import { NotificationService } from '@/core/NotificationService.js';
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
import { MAX_NOTE_ATTACHMENTS } from '@/const.js';
+// misskey-js の rolePolicies と同期すべし
export type RolePolicies = {
gtlAvailable: boolean;
ltlAvailable: boolean;
@@ -69,6 +70,7 @@ export type RolePolicies = {
chatAvailability: 'available' | 'readonly' | 'unavailable';
uploadableFileTypes: string[];
noteDraftLimit: number;
+ scheduledNoteLimit: number;
watermarkAvailable: boolean;
canNote: boolean;
renotePolicy: 'allow' | 'renoteOnly' | 'disallow';
@@ -106,20 +108,21 @@ export const DEFAULT_POLICIES: RolePolicies = {
userEachUserListsLimit: 50,
rateLimitFactor: 1,
avatarDecorationLimit: 1,
- canImportAntennas: true,
- canImportBlocking: true,
- canImportFollowing: true,
- canImportMuting: true,
- canImportUserLists: true,
+ canImportAntennas: false,
+ canImportBlocking: false,
+ canImportFollowing: false,
+ canImportMuting: false,
+ canImportUserLists: false,
chatAvailability: 'available',
uploadableFileTypes: [
- 'text/plain',
+ 'text/*',
'application/json',
'image/*',
'video/*',
'audio/*',
],
noteDraftLimit: 10,
+ scheduledNoteLimit: 1,
watermarkAvailable: true,
canNote: true,
renotePolicy: 'allow',
@@ -322,7 +325,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
default:
return false;
}
- } catch (err) {
+ } catch (_) {
// TODO: log error
return false;
}
@@ -455,6 +458,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
return [...set];
}),
noteDraftLimit: calc('noteDraftLimit', vs => Math.max(...vs)),
+ scheduledNoteLimit: calc('scheduledNoteLimit', vs => Math.max(...vs)),
watermarkAvailable: calc('watermarkAvailable', vs => vs.some(v => v === true)),
canNote: calc('canNote', vs => vs.some(v => v === true)),
renotePolicy: calc('renotePolicy', aggregateRenotePolicy),
diff --git a/packages/backend/src/core/SearchService.ts b/packages/backend/src/core/SearchService.ts
index 71dc718916..87097ada93 100644
--- a/packages/backend/src/core/SearchService.ts
+++ b/packages/backend/src/core/SearchService.ts
@@ -190,8 +190,7 @@ export class SearchService {
return this.searchNoteByMeiliSearch(q, me, opts, pagination);
}
default: {
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const typeCheck: never = this.provider;
+ const _: never = this.provider;
return [];
}
}
diff --git a/packages/backend/src/core/UserSuspendService.ts b/packages/backend/src/core/UserSuspendService.ts
index 7920e58e36..3ecb912a64 100644
--- a/packages/backend/src/core/UserSuspendService.ts
+++ b/packages/backend/src/core/UserSuspendService.ts
@@ -49,8 +49,8 @@ export class UserSuspendService {
});
(async () => {
- await this.postSuspend(user).catch(e => {});
- await this.unFollowAll(user).catch(e => {});
+ await this.postSuspend(user).catch(_ => {});
+ await this.unFollowAll(user).catch(_ => {});
})();
}
@@ -67,7 +67,7 @@ export class UserSuspendService {
});
(async () => {
- await this.postUnsuspend(user).catch(e => {});
+ await this.postUnsuspend(user).catch(_ => {});
})();
}
diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts
index 67ec6cc7b0..e3ceebccae 100644
--- a/packages/backend/src/core/UtilityService.ts
+++ b/packages/backend/src/core/UtilityService.ts
@@ -98,7 +98,7 @@ export class UtilityService {
try {
// TODO: RE2インスタンスをキャッシュ
return new RE2(regexp[1], regexp[2]).test(text);
- } catch (err) {
+ } catch (_) {
// This should never happen due to input sanitisation.
return false;
}
@@ -133,6 +133,7 @@ export class UtilityService {
@bindThis
public isFederationAllowedHost(host: string): boolean {
+ if (this.isSelfHost(host)) return true;
if (this.meta.federation === 'none') return false;
if (this.meta.federation === 'specified' && !this.meta.federationHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`))) return false;
if (this.isBlockedHost(this.meta.blockedHosts, host)) return false;
diff --git a/packages/backend/src/core/WebAuthnService.ts b/packages/backend/src/core/WebAuthnService.ts
index 372e1e2ab7..31c8d67c60 100644
--- a/packages/backend/src/core/WebAuthnService.ts
+++ b/packages/backend/src/core/WebAuthnService.ts
@@ -66,7 +66,6 @@ export class WebAuthnService {
userID: isoUint8Array.fromUTF8String(userId),
userName: userName,
userDisplayName: userDisplayName,
- attestationType: 'indirect',
excludeCredentials: keys.map(key => (<{ id: string; transports?: AuthenticatorTransportFuture[]; }>{
id: key.id,
transports: key.transports ?? undefined,
diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts
index 907b5ea6be..b112912b1b 100644
--- a/packages/backend/src/core/WebhookTestService.ts
+++ b/packages/backend/src/core/WebhookTestService.ts
@@ -106,6 +106,7 @@ function generateDummyNote(override?: Partial): MiNote {
replyUserHost: null,
renoteUserId: null,
renoteUserHost: null,
+ renoteChannelId: null,
...override,
};
}
@@ -244,7 +245,6 @@ export class WebhookTestService {
case 'reaction':
return;
default: {
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
const _exhaustiveAssertion: never = params.type;
return;
}
@@ -327,7 +327,6 @@ export class WebhookTestService {
break;
}
default: {
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
const _exhaustiveAssertion: never = params.type;
return;
}
@@ -412,7 +411,7 @@ export class WebhookTestService {
name: user.name,
username: user.username,
host: user.host,
- avatarUrl: user.avatarId == null ? null : user.avatarUrl,
+ avatarUrl: (user.avatarId == null ? null : user.avatarUrl) ?? '',
avatarBlurhash: user.avatarId == null ? null : user.avatarBlurhash,
avatarDecorations: user.avatarDecorations.map(it => ({
id: it.id,
diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts
index e88f60b806..ff47ca930d 100644
--- a/packages/backend/src/core/activitypub/ApInboxService.ts
+++ b/packages/backend/src/core/activitypub/ApInboxService.ts
@@ -5,6 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
+import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
@@ -14,8 +15,8 @@ import { NotePiningService } from '@/core/NotePiningService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import { NoteDeleteService } from '@/core/NoteDeleteService.js';
import { NoteCreateService } from '@/core/NoteCreateService.js';
+import { acquireApObjectLock } from '@/misc/distributed-lock.js';
import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js';
-import { AppLockService } from '@/core/AppLockService.js';
import type Logger from '@/logger.js';
import { IdService } from '@/core/IdService.js';
import { StatusError } from '@/misc/status-error.js';
@@ -48,8 +49,8 @@ export class ApInboxService {
@Inject(DI.config)
private config: Config,
- @Inject(DI.meta)
- private meta: MiMeta,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -76,7 +77,6 @@ export class ApInboxService {
private userBlockingService: UserBlockingService,
private noteCreateService: NoteCreateService,
private noteDeleteService: NoteDeleteService,
- private appLockService: AppLockService,
private apResolverService: ApResolverService,
private apDbResolverService: ApDbResolverService,
private apLoggerService: ApLoggerService,
@@ -95,7 +95,7 @@ export class ApInboxService {
if (isCollectionOrOrderedCollection(activity)) {
const results = [] as [string, string | void][];
// eslint-disable-next-line no-param-reassign
- resolver ??= this.apResolverService.createResolver();
+ resolver ??= await this.apResolverService.createResolver();
const items = toArray(isCollection(activity) ? activity.items : activity.orderedItems);
if (items.length >= resolver.getRecursionLimit()) {
@@ -221,7 +221,7 @@ export class ApInboxService {
this.logger.info(`Accept: ${uri}`);
// eslint-disable-next-line no-param-reassign
- resolver ??= this.apResolverService.createResolver();
+ resolver ??= await this.apResolverService.createResolver();
const object = await resolver.resolve(activity.object).catch(err => {
this.logger.error(`Resolution failed: ${err}`);
@@ -284,7 +284,7 @@ export class ApInboxService {
this.logger.info(`Announce: ${uri}`);
// eslint-disable-next-line no-param-reassign
- resolver ??= this.apResolverService.createResolver();
+ resolver ??= await this.apResolverService.createResolver();
if (!activity.object) return 'skip: activity has no object property';
const targetUri = getApId(activity.object);
@@ -311,7 +311,7 @@ export class ApInboxService {
// アナウンス先が許可されているかチェック
if (!this.utilityService.isFederationAllowedUri(uri)) return;
- const unlock = await this.appLockService.getApLock(uri);
+ const unlock = await acquireApObjectLock(this.redisClient, uri);
try {
// 既に同じURIを持つものが登録されていないかチェック
@@ -406,7 +406,7 @@ export class ApInboxService {
}
// eslint-disable-next-line no-param-reassign
- resolver ??= this.apResolverService.createResolver();
+ resolver ??= await this.apResolverService.createResolver();
const object = await resolver.resolve(activity.object).catch(e => {
this.logger.error(`Resolution failed: ${e}`);
@@ -438,7 +438,7 @@ export class ApInboxService {
}
}
- const unlock = await this.appLockService.getApLock(uri);
+ const unlock = await acquireApObjectLock(this.redisClient, uri);
try {
const exist = await this.apNoteService.fetchNote(note);
@@ -522,7 +522,7 @@ export class ApInboxService {
private async deleteNote(actor: MiRemoteUser, uri: string): Promise {
this.logger.info(`Deleting the Note: ${uri}`);
- const unlock = await this.appLockService.getApLock(uri);
+ const unlock = await acquireApObjectLock(this.redisClient, uri);
try {
const note = await this.apDbResolverService.getNoteFromApId(uri);
@@ -575,7 +575,7 @@ export class ApInboxService {
this.logger.info(`Reject: ${uri}`);
// eslint-disable-next-line no-param-reassign
- resolver ??= this.apResolverService.createResolver();
+ resolver ??= await this.apResolverService.createResolver();
const object = await resolver.resolve(activity.object).catch(e => {
this.logger.error(`Resolution failed: ${e}`);
@@ -642,7 +642,7 @@ export class ApInboxService {
this.logger.info(`Undo: ${uri}`);
// eslint-disable-next-line no-param-reassign
- resolver ??= this.apResolverService.createResolver();
+ resolver ??= await this.apResolverService.createResolver();
const object = await resolver.resolve(activity.object).catch(e => {
this.logger.error(`Resolution failed: ${e}`);
@@ -774,7 +774,7 @@ export class ApInboxService {
this.logger.debug('Update');
// eslint-disable-next-line no-param-reassign
- resolver ??= this.apResolverService.createResolver();
+ resolver ??= await this.apResolverService.createResolver();
const object = await resolver.resolve(activity.object).catch(e => {
this.logger.error(`Resolution failed: ${e}`);
diff --git a/packages/backend/src/core/activitypub/ApMfmService.ts b/packages/backend/src/core/activitypub/ApMfmService.ts
index f4c07e472c..a928ed5ccf 100644
--- a/packages/backend/src/core/activitypub/ApMfmService.ts
+++ b/packages/backend/src/core/activitypub/ApMfmService.ts
@@ -5,7 +5,7 @@
import { Injectable } from '@nestjs/common';
import * as mfm from 'mfm-js';
-import { MfmService, Appender } from '@/core/MfmService.js';
+import { MfmService } from '@/core/MfmService.js';
import type { MiNote } from '@/models/Note.js';
import { bindThis } from '@/decorators.js';
import { extractApHashtagObjects } from './models/tag.js';
@@ -25,17 +25,17 @@ export class ApMfmService {
}
@bindThis
- public getNoteHtml(note: Pick, additionalAppender: Appender[] = []) {
+ public getNoteHtml(note: Pick, extraHtml: string | null = null) {
let noMisskeyContent = false;
const srcMfm = (note.text ?? '');
const parsed = mfm.parse(srcMfm);
- if (!additionalAppender.length && parsed.every(n => ['text', 'unicodeEmoji', 'emojiCode', 'mention', 'hashtag', 'url'].includes(n.type))) {
+ if (extraHtml == null && parsed.every(n => ['text', 'unicodeEmoji', 'emojiCode', 'mention', 'hashtag', 'url'].includes(n.type))) {
noMisskeyContent = true;
}
- const content = this.mfmService.toHtml(parsed, JSON.parse(note.mentionedRemoteUsers), additionalAppender);
+ const content = this.mfmService.toHtml(parsed, JSON.parse(note.mentionedRemoteUsers), extraHtml);
return {
content,
diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts
index 55521d6e3a..8c461b6031 100644
--- a/packages/backend/src/core/activitypub/ApRendererService.ts
+++ b/packages/backend/src/core/activitypub/ApRendererService.ts
@@ -19,7 +19,7 @@ import type { MiEmoji } from '@/models/Emoji.js';
import type { MiPoll } from '@/models/Poll.js';
import type { MiPollVote } from '@/models/PollVote.js';
import { UserKeypairService } from '@/core/UserKeypairService.js';
-import { MfmService, type Appender } from '@/core/MfmService.js';
+import { MfmService } from '@/core/MfmService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import type { MiUserKeypair } from '@/models/UserKeypair.js';
@@ -28,6 +28,7 @@ import { bindThis } from '@/decorators.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { IdService } from '@/core/IdService.js';
import { UtilityService } from '@/core/UtilityService.js';
+import { escapeHtml } from '@/misc/escape-html.js';
import { JsonLdService } from './JsonLdService.js';
import { ApMfmService } from './ApMfmService.js';
import { CONTEXT } from './misc/contexts.js';
@@ -384,7 +385,7 @@ export class ApRendererService {
inReplyTo = null;
}
- let quote;
+ let quote: string | undefined;
if (note.renoteId) {
const renote = await this.notesRepository.findOneBy({ id: note.renoteId });
@@ -430,29 +431,18 @@ export class ApRendererService {
poll = await this.pollsRepository.findOneBy({ noteId: note.id });
}
- const apAppend: Appender[] = [];
+ let extraHtml: string | null = null;
- if (quote) {
+ if (quote != null) {
// Append quote link as `
RE: ...`
- // the claas name `quote-inline` is used in non-misskey clients for styling quote notes.
+ // the class name `quote-inline` is used in non-misskey clients for styling quote notes.
// For compatibility, the span part should be kept as possible.
- apAppend.push((doc, body) => {
- body.appendChild(doc.createElement('br'));
- body.appendChild(doc.createElement('br'));
- const span = doc.createElement('span');
- span.className = 'quote-inline';
- span.appendChild(doc.createTextNode('RE: '));
- const link = doc.createElement('a');
- link.setAttribute('href', quote);
- link.textContent = quote;
- span.appendChild(link);
- body.appendChild(span);
- });
+ extraHtml = `
RE: ${escapeHtml(quote)}`;
}
const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
- const { content, noMisskeyContent } = this.apMfmService.getNoteHtml(note, apAppend);
+ const { content, noMisskeyContent } = this.apMfmService.getNoteHtml(note, extraHtml);
const emojis = await this.getEmojis(note.emojis);
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
@@ -525,7 +515,7 @@ export class ApRendererService {
const restPart = maybeUrl.slice(match[0].length);
return `${urlPart}${restPart}`;
- } catch (e) {
+ } catch (_) {
return maybeUrl;
}
};
diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts
index 61d328ccac..d14b82dc92 100644
--- a/packages/backend/src/core/activitypub/ApRequestService.ts
+++ b/packages/backend/src/core/activitypub/ApRequestService.ts
@@ -6,7 +6,7 @@
import * as crypto from 'node:crypto';
import { URL } from 'node:url';
import { Inject, Injectable } from '@nestjs/common';
-import { Window } from 'happy-dom';
+import * as htmlParser from 'node-html-parser';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type { MiUser } from '@/models/User.js';
@@ -215,29 +215,9 @@ export class ApRequestService {
_followAlternate === true
) {
const html = await res.text();
- const { window, happyDOM } = new Window({
- settings: {
- disableJavaScriptEvaluation: true,
- disableJavaScriptFileLoading: true,
- disableCSSFileLoading: true,
- disableComputedStyleRendering: true,
- handleDisabledFileLoadingAsSuccess: true,
- navigation: {
- disableMainFrameNavigation: true,
- disableChildFrameNavigation: true,
- disableChildPageNavigation: true,
- disableFallbackToSetURL: true,
- },
- timer: {
- maxTimeout: 0,
- maxIntervalTime: 0,
- maxIntervalIterations: 0,
- },
- },
- });
- const document = window.document;
+
try {
- document.documentElement.innerHTML = html;
+ const document = htmlParser.parse(html);
const alternate = document.querySelector('head > link[rel="alternate"][type="application/activity+json"]');
if (alternate) {
@@ -246,10 +226,8 @@ export class ApRequestService {
return await this.signedGet(href, user, allowSoftfail, false);
}
}
- } catch (e) {
+ } catch (_) {
// something went wrong parsing the HTML, ignore the whole thing
- } finally {
- happyDOM.close().catch(err => {});
}
}
//#endregion
diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts
index 646150455b..0f51b1ce8d 100644
--- a/packages/backend/src/core/activitypub/ApResolverService.ts
+++ b/packages/backend/src/core/activitypub/ApResolverService.ts
@@ -3,10 +3,17 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Inject, Injectable } from '@nestjs/common';
+import { Inject, Injectable, Scope } from '@nestjs/common';
import { IsNull, Not } from 'typeorm';
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
-import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js';
+import type {
+ FollowRequestsRepository,
+ MiMeta,
+ NoteReactionsRepository,
+ NotesRepository,
+ PollsRepository,
+ UsersRepository
+} from '@/models/_.js';
import type { Config } from '@/config.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { DI } from '@/di-symbols.js';
@@ -16,26 +23,43 @@ import { LoggerService } from '@/core/LoggerService.js';
import type Logger from '@/logger.js';
import { SystemAccountService } from '@/core/SystemAccountService.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
+import type { ICollection, IObject, IOrderedCollection } from './type.js';
import { isCollectionOrOrderedCollection } from './type.js';
import { ApDbResolverService } from './ApDbResolverService.js';
import { ApRendererService } from './ApRendererService.js';
import { ApRequestService } from './ApRequestService.js';
import { FetchAllowSoftFailMask } from './misc/check-against-url.js';
-import type { IObject, ICollection, IOrderedCollection } from './type.js';
+import { ModuleRef } from '@nestjs/core';
+@Injectable({ scope: Scope.TRANSIENT })
export class Resolver {
private history: Set;
private user?: MiLocalUser;
private logger: Logger;
+ private recursionLimit = 256;
constructor(
+ @Inject(DI.config)
private config: Config,
+
+ @Inject(DI.meta)
private meta: MiMeta,
+
+ @Inject(DI.usersRepository)
private usersRepository: UsersRepository,
+
+ @Inject(DI.notesRepository)
private notesRepository: NotesRepository,
+
+ @Inject(DI.pollsRepository)
private pollsRepository: PollsRepository,
+
+ @Inject(DI.noteReactionsRepository)
private noteReactionsRepository: NoteReactionsRepository,
+
+ @Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository,
+
private utilityService: UtilityService,
private systemAccountService: SystemAccountService,
private apRequestService: ApRequestService,
@@ -43,7 +67,6 @@ export class Resolver {
private apRendererService: ApRendererService,
private apDbResolverService: ApDbResolverService,
private loggerService: LoggerService,
- private recursionLimit = 256,
) {
this.history = new Set();
this.logger = this.loggerService.getLogger('ap-resolve');
@@ -180,54 +203,12 @@ export class Resolver {
@Injectable()
export class ApResolverService {
constructor(
- @Inject(DI.config)
- private config: Config,
-
- @Inject(DI.meta)
- private meta: MiMeta,
-
- @Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
-
- @Inject(DI.notesRepository)
- private notesRepository: NotesRepository,
-
- @Inject(DI.pollsRepository)
- private pollsRepository: PollsRepository,
-
- @Inject(DI.noteReactionsRepository)
- private noteReactionsRepository: NoteReactionsRepository,
-
- @Inject(DI.followRequestsRepository)
- private followRequestsRepository: FollowRequestsRepository,
-
- private utilityService: UtilityService,
- private systemAccountService: SystemAccountService,
- private apRequestService: ApRequestService,
- private httpRequestService: HttpRequestService,
- private apRendererService: ApRendererService,
- private apDbResolverService: ApDbResolverService,
- private loggerService: LoggerService,
+ private moduleRef: ModuleRef,
) {
}
@bindThis
- public createResolver(): Resolver {
- return new Resolver(
- this.config,
- this.meta,
- this.usersRepository,
- this.notesRepository,
- this.pollsRepository,
- this.noteReactionsRepository,
- this.followRequestsRepository,
- this.utilityService,
- this.systemAccountService,
- this.apRequestService,
- this.httpRequestService,
- this.apRendererService,
- this.apDbResolverService,
- this.loggerService,
- );
+ public async createResolver(): Promise {
+ return await this.moduleRef.create(Resolver);
}
}
diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts
index e7ece87b01..0496774c19 100644
--- a/packages/backend/src/core/activitypub/models/ApImageService.ts
+++ b/packages/backend/src/core/activitypub/models/ApImageService.ts
@@ -46,7 +46,7 @@ export class ApImageService {
throw new Error('actor has been suspended');
}
- const image = await this.apResolverService.createResolver().resolve(value);
+ const image = await (await this.apResolverService.createResolver()).resolve(value);
if (!isDocument(image)) return null;
diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index 8abacd293f..1fc5728c98 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -5,14 +5,15 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
+import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import type { PollsRepository, EmojisRepository, MiMeta } from '@/models/_.js';
import type { Config } from '@/config.js';
import type { MiRemoteUser } from '@/models/User.js';
import type { MiNote } from '@/models/Note.js';
+import { acquireApObjectLock } from '@/misc/distributed-lock.js';
import { toArray, toSingle, unique } from '@/misc/prelude/array.js';
import type { MiEmoji } from '@/models/Emoji.js';
-import { AppLockService } from '@/core/AppLockService.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import { NoteCreateService } from '@/core/NoteCreateService.js';
import type Logger from '@/logger.js';
@@ -48,6 +49,9 @@ export class ApNoteService {
@Inject(DI.meta)
private meta: MiMeta,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
@Inject(DI.pollsRepository)
private pollsRepository: PollsRepository,
@@ -67,7 +71,6 @@ export class ApNoteService {
private apMentionService: ApMentionService,
private apImageService: ApImageService,
private apQuestionService: ApQuestionService,
- private appLockService: AppLockService,
private pollService: PollService,
private noteCreateService: NoteCreateService,
private apDbResolverService: ApDbResolverService,
@@ -125,7 +128,7 @@ export class ApNoteService {
@bindThis
public async createNote(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver, silent = false): Promise {
// eslint-disable-next-line no-param-reassign
- if (resolver == null) resolver = this.apResolverService.createResolver();
+ if (resolver == null) resolver = await this.apResolverService.createResolver();
const object = await resolver.resolve(value);
@@ -354,7 +357,7 @@ export class ApNoteService {
throw new StatusError('blocked host', 451);
}
- const unlock = await this.appLockService.getApLock(uri);
+ const unlock = await acquireApObjectLock(this.redisClient, uri);
try {
//#region このサーバーに既に登録されていたらそれを返す
diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index e52078ed0f..ebe8e9c964 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -310,7 +310,7 @@ export class ApPersonService implements OnModuleInit {
}
// eslint-disable-next-line no-param-reassign
- if (resolver == null) resolver = this.apResolverService.createResolver();
+ if (resolver == null) resolver = await this.apResolverService.createResolver();
const object = await resolver.resolve(uri);
if (object.id == null) throw new Error('invalid object.id: ' + object.id);
@@ -500,7 +500,7 @@ export class ApPersonService implements OnModuleInit {
//#endregion
// eslint-disable-next-line no-param-reassign
- if (resolver == null) resolver = this.apResolverService.createResolver();
+ if (resolver == null) resolver = await this.apResolverService.createResolver();
const object = hint ?? await resolver.resolve(uri);
@@ -678,7 +678,7 @@ export class ApPersonService implements OnModuleInit {
// リモートサーバーからフェッチしてきて登録
// eslint-disable-next-line no-param-reassign
- if (resolver == null) resolver = this.apResolverService.createResolver();
+ if (resolver == null) resolver = await this.apResolverService.createResolver();
return await this.createPerson(uri, resolver);
}
@@ -707,7 +707,7 @@ export class ApPersonService implements OnModuleInit {
this.logger.info(`Updating the featured: ${user.uri}`);
- const _resolver = resolver ?? this.apResolverService.createResolver();
+ const _resolver = resolver ?? await this.apResolverService.createResolver();
// Resolve to (Ordered)Collection Object
const collection = await _resolver.resolveCollection(user.featured);
diff --git a/packages/backend/src/core/activitypub/models/ApQuestionService.ts b/packages/backend/src/core/activitypub/models/ApQuestionService.ts
index a2cdaf02ca..8ac2f21e26 100644
--- a/packages/backend/src/core/activitypub/models/ApQuestionService.ts
+++ b/packages/backend/src/core/activitypub/models/ApQuestionService.ts
@@ -45,7 +45,7 @@ export class ApQuestionService {
@bindThis
public async extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise {
// eslint-disable-next-line no-param-reassign
- if (resolver == null) resolver = this.apResolverService.createResolver();
+ if (resolver == null) resolver = await this.apResolverService.createResolver();
const question = await resolver.resolve(source);
if (!isQuestion(question)) throw new Error('invalid type');
@@ -91,7 +91,7 @@ export class ApQuestionService {
// resolve new Question object
// eslint-disable-next-line no-param-reassign
- if (resolver == null) resolver = this.apResolverService.createResolver();
+ if (resolver == null) resolver = await this.apResolverService.createResolver();
const question = await resolver.resolve(value);
this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
diff --git a/packages/backend/src/core/chart/charts/active-users.ts b/packages/backend/src/core/chart/charts/active-users.ts
index 05905f3782..7b9840af87 100644
--- a/packages/backend/src/core/chart/charts/active-users.ts
+++ b/packages/backend/src/core/chart/charts/active-users.ts
@@ -5,11 +5,12 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
-import { AppLockService } from '@/core/AppLockService.js';
+import * as Redis from 'ioredis';
import type { MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js';
import { name, schema } from './entities/active-users.js';
@@ -28,11 +29,13 @@ export default class ActiveUsersChart extends Chart { // eslint-d
@Inject(DI.db)
private db: DataSource,
- private appLockService: AppLockService,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
private chartLoggerService: ChartLoggerService,
private idService: IdService,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/ap-request.ts b/packages/backend/src/core/chart/charts/ap-request.ts
index 04e771a95b..ed790de7b5 100644
--- a/packages/backend/src/core/chart/charts/ap-request.ts
+++ b/packages/backend/src/core/chart/charts/ap-request.ts
@@ -5,9 +5,10 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
-import { AppLockService } from '@/core/AppLockService.js';
+import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js';
import { name, schema } from './entities/ap-request.js';
@@ -22,10 +23,12 @@ export default class ApRequestChart extends Chart { // eslint-dis
@Inject(DI.db)
private db: DataSource,
- private appLockService: AppLockService,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/drive.ts b/packages/backend/src/core/chart/charts/drive.ts
index 613e074a9f..782873809a 100644
--- a/packages/backend/src/core/chart/charts/drive.ts
+++ b/packages/backend/src/core/chart/charts/drive.ts
@@ -5,10 +5,11 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
+import * as Redis from 'ioredis';
import type { MiDriveFile } from '@/models/DriveFile.js';
-import { AppLockService } from '@/core/AppLockService.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js';
import { name, schema } from './entities/drive.js';
@@ -23,10 +24,12 @@ export default class DriveChart extends Chart { // eslint-disable
@Inject(DI.db)
private db: DataSource,
- private appLockService: AppLockService,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/federation.ts b/packages/backend/src/core/chart/charts/federation.ts
index c9b43cc66d..b7a7f640b8 100644
--- a/packages/backend/src/core/chart/charts/federation.ts
+++ b/packages/backend/src/core/chart/charts/federation.ts
@@ -5,10 +5,11 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
+import * as Redis from 'ioredis';
import type { FollowingsRepository, InstancesRepository, MiMeta } from '@/models/_.js';
-import { AppLockService } from '@/core/AppLockService.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js';
import { name, schema } from './entities/federation.js';
@@ -26,16 +27,18 @@ export default class FederationChart extends Chart { // eslint-di
@Inject(DI.meta)
private meta: MiMeta,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository,
- private appLockService: AppLockService,
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/instance.ts b/packages/backend/src/core/chart/charts/instance.ts
index 97f3bc6f2b..b1657e0a0b 100644
--- a/packages/backend/src/core/chart/charts/instance.ts
+++ b/packages/backend/src/core/chart/charts/instance.ts
@@ -5,13 +5,14 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
+import * as Redis from 'ioredis';
import type { DriveFilesRepository, FollowingsRepository, UsersRepository, NotesRepository } from '@/models/_.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiNote } from '@/models/Note.js';
-import { AppLockService } from '@/core/AppLockService.js';
import { DI } from '@/di-symbols.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js';
import { name, schema } from './entities/instance.js';
@@ -26,6 +27,9 @@ export default class InstanceChart extends Chart { // eslint-disa
@Inject(DI.db)
private db: DataSource,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -39,10 +43,9 @@ export default class InstanceChart extends Chart { // eslint-disa
private followingsRepository: FollowingsRepository,
private utilityService: UtilityService,
- private appLockService: AppLockService,
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema, true);
}
protected async tickMajor(group: string): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/notes.ts b/packages/backend/src/core/chart/charts/notes.ts
index f763b5fffa..aa64e2329a 100644
--- a/packages/backend/src/core/chart/charts/notes.ts
+++ b/packages/backend/src/core/chart/charts/notes.ts
@@ -5,11 +5,12 @@
import { Injectable, Inject } from '@nestjs/common';
import { Not, IsNull, DataSource } from 'typeorm';
+import * as Redis from 'ioredis';
import type { NotesRepository } from '@/models/_.js';
import type { MiNote } from '@/models/Note.js';
-import { AppLockService } from '@/core/AppLockService.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js';
import { name, schema } from './entities/notes.js';
@@ -24,13 +25,15 @@ export default class NotesChart extends Chart { // eslint-disable
@Inject(DI.db)
private db: DataSource,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
- private appLockService: AppLockService,
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/per-user-drive.ts b/packages/backend/src/core/chart/charts/per-user-drive.ts
index 404964d8b7..f7e92aecea 100644
--- a/packages/backend/src/core/chart/charts/per-user-drive.ts
+++ b/packages/backend/src/core/chart/charts/per-user-drive.ts
@@ -5,12 +5,13 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
+import * as Redis from 'ioredis';
import type { DriveFilesRepository } from '@/models/_.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
-import { AppLockService } from '@/core/AppLockService.js';
import { DI } from '@/di-symbols.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js';
import { name, schema } from './entities/per-user-drive.js';
@@ -25,14 +26,16 @@ export default class PerUserDriveChart extends Chart { // eslint-
@Inject(DI.db)
private db: DataSource,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
- private appLockService: AppLockService,
private driveFileEntityService: DriveFileEntityService,
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema, true);
}
protected async tickMajor(group: string): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/per-user-following.ts b/packages/backend/src/core/chart/charts/per-user-following.ts
index 588ac638de..ea431a5131 100644
--- a/packages/backend/src/core/chart/charts/per-user-following.ts
+++ b/packages/backend/src/core/chart/charts/per-user-following.ts
@@ -5,12 +5,13 @@
import { Injectable, Inject } from '@nestjs/common';
import { Not, IsNull, DataSource } from 'typeorm';
+import * as Redis from 'ioredis';
import type { MiUser } from '@/models/User.js';
-import { AppLockService } from '@/core/AppLockService.js';
import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { FollowingsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js';
import { name, schema } from './entities/per-user-following.js';
@@ -25,14 +26,16 @@ export default class PerUserFollowingChart extends Chart { // esl
@Inject(DI.db)
private db: DataSource,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
- private appLockService: AppLockService,
private userEntityService: UserEntityService,
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema, true);
}
protected async tickMajor(group: string): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/per-user-notes.ts b/packages/backend/src/core/chart/charts/per-user-notes.ts
index e4900772bb..824d60042d 100644
--- a/packages/backend/src/core/chart/charts/per-user-notes.ts
+++ b/packages/backend/src/core/chart/charts/per-user-notes.ts
@@ -5,12 +5,13 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
+import * as Redis from 'ioredis';
import type { MiUser } from '@/models/User.js';
import type { MiNote } from '@/models/Note.js';
-import { AppLockService } from '@/core/AppLockService.js';
import { DI } from '@/di-symbols.js';
import type { NotesRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js';
import { name, schema } from './entities/per-user-notes.js';
@@ -25,13 +26,15 @@ export default class PerUserNotesChart extends Chart { // eslint-
@Inject(DI.db)
private db: DataSource,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
- private appLockService: AppLockService,
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema, true);
}
protected async tickMajor(group: string): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/per-user-pv.ts b/packages/backend/src/core/chart/charts/per-user-pv.ts
index 31708fefa8..b3e1b2cea1 100644
--- a/packages/backend/src/core/chart/charts/per-user-pv.ts
+++ b/packages/backend/src/core/chart/charts/per-user-pv.ts
@@ -5,10 +5,11 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
+import * as Redis from 'ioredis';
import type { MiUser } from '@/models/User.js';
-import { AppLockService } from '@/core/AppLockService.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js';
import { name, schema } from './entities/per-user-pv.js';
@@ -23,10 +24,12 @@ export default class PerUserPvChart extends Chart { // eslint-dis
@Inject(DI.db)
private db: DataSource,
- private appLockService: AppLockService,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema, true);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/per-user-reactions.ts b/packages/backend/src/core/chart/charts/per-user-reactions.ts
index c29c4d2870..7bc1d9e7fa 100644
--- a/packages/backend/src/core/chart/charts/per-user-reactions.ts
+++ b/packages/backend/src/core/chart/charts/per-user-reactions.ts
@@ -5,12 +5,13 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
+import * as Redis from 'ioredis';
import type { MiUser } from '@/models/User.js';
import type { MiNote } from '@/models/Note.js';
-import { AppLockService } from '@/core/AppLockService.js';
import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js';
import { name, schema } from './entities/per-user-reactions.js';
@@ -25,11 +26,13 @@ export default class PerUserReactionsChart extends Chart { // esl
@Inject(DI.db)
private db: DataSource,
- private appLockService: AppLockService,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
private userEntityService: UserEntityService,
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema, true);
}
protected async tickMajor(group: string): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/test-grouped.ts b/packages/backend/src/core/chart/charts/test-grouped.ts
index 7a2844f4ed..8dd1a5d996 100644
--- a/packages/backend/src/core/chart/charts/test-grouped.ts
+++ b/packages/backend/src/core/chart/charts/test-grouped.ts
@@ -5,10 +5,11 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
-import { AppLockService } from '@/core/AppLockService.js';
+import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { name, schema } from './entities/test-grouped.js';
import type { KVs } from '../core.js';
@@ -24,10 +25,12 @@ export default class TestGroupedChart extends Chart { // eslint-d
@Inject(DI.db)
private db: DataSource,
- private appLockService: AppLockService,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
logger: Logger,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), logger, name, schema, true);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), logger, name, schema, true);
}
protected async tickMajor(group: string): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/test-intersection.ts b/packages/backend/src/core/chart/charts/test-intersection.ts
index b8d0556c9f..23b8649cce 100644
--- a/packages/backend/src/core/chart/charts/test-intersection.ts
+++ b/packages/backend/src/core/chart/charts/test-intersection.ts
@@ -5,10 +5,11 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
-import { AppLockService } from '@/core/AppLockService.js';
+import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { name, schema } from './entities/test-intersection.js';
import type { KVs } from '../core.js';
@@ -22,10 +23,12 @@ export default class TestIntersectionChart extends Chart { // esl
@Inject(DI.db)
private db: DataSource,
- private appLockService: AppLockService,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
logger: Logger,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), logger, name, schema);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), logger, name, schema);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/test-unique.ts b/packages/backend/src/core/chart/charts/test-unique.ts
index f94e008059..b84dd419ba 100644
--- a/packages/backend/src/core/chart/charts/test-unique.ts
+++ b/packages/backend/src/core/chart/charts/test-unique.ts
@@ -5,10 +5,11 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
-import { AppLockService } from '@/core/AppLockService.js';
+import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { name, schema } from './entities/test-unique.js';
import type { KVs } from '../core.js';
@@ -22,10 +23,12 @@ export default class TestUniqueChart extends Chart { // eslint-di
@Inject(DI.db)
private db: DataSource,
- private appLockService: AppLockService,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
logger: Logger,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), logger, name, schema);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), logger, name, schema);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/test.ts b/packages/backend/src/core/chart/charts/test.ts
index a90dc8f99b..0e95ce9239 100644
--- a/packages/backend/src/core/chart/charts/test.ts
+++ b/packages/backend/src/core/chart/charts/test.ts
@@ -5,10 +5,11 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
-import { AppLockService } from '@/core/AppLockService.js';
+import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { name, schema } from './entities/test.js';
import type { KVs } from '../core.js';
@@ -24,10 +25,12 @@ export default class TestChart extends Chart { // eslint-disable-
@Inject(DI.db)
private db: DataSource,
- private appLockService: AppLockService,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
logger: Logger,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), logger, name, schema);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), logger, name, schema);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/users.ts b/packages/backend/src/core/chart/charts/users.ts
index d148fc629b..4471c1df23 100644
--- a/packages/backend/src/core/chart/charts/users.ts
+++ b/packages/backend/src/core/chart/charts/users.ts
@@ -5,12 +5,13 @@
import { Injectable, Inject } from '@nestjs/common';
import { Not, IsNull, DataSource } from 'typeorm';
+import * as Redis from 'ioredis';
import type { MiUser } from '@/models/User.js';
-import { AppLockService } from '@/core/AppLockService.js';
import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { UsersRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js';
import { name, schema } from './entities/users.js';
@@ -25,14 +26,16 @@ export default class UsersChart extends Chart { // eslint-disable
@Inject(DI.db)
private db: DataSource,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
- private appLockService: AppLockService,
private userEntityService: UserEntityService,
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/entities/ChannelEntityService.ts b/packages/backend/src/core/entities/ChannelEntityService.ts
index 1ba7ca8e57..e26cddd281 100644
--- a/packages/backend/src/core/entities/ChannelEntityService.ts
+++ b/packages/backend/src/core/entities/ChannelEntityService.ts
@@ -4,36 +4,40 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
-import type { ChannelFavoritesRepository, ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NotesRepository } from '@/models/_.js';
+import type {
+ ChannelFavoritesRepository,
+ ChannelFollowingsRepository, ChannelMutingRepository,
+ ChannelsRepository,
+ DriveFilesRepository,
+ MiDriveFile,
+ MiNote,
+ NotesRepository,
+} from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js';
-import type { } from '@/models/Blocking.js';
import type { MiUser } from '@/models/User.js';
import type { MiChannel } from '@/models/Channel.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
import { DriveFileEntityService } from './DriveFileEntityService.js';
import { NoteEntityService } from './NoteEntityService.js';
-import { In } from 'typeorm';
@Injectable()
export class ChannelEntityService {
constructor(
@Inject(DI.channelsRepository)
private channelsRepository: ChannelsRepository,
-
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
-
@Inject(DI.channelFavoritesRepository)
private channelFavoritesRepository: ChannelFavoritesRepository,
-
+ @Inject(DI.channelMutingRepository)
+ private channelMutingRepository: ChannelMutingRepository,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
-
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
-
private noteEntityService: NoteEntityService,
private driveFileEntityService: DriveFileEntityService,
private idService: IdService,
@@ -45,31 +49,59 @@ export class ChannelEntityService {
src: MiChannel['id'] | MiChannel,
me?: { id: MiUser['id'] } | null | undefined,
detailed?: boolean,
+ opts?: {
+ bannerFiles?: Map;
+ followings?: Set;
+ favorites?: Set;
+ muting?: Set;
+ pinnedNotes?: Map;
+ },
): Promise> {
const channel = typeof src === 'object' ? src : await this.channelsRepository.findOneByOrFail({ id: src });
- const meId = me ? me.id : null;
- const banner = channel.bannerId ? await this.driveFilesRepository.findOneBy({ id: channel.bannerId }) : null;
+ let bannerFile: MiDriveFile | null = null;
+ if (channel.bannerId) {
+ bannerFile = opts?.bannerFiles?.get(channel.bannerId)
+ ?? await this.driveFilesRepository.findOneByOrFail({ id: channel.bannerId });
+ }
- const isFollowing = meId ? await this.channelFollowingsRepository.exists({
- where: {
- followerId: meId,
- followeeId: channel.id,
- },
- }) : false;
+ let isFollowing = false;
+ let isFavorited = false;
+ let isMuting = false;
+ if (me) {
+ isFollowing = opts?.followings?.has(channel.id) ?? await this.channelFollowingsRepository.exists({
+ where: {
+ followerId: me.id,
+ followeeId: channel.id,
+ },
+ });
- const isFavorited = meId ? await this.channelFavoritesRepository.exists({
- where: {
- userId: meId,
- channelId: channel.id,
- },
- }) : false;
+ isFavorited = opts?.favorites?.has(channel.id) ?? await this.channelFavoritesRepository.exists({
+ where: {
+ userId: me.id,
+ channelId: channel.id,
+ },
+ });
- const pinnedNotes = channel.pinnedNoteIds.length > 0 ? await this.notesRepository.find({
- where: {
- id: In(channel.pinnedNoteIds),
- },
- }) : [];
+ isMuting = opts?.muting?.has(channel.id) ?? await this.channelMutingRepository.exists({
+ where: {
+ userId: me.id,
+ channelId: channel.id,
+ },
+ });
+ }
+
+ const pinnedNotes = Array.of();
+ if (channel.pinnedNoteIds.length > 0) {
+ pinnedNotes.push(
+ ...(
+ opts?.pinnedNotes
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ ? channel.pinnedNoteIds.map(it => opts.pinnedNotes!.get(it)).filter(it => it != null)
+ : await this.notesRepository.findBy({ id: In(channel.pinnedNoteIds) })
+ ),
+ );
+ }
return {
id: channel.id,
@@ -78,7 +110,8 @@ export class ChannelEntityService {
name: channel.name,
description: channel.description,
userId: channel.userId,
- bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
+ bannerUrl: bannerFile ? this.driveFileEntityService.getPublicUrl(bannerFile) : null,
+ bannerId: channel.bannerId,
pinnedNoteIds: channel.pinnedNoteIds,
color: channel.color,
isArchived: channel.isArchived,
@@ -90,6 +123,7 @@ export class ChannelEntityService {
...(me ? {
isFollowing,
isFavorited,
+ isMuting,
hasUnreadNote: false, // 後方互換性のため
} : {}),
@@ -98,5 +132,72 @@ export class ChannelEntityService {
} : {}),
};
}
+
+ @bindThis
+ public async packMany(
+ src: MiChannel['id'][] | MiChannel[],
+ me?: { id: MiUser['id'] } | null | undefined,
+ detailed?: boolean,
+ ): Promise[]> {
+ // IDのみの要素がある場合、DBからオブジェクトを取得して補う
+ const channels = src.filter(it => typeof it === 'object') as MiChannel[];
+ channels.push(
+ ...(await this.channelsRepository.find({
+ where: {
+ id: In(src.filter(it => typeof it !== 'object') as MiChannel['id'][]),
+ },
+ })),
+ );
+ channels.sort((a, b) => a.id.localeCompare(b.id));
+
+ const bannerFiles = await this.driveFilesRepository
+ .findBy({
+ id: In(channels.map(it => it.bannerId).filter(it => it != null)),
+ })
+ .then(it => new Map(it.map(it => [it.id, it])));
+
+ const followings = me
+ ? await this.channelFollowingsRepository
+ .findBy({
+ followerId: me.id,
+ followeeId: In(channels.map(it => it.id)),
+ })
+ .then(it => new Set(it.map(it => it.followeeId)))
+ : new Set();
+
+ const favorites = me
+ ? await this.channelFavoritesRepository
+ .findBy({
+ userId: me.id,
+ channelId: In(channels.map(it => it.id)),
+ })
+ .then(it => new Set(it.map(it => it.channelId)))
+ : new Set();
+
+ const muting = me
+ ? await this.channelMutingRepository
+ .findBy({
+ userId: me.id,
+ channelId: In(channels.map(it => it.id)),
+ })
+ .then(it => new Set(it.map(it => it.channelId)))
+ : new Set();
+
+ const pinnedNotes = await this.notesRepository
+ .find({
+ where: {
+ id: In(channels.flatMap(it => it.pinnedNoteIds)),
+ },
+ })
+ .then(it => new Map(it.map(it => [it.id, it])));
+
+ return Promise.all(channels.map(it => this.pack(it, me, detailed, {
+ bannerFiles,
+ followings,
+ favorites,
+ muting,
+ pinnedNotes,
+ })));
+ }
}
diff --git a/packages/backend/src/core/entities/ChatEntityService.ts b/packages/backend/src/core/entities/ChatEntityService.ts
index 6bce2413fd..f69a484398 100644
--- a/packages/backend/src/core/entities/ChatEntityService.ts
+++ b/packages/backend/src/core/entities/ChatEntityService.ts
@@ -54,12 +54,13 @@ export class ChatEntityService {
const message = typeof src === 'object' ? src : await this.chatMessagesRepository.findOneByOrFail({ id: src });
- const reactions: { user: Packed<'UserLite'>; reaction: string; }[] = [];
+ // userは削除されている可能性があるのでnull許容
+ const reactions: { user: Packed<'UserLite'> | null; reaction: string; }[] = [];
for (const record of message.reactions) {
const [userId, reaction] = record.split('/');
reactions.push({
- user: packedUsers?.get(userId) ?? await this.userEntityService.pack(userId),
+ user: packedUsers?.get(userId) ?? await this.userEntityService.pack(userId).catch(() => null),
reaction,
});
}
@@ -76,7 +77,7 @@ export class ChatEntityService {
toRoom: message.toRoomId ? (packedRooms?.get(message.toRoomId) ?? await this.packRoom(message.toRoom ?? message.toRoomId, me)) : undefined,
fileId: message.fileId,
file: message.fileId ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file ?? message.fileId)) : null,
- reactions,
+ reactions: reactions.filter((r): r is { user: Packed<'UserLite'>; reaction: string; } => r.user != null),
};
}
@@ -108,6 +109,7 @@ export class ChatEntityService {
}
}
+ // TODO: packedUsersに削除されたユーザーもnullとして含める
const [packedUsers, packedFiles, packedRooms] = await Promise.all([
this.userEntityService.packMany(users, me)
.then(users => new Map(users.map(u => [u.id, u]))),
@@ -136,7 +138,7 @@ export class ChatEntityService {
const reactions: { reaction: string; }[] = [];
for (const record of message.reactions) {
- const [userId, reaction] = record.split('/');
+ const [, reaction] = record.split('/');
reactions.push({
reaction,
});
@@ -183,12 +185,13 @@ export class ChatEntityService {
const message = typeof src === 'object' ? src : await this.chatMessagesRepository.findOneByOrFail({ id: src });
- const reactions: { user: Packed<'UserLite'>; reaction: string; }[] = [];
+ // userは削除されている可能性があるのでnull許容
+ const reactions: { user: Packed<'UserLite'> | null; reaction: string; }[] = [];
for (const record of message.reactions) {
const [userId, reaction] = record.split('/');
reactions.push({
- user: packedUsers?.get(userId) ?? await this.userEntityService.pack(userId),
+ user: packedUsers?.get(userId) ?? await this.userEntityService.pack(userId).catch(() => null),
reaction,
});
}
@@ -202,7 +205,7 @@ export class ChatEntityService {
toRoomId: message.toRoomId!,
fileId: message.fileId,
file: message.fileId ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file ?? message.fileId)) : null,
- reactions,
+ reactions: reactions.filter((r): r is { user: Packed<'UserLite'>; reaction: string; } => r.user != null),
};
}
diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts
index a6f7f369a6..1865d494c4 100644
--- a/packages/backend/src/core/entities/DriveFileEntityService.ts
+++ b/packages/backend/src/core/entities/DriveFileEntityService.ts
@@ -17,6 +17,7 @@ import { deepClone } from '@/misc/clone.js';
import { bindThis } from '@/decorators.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
import { IdService } from '@/core/IdService.js';
+import { uniqueByKey } from '@/misc/unique-by-key.js';
import { UtilityService } from '../UtilityService.js';
import { VideoProcessingService } from '../VideoProcessingService.js';
import { UserEntityService } from './UserEntityService.js';
@@ -226,6 +227,7 @@ export class DriveFileEntityService {
options?: PackOptions,
hint?: {
packedUser?: Packed<'UserLite'>
+ packedFolder?: Packed<'DriveFolder'>
},
): Promise | null> {
const opts = Object.assign({
@@ -250,9 +252,9 @@ export class DriveFileEntityService {
thumbnailUrl: this.getThumbnailUrl(file),
comment: file.comment,
folderId: file.folderId,
- folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
+ folder: opts.detail && file.folderId ? (hint?.packedFolder ?? this.driveFolderEntityService.pack(file.folderId, {
detail: true,
- }) : null,
+ })) : null,
userId: file.userId,
user: (opts.withUser && file.userId) ? hint?.packedUser ?? this.userEntityService.pack(file.userId) : null,
});
@@ -263,10 +265,41 @@ export class DriveFileEntityService {
files: MiDriveFile[],
options?: PackOptions,
): Promise[]> {
- 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) } : {})));
+ // -- ユーザ情報の事前取得 --
+
+ let userMap: Map> | null = null;
+ if (options?.withUser) {
+ const users = files
+ .map(({ user, userId }) => user ?? userId)
+ .filter(x => x != null);
+
+ const uniqueUsers = uniqueByKey(users, (user) => typeof user === 'string' ? user : user.id);
+ const packedUsers = await this.userEntityService.packMany(uniqueUsers);
+ userMap = new Map(packedUsers.map(user => [user.id, user]));
+ }
+
+ // -- フォルダ情報の事前取得 --
+
+ let folderMap: Map> | null = null;
+ if (options?.detail) {
+ const folders = files
+ .map(({ folder, folderId }) => folder ?? folderId)
+ .filter(x => x != null);
+
+ const uniqueFolders = uniqueByKey(folders, (folder) => typeof folder === 'string' ? folder : folder.id);
+ const packedFolders = await this.driveFolderEntityService.packMany(uniqueFolders, { detail: true });
+ folderMap = new Map(packedFolders.map(folder => [folder.id, folder]));
+ }
+
+ const items = await Promise.all(files.map(f => this.packNullable(
+ f,
+ options,
+ {
+ packedUser: f.userId ? userMap?.get(f.userId) : undefined,
+ packedFolder: f.folderId ? folderMap?.get(f.folderId) : undefined,
+ },
+ )));
+
return items.filter(x => x != null);
}
diff --git a/packages/backend/src/core/entities/DriveFolderEntityService.ts b/packages/backend/src/core/entities/DriveFolderEntityService.ts
index 299f23ad38..326421e149 100644
--- a/packages/backend/src/core/entities/DriveFolderEntityService.ts
+++ b/packages/backend/src/core/entities/DriveFolderEntityService.ts
@@ -12,6 +12,9 @@ import type { } from '@/models/Blocking.js';
import type { MiDriveFolder } from '@/models/DriveFolder.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
+import { In } from 'typeorm';
+import { uniqueByKey } from '@/misc/unique-by-key.js';
+import { splitIdAndObjects } from '@/misc/split-id-and-objects.js';
@Injectable()
export class DriveFolderEntityService {
@@ -32,12 +35,20 @@ export class DriveFolderEntityService {
options?: {
detail: boolean
},
+ hint?: {
+ folderMap?: Map;
+ foldersCountMap?: Map | null;
+ filesCountMap?: Map | null;
+ parentPacker?: (id: string) => Promise>;
+ },
): Promise> {
const opts = Object.assign({
detail: false,
}, options);
- const folder = typeof src === 'object' ? src : await this.driveFoldersRepository.findOneByOrFail({ id: src });
+ const folder = typeof src === 'object'
+ ? src
+ : hint?.folderMap?.get(src) ?? await this.driveFoldersRepository.findOneByOrFail({ id: src });
return await awaitAll({
id: folder.id,
@@ -46,20 +57,141 @@ export class DriveFolderEntityService {
parentId: folder.parentId,
...(opts.detail ? {
- foldersCount: this.driveFoldersRepository.countBy({
- parentId: folder.id,
- }),
- filesCount: this.driveFilesRepository.countBy({
- folderId: folder.id,
- }),
+ foldersCount: hint?.foldersCountMap?.get(folder.id)
+ ?? this.driveFoldersRepository.countBy({
+ parentId: folder.id,
+ }),
+ filesCount: hint?.filesCountMap?.get(folder.id)
+ ?? this.driveFilesRepository.countBy({
+ folderId: folder.id,
+ }),
...(folder.parentId ? {
- parent: this.pack(folder.parentId, {
- detail: true,
- }),
+ parent: hint?.parentPacker
+ ? hint.parentPacker(folder.parentId)
+ : this.pack(folder.parentId, { detail: true }, hint),
} : {}),
} : {}),
});
}
-}
+ public async packMany(
+ src: Array,
+ options?: {
+ detail: boolean
+ },
+ ): Promise>> {
+ /**
+ * 重複を除去しつつ、必要なDriveFolderオブジェクトをすべて取得する
+ */
+ const collectUniqueObjects = async (src: Array) => {
+ const uniqueSrc = uniqueByKey(
+ src,
+ (s) => typeof s === 'string' ? s : s.id,
+ );
+ const { ids, objects } = splitIdAndObjects(uniqueSrc);
+
+ const uniqueObjects = new Map(objects.map(s => [s.id, s]));
+ const needsFetchIds = ids.filter(id => !uniqueObjects.has(id));
+
+ if (needsFetchIds.length > 0) {
+ const fetchedObjects = await this.driveFoldersRepository.find({
+ where: {
+ id: In(needsFetchIds),
+ },
+ });
+ for (const obj of fetchedObjects) {
+ uniqueObjects.set(obj.id, obj);
+ }
+ }
+
+ return uniqueObjects;
+ };
+
+ /**
+ * 親フォルダーを再帰的に収集する
+ */
+ const collectAncestors = async (folderMap: Map) => {
+ for (;;) {
+ const parentIds = new Set();
+ for (const folder of folderMap.values()) {
+ if (folder.parentId != null && !folderMap.has(folder.parentId)) {
+ parentIds.add(folder.parentId);
+ }
+ }
+
+ if (parentIds.size === 0) break;
+
+ const fetchedParents = await this.driveFoldersRepository.find({
+ where: {
+ id: In([...parentIds]),
+ },
+ });
+
+ if (fetchedParents.length === 0) break;
+
+ for (const parent of fetchedParents) {
+ folderMap.set(parent.id, parent);
+ }
+ }
+ };
+
+ const opts = Object.assign({
+ detail: false,
+ }, options);
+
+ const folderMap = await collectUniqueObjects(src);
+
+ let foldersCountMap: Map | null = null;
+ let filesCountMap: Map | null = null;
+ if (opts.detail) {
+ await collectAncestors(folderMap);
+
+ const ids = [...folderMap.keys()];
+ if (ids.length > 0) {
+ const folderCounts = await this.driveFoldersRepository.createQueryBuilder('folder')
+ .select('folder.parentId', 'parentId')
+ .addSelect('COUNT(*)', 'count')
+ .where('folder.parentId IN (:...ids)', { ids })
+ .groupBy('folder.parentId')
+ .getRawMany<{ parentId: string; count: string }>();
+
+ const fileCounts = await this.driveFilesRepository.createQueryBuilder('file')
+ .select('file.folderId', 'folderId')
+ .addSelect('COUNT(*)', 'count')
+ .where('file.folderId IN (:...ids)', { ids })
+ .groupBy('file.folderId')
+ .getRawMany<{ folderId: string; count: string }>();
+
+ foldersCountMap = new Map(folderCounts.map(row => [row.parentId, Number(row.count)]));
+ filesCountMap = new Map(fileCounts.map(row => [row.folderId, Number(row.count)]));
+ } else {
+ foldersCountMap = new Map();
+ filesCountMap = new Map();
+ }
+ }
+
+ const packedMap = new Map>>();
+ const packFromId = (id: string): Promise> => {
+ const cached = packedMap.get(id);
+ if (cached) return cached;
+
+ const folder = folderMap.get(id);
+ if (!folder) {
+ throw new Error(`DriveFolder not found: ${id}`);
+ }
+
+ const packedPromise = this.pack(folder, options, {
+ folderMap,
+ foldersCountMap,
+ filesCountMap,
+ parentPacker: packFromId,
+ });
+ packedMap.set(id, packedPromise);
+
+ return packedPromise;
+ };
+
+ return Promise.all(src.map(s => packFromId(typeof s === 'string' ? s : s.id)));
+ }
+}
diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts
index 490d3f2511..309de3b08f 100644
--- a/packages/backend/src/core/entities/EmojiEntityService.ts
+++ b/packages/backend/src/core/entities/EmojiEntityService.ts
@@ -41,7 +41,7 @@ export class EmojiEntityService {
@bindThis
public packSimpleMany(
- emojis: any[],
+ emojis: (MiEmoji['id'] | MiEmoji)[],
) {
return Promise.all(emojis.map(x => this.packSimple(x)));
}
@@ -69,7 +69,7 @@ export class EmojiEntityService {
@bindThis
public packDetailedMany(
- emojis: any[],
+ emojis: (MiEmoji['id'] | MiEmoji)[],
): Promise[]> {
return Promise.all(emojis.map(x => this.packDetailed(x)));
}
diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts
index f8abfb2f98..8e56ddbc02 100644
--- a/packages/backend/src/core/entities/MetaEntityService.ts
+++ b/packages/backend/src/core/entities/MetaEntityService.ts
@@ -55,13 +55,13 @@ export class MetaEntityService {
if (instance.defaultLightTheme) {
try {
defaultLightTheme = JSON.stringify(JSON5.parse(instance.defaultLightTheme));
- } catch (e) {
+ } catch (_) {
}
}
if (instance.defaultDarkTheme) {
try {
defaultDarkTheme = JSON.stringify(JSON5.parse(instance.defaultDarkTheme));
- } catch (e) {
+ } catch (_) {
}
}
@@ -117,6 +117,7 @@ export class MetaEntityService {
ratio: ad.ratio,
imageUrl: ad.imageUrl,
dayOfWeek: ad.dayOfWeek,
+ isSensitive: ad.isSensitive ? true : undefined,
})),
notesPerOneAd: instance.notesPerOneAd,
enableEmail: instance.enableEmail,
diff --git a/packages/backend/src/core/entities/NoteDraftEntityService.ts b/packages/backend/src/core/entities/NoteDraftEntityService.ts
index 3ef8cdaa12..71e41a588d 100644
--- a/packages/backend/src/core/entities/NoteDraftEntityService.ts
+++ b/packages/backend/src/core/entities/NoteDraftEntityService.ts
@@ -105,6 +105,8 @@ export class NoteDraftEntityService implements OnModuleInit {
const packed: Packed<'NoteDraft'> = await awaitAll({
id: noteDraft.id,
createdAt: this.idService.parse(noteDraft.id).date.toISOString(),
+ scheduledAt: noteDraft.scheduledAt?.getTime() ?? null,
+ isActuallyScheduled: noteDraft.isActuallyScheduled,
userId: noteDraft.userId,
user: packedUsers?.get(noteDraft.userId) ?? this.userEntityService.pack(noteDraft.user ?? noteDraft.userId, me),
text: text,
@@ -112,13 +114,13 @@ export class NoteDraftEntityService implements OnModuleInit {
visibility: noteDraft.visibility,
localOnly: noteDraft.localOnly,
reactionAcceptance: noteDraft.reactionAcceptance,
- visibleUserIds: noteDraft.visibility === 'specified' ? noteDraft.visibleUserIds : undefined,
- hashtag: noteDraft.hashtag ?? undefined,
+ visibleUserIds: noteDraft.visibleUserIds,
+ hashtag: noteDraft.hashtag,
fileIds: noteDraft.fileIds,
files: packedFiles != null ? this.packAttachedFiles(noteDraft.fileIds, packedFiles) : this.driveFileEntityService.packManyByIds(noteDraft.fileIds),
replyId: noteDraft.replyId,
renoteId: noteDraft.renoteId,
- channelId: noteDraft.channelId ?? undefined,
+ channelId: noteDraft.channelId,
channel: channel ? {
id: channel.id,
name: channel.name,
@@ -127,6 +129,12 @@ export class NoteDraftEntityService implements OnModuleInit {
allowRenoteToExternal: channel.allowRenoteToExternal,
userId: channel.userId,
} : undefined,
+ poll: noteDraft.hasPoll ? {
+ choices: noteDraft.pollChoices,
+ multiple: noteDraft.pollMultiple,
+ expiresAt: noteDraft.pollExpiresAt?.toISOString(),
+ expiredAfter: noteDraft.pollExpiredAfter,
+ } : null,
...(opts.detail ? {
reply: noteDraft.replyId ? nullIfEntityNotFound(this.noteEntityService.pack(noteDraft.replyId, me, {
@@ -138,13 +146,6 @@ export class NoteDraftEntityService implements OnModuleInit {
detail: true,
skipHide: opts.skipHide,
})) : undefined,
-
- poll: noteDraft.hasPoll ? {
- choices: noteDraft.pollChoices,
- multiple: noteDraft.pollMultiple,
- expiresAt: noteDraft.pollExpiresAt?.toISOString(),
- expiredAfter: noteDraft.pollExpiredAfter,
- } : undefined,
} : {} ),
});
diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts
index 6871ba2c72..e7847ba74e 100644
--- a/packages/backend/src/core/entities/NoteEntityService.ts
+++ b/packages/backend/src/core/entities/NoteEntityService.ts
@@ -15,6 +15,7 @@ import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepos
import { bindThis } from '@/decorators.js';
import { DebounceLoader } from '@/misc/loader.js';
import { IdService } from '@/core/IdService.js';
+import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.js';
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
import type { OnModuleInit } from '@nestjs/common';
import type { CustomEmojiService } from '../CustomEmojiService.js';
@@ -116,12 +117,7 @@ export class NoteEntityService implements OnModuleInit {
private treatVisibility(packedNote: Packed<'Note'>): Packed<'Note'>['visibility'] {
if (packedNote.visibility === 'public' || packedNote.visibility === 'home') {
const followersOnlyBefore = packedNote.user.makeNotesFollowersOnlyBefore;
- if ((followersOnlyBefore != null)
- && (
- (followersOnlyBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (followersOnlyBefore * 1000)))
- || (followersOnlyBefore > 0 && (new Date(packedNote.createdAt).getTime() < followersOnlyBefore * 1000))
- )
- ) {
+ if (shouldHideNoteByTime(followersOnlyBefore, packedNote.createdAt)) {
packedNote.visibility = 'followers';
}
}
@@ -141,12 +137,7 @@ export class NoteEntityService implements OnModuleInit {
if (!hide) {
const hiddenBefore = packedNote.user.makeNotesHiddenBefore;
- if ((hiddenBefore != null)
- && (
- (hiddenBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (hiddenBefore * 1000)))
- || (hiddenBefore > 0 && (new Date(packedNote.createdAt).getTime() < hiddenBefore * 1000))
- )
- ) {
+ if (shouldHideNoteByTime(hiddenBefore, packedNote.createdAt)) {
hide = true;
}
}
diff --git a/packages/backend/src/core/entities/NoteReactionEntityService.ts b/packages/backend/src/core/entities/NoteReactionEntityService.ts
index 46ec13704c..fe4926bfe3 100644
--- a/packages/backend/src/core/entities/NoteReactionEntityService.ts
+++ b/packages/backend/src/core/entities/NoteReactionEntityService.ts
@@ -49,15 +49,12 @@ export class NoteReactionEntityService implements OnModuleInit {
public async pack(
src: MiNoteReaction['id'] | MiNoteReaction,
me?: { id: MiUser['id'] } | null | undefined,
- options?: {
- withNote: boolean;
- },
+ options?: object,
hints?: {
packedUser?: Packed<'UserLite'>
},
): Promise> {
- const opts = Object.assign({
- withNote: false,
+ const _opts = Object.assign({
}, options);
const reaction = typeof src === 'object' ? src : await this.noteReactionsRepository.findOneByOrFail({ id: src });
@@ -67,9 +64,6 @@ export class NoteReactionEntityService implements OnModuleInit {
createdAt: this.idService.parse(reaction.id).date.toISOString(),
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),
- } : {}),
};
}
@@ -77,16 +71,50 @@ export class NoteReactionEntityService implements OnModuleInit {
public async packMany(
reactions: MiNoteReaction[],
me?: { id: MiUser['id'] } | null | undefined,
- options?: {
- withNote: boolean;
- },
+ options?: object,
): Promise[]> {
const opts = Object.assign({
- withNote: false,
}, options);
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) })));
}
+
+ @bindThis
+ public async packWithNote(
+ src: MiNoteReaction['id'] | MiNoteReaction,
+ me?: { id: MiUser['id'] } | null | undefined,
+ options?: object,
+ hints?: {
+ packedUser?: Packed<'UserLite'>
+ },
+ ): Promise> {
+ const _opts = Object.assign({
+ }, options);
+
+ const reaction = typeof src === 'object' ? src : await this.noteReactionsRepository.findOneByOrFail({ id: src });
+
+ return {
+ id: reaction.id,
+ createdAt: this.idService.parse(reaction.id).date.toISOString(),
+ user: hints?.packedUser ?? await this.userEntityService.pack(reaction.user ?? reaction.userId, me),
+ type: this.reactionService.convertLegacyReaction(reaction.reaction),
+ note: await this.noteEntityService.pack(reaction.note ?? reaction.noteId, me),
+ };
+ }
+
+ @bindThis
+ public async packManyWithNote(
+ reactions: MiNoteReaction[],
+ me?: { id: MiUser['id'] } | null | undefined,
+ options?: object,
+ ): Promise[]> {
+ const opts = Object.assign({
+ }, options);
+ 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.packWithNote(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 e91fb9eb51..0e96237d32 100644
--- a/packages/backend/src/core/entities/NotificationEntityService.ts
+++ b/packages/backend/src/core/entities/NotificationEntityService.ts
@@ -21,7 +21,18 @@ import type { OnModuleInit } from '@nestjs/common';
import type { UserEntityService } from './UserEntityService.js';
import type { NoteEntityService } from './NoteEntityService.js';
-const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded'] as (typeof groupedNotificationTypes[number])[]);
+const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set([
+ 'note',
+ 'mention',
+ 'reply',
+ 'renote',
+ 'renote:grouped',
+ 'quote',
+ 'reaction',
+ 'reaction:grouped',
+ 'pollEnded',
+ 'scheduledNotePosted',
+] as (typeof groupedNotificationTypes[number])[]);
@Injectable()
export class NotificationEntityService implements OnModuleInit {
diff --git a/packages/backend/src/core/entities/ReversiGameEntityService.ts b/packages/backend/src/core/entities/ReversiGameEntityService.ts
index df042e75c1..21099bad3e 100644
--- a/packages/backend/src/core/entities/ReversiGameEntityService.ts
+++ b/packages/backend/src/core/entities/ReversiGameEntityService.ts
@@ -14,6 +14,10 @@ import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
import { UserEntityService } from './UserEntityService.js';
+function assertBw(bw: string): bw is Packed<'ReversiGameDetailed'>['bw'] {
+ return ['random', '1', '2'].includes(bw);
+}
+
@Injectable()
export class ReversiGameEntityService {
constructor(
@@ -58,7 +62,7 @@ export class ReversiGameEntityService {
surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId,
black: game.black,
- bw: game.bw,
+ bw: assertBw(game.bw) ? game.bw : 'random',
isLlotheo: game.isLlotheo,
canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard,
@@ -116,7 +120,7 @@ export class ReversiGameEntityService {
surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId,
black: game.black,
- bw: game.bw,
+ bw: assertBw(game.bw) ? game.bw : 'random',
isLlotheo: game.isLlotheo,
canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard,
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index d4769d24d4..0f4051e7b8 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -471,8 +471,8 @@ export class UserEntityService implements OnModuleInit {
(profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
null;
- const isModerator = isMe && isDetailed ? this.roleService.isModerator(user) : null;
- const isAdmin = isMe && isDetailed ? this.roleService.isAdministrator(user) : null;
+ const isModerator = isMe && isDetailed ? this.roleService.isModerator(user) : undefined;
+ const isAdmin = isMe && isDetailed ? this.roleService.isAdministrator(user) : undefined;
const unreadAnnouncements = isMe && isDetailed ?
(await this.announcementService.getUnreadAnnouncements(user)).map((announcement) => ({
createdAt: this.idService.parse(announcement.id).date.toISOString(),
@@ -481,6 +481,7 @@ export class UserEntityService implements OnModuleInit {
const notificationsInfo = isMe && isDetailed ? await this.getNotificationsInfo(user.id) : null;
+ // TODO: 例えば avatarUrl: true など間違った型を設定しても型エラーにならないのをどうにかする(ジェネリクス使わない方法で実装するしかなさそう?)
const packed = {
id: user.id,
name: user.name,
@@ -511,8 +512,8 @@ export class UserEntityService implements OnModuleInit {
} : undefined) : undefined,
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
onlineStatus: this.getOnlineStatus(user),
- // パフォーマンス上の理由でローカルユーザーのみ
- badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then((rs) => rs
+ // パフォーマンス上の理由で、明示的に設定しない場合はローカルユーザーのみ取得
+ badgeRoles: (this.meta.showRoleBadgesOfRemoteUsers || 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) => ({
@@ -719,7 +720,7 @@ export class UserEntityService implements OnModuleInit {
me,
{
...options,
- userProfile: profilesMap.get(u.id),
+ userProfile: profilesMap?.get(u.id),
userRelations: userRelations,
userMemos: userMemos,
pinNotes: pinNotes,
diff --git a/packages/backend/src/daemons/ServerStatsService.ts b/packages/backend/src/daemons/ServerStatsService.ts
index d229efb123..a972e5861c 100644
--- a/packages/backend/src/daemons/ServerStatsService.ts
+++ b/packages/backend/src/daemons/ServerStatsService.ts
@@ -4,13 +4,12 @@
*/
import { Inject, Injectable } from '@nestjs/common';
-import si from 'systeminformation';
import Xev from 'xev';
import * as osUtils from 'os-utils';
import { bindThis } from '@/decorators.js';
-import type { OnApplicationShutdown } from '@nestjs/common';
import { MiMeta } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
+import type { OnApplicationShutdown } from '@nestjs/common';
const ev = new Xev();
@@ -97,12 +96,14 @@ function cpuUsage(): Promise {
// MEMORY STAT
async function mem() {
+ const si = await import('systeminformation');
const data = await si.mem();
return data;
}
// NETWORK STAT
async function net() {
+ const si = await import('systeminformation');
const iface = await si.networkInterfaceDefault();
const data = await si.networkStats(iface);
return data[0];
@@ -110,5 +111,6 @@ async function net() {
// FS STAT
async function fs() {
+ const si = await import('systeminformation');
return await si.disksIO().catch(() => ({ rIO_sec: 0, wIO_sec: 0 }));
}
diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts
index c915133453..b9ca76233c 100644
--- a/packages/backend/src/di-symbols.ts
+++ b/packages/backend/src/di-symbols.ts
@@ -70,6 +70,7 @@ export const DI = {
channelsRepository: Symbol('channelsRepository'),
channelFollowingsRepository: Symbol('channelFollowingsRepository'),
channelFavoritesRepository: Symbol('channelFavoritesRepository'),
+ channelMutingRepository: Symbol('channelMutingRepository'),
registryItemsRepository: Symbol('registryItemsRepository'),
webhooksRepository: Symbol('webhooksRepository'),
systemWebhooksRepository: Symbol('systemWebhooksRepository'),
diff --git a/packages/backend/src/misc/check-word-mute.ts b/packages/backend/src/misc/check-word-mute.ts
index c50f2b723c..0d1c7ee46e 100644
--- a/packages/backend/src/misc/check-word-mute.ts
+++ b/packages/backend/src/misc/check-word-mute.ts
@@ -56,7 +56,7 @@ export async function checkWordMute(note: NoteLike, me: UserLike | null | undefi
try {
return new RE2(regexp[1], regexp[2]).test(text);
- } catch (err) {
+ } catch (_) {
// This should never happen due to input sanitisation.
return false;
}
diff --git a/packages/backend/src/misc/distributed-lock.ts b/packages/backend/src/misc/distributed-lock.ts
new file mode 100644
index 0000000000..93bd741f62
--- /dev/null
+++ b/packages/backend/src/misc/distributed-lock.ts
@@ -0,0 +1,49 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Redis from 'ioredis';
+
+export async function acquireDistributedLock(
+ redis: Redis.Redis,
+ name: string,
+ timeout: number,
+ maxRetries: number,
+ retryInterval: number,
+): Promise<() => Promise> {
+ const lockKey = `lock:${name}`;
+ const identifier = Math.random().toString(36).slice(2);
+
+ let retries = 0;
+ while (retries < maxRetries) {
+ const result = await redis.set(lockKey, identifier, 'PX', timeout, 'NX');
+ if (result === 'OK') {
+ return async () => {
+ const currentIdentifier = await redis.get(lockKey);
+ if (currentIdentifier === identifier) {
+ await redis.del(lockKey);
+ }
+ };
+ }
+
+ await new Promise(resolve => setTimeout(resolve, retryInterval));
+ retries++;
+ }
+
+ throw new Error(`Failed to acquire lock ${name}`);
+}
+
+export function acquireApObjectLock(
+ redis: Redis.Redis,
+ uri: string,
+): Promise<() => Promise> {
+ return acquireDistributedLock(redis, `ap-object:${uri}`, 30 * 1000, 50, 100);
+}
+
+export function acquireChartInsertLock(
+ redis: Redis.Redis,
+ name: string,
+): Promise<() => Promise> {
+ return acquireDistributedLock(redis, `chart-insert:${name}`, 30 * 1000, 50, 500);
+}
diff --git a/packages/backend/src/misc/escape-html.ts b/packages/backend/src/misc/escape-html.ts
new file mode 100644
index 0000000000..819aeeed52
--- /dev/null
+++ b/packages/backend/src/misc/escape-html.ts
@@ -0,0 +1,13 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export function escapeHtml(text: string): string {
+ return text
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+}
diff --git a/packages/backend/src/misc/get-ip-hash.ts b/packages/backend/src/misc/get-ip-hash.ts
index e132fa8f31..571996973b 100644
--- a/packages/backend/src/misc/get-ip-hash.ts
+++ b/packages/backend/src/misc/get-ip-hash.ts
@@ -12,7 +12,7 @@ export function getIpHash(ip: string): string {
// (this means for IPv4 the entire address is used)
const prefix = IPCIDR.createAddress(ip).mask(64);
return 'ip-' + BigInt('0b' + prefix).toString(36);
- } catch (e) {
+ } catch (_) {
const prefix = IPCIDR.createAddress(ip.replace(/:[0-9]+$/, '')).mask(64);
return 'ip-' + BigInt('0b' + prefix).toString(36);
}
diff --git a/packages/backend/src/misc/i18n.ts b/packages/backend/src/misc/i18n.ts
index 6cbbdef74c..40067cacf5 100644
--- a/packages/backend/src/misc/i18n.ts
+++ b/packages/backend/src/misc/i18n.ts
@@ -26,7 +26,7 @@ export class I18n> {
}
}
return str;
- } catch (e) {
+ } catch (_) {
console.warn(`missing localization '${key}'`);
return key;
}
diff --git a/packages/backend/src/misc/is-channel-related.ts b/packages/backend/src/misc/is-channel-related.ts
new file mode 100644
index 0000000000..fef736dad6
--- /dev/null
+++ b/packages/backend/src/misc/is-channel-related.ts
@@ -0,0 +1,31 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { MiNote } from '@/models/Note.js';
+import { Packed } from '@/misc/json-schema.js';
+
+/**
+ * {@link note}が{@link channelIds}のチャンネルに関連するかどうかを判定し、関連する場合はtrueを返します。
+ * 関連するというのは、{@link channelIds}のチャンネルに向けての投稿であるか、またはそのチャンネルの投稿をリノート・引用リノートした投稿であるかを指します。
+ *
+ * @param note 確認対象のノート
+ * @param channelIds 確認対象のチャンネルID一覧
+ * @param ignoreAuthor trueの場合、ノートの所属チャンネルが{@link channelIds}に含まれていても無視します(デフォルトはfalse)
+ */
+export function isChannelRelated(note: MiNote | Packed<'Note'>, channelIds: Set, ignoreAuthor = false): boolean {
+ // ノートの所属チャンネルが確認対象のチャンネルID一覧に含まれている場合
+ if (!ignoreAuthor && note.channelId && channelIds.has(note.channelId)) {
+ return true;
+ }
+
+ const renoteChannelId = note.renote?.channelId;
+ if (renoteChannelId != null && renoteChannelId !== note.channelId && channelIds.has(renoteChannelId)) {
+ return true;
+ }
+
+ // NOTE: リプライはchannelIdのチェックだけでOKなはずなので見てない(チャンネルのノートにチャンネル外からのリプライまたはその逆はないはずなので)
+
+ return false;
+}
diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts
index ed47edff9b..cf233defd9 100644
--- a/packages/backend/src/misc/json-schema.ts
+++ b/packages/backend/src/misc/json-schema.ts
@@ -22,7 +22,7 @@ import { packedFollowingSchema } from '@/models/json-schema/following.js';
import { packedMutingSchema } from '@/models/json-schema/muting.js';
import { packedRenoteMutingSchema } from '@/models/json-schema/renote-muting.js';
import { packedBlockingSchema } from '@/models/json-schema/blocking.js';
-import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js';
+import { packedNoteReactionSchema, packedNoteReactionWithNoteSchema } 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 { packedPageBlockSchema, packedPageSchema } from '@/models/json-schema/page.js';
@@ -64,7 +64,9 @@ import {
packedMetaDetailedOnlySchema,
packedMetaDetailedSchema,
packedMetaLiteSchema,
+ packedMetaClientOptionsSchema,
} from '@/models/json-schema/meta.js';
+import { packedUserWebhookSchema } from '@/models/json-schema/user-webhook.js';
import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js';
import { packedChatMessageSchema, packedChatMessageLiteSchema, packedChatMessageLiteForRoomSchema, packedChatMessageLiteFor1on1Schema } from '@/models/json-schema/chat-message.js';
@@ -92,6 +94,7 @@ export const refs = {
Note: packedNoteSchema,
NoteDraft: packedNoteDraftSchema,
NoteReaction: packedNoteReactionSchema,
+ NoteReactionWithNote: packedNoteReactionWithNoteSchema,
NoteFavorite: packedNoteFavoriteSchema,
Notification: packedNotificationSchema,
DriveFile: packedDriveFileSchema,
@@ -133,6 +136,8 @@ export const refs = {
MetaLite: packedMetaLiteSchema,
MetaDetailedOnly: packedMetaDetailedOnlySchema,
MetaDetailed: packedMetaDetailedSchema,
+ MetaClientOptions: packedMetaClientOptionsSchema,
+ UserWebhook: packedUserWebhookSchema,
SystemWebhook: packedSystemWebhookSchema,
AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,
ChatMessage: packedChatMessageSchema,
@@ -259,8 +264,6 @@ type ObjectSchemaTypeDef =
never :
any;
-type ObjectSchemaType
= NullOrUndefined
>;
-
export type SchemaTypeDef
=
p['type'] extends 'null' ? null :
p['type'] extends 'integer' ? number :
diff --git a/packages/backend/src/misc/json-stringify-html-safe.ts b/packages/backend/src/misc/json-stringify-html-safe.ts
new file mode 100644
index 0000000000..aac12d57db
--- /dev/null
+++ b/packages/backend/src/misc/json-stringify-html-safe.ts
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+const ESCAPE_LOOKUP = {
+ '&': '\\u0026',
+ '>': '\\u003e',
+ '<': '\\u003c',
+ '\u2028': '\\u2028',
+ '\u2029': '\\u2029',
+} as Record;
+
+const ESCAPE_REGEX = /[&><\u2028\u2029]/g;
+
+export function htmlSafeJsonStringify(obj: any): string {
+ return JSON.stringify(obj).replace(ESCAPE_REGEX, x => ESCAPE_LOOKUP[x]);
+}
diff --git a/packages/backend/src/misc/should-hide-note-by-time.ts b/packages/backend/src/misc/should-hide-note-by-time.ts
new file mode 100644
index 0000000000..ea1951e66c
--- /dev/null
+++ b/packages/backend/src/misc/should-hide-note-by-time.ts
@@ -0,0 +1,29 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/**
+ * ノートが指定された時間条件に基づいて非表示対象かどうかを判定する
+ * @param hiddenBefore 非表示条件(負の値: 作成からの経過秒数、正の値: UNIXタイムスタンプ秒、null: 判定しない)
+ * @param createdAt ノートの作成日時(ISO 8601形式の文字列 または Date オブジェクト)
+ * @returns 非表示にすべき場合は true
+ */
+export function shouldHideNoteByTime(hiddenBefore: number | null | undefined, createdAt: string | Date): boolean {
+ if (hiddenBefore == null) {
+ return false;
+ }
+
+ const createdAtTime = typeof createdAt === 'string' ? new Date(createdAt).getTime() : createdAt.getTime();
+
+ if (hiddenBefore <= 0) {
+ // 負の値: 作成からの経過時間(秒)で判定
+ const elapsedSeconds = (Date.now() - createdAtTime) / 1000;
+ const hideAfterSeconds = Math.abs(hiddenBefore);
+ return elapsedSeconds >= hideAfterSeconds;
+ } else {
+ // 正の値: 絶対的なタイムスタンプ(秒)で判定
+ const createdAtSeconds = createdAtTime / 1000;
+ return createdAtSeconds <= hiddenBefore;
+ }
+}
diff --git a/packages/backend/src/misc/show-machine-info.ts b/packages/backend/src/misc/show-machine-info.ts
index 8ddec35f23..b279eb9546 100644
--- a/packages/backend/src/misc/show-machine-info.ts
+++ b/packages/backend/src/misc/show-machine-info.ts
@@ -4,15 +4,11 @@
*/
import * as os from 'node:os';
-import sysUtils from 'systeminformation';
import type Logger from '@/logger.js';
export async function showMachineInfo(parentLogger: Logger) {
const logger = parentLogger.createSubLogger('machine');
logger.debug(`Hostname: ${os.hostname()}`);
logger.debug(`Platform: ${process.platform} Arch: ${process.arch}`);
- const mem = await sysUtils.mem();
- const totalmem = (mem.total / 1024 / 1024 / 1024).toFixed(1);
- const availmem = (mem.available / 1024 / 1024 / 1024).toFixed(1);
- logger.debug(`CPU: ${os.cpus().length} core MEM: ${totalmem}GB (available: ${availmem}GB)`);
+ logger.debug(`CPU: ${os.cpus().length} core MEM: ${(os.totalmem() / 1024 / 1024 / 1024).toFixed(1)}GB (available: ${(os.freemem() / 1024 / 1024 / 1024).toFixed(1)}GB)`);
}
diff --git a/packages/backend/src/misc/split-id-and-objects.ts b/packages/backend/src/misc/split-id-and-objects.ts
new file mode 100644
index 0000000000..d23bb93695
--- /dev/null
+++ b/packages/backend/src/misc/split-id-and-objects.ts
@@ -0,0 +1,27 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/**
+ * idとオブジェクトを分離する
+ * @param input idまたはオブジェクトの配列
+ * @returns idの配列とオブジェクトの配列
+ */
+export function splitIdAndObjects(input: (T | string)[]): { ids: string[]; objects: T[] } {
+ const ids: string[] = [];
+ const objects : T[] = [];
+
+ for (const item of input) {
+ if (typeof item === 'string') {
+ ids.push(item);
+ } else {
+ objects.push(item);
+ }
+ }
+
+ return {
+ ids,
+ objects,
+ };
+}
diff --git a/packages/backend/src/misc/unique-by-key.ts b/packages/backend/src/misc/unique-by-key.ts
new file mode 100644
index 0000000000..4308e29d21
--- /dev/null
+++ b/packages/backend/src/misc/unique-by-key.ts
@@ -0,0 +1,21 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/**
+ * itemsの中でkey関数が返す値が重複しないようにした配列を返す
+ * @param items 重複を除去したい配列
+ * @param key 重複判定に使うキーを返す関数
+ * @returns 重複を除去した配列
+ */
+export function uniqueByKey(items: Iterable, key: (item: TItem) => TKey): TItem[] {
+ const map = new Map();
+ for (const item of items) {
+ const k = key(item);
+ if (!map.has(k)) {
+ map.set(k, item);
+ }
+ }
+ return [...map.values()];
+}
diff --git a/packages/backend/src/models/AbuseReportNotificationRecipient.ts b/packages/backend/src/models/AbuseReportNotificationRecipient.ts
index 17ec6abed5..daed81c174 100644
--- a/packages/backend/src/models/AbuseReportNotificationRecipient.ts
+++ b/packages/backend/src/models/AbuseReportNotificationRecipient.ts
@@ -67,7 +67,7 @@ export class MiAbuseReportNotificationRecipient {
/**
* 通知先のユーザ.
*/
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'userId', referencedColumnName: 'id', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_userId1' })
@@ -76,7 +76,7 @@ export class MiAbuseReportNotificationRecipient {
/**
* 通知先のユーザプロフィール.
*/
- @ManyToOne(type => MiUserProfile, {
+ @ManyToOne(() => MiUserProfile, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'userId', referencedColumnName: 'userId', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_userId2' })
@@ -96,7 +96,7 @@ export class MiAbuseReportNotificationRecipient {
/**
* 通知先のシステムWebhook.
*/
- @ManyToOne(type => MiSystemWebhook, {
+ @ManyToOne(() => MiSystemWebhook, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'systemWebhookId', referencedColumnName: 'id', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_systemWebhookId' })
diff --git a/packages/backend/src/models/AbuseUserReport.ts b/packages/backend/src/models/AbuseUserReport.ts
index d43ebf9342..cd49fcddfe 100644
--- a/packages/backend/src/models/AbuseUserReport.ts
+++ b/packages/backend/src/models/AbuseUserReport.ts
@@ -18,7 +18,7 @@ export class MiAbuseUserReport {
@Column(id())
public targetUserId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -28,7 +28,7 @@ export class MiAbuseUserReport {
@Column(id())
public reporterId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -40,7 +40,7 @@ export class MiAbuseUserReport {
})
public assigneeId: MiUser['id'] | null;
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'SET NULL',
})
@JoinColumn()
diff --git a/packages/backend/src/models/AccessToken.ts b/packages/backend/src/models/AccessToken.ts
index 6f98c14ec1..a853dcc6cb 100644
--- a/packages/backend/src/models/AccessToken.ts
+++ b/packages/backend/src/models/AccessToken.ts
@@ -41,7 +41,7 @@ export class MiAccessToken {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -53,7 +53,7 @@ export class MiAccessToken {
})
public appId: MiApp['id'] | null;
- @ManyToOne(type => MiApp, {
+ @ManyToOne(() => MiApp, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Ad.ts b/packages/backend/src/models/Ad.ts
index 108e991c70..0d402fcbe8 100644
--- a/packages/backend/src/models/Ad.ts
+++ b/packages/backend/src/models/Ad.ts
@@ -54,10 +54,17 @@ export class MiAd {
length: 8192, nullable: false,
})
public memo: string;
+
@Column('integer', {
default: 0, nullable: false,
})
public dayOfWeek: number;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public isSensitive: boolean;
+
constructor(data: Partial) {
if (data == null) return;
diff --git a/packages/backend/src/models/Announcement.ts b/packages/backend/src/models/Announcement.ts
index d0c59fff50..f664c75262 100644
--- a/packages/backend/src/models/Announcement.ts
+++ b/packages/backend/src/models/Announcement.ts
@@ -79,7 +79,7 @@ export class MiAnnouncement {
})
public userId: MiUser['id'] | null;
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/AnnouncementRead.ts b/packages/backend/src/models/AnnouncementRead.ts
index 47de8dd180..2133cff140 100644
--- a/packages/backend/src/models/AnnouncementRead.ts
+++ b/packages/backend/src/models/AnnouncementRead.ts
@@ -18,7 +18,7 @@ export class MiAnnouncementRead {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -28,7 +28,7 @@ export class MiAnnouncementRead {
@Column(id())
public announcementId: MiAnnouncement['id'];
- @ManyToOne(type => MiAnnouncement, {
+ @ManyToOne(() => MiAnnouncement, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Antenna.ts b/packages/backend/src/models/Antenna.ts
index ccc8823703..3433cf20af 100644
--- a/packages/backend/src/models/Antenna.ts
+++ b/packages/backend/src/models/Antenna.ts
@@ -24,7 +24,7 @@ export class MiAntenna {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -45,7 +45,7 @@ export class MiAntenna {
})
public userListId: MiUserList['id'] | null;
- @ManyToOne(type => MiUserList, {
+ @ManyToOne(() => MiUserList, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/App.ts b/packages/backend/src/models/App.ts
index 0185e2995c..bbb80b99ef 100644
--- a/packages/backend/src/models/App.ts
+++ b/packages/backend/src/models/App.ts
@@ -20,7 +20,7 @@ export class MiApp {
})
public userId: MiUser['id'] | null;
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'SET NULL',
nullable: true,
})
diff --git a/packages/backend/src/models/AuthSession.ts b/packages/backend/src/models/AuthSession.ts
index 03050ba955..a7273e63bf 100644
--- a/packages/backend/src/models/AuthSession.ts
+++ b/packages/backend/src/models/AuthSession.ts
@@ -25,7 +25,7 @@ export class MiAuthSession {
})
public userId: MiUser['id'] | null;
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
nullable: true,
})
@@ -35,7 +35,7 @@ export class MiAuthSession {
@Column(id())
public appId: MiApp['id'];
- @ManyToOne(type => MiApp, {
+ @ManyToOne(() => MiApp, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Blocking.ts b/packages/backend/src/models/Blocking.ts
index 34a6efe5a6..49b584f509 100644
--- a/packages/backend/src/models/Blocking.ts
+++ b/packages/backend/src/models/Blocking.ts
@@ -20,7 +20,7 @@ export class MiBlocking {
})
public blockeeId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -33,7 +33,7 @@ export class MiBlocking {
})
public blockerId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/BubbleGameRecord.ts b/packages/backend/src/models/BubbleGameRecord.ts
index 686e39c118..5dd7009fc6 100644
--- a/packages/backend/src/models/BubbleGameRecord.ts
+++ b/packages/backend/src/models/BubbleGameRecord.ts
@@ -18,7 +18,7 @@ export class MiBubbleGameRecord {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Channel.ts b/packages/backend/src/models/Channel.ts
index f5e9b17e3e..5a5b914eb1 100644
--- a/packages/backend/src/models/Channel.ts
+++ b/packages/backend/src/models/Channel.ts
@@ -27,7 +27,7 @@ export class MiChannel {
})
public userId: MiUser['id'] | null;
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'SET NULL',
})
@JoinColumn()
@@ -52,7 +52,7 @@ export class MiChannel {
})
public bannerId: MiDriveFile['id'] | null;
- @ManyToOne(type => MiDriveFile, {
+ @ManyToOne(() => MiDriveFile, {
onDelete: 'SET NULL',
})
@JoinColumn()
diff --git a/packages/backend/src/models/ChannelFavorite.ts b/packages/backend/src/models/ChannelFavorite.ts
index 167f41cf16..4f49468598 100644
--- a/packages/backend/src/models/ChannelFavorite.ts
+++ b/packages/backend/src/models/ChannelFavorite.ts
@@ -20,7 +20,7 @@ export class MiChannelFavorite {
})
public channelId: MiChannel['id'];
- @ManyToOne(type => MiChannel, {
+ @ManyToOne(() => MiChannel, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -32,7 +32,7 @@ export class MiChannelFavorite {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/ChannelFollowing.ts b/packages/backend/src/models/ChannelFollowing.ts
index c7afdd05b0..7597e704a8 100644
--- a/packages/backend/src/models/ChannelFollowing.ts
+++ b/packages/backend/src/models/ChannelFollowing.ts
@@ -21,7 +21,7 @@ export class MiChannelFollowing {
})
public followeeId: MiChannel['id'];
- @ManyToOne(type => MiChannel, {
+ @ManyToOne(() => MiChannel, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -34,7 +34,7 @@ export class MiChannelFollowing {
})
public followerId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/ChannelMuting.ts b/packages/backend/src/models/ChannelMuting.ts
new file mode 100644
index 0000000000..b7054c9c5f
--- /dev/null
+++ b/packages/backend/src/models/ChannelMuting.ts
@@ -0,0 +1,46 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
+import { id } from './util/id.js';
+import { MiUser } from './User.js';
+import { MiChannel } from './Channel.js';
+
+@Entity('channel_muting')
+@Index(['userId', 'channelId'], {})
+export class MiChannelMuting {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column({
+ ...id(),
+ })
+ public userId: MiUser['id'];
+
+ @ManyToOne(() => MiUser, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public user: MiUser | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ })
+ public channelId: MiChannel['id'];
+
+ @ManyToOne(() => MiChannel, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public channel: MiChannel | null;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ nullable: true,
+ })
+ public expiresAt: Date | null;
+}
diff --git a/packages/backend/src/models/ChatApproval.ts b/packages/backend/src/models/ChatApproval.ts
index 55c9f07e9a..bd2509b67f 100644
--- a/packages/backend/src/models/ChatApproval.ts
+++ b/packages/backend/src/models/ChatApproval.ts
@@ -19,7 +19,7 @@ export class MiChatApproval {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -31,7 +31,7 @@ export class MiChatApproval {
})
public otherId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/ChatMessage.ts b/packages/backend/src/models/ChatMessage.ts
index 3d2b64268e..530ef9b842 100644
--- a/packages/backend/src/models/ChatMessage.ts
+++ b/packages/backend/src/models/ChatMessage.ts
@@ -20,7 +20,7 @@ export class MiChatMessage {
})
public fromUserId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -32,7 +32,7 @@ export class MiChatMessage {
})
public toUserId: MiUser['id'] | null;
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -44,7 +44,7 @@ export class MiChatMessage {
})
public toRoomId: MiChatRoom['id'] | null;
- @ManyToOne(type => MiChatRoom, {
+ @ManyToOne(() => MiChatRoom, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -72,7 +72,7 @@ export class MiChatMessage {
})
public fileId: MiDriveFile['id'] | null;
- @ManyToOne(type => MiDriveFile, {
+ @ManyToOne(() => MiDriveFile, {
onDelete: 'SET NULL',
})
@JoinColumn()
diff --git a/packages/backend/src/models/ChatRoom.ts b/packages/backend/src/models/ChatRoom.ts
index ad2a910b78..c148b16af8 100644
--- a/packages/backend/src/models/ChatRoom.ts
+++ b/packages/backend/src/models/ChatRoom.ts
@@ -23,7 +23,7 @@ export class MiChatRoom {
})
public ownerId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/ChatRoomInvitation.ts b/packages/backend/src/models/ChatRoomInvitation.ts
index 36ce12bc92..5827d0401d 100644
--- a/packages/backend/src/models/ChatRoomInvitation.ts
+++ b/packages/backend/src/models/ChatRoomInvitation.ts
@@ -20,7 +20,7 @@ export class MiChatRoomInvitation {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -32,7 +32,7 @@ export class MiChatRoomInvitation {
})
public roomId: MiChatRoom['id'];
- @ManyToOne(type => MiChatRoom, {
+ @ManyToOne(() => MiChatRoom, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/ChatRoomMembership.ts b/packages/backend/src/models/ChatRoomMembership.ts
index 3cb5524859..d59b4426df 100644
--- a/packages/backend/src/models/ChatRoomMembership.ts
+++ b/packages/backend/src/models/ChatRoomMembership.ts
@@ -20,7 +20,7 @@ export class MiChatRoomMembership {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -32,7 +32,7 @@ export class MiChatRoomMembership {
})
public roomId: MiChatRoom['id'];
- @ManyToOne(type => MiChatRoom, {
+ @ManyToOne(() => MiChatRoom, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Clip.ts b/packages/backend/src/models/Clip.ts
index 6295a329fb..ddd0298f44 100644
--- a/packages/backend/src/models/Clip.ts
+++ b/packages/backend/src/models/Clip.ts
@@ -25,7 +25,7 @@ export class MiClip {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/ClipFavorite.ts b/packages/backend/src/models/ClipFavorite.ts
index 40bdb9f4aa..2d46fd0f0e 100644
--- a/packages/backend/src/models/ClipFavorite.ts
+++ b/packages/backend/src/models/ClipFavorite.ts
@@ -18,7 +18,7 @@ export class MiClipFavorite {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -27,7 +27,7 @@ export class MiClipFavorite {
@Column(id())
public clipId: MiClip['id'];
- @ManyToOne(type => MiClip, {
+ @ManyToOne(() => MiClip, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/ClipNote.ts b/packages/backend/src/models/ClipNote.ts
index 6e1d2bec4c..23df66c4e0 100644
--- a/packages/backend/src/models/ClipNote.ts
+++ b/packages/backend/src/models/ClipNote.ts
@@ -21,7 +21,7 @@ export class MiClipNote {
})
public noteId: MiNote['id'];
- @ManyToOne(type => MiNote, {
+ @ManyToOne(() => MiNote, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -34,7 +34,7 @@ export class MiClipNote {
})
public clipId: MiClip['id'];
- @ManyToOne(type => MiClip, {
+ @ManyToOne(() => MiClip, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/DriveFile.ts b/packages/backend/src/models/DriveFile.ts
index 7b03e3e494..79189b10eb 100644
--- a/packages/backend/src/models/DriveFile.ts
+++ b/packages/backend/src/models/DriveFile.ts
@@ -22,7 +22,7 @@ export class MiDriveFile {
})
public userId: MiUser['id'] | null;
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'SET NULL',
})
@JoinColumn()
@@ -142,7 +142,7 @@ export class MiDriveFile {
})
public folderId: MiDriveFolder['id'] | null;
- @ManyToOne(type => MiDriveFolder, {
+ @ManyToOne(() => MiDriveFolder, {
onDelete: 'SET NULL',
})
@JoinColumn()
diff --git a/packages/backend/src/models/DriveFolder.ts b/packages/backend/src/models/DriveFolder.ts
index 07046d6e11..7e34c07f46 100644
--- a/packages/backend/src/models/DriveFolder.ts
+++ b/packages/backend/src/models/DriveFolder.ts
@@ -26,7 +26,7 @@ export class MiDriveFolder {
})
public userId: MiUser['id'] | null;
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -40,7 +40,7 @@ export class MiDriveFolder {
})
public parentId: MiDriveFolder['id'] | null;
- @ManyToOne(type => MiDriveFolder, {
+ @ManyToOne(() => MiDriveFolder, {
onDelete: 'SET NULL',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Flash.ts b/packages/backend/src/models/Flash.ts
index 5db7dca992..ed677a9de3 100644
--- a/packages/backend/src/models/Flash.ts
+++ b/packages/backend/src/models/Flash.ts
@@ -38,7 +38,7 @@ export class MiFlash {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/FlashLike.ts b/packages/backend/src/models/FlashLike.ts
index a9fb48123e..0d99c2a9ae 100644
--- a/packages/backend/src/models/FlashLike.ts
+++ b/packages/backend/src/models/FlashLike.ts
@@ -18,7 +18,7 @@ export class MiFlashLike {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -27,7 +27,7 @@ export class MiFlashLike {
@Column(id())
public flashId: MiFlash['id'];
- @ManyToOne(type => MiFlash, {
+ @ManyToOne(() => MiFlash, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/FollowRequest.ts b/packages/backend/src/models/FollowRequest.ts
index 3ff5e7a478..468829b7e8 100644
--- a/packages/backend/src/models/FollowRequest.ts
+++ b/packages/backend/src/models/FollowRequest.ts
@@ -20,7 +20,7 @@ export class MiFollowRequest {
})
public followeeId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -33,7 +33,7 @@ export class MiFollowRequest {
})
public followerId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Following.ts b/packages/backend/src/models/Following.ts
index 62cbc29f26..fe62166287 100644
--- a/packages/backend/src/models/Following.ts
+++ b/packages/backend/src/models/Following.ts
@@ -21,7 +21,7 @@ export class MiFollowing {
})
public followeeId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -34,7 +34,7 @@ export class MiFollowing {
})
public followerId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/GalleryLike.ts b/packages/backend/src/models/GalleryLike.ts
index ed0963122d..787b38e46d 100644
--- a/packages/backend/src/models/GalleryLike.ts
+++ b/packages/backend/src/models/GalleryLike.ts
@@ -18,7 +18,7 @@ export class MiGalleryLike {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -27,7 +27,7 @@ export class MiGalleryLike {
@Column(id())
public postId: MiGalleryPost['id'];
- @ManyToOne(type => MiGalleryPost, {
+ @ManyToOne(() => MiGalleryPost, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/GalleryPost.ts b/packages/backend/src/models/GalleryPost.ts
index 04d8823e37..f66956628b 100644
--- a/packages/backend/src/models/GalleryPost.ts
+++ b/packages/backend/src/models/GalleryPost.ts
@@ -36,7 +36,7 @@ export class MiGalleryPost {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts
index f8021a7a84..620853450c 100644
--- a/packages/backend/src/models/Meta.ts
+++ b/packages/backend/src/models/Meta.ts
@@ -21,7 +21,7 @@ export class MiMeta {
})
public rootUserId: MiUser['id'] | null;
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'SET NULL',
nullable: true,
})
@@ -717,10 +717,19 @@ export class MiMeta {
})
public remoteNotesCleaningExpiryDaysForEachNotes: number;
+ @Column('boolean', {
+ default: false,
+ })
+ public showRoleBadgesOfRemoteUsers: boolean;
+
@Column('jsonb', {
default: { },
})
- public clientOptions: Record;
+ public clientOptions: {
+ entrancePageStyle: 'classic' | 'simple';
+ showTimelineForVisitor: boolean;
+ showActivitiesForVisitor: boolean;
+ };
}
export type SoftwareSuspension = {
diff --git a/packages/backend/src/models/ModerationLog.ts b/packages/backend/src/models/ModerationLog.ts
index edde315fdf..c22114a36d 100644
--- a/packages/backend/src/models/ModerationLog.ts
+++ b/packages/backend/src/models/ModerationLog.ts
@@ -16,7 +16,7 @@ export class MiModerationLog {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Muting.ts b/packages/backend/src/models/Muting.ts
index e1240b9c4e..9406b97a62 100644
--- a/packages/backend/src/models/Muting.ts
+++ b/packages/backend/src/models/Muting.ts
@@ -26,7 +26,7 @@ export class MiMuting {
})
public muteeId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -39,7 +39,7 @@ export class MiMuting {
})
public muterId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts
index 26d5c1d535..089fe8f188 100644
--- a/packages/backend/src/models/Note.ts
+++ b/packages/backend/src/models/Note.ts
@@ -35,7 +35,7 @@ export class MiNote {
})
public replyId: MiNote['id'] | null;
- @ManyToOne(type => MiNote, {
+ @ManyToOne(() => MiNote, {
createForeignKeyConstraints: false,
})
@JoinColumn()
@@ -49,7 +49,7 @@ export class MiNote {
})
public renoteId: MiNote['id'] | null;
- @ManyToOne(type => MiNote, {
+ @ManyToOne(() => MiNote, {
createForeignKeyConstraints: false,
})
@JoinColumn()
@@ -83,7 +83,7 @@ export class MiNote {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -208,7 +208,7 @@ export class MiNote {
})
public channelId: MiChannel['id'] | null;
- @ManyToOne(type => MiChannel, {
+ @ManyToOne(() => MiChannel, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -248,6 +248,14 @@ export class MiNote {
})
public renoteUserHost: string | null;
+ @Column({
+ ...id(),
+ nullable: true,
+ comment: '[Denormalized]',
+ })
+ public renoteChannelId: MiChannel['id'] | null;
+ //#endregion
+
constructor(data: Partial) {
if (data == null) return;
diff --git a/packages/backend/src/models/NoteDraft.ts b/packages/backend/src/models/NoteDraft.ts
index 6483748bc2..5bfd9699fe 100644
--- a/packages/backend/src/models/NoteDraft.ts
+++ b/packages/backend/src/models/NoteDraft.ts
@@ -27,7 +27,7 @@ export class MiNoteDraft {
public replyId: MiNote['id'] | null;
// There is a possibility that replyId is not null but reply is null when the reply note is deleted.
- @ManyToOne(type => MiNote, {
+ @ManyToOne(() => MiNote, {
createForeignKeyConstraints: false,
})
@JoinColumn()
@@ -42,7 +42,7 @@ export class MiNoteDraft {
public renoteId: MiNote['id'] | null;
// There is a possibility that renoteId is not null but renote is null when the renote note is deleted.
- @ManyToOne(type => MiNote, {
+ @ManyToOne(() => MiNote, {
createForeignKeyConstraints: false,
})
@JoinColumn()
@@ -66,7 +66,7 @@ export class MiNoteDraft {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -120,13 +120,13 @@ export class MiNoteDraft {
// There is a possibility that channelId is not null but channel is null when the channel is deleted.
// (deleting channel is not implemented so it's not happening now but may happen in the future)
- @ManyToOne(type => MiChannel, {
+ @ManyToOne(() => MiChannel, {
createForeignKeyConstraints: false,
})
@JoinColumn()
public channel: MiChannel | null;
- // 以下、Pollについて追加
+ //#region 以下、Pollについて追加
@Column('boolean', {
default: false,
@@ -151,13 +151,18 @@ export class MiNoteDraft {
})
public pollExpiredAfter: number | null;
- // ここまで追加
+ //#endregion
- constructor(data: Partial) {
- if (data == null) return;
+ // 予約日時
+ // これがあるだけでは実際に予約されているかどうかはわからない
+ @Column('timestamp with time zone', {
+ nullable: true,
+ })
+ public scheduledAt: Date | null;
- for (const [k, v] of Object.entries(data)) {
- (this as any)[k] = v;
- }
- }
+ // scheduledAtに基づいて実際にスケジュールされているか
+ @Column('boolean', {
+ default: false,
+ })
+ public isActuallyScheduled: boolean;
}
diff --git a/packages/backend/src/models/NoteFavorite.ts b/packages/backend/src/models/NoteFavorite.ts
index cf76c767b0..0e498eb70d 100644
--- a/packages/backend/src/models/NoteFavorite.ts
+++ b/packages/backend/src/models/NoteFavorite.ts
@@ -18,7 +18,7 @@ export class MiNoteFavorite {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -27,7 +27,7 @@ export class MiNoteFavorite {
@Column(id())
public noteId: MiNote['id'];
- @ManyToOne(type => MiNote, {
+ @ManyToOne(() => MiNote, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/NoteReaction.ts b/packages/backend/src/models/NoteReaction.ts
index 42dfcaa9ad..98263081ab 100644
--- a/packages/backend/src/models/NoteReaction.ts
+++ b/packages/backend/src/models/NoteReaction.ts
@@ -18,7 +18,7 @@ export class MiNoteReaction {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -28,7 +28,7 @@ export class MiNoteReaction {
@Column(id())
public noteId: MiNote['id'];
- @ManyToOne(type => MiNote, {
+ @ManyToOne(() => MiNote, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/NoteThreadMuting.ts b/packages/backend/src/models/NoteThreadMuting.ts
index e7bd39f348..32bb829c0b 100644
--- a/packages/backend/src/models/NoteThreadMuting.ts
+++ b/packages/backend/src/models/NoteThreadMuting.ts
@@ -19,7 +19,7 @@ export class MiNoteThreadMuting {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts
index 5764a307b0..7fa17e20fa 100644
--- a/packages/backend/src/models/Notification.ts
+++ b/packages/backend/src/models/Notification.ts
@@ -9,7 +9,9 @@ import { MiNote } from './Note.js';
import { MiAccessToken } from './AccessToken.js';
import { MiRole } from './Role.js';
import { MiDriveFile } from './DriveFile.js';
+import { MiNoteDraft } from './NoteDraft.js';
+// misskey-js の notificationTypes と同期すべし
export type MiNotification = {
type: 'note';
id: string;
@@ -59,6 +61,16 @@ export type MiNotification = {
createdAt: string;
notifierId: MiUser['id'];
noteId: MiNote['id'];
+} | {
+ type: 'scheduledNotePosted';
+ id: string;
+ createdAt: string;
+ noteId: MiNote['id'];
+} | {
+ type: 'scheduledNotePostFailed';
+ id: string;
+ createdAt: string;
+ noteDraftId: MiNoteDraft['id'];
} | {
type: 'receiveFollowRequest';
id: string;
diff --git a/packages/backend/src/models/Page.ts b/packages/backend/src/models/Page.ts
index 0b59e7a92c..8811200801 100644
--- a/packages/backend/src/models/Page.ts
+++ b/packages/backend/src/models/Page.ts
@@ -47,7 +47,7 @@ export class MiPage {
@Column('varchar', {
length: 32,
})
- public font: string;
+ public font: 'serif' | 'sans-serif';
@Index()
@Column({
@@ -56,7 +56,7 @@ export class MiPage {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -68,8 +68,8 @@ export class MiPage {
})
public eyeCatchingImageId: MiDriveFile['id'] | null;
- @ManyToOne(type => MiDriveFile, {
- onDelete: 'CASCADE',
+ @ManyToOne(() => MiDriveFile, {
+ onDelete: 'SET NULL',
})
@JoinColumn()
public eyeCatchingImage: MiDriveFile | null;
diff --git a/packages/backend/src/models/PageLike.ts b/packages/backend/src/models/PageLike.ts
index 05ca22cf2c..cf3025ae1c 100644
--- a/packages/backend/src/models/PageLike.ts
+++ b/packages/backend/src/models/PageLike.ts
@@ -18,7 +18,7 @@ export class MiPageLike {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -27,7 +27,7 @@ export class MiPageLike {
@Column(id())
public pageId: MiPage['id'];
- @ManyToOne(type => MiPage, {
+ @ManyToOne(() => MiPage, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/PasswordResetRequest.ts b/packages/backend/src/models/PasswordResetRequest.ts
index fdaf21056b..3379b540ee 100644
--- a/packages/backend/src/models/PasswordResetRequest.ts
+++ b/packages/backend/src/models/PasswordResetRequest.ts
@@ -24,7 +24,7 @@ export class MiPasswordResetRequest {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Poll.ts b/packages/backend/src/models/Poll.ts
index ca985c8b24..d82e29fb85 100644
--- a/packages/backend/src/models/Poll.ts
+++ b/packages/backend/src/models/Poll.ts
@@ -15,7 +15,7 @@ export class MiPoll {
@PrimaryColumn(id())
public noteId: MiNote['id'];
- @OneToOne(type => MiNote, {
+ @OneToOne(() => MiNote, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/PollVote.ts b/packages/backend/src/models/PollVote.ts
index b5c780293c..600ca8ea41 100644
--- a/packages/backend/src/models/PollVote.ts
+++ b/packages/backend/src/models/PollVote.ts
@@ -18,7 +18,7 @@ export class MiPollVote {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -28,7 +28,7 @@ export class MiPollVote {
@Column(id())
public noteId: MiNote['id'];
- @ManyToOne(type => MiNote, {
+ @ManyToOne(() => MiNote, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/PromoNote.ts b/packages/backend/src/models/PromoNote.ts
index ae27adec9e..871f7471fc 100644
--- a/packages/backend/src/models/PromoNote.ts
+++ b/packages/backend/src/models/PromoNote.ts
@@ -13,7 +13,7 @@ export class MiPromoNote {
@PrimaryColumn(id())
public noteId: MiNote['id'];
- @OneToOne(type => MiNote, {
+ @OneToOne(() => MiNote, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/PromoRead.ts b/packages/backend/src/models/PromoRead.ts
index b2a698cc7b..15a3573ef3 100644
--- a/packages/backend/src/models/PromoRead.ts
+++ b/packages/backend/src/models/PromoRead.ts
@@ -18,7 +18,7 @@ export class MiPromoRead {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -27,7 +27,7 @@ export class MiPromoRead {
@Column(id())
public noteId: MiNote['id'];
- @ManyToOne(type => MiNote, {
+ @ManyToOne(() => MiNote, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/RegistrationTicket.ts b/packages/backend/src/models/RegistrationTicket.ts
index 0a4e4b9189..07216599d3 100644
--- a/packages/backend/src/models/RegistrationTicket.ts
+++ b/packages/backend/src/models/RegistrationTicket.ts
@@ -23,7 +23,7 @@ export class MiRegistrationTicket {
})
public expiresAt: Date | null;
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -36,7 +36,7 @@ export class MiRegistrationTicket {
})
public createdById: MiUser['id'] | null;
- @OneToOne(type => MiUser, {
+ @OneToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/RegistryItem.ts b/packages/backend/src/models/RegistryItem.ts
index 335e8b9eab..869980bbff 100644
--- a/packages/backend/src/models/RegistryItem.ts
+++ b/packages/backend/src/models/RegistryItem.ts
@@ -25,7 +25,7 @@ export class MiRegistryItem {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/RenoteMuting.ts b/packages/backend/src/models/RenoteMuting.ts
index 448a0b7663..b760a09c53 100644
--- a/packages/backend/src/models/RenoteMuting.ts
+++ b/packages/backend/src/models/RenoteMuting.ts
@@ -20,7 +20,7 @@ export class MiRenoteMuting {
})
public muteeId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -33,7 +33,7 @@ export class MiRenoteMuting {
})
public muterId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts
index 146dbbc3b8..e3db6f8838 100644
--- a/packages/backend/src/models/RepositoryModule.ts
+++ b/packages/backend/src/models/RepositoryModule.ts
@@ -21,6 +21,7 @@ import {
MiChannel,
MiChannelFavorite,
MiChannelFollowing,
+ MiChannelMuting,
MiClip,
MiClipFavorite,
MiClipNote,
@@ -429,6 +430,12 @@ const $channelFavoritesRepository: Provider = {
inject: [DI.db],
};
+const $channelMutingRepository: Provider = {
+ provide: DI.channelMutingRepository,
+ useFactory: (db: DataSource) => db.getRepository(MiChannelMuting).extend(miRepository as MiRepository),
+ inject: [DI.db],
+};
+
const $registryItemsRepository: Provider = {
provide: DI.registryItemsRepository,
useFactory: (db: DataSource) => db.getRepository(MiRegistryItem).extend(miRepository as MiRepository),
@@ -597,6 +604,7 @@ const $reversiGamesRepository: Provider = {
$channelsRepository,
$channelFollowingsRepository,
$channelFavoritesRepository,
+ $channelMutingRepository,
$registryItemsRepository,
$webhooksRepository,
$systemWebhooksRepository,
@@ -674,6 +682,7 @@ const $reversiGamesRepository: Provider = {
$channelsRepository,
$channelFollowingsRepository,
$channelFavoritesRepository,
+ $channelMutingRepository,
$registryItemsRepository,
$webhooksRepository,
$systemWebhooksRepository,
diff --git a/packages/backend/src/models/ReversiGame.ts b/packages/backend/src/models/ReversiGame.ts
index 6b29a0ce8c..fbbf24792f 100644
--- a/packages/backend/src/models/ReversiGame.ts
+++ b/packages/backend/src/models/ReversiGame.ts
@@ -27,7 +27,7 @@ export class MiReversiGame {
@Column(id())
public user1Id: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -36,7 +36,7 @@ export class MiReversiGame {
@Column(id())
public user2Id: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/RoleAssignment.ts b/packages/backend/src/models/RoleAssignment.ts
index 37755d631b..cb96377f66 100644
--- a/packages/backend/src/models/RoleAssignment.ts
+++ b/packages/backend/src/models/RoleAssignment.ts
@@ -21,7 +21,7 @@ export class MiRoleAssignment {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -34,7 +34,7 @@ export class MiRoleAssignment {
})
public roleId: MiRole['id'];
- @ManyToOne(type => MiRole, {
+ @ManyToOne(() => MiRole, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Signin.ts b/packages/backend/src/models/Signin.ts
index f8ff9c57d7..59cbad735d 100644
--- a/packages/backend/src/models/Signin.ts
+++ b/packages/backend/src/models/Signin.ts
@@ -16,7 +16,7 @@ export class MiSignin {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/SwSubscription.ts b/packages/backend/src/models/SwSubscription.ts
index 0c531132b3..a95aede44f 100644
--- a/packages/backend/src/models/SwSubscription.ts
+++ b/packages/backend/src/models/SwSubscription.ts
@@ -16,7 +16,7 @@ export class MiSwSubscription {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/SystemAccount.ts b/packages/backend/src/models/SystemAccount.ts
index f32880b81d..2a48e62ed1 100644
--- a/packages/backend/src/models/SystemAccount.ts
+++ b/packages/backend/src/models/SystemAccount.ts
@@ -18,7 +18,7 @@ export class MiSystemAccount {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts
index a6e9edcf5f..084dd35485 100644
--- a/packages/backend/src/models/User.ts
+++ b/packages/backend/src/models/User.ts
@@ -99,7 +99,7 @@ export class MiUser {
})
public avatarId: MiDriveFile['id'] | null;
- @OneToOne(type => MiDriveFile, {
+ @OneToOne(() => MiDriveFile, {
onDelete: 'SET NULL',
})
@JoinColumn()
@@ -112,7 +112,7 @@ export class MiUser {
})
public bannerId: MiDriveFile['id'] | null;
- @OneToOne(type => MiDriveFile, {
+ @OneToOne(() => MiDriveFile, {
onDelete: 'SET NULL',
})
@JoinColumn()
diff --git a/packages/backend/src/models/UserKeypair.ts b/packages/backend/src/models/UserKeypair.ts
index f5252d126c..894739c84c 100644
--- a/packages/backend/src/models/UserKeypair.ts
+++ b/packages/backend/src/models/UserKeypair.ts
@@ -12,7 +12,7 @@ export class MiUserKeypair {
@PrimaryColumn(id())
public userId: MiUser['id'];
- @OneToOne(type => MiUser, {
+ @OneToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/UserList.ts b/packages/backend/src/models/UserList.ts
index 5fb991a87d..05fd833b6f 100644
--- a/packages/backend/src/models/UserList.ts
+++ b/packages/backend/src/models/UserList.ts
@@ -25,7 +25,7 @@ export class MiUserList {
})
public isPublic: boolean;
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/UserListFavorite.ts b/packages/backend/src/models/UserListFavorite.ts
index 80b2d61eb7..67ab92d98c 100644
--- a/packages/backend/src/models/UserListFavorite.ts
+++ b/packages/backend/src/models/UserListFavorite.ts
@@ -18,7 +18,7 @@ export class MiUserListFavorite {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -27,7 +27,7 @@ export class MiUserListFavorite {
@Column(id())
public userListId: MiUserList['id'];
- @ManyToOne(type => MiUserList, {
+ @ManyToOne(() => MiUserList, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/UserListMembership.ts b/packages/backend/src/models/UserListMembership.ts
index af659d071d..1a2b3fffc1 100644
--- a/packages/backend/src/models/UserListMembership.ts
+++ b/packages/backend/src/models/UserListMembership.ts
@@ -21,7 +21,7 @@ export class MiUserListMembership {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -34,7 +34,7 @@ export class MiUserListMembership {
})
public userListId: MiUserList['id'];
- @ManyToOne(type => MiUserList, {
+ @ManyToOne(() => MiUserList, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/UserMemo.ts b/packages/backend/src/models/UserMemo.ts
index 29e28d290a..facc8c6b1c 100644
--- a/packages/backend/src/models/UserMemo.ts
+++ b/packages/backend/src/models/UserMemo.ts
@@ -20,7 +20,7 @@ export class MiUserMemo {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -33,7 +33,7 @@ export class MiUserMemo {
})
public targetUserId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/UserNotePining.ts b/packages/backend/src/models/UserNotePining.ts
index 92c5cd55d0..950da2ad22 100644
--- a/packages/backend/src/models/UserNotePining.ts
+++ b/packages/backend/src/models/UserNotePining.ts
@@ -18,7 +18,7 @@ export class MiUserNotePining {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -27,7 +27,7 @@ export class MiUserNotePining {
@Column(id())
public noteId: MiNote['id'];
- @ManyToOne(type => MiNote, {
+ @ManyToOne(() => MiNote, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts
index 501b539210..b05bf14ef9 100644
--- a/packages/backend/src/models/UserProfile.ts
+++ b/packages/backend/src/models/UserProfile.ts
@@ -17,7 +17,7 @@ export class MiUserProfile {
@PrimaryColumn(id())
public userId: MiUser['id'];
- @OneToOne(type => MiUser, {
+ @OneToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -215,7 +215,7 @@ export class MiUserProfile {
})
public pinnedPageId: MiPage['id'] | null;
- @OneToOne(type => MiPage, {
+ @OneToOne(() => MiPage, {
onDelete: 'SET NULL',
})
@JoinColumn()
diff --git a/packages/backend/src/models/UserPublickey.ts b/packages/backend/src/models/UserPublickey.ts
index 6bcd785304..8c23d368e9 100644
--- a/packages/backend/src/models/UserPublickey.ts
+++ b/packages/backend/src/models/UserPublickey.ts
@@ -12,7 +12,7 @@ export class MiUserPublickey {
@PrimaryColumn(id())
public userId: MiUser['id'];
- @OneToOne(type => MiUser, {
+ @OneToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/UserSecurityKey.ts b/packages/backend/src/models/UserSecurityKey.ts
index 0babbe1abe..577ec359e4 100644
--- a/packages/backend/src/models/UserSecurityKey.ts
+++ b/packages/backend/src/models/UserSecurityKey.ts
@@ -18,7 +18,7 @@ export class MiUserSecurityKey {
@Column(id())
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/Webhook.ts b/packages/backend/src/models/Webhook.ts
index b4cab4edc8..5f833115cc 100644
--- a/packages/backend/src/models/Webhook.ts
+++ b/packages/backend/src/models/Webhook.ts
@@ -22,7 +22,7 @@ export class MiWebhook {
})
public userId: MiUser['id'];
- @ManyToOne(type => MiUser, {
+ @ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts
index 84b5cbed0a..c4528e3a77 100644
--- a/packages/backend/src/models/_.ts
+++ b/packages/backend/src/models/_.ts
@@ -5,18 +5,9 @@
import {
FindOneOptions,
- InsertQueryBuilder,
ObjectLiteral,
- QueryRunner,
Repository,
- SelectQueryBuilder,
} from 'typeorm';
-import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions.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 { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js';
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import { MiAccessToken } from '@/models/AccessToken.js';
@@ -32,6 +23,7 @@ import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
import { MiChannel } from '@/models/Channel.js';
import { MiChannelFavorite } from '@/models/ChannelFavorite.js';
import { MiChannelFollowing } from '@/models/ChannelFollowing.js';
+import { MiChannelMuting } from "@/models/ChannelMuting.js";
import { MiChatApproval } from '@/models/ChatApproval.js';
import { MiChatMessage } from '@/models/ChatMessage.js';
import { MiChatRoom } from '@/models/ChatRoom.js';
@@ -95,66 +87,12 @@ import { MiWebhook } from '@/models/Webhook.js';
import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
export interface MiRepository {
- createTableColumnNames(this: Repository & MiRepository): string[];
-
insertOne(this: Repository & MiRepository, entity: QueryDeepPartialEntity, findOptions?: Pick, 'relations'>): Promise;
-
- insertOneImpl(this: Repository & MiRepository, entity: QueryDeepPartialEntity, findOptions?: Pick, 'relations'>, queryRunner?: QueryRunner): 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 opt = this.manager.connection.options as PostgresConnectionOptions;
- if (opt.replication) {
- const queryRunner = this.manager.connection.createQueryRunner('master');
- try {
- return this.insertOneImpl(entity, findOptions, queryRunner);
- } finally {
- await queryRunner.release();
- }
- } else {
- return this.insertOneImpl(entity, findOptions);
- }
- },
- async insertOneImpl(entity, findOptions?, queryRunner?) {
- // ---- insert + returningの結果を共通テーブル式(CTE)に保持するクエリを生成 ----
-
- 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));
-
- // ---- 共通テーブル式(CTE)から結果を取得 ----
- const builder = this.createQueryBuilder(undefined, queryRunner).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}`);
- }
+ return await this.insert(entity).then(x => this.findOneOrFail({ where: x.identifiers[0], ...findOptions }));
},
} satisfies MiRepository;
@@ -172,6 +110,7 @@ export {
MiBlocking,
MiChannelFollowing,
MiChannelFavorite,
+ MiChannelMuting,
MiClip,
MiClipNote,
MiClipFavorite,
@@ -251,6 +190,7 @@ export type AuthSessionsRepository = Repository & MiRepository & MiRepository;
export type ChannelFollowingsRepository = Repository & MiRepository;
export type ChannelFavoritesRepository = Repository & MiRepository;
+export type ChannelMutingRepository = Repository & MiRepository;
export type ClipsRepository = Repository & MiRepository;
export type ClipNotesRepository = Repository & MiRepository;
export type ClipFavoritesRepository = Repository & MiRepository;
diff --git a/packages/backend/src/models/json-schema/ad.ts b/packages/backend/src/models/json-schema/ad.ts
index b01b39a38b..d88ac23894 100644
--- a/packages/backend/src/models/json-schema/ad.ts
+++ b/packages/backend/src/models/json-schema/ad.ts
@@ -60,5 +60,10 @@ export const packedAdSchema = {
optional: false,
nullable: false,
},
+ isSensitive: {
+ type: 'boolean',
+ optional: false,
+ nullable: false,
+ },
},
} as const;
diff --git a/packages/backend/src/models/json-schema/channel.ts b/packages/backend/src/models/json-schema/channel.ts
index d233f7858d..e7613290d1 100644
--- a/packages/backend/src/models/json-schema/channel.ts
+++ b/packages/backend/src/models/json-schema/channel.ts
@@ -40,6 +40,11 @@ export const packedChannelSchema = {
format: 'url',
nullable: true, optional: false,
},
+ bannerId: {
+ type: 'string',
+ nullable: true, optional: false,
+ format: 'id',
+ },
pinnedNoteIds: {
type: 'array',
nullable: false, optional: false,
@@ -80,6 +85,10 @@ export const packedChannelSchema = {
type: 'boolean',
optional: true, nullable: false,
},
+ isMuting: {
+ type: 'boolean',
+ optional: true, nullable: false,
+ },
pinnedNotes: {
type: 'array',
optional: true, nullable: false,
diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts
index 357ff26041..0c3ec141bc 100644
--- a/packages/backend/src/models/json-schema/meta.ts
+++ b/packages/backend/src/models/json-schema/meta.ts
@@ -72,8 +72,7 @@ export const packedMetaLiteSchema = {
optional: false, nullable: true,
},
clientOptions: {
- type: 'object',
- optional: false, nullable: false,
+ ref: 'MetaClientOptions',
},
disableRegistration: {
type: 'boolean',
@@ -195,6 +194,10 @@ export const packedMetaLiteSchema = {
type: 'integer',
optional: false, nullable: false,
},
+ isSensitive: {
+ type: 'boolean',
+ optional: true, nullable: false,
+ },
},
},
},
@@ -393,3 +396,23 @@ export const packedMetaDetailedSchema = {
},
],
} as const;
+
+export const packedMetaClientOptionsSchema = {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ entrancePageStyle: {
+ type: 'string',
+ enum: ['classic', 'simple'],
+ optional: false, nullable: false,
+ },
+ showTimelineForVisitor: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ showActivitiesForVisitor: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ },
+} as const;
diff --git a/packages/backend/src/models/json-schema/note-draft.ts b/packages/backend/src/models/json-schema/note-draft.ts
index 504b263a6d..8144ac7b3b 100644
--- a/packages/backend/src/models/json-schema/note-draft.ts
+++ b/packages/backend/src/models/json-schema/note-draft.ts
@@ -23,7 +23,7 @@ export const packedNoteDraftSchema = {
},
cw: {
type: 'string',
- optional: true, nullable: true,
+ optional: false, nullable: true,
},
userId: {
type: 'string',
@@ -37,27 +37,23 @@ export const packedNoteDraftSchema = {
},
replyId: {
type: 'string',
- optional: true, nullable: true,
+ optional: false, nullable: true,
format: 'id',
- example: 'xxxxxxxxxx',
},
renoteId: {
type: 'string',
- optional: true, nullable: true,
+ optional: false, nullable: true,
format: 'id',
- example: 'xxxxxxxxxx',
},
reply: {
type: 'object',
optional: true, nullable: true,
ref: 'Note',
- description: 'The reply target note contents if exists. If the reply target has been deleted since the draft was created, this will be null while replyId is not null.',
},
renote: {
type: 'object',
optional: true, nullable: true,
ref: 'Note',
- description: 'The renote target note contents if exists. If the renote target has been deleted since the draft was created, this will be null while renoteId is not null.',
},
visibility: {
type: 'string',
@@ -66,7 +62,7 @@ export const packedNoteDraftSchema = {
},
visibleUserIds: {
type: 'array',
- optional: true, nullable: false,
+ optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
@@ -75,7 +71,7 @@ export const packedNoteDraftSchema = {
},
fileIds: {
type: 'array',
- optional: true, nullable: false,
+ optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
@@ -93,11 +89,11 @@ export const packedNoteDraftSchema = {
},
hashtag: {
type: 'string',
- optional: true, nullable: false,
+ optional: false, nullable: true,
},
poll: {
type: 'object',
- optional: true, nullable: true,
+ optional: false, nullable: true,
properties: {
expiresAt: {
type: 'string',
@@ -124,9 +120,8 @@ export const packedNoteDraftSchema = {
},
channelId: {
type: 'string',
- optional: true, nullable: true,
+ optional: false, nullable: true,
format: 'id',
- example: 'xxxxxxxxxx',
},
channel: {
type: 'object',
@@ -160,12 +155,20 @@ export const packedNoteDraftSchema = {
},
localOnly: {
type: 'boolean',
- optional: true, nullable: false,
+ optional: false, nullable: false,
},
reactionAcceptance: {
type: 'string',
optional: false, nullable: true,
enum: ['likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote', null],
},
+ scheduledAt: {
+ type: 'number',
+ optional: false, nullable: true,
+ },
+ isActuallyScheduled: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
},
} as const;
diff --git a/packages/backend/src/models/json-schema/note-reaction.ts b/packages/backend/src/models/json-schema/note-reaction.ts
index 95658ace1f..04c9f34232 100644
--- a/packages/backend/src/models/json-schema/note-reaction.ts
+++ b/packages/backend/src/models/json-schema/note-reaction.ts
@@ -10,7 +10,6 @@ export const packedNoteReactionSchema = {
type: 'string',
optional: false, nullable: false,
format: 'id',
- example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string',
@@ -28,3 +27,33 @@ export const packedNoteReactionSchema = {
},
},
} as const;
+
+export const packedNoteReactionWithNoteSchema = {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'id',
+ },
+ createdAt: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'date-time',
+ },
+ user: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'UserLite',
+ },
+ type: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ note: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'Note',
+ },
+ },
+} as const;
diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts
index 6de120c8d7..30e9c9327a 100644
--- a/packages/backend/src/models/json-schema/notification.ts
+++ b/packages/backend/src/models/json-schema/notification.ts
@@ -207,6 +207,36 @@ export const packedNotificationSchema = {
optional: false, nullable: false,
},
},
+ }, {
+ type: 'object',
+ properties: {
+ ...baseSchema.properties,
+ type: {
+ type: 'string',
+ optional: false, nullable: false,
+ enum: ['scheduledNotePosted'],
+ },
+ note: {
+ type: 'object',
+ ref: 'Note',
+ optional: false, nullable: false,
+ },
+ },
+ }, {
+ type: 'object',
+ properties: {
+ ...baseSchema.properties,
+ type: {
+ type: 'string',
+ optional: false, nullable: false,
+ enum: ['scheduledNotePostFailed'],
+ },
+ noteDraft: {
+ type: 'object',
+ ref: 'NoteDraft',
+ optional: false, nullable: false,
+ },
+ },
}, {
type: 'object',
properties: {
diff --git a/packages/backend/src/models/json-schema/page.ts b/packages/backend/src/models/json-schema/page.ts
index 748d6f1245..8f6d5c675d 100644
--- a/packages/backend/src/models/json-schema/page.ts
+++ b/packages/backend/src/models/json-schema/page.ts
@@ -174,6 +174,7 @@ export const packedPageSchema = {
font: {
type: 'string',
optional: false, nullable: false,
+ enum: ['serif', 'sans-serif'],
},
script: {
type: 'string',
diff --git a/packages/backend/src/models/json-schema/reversi-game.ts b/packages/backend/src/models/json-schema/reversi-game.ts
index cb37200384..378ae41cb5 100644
--- a/packages/backend/src/models/json-schema/reversi-game.ts
+++ b/packages/backend/src/models/json-schema/reversi-game.ts
@@ -81,6 +81,7 @@ export const packedReversiGameLiteSchema = {
bw: {
type: 'string',
optional: false, nullable: false,
+ enum: ['random', '1', '2'],
},
noIrregularRules: {
type: 'boolean',
@@ -199,6 +200,7 @@ export const packedReversiGameDetailedSchema = {
bw: {
type: 'string',
optional: false, nullable: false,
+ enum: ['random', '1', '2'],
},
noIrregularRules: {
type: 'boolean',
diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts
index 48b4d6372a..373ff38fbb 100644
--- a/packages/backend/src/models/json-schema/role.ts
+++ b/packages/backend/src/models/json-schema/role.ts
@@ -317,6 +317,10 @@ export const packedRolePoliciesSchema = {
type: 'integer',
optional: false, nullable: false,
},
+ scheduledNoteLimit: {
+ type: 'integer',
+ optional: false, nullable: false,
+ },
watermarkAvailable: {
type: 'boolean',
optional: false, nullable: false,
diff --git a/packages/backend/src/models/json-schema/user-webhook.ts b/packages/backend/src/models/json-schema/user-webhook.ts
new file mode 100644
index 0000000000..8ea0991716
--- /dev/null
+++ b/packages/backend/src/models/json-schema/user-webhook.ts
@@ -0,0 +1,55 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { webhookEventTypes } from '@/models/Webhook.js';
+
+export const packedUserWebhookSchema = {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ format: 'id',
+ optional: false, nullable: false,
+ },
+ userId: {
+ type: 'string',
+ format: 'id',
+ optional: false, nullable: false,
+ },
+ name: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ on: {
+ type: 'array',
+ items: {
+ type: 'string',
+ optional: false, nullable: false,
+ enum: webhookEventTypes,
+ },
+ },
+ url: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ secret: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ active: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ latestSentAt: {
+ type: 'string',
+ format: 'date-time',
+ optional: false, nullable: true,
+ },
+ latestStatus: {
+ type: 'integer',
+ optional: false, nullable: true,
+ },
+ },
+} as const;
diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts
index 2b5f706ff9..f71ec1d023 100644
--- a/packages/backend/src/models/json-schema/user.ts
+++ b/packages/backend/src/models/json-schema/user.ts
@@ -65,7 +65,7 @@ export const packedUserLiteSchema = {
avatarUrl: {
type: 'string',
format: 'url',
- nullable: true, optional: false,
+ nullable: false, optional: false,
},
avatarBlurhash: {
type: 'string',
@@ -465,11 +465,11 @@ export const packedMeDetailedOnlySchema = {
},
isModerator: {
type: 'boolean',
- nullable: true, optional: false,
+ nullable: false, optional: false,
},
isAdmin: {
type: 'boolean',
- nullable: true, optional: false,
+ nullable: false, optional: false,
},
injectFeaturedNote: {
type: 'boolean',
@@ -591,7 +591,7 @@ export const packedMeDetailedOnlySchema = {
},
mutedInstances: {
type: 'array',
- nullable: true, optional: false,
+ nullable: false, optional: false,
items: {
type: 'string',
nullable: false, optional: false,
@@ -609,6 +609,8 @@ export const packedMeDetailedOnlySchema = {
quote: { optional: true, ...notificationRecieveConfig },
reaction: { optional: true, ...notificationRecieveConfig },
pollEnded: { optional: true, ...notificationRecieveConfig },
+ scheduledNotePosted: { optional: true, ...notificationRecieveConfig },
+ scheduledNotePostFailed: { optional: true, ...notificationRecieveConfig },
receiveFollowRequest: { optional: true, ...notificationRecieveConfig },
followRequestAccepted: { optional: true, ...notificationRecieveConfig },
roleAssigned: { optional: true, ...notificationRecieveConfig },
@@ -616,6 +618,9 @@ export const packedMeDetailedOnlySchema = {
achievementEarned: { optional: true, ...notificationRecieveConfig },
app: { optional: true, ...notificationRecieveConfig },
test: { optional: true, ...notificationRecieveConfig },
+ login: { optional: true, ...notificationRecieveConfig },
+ createToken: { optional: true, ...notificationRecieveConfig },
+ exportCompleted: { optional: true, ...notificationRecieveConfig },
},
},
emailNotificationTypes: {
diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts
index f6cbbbe64c..3dcd3f0965 100644
--- a/packages/backend/src/postgres.ts
+++ b/packages/backend/src/postgres.ts
@@ -6,7 +6,6 @@
// https://github.com/typeorm/typeorm/issues/2400
import pg from 'pg';
import { DataSource, Logger, type QueryRunner } from 'typeorm';
-import * as highlight from 'cli-highlight';
import { entities as charts } from '@/core/chart/entities.js';
import { Config } from '@/config.js';
import MisskeyLogger from '@/logger.js';
@@ -25,6 +24,7 @@ import { MiAuthSession } from '@/models/AuthSession.js';
import { MiBlocking } from '@/models/Blocking.js';
import { MiChannelFollowing } from '@/models/ChannelFollowing.js';
import { MiChannelFavorite } from '@/models/ChannelFavorite.js';
+import { MiChannelMuting } from '@/models/ChannelMuting.js';
import { MiClip } from '@/models/Clip.js';
import { MiClipNote } from '@/models/ClipNote.js';
import { MiClipFavorite } from '@/models/ClipFavorite.js';
@@ -100,12 +100,6 @@ export type LoggerProps = {
printReplicationMode?: boolean,
};
-function highlightSql(sql: string) {
- return highlight.highlight(sql, {
- language: 'sql', ignoreIllegals: true,
- });
-}
-
function truncateSql(sql: string) {
return sql.length > 100 ? `${sql.substring(0, 100)}...` : sql;
}
@@ -131,7 +125,7 @@ class MyCustomLogger implements Logger {
modded = truncateSql(modded);
}
- return highlightSql(modded);
+ return modded;
}
@bindThis
@@ -239,6 +233,7 @@ export const entities = [
MiChannel,
MiChannelFollowing,
MiChannelFavorite,
+ MiChannelMuting,
MiRegistryItem,
MiAd,
MiPasswordResetRequest,
diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts
index e01414cd53..e64882c4df 100644
--- a/packages/backend/src/queue/QueueProcessorModule.ts
+++ b/packages/backend/src/queue/QueueProcessorModule.ts
@@ -10,6 +10,7 @@ import { QueueLoggerService } from './QueueLoggerService.js';
import { QueueProcessorService } from './QueueProcessorService.js';
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
+import { PostScheduledNoteProcessorService } from './processors/PostScheduledNoteProcessorService.js';
import { InboxProcessorService } from './processors/InboxProcessorService.js';
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
@@ -79,6 +80,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
UserWebhookDeliverProcessorService,
SystemWebhookDeliverProcessorService,
EndedPollNotificationProcessorService,
+ PostScheduledNoteProcessorService,
DeliverProcessorService,
InboxProcessorService,
AggregateRetentionProcessorService,
diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts
index 7b64182754..2b3b3fc0ad 100644
--- a/packages/backend/src/queue/QueueProcessorService.ts
+++ b/packages/backend/src/queue/QueueProcessorService.ts
@@ -5,7 +5,6 @@
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import * as Bull from 'bullmq';
-import * as Sentry from '@sentry/node';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import type Logger from '@/logger.js';
@@ -14,6 +13,7 @@ import { CheckModeratorsActivityProcessorService } from '@/queue/processors/Chec
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
+import { PostScheduledNoteProcessorService } from './processors/PostScheduledNoteProcessorService.js';
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
import { InboxProcessorService } from './processors/InboxProcessorService.js';
import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js';
@@ -85,6 +85,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
private relationshipQueueWorker: Bull.Worker;
private objectStorageQueueWorker: Bull.Worker;
private endedPollNotificationQueueWorker: Bull.Worker;
+ private postScheduledNoteQueueWorker: Bull.Worker;
constructor(
@Inject(DI.config)
@@ -94,6 +95,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService,
private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService,
private endedPollNotificationProcessorService: EndedPollNotificationProcessorService,
+ private postScheduledNoteProcessorService: PostScheduledNoteProcessorService,
private deliverProcessorService: DeliverProcessorService,
private inboxProcessorService: InboxProcessorService,
private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService,
@@ -154,6 +156,13 @@ export class QueueProcessorService implements OnApplicationShutdown {
};
}
+ let Sentry: typeof import('@sentry/node') | undefined;
+ if (this.config.sentryForBackend) {
+ import('@sentry/node').then((mod) => {
+ Sentry = mod;
+ });
+ }
+
//#region system
{
const processer = (job: Bull.Job) => {
@@ -172,7 +181,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
};
this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => {
- if (this.config.sentryForBackend) {
+ if (Sentry != null) {
return Sentry.startSpan({ name: 'Queue: System: ' + job.name }, () => processer(job));
} else {
return processer(job);
@@ -189,7 +198,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err: Error) => {
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
- if (config.sentryForBackend) {
+ if (Sentry != null) {
Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
@@ -229,7 +238,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
};
this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => {
- if (this.config.sentryForBackend) {
+ if (Sentry != null) {
return Sentry.startSpan({ name: 'Queue: DB: ' + job.name }, () => processer(job));
} else {
return processer(job);
@@ -246,7 +255,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => {
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
- if (config.sentryForBackend) {
+ if (Sentry != null) {
Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
@@ -261,7 +270,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
//#region deliver
{
this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => {
- if (this.config.sentryForBackend) {
+ if (Sentry != null) {
return Sentry.startSpan({ name: 'Queue: Deliver' }, () => this.deliverProcessorService.process(job));
} else {
return this.deliverProcessorService.process(job);
@@ -286,7 +295,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => {
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
- if (config.sentryForBackend) {
+ if (Sentry != null) {
Sentry.captureMessage(`Queue: Deliver: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
@@ -301,7 +310,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
//#region inbox
{
this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => {
- if (this.config.sentryForBackend) {
+ if (Sentry != null) {
return Sentry.startSpan({ name: 'Queue: Inbox' }, () => this.inboxProcessorService.process(job));
} else {
return this.inboxProcessorService.process(job);
@@ -326,7 +335,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
.on('failed', (job, err) => {
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job: renderJob(job), e: renderError(err) });
- if (config.sentryForBackend) {
+ if (Sentry != null) {
Sentry.captureMessage(`Queue: Inbox: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
@@ -341,7 +350,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
//#region user-webhook deliver
{
this.userWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.USER_WEBHOOK_DELIVER, (job) => {
- if (this.config.sentryForBackend) {
+ if (Sentry != null) {
return Sentry.startSpan({ name: 'Queue: UserWebhookDeliver' }, () => this.userWebhookDeliverProcessorService.process(job));
} else {
return this.userWebhookDeliverProcessorService.process(job);
@@ -366,7 +375,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => {
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
- if (config.sentryForBackend) {
+ if (Sentry != null) {
Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
@@ -381,7 +390,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
//#region system-webhook deliver
{
this.systemWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.SYSTEM_WEBHOOK_DELIVER, (job) => {
- if (this.config.sentryForBackend) {
+ if (Sentry != null) {
return Sentry.startSpan({ name: 'Queue: SystemWebhookDeliver' }, () => this.systemWebhookDeliverProcessorService.process(job));
} else {
return this.systemWebhookDeliverProcessorService.process(job);
@@ -406,7 +415,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => {
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
- if (config.sentryForBackend) {
+ if (Sentry != null) {
Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
@@ -431,7 +440,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
};
this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => {
- if (this.config.sentryForBackend) {
+ if (Sentry != null) {
return Sentry.startSpan({ name: 'Queue: Relationship: ' + job.name }, () => processer(job));
} else {
return processer(job);
@@ -453,7 +462,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => {
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
- if (config.sentryForBackend) {
+ if (Sentry != null) {
Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
@@ -476,7 +485,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
};
this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => {
- if (this.config.sentryForBackend) {
+ if (Sentry != null) {
return Sentry.startSpan({ name: 'Queue: ObjectStorage: ' + job.name }, () => processer(job));
} else {
return processer(job);
@@ -494,7 +503,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => {
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
- if (config.sentryForBackend) {
+ if (Sentry != null) {
Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
@@ -509,7 +518,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
//#region ended poll notification
{
this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => {
- if (this.config.sentryForBackend) {
+ if (Sentry != null) {
return Sentry.startSpan({ name: 'Queue: EndedPollNotification' }, () => this.endedPollNotificationProcessorService.process(job));
} else {
return this.endedPollNotificationProcessorService.process(job);
@@ -520,6 +529,21 @@ export class QueueProcessorService implements OnApplicationShutdown {
});
}
//#endregion
+
+ //#region post scheduled note
+ {
+ this.postScheduledNoteQueueWorker = new Bull.Worker(QUEUE.POST_SCHEDULED_NOTE, async (job) => {
+ if (Sentry != null) {
+ return Sentry.startSpan({ name: 'Queue: PostScheduledNote' }, () => this.postScheduledNoteProcessorService.process(job));
+ } else {
+ return this.postScheduledNoteProcessorService.process(job);
+ }
+ }, {
+ ...baseWorkerOptions(this.config, QUEUE.POST_SCHEDULED_NOTE),
+ autorun: false,
+ });
+ }
+ //#endregion
}
@bindThis
@@ -534,6 +558,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.relationshipQueueWorker.run(),
this.objectStorageQueueWorker.run(),
this.endedPollNotificationQueueWorker.run(),
+ this.postScheduledNoteQueueWorker.run(),
]);
}
@@ -549,6 +574,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.relationshipQueueWorker.close(),
this.objectStorageQueueWorker.close(),
this.endedPollNotificationQueueWorker.close(),
+ this.postScheduledNoteQueueWorker.close(),
]);
}
diff --git a/packages/backend/src/queue/const.ts b/packages/backend/src/queue/const.ts
index 7e146a7e03..625204b7ad 100644
--- a/packages/backend/src/queue/const.ts
+++ b/packages/backend/src/queue/const.ts
@@ -12,6 +12,7 @@ export const QUEUE = {
INBOX: 'inbox',
SYSTEM: 'system',
ENDED_POLL_NOTIFICATION: 'endedPollNotification',
+ POST_SCHEDULED_NOTE: 'postScheduledNote',
DB: 'db',
RELATIONSHIP: 'relationship',
OBJECT_STORAGE: 'objectStorage',
diff --git a/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts
index 448fc9c763..e898e6dd48 100644
--- a/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts
+++ b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts
@@ -4,14 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
-import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { MutingsRepository } from '@/models/_.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { UserMutingService } from '@/core/UserMutingService.js';
+import { ChannelMutingService } from '@/core/ChannelMutingService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
-import type * as Bull from 'bullmq';
@Injectable()
export class CheckExpiredMutingsProcessorService {
@@ -22,6 +21,7 @@ export class CheckExpiredMutingsProcessorService {
private mutingsRepository: MutingsRepository,
private userMutingService: UserMutingService,
+ private channelMutingService: ChannelMutingService,
private queueLoggerService: QueueLoggerService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('check-expired-mutings');
@@ -41,6 +41,8 @@ export class CheckExpiredMutingsProcessorService {
await this.userMutingService.unmute(expired);
}
+ await this.channelMutingService.eraseExpiredMutings();
+
this.logger.succ('All expired mutings checked.');
}
}
diff --git a/packages/backend/src/queue/processors/CleanRemoteNotesProcessorService.ts b/packages/backend/src/queue/processors/CleanRemoteNotesProcessorService.ts
index 36c34c753c..bc99dea000 100644
--- a/packages/backend/src/queue/processors/CleanRemoteNotesProcessorService.ts
+++ b/packages/backend/src/queue/processors/CleanRemoteNotesProcessorService.ts
@@ -51,6 +51,17 @@ export class CleanRemoteNotesProcessorService {
skipped: boolean;
transientErrors: number;
}> {
+ const getConfig = () => {
+ return {
+ enabled: this.meta.enableRemoteNotesCleaning,
+ maxDuration: this.meta.remoteNotesCleaningMaxProcessingDurationInMinutes * 60 * 1000, // Convert minutes to milliseconds
+ // The date limit for the newest note to be considered for deletion.
+ // All notes newer than this limit will always be retained.
+ newestLimit: this.idService.gen(Date.now() - (1000 * 60 * 60 * 24 * this.meta.remoteNotesCleaningExpiryDaysForEachNotes)),
+ };
+ };
+
+ const initialConfig = getConfig();
if (!this.meta.enableRemoteNotesCleaning) {
this.logger.info('Remote notes cleaning is disabled, skipping...');
return {
@@ -64,13 +75,9 @@ export class CleanRemoteNotesProcessorService {
this.logger.info('cleaning remote notes...');
- const maxDuration = this.meta.remoteNotesCleaningMaxProcessingDurationInMinutes * 60 * 1000; // Convert minutes to milliseconds
const startAt = Date.now();
//#region queries
- // The date limit for the newest note to be considered for deletion.
- // All notes newer than this limit will always be retained.
- const newestLimit = this.idService.gen(Date.now() - (1000 * 60 * 60 * 24 * this.meta.remoteNotesCleaningExpiryDaysForEachNotes));
// The condition for removing the notes.
// The note must be:
@@ -92,7 +99,7 @@ export class CleanRemoteNotesProcessorService {
const minId = (await this.notesRepository.createQueryBuilder('note')
.select('MIN(note.id)', 'minId')
.where({
- id: LessThan(newestLimit),
+ id: LessThan(initialConfig.newestLimit),
userHost: Not(IsNull()),
replyId: IsNull(),
renoteId: IsNull(),
@@ -155,12 +162,12 @@ export class CleanRemoteNotesProcessorService {
// | fff | fff | TRUE |
// | ggg | ggg | FALSE |
//
- const candidateNotesQuery = this.db.createQueryBuilder()
+ const candidateNotesQuery = ({ limit }: { limit: number }) => this.db.createQueryBuilder()
.select(`"${candidateNotesCteName}"."id"`, 'id')
.addSelect('unremovable."id" IS NULL', 'isRemovable')
.addSelect(`BOOL_OR("${candidateNotesCteName}"."isBase")`, 'isBase')
.addCommonTableExpression(
- `((SELECT "base".* FROM (${candidateNotesQueryBase.orderBy('note.id', 'ASC').limit(currentLimit).getQuery()}) AS "base") UNION ${candidateNotesQueryInductive.getQuery()})`,
+ `((SELECT "base".* FROM (${candidateNotesQueryBase.orderBy('note.id', 'ASC').limit(limit).getQuery()}) AS "base") UNION ${candidateNotesQueryInductive.getQuery()})`,
candidateNotesCteName,
{ recursive: true },
)
@@ -178,6 +185,11 @@ export class CleanRemoteNotesProcessorService {
let lowThroughputWarned = false;
let transientErrors = 0;
for (;;) {
+ const { enabled, maxDuration, newestLimit } = getConfig();
+ if (!enabled) {
+ this.logger.info('Remote notes cleaning is disabled, processing stopped...');
+ break;
+ }
//#region check time
const batchBeginAt = Date.now();
@@ -205,13 +217,38 @@ export class CleanRemoteNotesProcessorService {
let noteIds = null;
try {
- noteIds = await candidateNotesQuery.setParameters(
+ noteIds = await candidateNotesQuery({ limit: currentLimit }).setParameters(
{ newestLimit, cursorLeft },
).getRawMany<{ id: MiNote['id'], isRemovable: boolean, isBase: boolean }>();
} catch (e) {
- if (currentLimit > minimumLimit && e instanceof QueryFailedError && e.driverError?.code === '57014') {
- // Statement timeout (maybe suddenly hit a large note tree), reduce the limit and try again
- // continuous failures will eventually converge to currentLimit == minimumLimit and then throw
+ if (e instanceof QueryFailedError && e.driverError?.code === '57014') {
+ // Statement timeout (maybe suddenly hit a large note tree), if possible, reduce the limit and try again
+ // if not possible, skip the current batch of notes and find the next root note
+ if (currentLimit <= minimumLimit) {
+ job.log('Local note tree complexity is too high, finding next root note...');
+
+ const idWindow = await this.notesRepository.createQueryBuilder('note')
+ .select('id')
+ .where('note.id > :cursorLeft')
+ .andWhere(removalCriteria)
+ .andWhere({ replyId: IsNull(), renoteId: IsNull() })
+ .orderBy('note.id', 'ASC')
+ .limit(minimumLimit + 1)
+ .setParameters({ cursorLeft, newestLimit })
+ .getRawMany<{ id?: MiNote['id'] }>();
+
+ job.log(`Skipped note IDs: ${idWindow.slice(0, minimumLimit).map(id => id.id).join(', ')}`);
+
+ const lastId = idWindow.at(minimumLimit)?.id;
+
+ if (!lastId) {
+ job.log('No more notes to clean.');
+ break;
+ }
+
+ cursorLeft = lastId;
+ continue;
+ }
currentLimit = Math.max(minimumLimit, Math.floor(currentLimit * 0.25));
continue;
}
diff --git a/packages/backend/src/queue/processors/ExportClipsProcessorService.ts b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts
index 486dc4c01f..be7d4e9e21 100644
--- a/packages/backend/src/queue/processors/ExportClipsProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts
@@ -5,21 +5,20 @@
import * as fs from 'node:fs';
import { Writable } from 'node:stream';
-import { Inject, Injectable, StreamableFile } from '@nestjs/common';
-import { MoreThan } from 'typeorm';
+import { Inject, Injectable } from '@nestjs/common';
import { format as dateFormat } from 'date-fns';
import { DI } from '@/di-symbols.js';
-import type { ClipNotesRepository, ClipsRepository, MiClip, MiClipNote, MiUser, NotesRepository, PollsRepository, UsersRepository } from '@/models/_.js';
+import type { ClipNotesRepository, ClipsRepository, MiClip, MiClipNote, MiUser, PollsRepository, UsersRepository } from '@/models/_.js';
import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
import { createTemp } from '@/misc/create-temp.js';
import type { MiPoll } from '@/models/Poll.js';
import type { MiNote } from '@/models/Note.js';
import { bindThis } from '@/decorators.js';
-import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
-import { Packed } from '@/misc/json-schema.js';
import { IdService } from '@/core/IdService.js';
import { NotificationService } from '@/core/NotificationService.js';
+import { QueryService } from '@/core/QueryService.js';
+import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
import type { DbJobDataWithUser } from '../types.js';
@@ -43,6 +42,7 @@ export class ExportClipsProcessorService {
private driveService: DriveService,
private queueLoggerService: QueueLoggerService,
+ private queryService: QueryService,
private idService: IdService,
private notificationService: NotificationService,
) {
@@ -100,16 +100,16 @@ export class ExportClipsProcessorService {
});
while (true) {
- const clips = await this.clipsRepository.find({
- where: {
- userId: user.id,
- ...(cursor ? { id: MoreThan(cursor) } : {}),
- },
- take: 100,
- order: {
- id: 1,
- },
- });
+ const query = this.clipsRepository.createQueryBuilder('clip')
+ .where('clip.userId = :userId', { userId: user.id })
+ .orderBy('clip.id', 'ASC')
+ .take(100);
+
+ if (cursor) {
+ query.andWhere('clip.id > :cursor', { cursor });
+ }
+
+ const clips = await query.getMany();
if (clips.length === 0) {
job.updateProgress(100);
@@ -124,7 +124,7 @@ export class ExportClipsProcessorService {
const isFirst = exportedClipsCount === 0;
await writer.write(isFirst ? content : ',\n' + content);
- await this.processClipNotes(writer, clip.id);
+ await this.processClipNotes(writer, clip.id, user.id);
await writer.write(']}');
exportedClipsCount++;
@@ -134,22 +134,25 @@ export class ExportClipsProcessorService {
}
}
- async processClipNotes(writer: WritableStreamDefaultWriter, clipId: string): Promise {
+ async processClipNotes(writer: WritableStreamDefaultWriter, clipId: string, userId: string): Promise {
let exportedClipNotesCount = 0;
let cursor: MiClipNote['id'] | null = null;
while (true) {
- const clipNotes = await this.clipNotesRepository.find({
- where: {
- clipId,
- ...(cursor ? { id: MoreThan(cursor) } : {}),
- },
- take: 100,
- order: {
- id: 1,
- },
- relations: ['note', 'note.user'],
- }) as (MiClipNote & { note: MiNote & { user: MiUser } })[];
+ const query = this.clipNotesRepository.createQueryBuilder('clipNote')
+ .leftJoinAndSelect('clipNote.note', 'note')
+ .leftJoinAndSelect('note.user', 'user')
+ .where('clipNote.clipId = :clipId', { clipId })
+ .orderBy('clipNote.id', 'ASC')
+ .take(100);
+
+ if (cursor) {
+ query.andWhere('clipNote.id > :cursor', { cursor });
+ }
+
+ this.queryService.generateVisibilityQuery(query, { id: userId });
+
+ const clipNotes = await query.getMany() as (MiClipNote & { note: MiNote & { user: MiUser } })[];
if (clipNotes.length === 0) {
break;
@@ -158,6 +161,11 @@ export class ExportClipsProcessorService {
cursor = clipNotes.at(-1)?.id ?? null;
for (const clipNote of clipNotes) {
+ const noteCreatedAt = this.idService.parse(clipNote.note.id).date;
+ if (shouldHideNoteByTime(clipNote.note.user.makeNotesHiddenBefore, noteCreatedAt)) {
+ continue;
+ }
+
let poll: MiPoll | undefined;
if (clipNote.note.hasPoll) {
poll = await this.pollsRepository.findOneByOrFail({ noteId: clipNote.note.id });
diff --git a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts
index e237cd4975..53ecd2d180 100644
--- a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts
@@ -123,8 +123,8 @@ export class ExportCustomEmojisProcessorService {
metaStream.end();
// Create archive
- await new Promise(async (resolve) => {
- const [archivePath, archiveCleanup] = await createTemp();
+ const [archivePath, archiveCleanup] = await createTemp();
+ await new Promise((resolve) => {
const archiveStream = fs.createWriteStream(archivePath);
const archive = archiver('zip', {
zlib: { level: 0 },
diff --git a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts
index 7918c8ccb5..87a8ded307 100644
--- a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts
@@ -5,7 +5,6 @@
import * as fs from 'node:fs';
import { Inject, Injectable } from '@nestjs/common';
-import { MoreThan } from 'typeorm';
import { format as dateFormat } from 'date-fns';
import { DI } from '@/di-symbols.js';
import type { MiNoteFavorite, NoteFavoritesRepository, PollsRepository, MiUser, UsersRepository } from '@/models/_.js';
@@ -17,6 +16,8 @@ import type { MiNote } from '@/models/Note.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
import { NotificationService } from '@/core/NotificationService.js';
+import { QueryService } from '@/core/QueryService.js';
+import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
import type { DbJobDataWithUser } from '../types.js';
@@ -37,6 +38,7 @@ export class ExportFavoritesProcessorService {
private driveService: DriveService,
private queueLoggerService: QueueLoggerService,
+ private queryService: QueryService,
private idService: IdService,
private notificationService: NotificationService,
) {
@@ -83,17 +85,20 @@ export class ExportFavoritesProcessorService {
});
while (true) {
- const favorites = await this.noteFavoritesRepository.find({
- where: {
- userId: user.id,
- ...(cursor ? { id: MoreThan(cursor) } : {}),
- },
- take: 100,
- order: {
- id: 1,
- },
- relations: ['note', 'note.user'],
- }) as (MiNoteFavorite & { note: MiNote & { user: MiUser } })[];
+ const query = this.noteFavoritesRepository.createQueryBuilder('favorite')
+ .leftJoinAndSelect('favorite.note', 'note')
+ .leftJoinAndSelect('note.user', 'user')
+ .where('favorite.userId = :userId', { userId: user.id })
+ .orderBy('favorite.id', 'ASC')
+ .take(100);
+
+ if (cursor) {
+ query.andWhere('favorite.id > :cursor', { cursor });
+ }
+
+ this.queryService.generateVisibilityQuery(query, { id: user.id });
+
+ const favorites = await query.getMany() as (MiNoteFavorite & { note: MiNote & { user: MiUser } })[];
if (favorites.length === 0) {
job.updateProgress(100);
@@ -103,6 +108,11 @@ export class ExportFavoritesProcessorService {
cursor = favorites.at(-1)?.id ?? null;
for (const favorite of favorites) {
+ const noteCreatedAt = this.idService.parse(favorite.note.id).date;
+ if (shouldHideNoteByTime(favorite.note.user.makeNotesHiddenBefore, noteCreatedAt)) {
+ continue;
+ }
+
let poll: MiPoll | undefined;
if (favorite.note.hasPoll) {
poll = await this.pollsRepository.findOneByOrFail({ noteId: favorite.note.id });
diff --git a/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts b/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts
new file mode 100644
index 0000000000..719a09980c
--- /dev/null
+++ b/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts
@@ -0,0 +1,72 @@
+/*
+ * 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 { NoteDraftsRepository } from '@/models/_.js';
+import type Logger from '@/logger.js';
+import { NotificationService } from '@/core/NotificationService.js';
+import { bindThis } from '@/decorators.js';
+import { NoteCreateService } from '@/core/NoteCreateService.js';
+import { QueueLoggerService } from '../QueueLoggerService.js';
+import type * as Bull from 'bullmq';
+import type { PostScheduledNoteJobData } from '../types.js';
+
+@Injectable()
+export class PostScheduledNoteProcessorService {
+ private logger: Logger;
+
+ constructor(
+ @Inject(DI.noteDraftsRepository)
+ private noteDraftsRepository: NoteDraftsRepository,
+
+ private noteCreateService: NoteCreateService,
+ private notificationService: NotificationService,
+ private queueLoggerService: QueueLoggerService,
+ ) {
+ this.logger = this.queueLoggerService.logger.createSubLogger('post-scheduled-note');
+ }
+
+ @bindThis
+ public async process(job: Bull.Job): Promise {
+ const draft = await this.noteDraftsRepository.findOne({ where: { id: job.data.noteDraftId }, relations: ['user'] });
+ if (draft == null || draft.user == null || draft.scheduledAt == null || !draft.isActuallyScheduled) {
+ return;
+ }
+
+ try {
+ const note = await this.noteCreateService.fetchAndCreate(draft.user, {
+ createdAt: new Date(),
+ fileIds: draft.fileIds,
+ poll: draft.hasPoll ? {
+ choices: draft.pollChoices,
+ multiple: draft.pollMultiple,
+ expiresAt: draft.pollExpiredAfter ? new Date(Date.now() + draft.pollExpiredAfter) : draft.pollExpiresAt ? new Date(draft.pollExpiresAt) : null,
+ } : null,
+ text: draft.text ?? null,
+ replyId: draft.replyId,
+ renoteId: draft.renoteId,
+ cw: draft.cw,
+ localOnly: draft.localOnly,
+ reactionAcceptance: draft.reactionAcceptance,
+ visibility: draft.visibility,
+ visibleUserIds: draft.visibleUserIds,
+ channelId: draft.channelId,
+ });
+
+ // await不要
+ this.noteDraftsRepository.remove(draft);
+
+ // await不要
+ this.notificationService.createNotification(draft.userId, 'scheduledNotePosted', {
+ noteId: note.id,
+ });
+ } catch (_) {
+ this.notificationService.createNotification(draft.userId, 'scheduledNotePostFailed', {
+ noteDraftId: draft.id,
+ });
+ }
+ }
+}
diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts
index 757daea88b..1cb2b93918 100644
--- a/packages/backend/src/queue/types.ts
+++ b/packages/backend/src/queue/types.ts
@@ -109,6 +109,10 @@ export type EndedPollNotificationJobData = {
noteId: MiNote['id'];
};
+export type PostScheduledNoteJobData = {
+ noteDraftId: string;
+};
+
export type SystemWebhookDeliverJobData = {
type: T;
content: SystemWebhookPayload;
diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts
index a5fb5b82e3..54ffeecc6b 100644
--- a/packages/backend/src/server/ActivityPubServerService.ts
+++ b/packages/backend/src/server/ActivityPubServerService.ts
@@ -116,7 +116,7 @@ export class ActivityPubServerService {
try {
signature = httpSignature.parseRequest(request.raw, { 'headers': ['(request-target)', 'host', 'date'], authorizationHeaderName: 'signature' });
- } catch (e) {
+ } catch (_) {
reply.code(401);
return;
}
diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts
index 772c37094c..f5034d0733 100644
--- a/packages/backend/src/server/FileServerService.ts
+++ b/packages/backend/src/server/FileServerService.ts
@@ -7,27 +7,22 @@ import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import { Inject, Injectable } from '@nestjs/common';
-import rename from 'rename';
-import sharp from 'sharp';
-import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
import type { Config } from '@/config.js';
-import type { MiDriveFile, DriveFilesRepository } from '@/models/_.js';
+import type { DriveFilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
-import { createTemp } from '@/misc/create-temp.js';
-import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
import { StatusError } from '@/misc/status-error.js';
import type Logger from '@/logger.js';
import { DownloadService } from '@/core/DownloadService.js';
-import { IImageStreamable, ImageProcessingService, webpDefault } from '@/core/ImageProcessingService.js';
-import { VideoProcessingService } from '@/core/VideoProcessingService.js';
import { InternalStorageService } from '@/core/InternalStorageService.js';
-import { contentDisposition } from '@/misc/content-disposition.js';
import { FileInfoService } from '@/core/FileInfoService.js';
+import { ImageProcessingService } from '@/core/ImageProcessingService.js';
+import { VideoProcessingService } from '@/core/VideoProcessingService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
-import { isMimeImage } from '@/misc/is-mime-image.js';
-import { correctFilename } from '@/misc/correct-filename.js';
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
+import { FileServerDriveHandler } from './file/FileServerDriveHandler.js';
+import { FileServerFileResolver } from './file/FileServerFileResolver.js';
+import { FileServerProxyHandler } from './file/FileServerProxyHandler.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
const _filename = fileURLToPath(import.meta.url);
@@ -38,6 +33,9 @@ const assets = `${_dirname}/../../server/file/assets/`;
@Injectable()
export class FileServerService {
private logger: Logger;
+ private driveHandler: FileServerDriveHandler;
+ private proxyHandler: FileServerProxyHandler;
+ private fileResolver: FileServerFileResolver;
constructor(
@Inject(DI.config)
@@ -54,6 +52,24 @@ export class FileServerService {
private loggerService: LoggerService,
) {
this.logger = this.loggerService.getLogger('server', 'gray');
+ this.fileResolver = new FileServerFileResolver(
+ this.driveFilesRepository,
+ this.fileInfoService,
+ this.downloadService,
+ this.internalStorageService,
+ );
+ this.driveHandler = new FileServerDriveHandler(
+ this.config,
+ this.fileResolver,
+ assets,
+ this.videoProcessingService,
+ );
+ this.proxyHandler = new FileServerProxyHandler(
+ this.config,
+ this.fileResolver,
+ assets,
+ this.imageProcessingService,
+ );
//this.createServer = this.createServer.bind(this);
}
@@ -78,7 +94,7 @@ export class FileServerService {
});
fastify.get<{ Params: { key: string; } }>('/files/:key', async (request, reply) => {
- return await this.sendDriveFile(request, reply)
+ return await this.driveHandler.handle(request, reply)
.catch(err => this.errorHandler(request, reply, err));
});
fastify.get<{ Params: { key: string; } }>('/files/:key/*', async (request, reply) => {
@@ -91,7 +107,7 @@ export class FileServerService {
Params: { url: string; };
Querystring: { url?: string; };
}>('/proxy/:url*', async (request, reply) => {
- return await this.proxyHandler(request, reply)
+ return await this.proxyHandler.handle(request, reply)
.catch(err => this.errorHandler(request, reply, err));
});
@@ -116,462 +132,4 @@ export class FileServerService {
reply.code(500);
return;
}
-
- @bindThis
- private async sendDriveFile(request: FastifyRequest<{ Params: { key: string; } }>, reply: FastifyReply) {
- const key = request.params.key;
- const file = await this.getFileFromKey(key).then();
-
- if (file === '404') {
- reply.code(404);
- reply.header('Cache-Control', 'max-age=86400');
- return reply.sendFile('/dummy.png', assets);
- }
-
- if (file === '204') {
- reply.code(204);
- reply.header('Cache-Control', 'max-age=86400');
- return;
- }
-
- try {
- if (file.state === 'remote') {
- let image: IImageStreamable | null = null;
-
- if (file.fileRole === 'thumbnail') {
- if (isMimeImage(file.mime, 'sharp-convertible-image-with-bmp')) {
- reply.header('Cache-Control', 'max-age=31536000, immutable');
-
- const url = new URL(`${this.config.mediaProxy}/static.webp`);
- url.searchParams.set('url', file.url);
- url.searchParams.set('static', '1');
-
- file.cleanup();
- return await reply.redirect(url.toString(), 301);
- } else if (file.mime.startsWith('video/')) {
- const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url);
- if (externalThumbnail) {
- file.cleanup();
- return await reply.redirect(externalThumbnail, 301);
- }
-
- image = await this.videoProcessingService.generateVideoThumbnail(file.path);
- }
- }
-
- if (file.fileRole === 'webpublic') {
- if (['image/svg+xml'].includes(file.mime)) {
- reply.header('Cache-Control', 'max-age=31536000, immutable');
-
- const url = new URL(`${this.config.mediaProxy}/svg.webp`);
- url.searchParams.set('url', file.url);
-
- file.cleanup();
- return await reply.redirect(url.toString(), 301);
- }
- }
-
- if (!image) {
- if (request.headers.range && file.file.size > 0) {
- const range = request.headers.range as string;
- const parts = range.replace(/bytes=/, '').split('-');
- const start = parseInt(parts[0], 10);
- let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
- if (end > file.file.size) {
- end = file.file.size - 1;
- }
- const chunksize = end - start + 1;
-
- image = {
- data: fs.createReadStream(file.path, {
- start,
- end,
- }),
- ext: file.ext,
- type: file.mime,
- };
-
- reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
- reply.header('Accept-Ranges', 'bytes');
- reply.header('Content-Length', chunksize);
- reply.code(206);
- } else {
- image = {
- data: fs.createReadStream(file.path),
- ext: file.ext,
- type: file.mime,
- };
- }
- }
-
- if ('pipe' in image.data && typeof image.data.pipe === 'function') {
- // image.dataがstreamなら、stream終了後にcleanup
- image.data.on('end', file.cleanup);
- image.data.on('close', file.cleanup);
- } else {
- // image.dataがstreamでないなら直ちにcleanup
- file.cleanup();
- }
-
- reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream');
- reply.header('Content-Length', file.file.size);
- reply.header('Cache-Control', 'max-age=31536000, immutable');
- reply.header('Content-Disposition',
- contentDisposition(
- 'inline',
- correctFilename(file.filename, image.ext),
- ),
- );
- return image.data;
- }
-
- if (file.fileRole !== 'original') {
- const filename = rename(file.filename, {
- suffix: file.fileRole === 'thumbnail' ? '-thumb' : '-web',
- extname: file.ext ? `.${file.ext}` : '.unknown',
- }).toString();
-
- reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.mime) ? file.mime : 'application/octet-stream');
- reply.header('Cache-Control', 'max-age=31536000, immutable');
- reply.header('Content-Disposition', contentDisposition('inline', filename));
-
- if (request.headers.range && file.file.size > 0) {
- const range = request.headers.range as string;
- const parts = range.replace(/bytes=/, '').split('-');
- const start = parseInt(parts[0], 10);
- let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
- if (end > file.file.size) {
- end = file.file.size - 1;
- }
- const chunksize = end - start + 1;
- const fileStream = fs.createReadStream(file.path, {
- start,
- end,
- });
- reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
- reply.header('Accept-Ranges', 'bytes');
- reply.header('Content-Length', chunksize);
- reply.code(206);
- return fileStream;
- }
-
- return fs.createReadStream(file.path);
- } else {
- reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.file.type) ? file.file.type : 'application/octet-stream');
- reply.header('Content-Length', file.file.size);
- reply.header('Cache-Control', 'max-age=31536000, immutable');
- reply.header('Content-Disposition', contentDisposition('inline', file.filename));
-
- if (request.headers.range && file.file.size > 0) {
- const range = request.headers.range as string;
- const parts = range.replace(/bytes=/, '').split('-');
- const start = parseInt(parts[0], 10);
- let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
- if (end > file.file.size) {
- end = file.file.size - 1;
- }
- const chunksize = end - start + 1;
- const fileStream = fs.createReadStream(file.path, {
- start,
- end,
- });
- reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
- reply.header('Accept-Ranges', 'bytes');
- reply.header('Content-Length', chunksize);
- reply.code(206);
- return fileStream;
- }
-
- return fs.createReadStream(file.path);
- }
- } catch (e) {
- if ('cleanup' in file) file.cleanup();
- throw e;
- }
- }
-
- @bindThis
- private async proxyHandler(request: FastifyRequest<{ Params: { url: string; }; Querystring: { url?: string; }; }>, reply: FastifyReply) {
- const url = 'url' in request.query ? request.query.url : 'https://' + request.params.url;
-
- if (typeof url !== 'string') {
- reply.code(400);
- return;
- }
-
- // アバタークロップなど、どうしてもオリジンである必要がある場合
- const mustOrigin = 'origin' in request.query;
-
- if (this.config.externalMediaProxyEnabled && !mustOrigin) {
- // 外部のメディアプロキシが有効なら、そちらにリダイレクト
-
- reply.header('Cache-Control', 'public, max-age=259200'); // 3 days
-
- const url = new URL(`${this.config.mediaProxy}/${request.params.url || ''}`);
-
- for (const [key, value] of Object.entries(request.query)) {
- url.searchParams.append(key, value);
- }
-
- return await reply.redirect(
- url.toString(),
- 301,
- );
- }
-
- if (!request.headers['user-agent']) {
- throw new StatusError('User-Agent is required', 400, 'User-Agent is required');
- } else if (request.headers['user-agent'].toLowerCase().indexOf('misskey/') !== -1) {
- throw new StatusError('Refusing to proxy a request from another proxy', 403, 'Proxy is recursive');
- }
-
- // Create temp file
- const file = await this.getStreamAndTypeFromUrl(url);
- if (file === '404') {
- reply.code(404);
- reply.header('Cache-Control', 'max-age=86400');
- return reply.sendFile('/dummy.png', assets);
- }
-
- if (file === '204') {
- reply.code(204);
- reply.header('Cache-Control', 'max-age=86400');
- return;
- }
-
- try {
- const isConvertibleImage = isMimeImage(file.mime, 'sharp-convertible-image-with-bmp');
- const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image-with-bmp');
-
- if (
- 'emoji' in request.query ||
- 'avatar' in request.query ||
- 'static' in request.query ||
- 'preview' in request.query ||
- 'badge' in request.query
- ) {
- if (!isConvertibleImage) {
- // 画像でないなら404でお茶を濁す
- throw new StatusError('Unexpected mime', 404);
- }
- }
-
- let image: IImageStreamable | null = null;
- if ('emoji' in request.query || 'avatar' in request.query) {
- if (!isAnimationConvertibleImage && !('static' in request.query)) {
- image = {
- data: fs.createReadStream(file.path),
- ext: file.ext,
- type: file.mime,
- };
- } else {
- const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in request.query) }))
- .resize({
- height: 'emoji' in request.query ? 128 : 320,
- withoutEnlargement: true,
- })
- .webp(webpDefault);
-
- image = {
- data,
- ext: 'webp',
- type: 'image/webp',
- };
- }
- } else if ('static' in request.query) {
- image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 498, 422);
- } else if ('preview' in request.query) {
- image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 200, 200);
- } else if ('badge' in request.query) {
- const mask = (await sharpBmp(file.path, file.mime))
- .resize(96, 96, {
- fit: 'contain',
- position: 'centre',
- withoutEnlargement: false,
- })
- .greyscale()
- .normalise()
- .linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast
- .flatten({ background: '#000' })
- .toColorspace('b-w');
-
- const stats = await mask.clone().stats();
-
- if (stats.entropy < 0.1) {
- // エントロピーがあまりない場合は404にする
- throw new StatusError('Skip to provide badge', 404);
- }
-
- const data = sharp({
- create: { width: 96, height: 96, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } },
- })
- .pipelineColorspace('b-w')
- .boolean(await mask.png().toBuffer(), 'eor');
-
- image = {
- data: await data.png().toBuffer(),
- ext: 'png',
- type: 'image/png',
- };
- } else if (file.mime === 'image/svg+xml') {
- image = this.imageProcessingService.convertToWebpStream(file.path, 2048, 2048);
- } else if (!file.mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(file.mime)) {
- throw new StatusError('Rejected type', 403, 'Rejected type');
- }
-
- if (!image) {
- if (request.headers.range && file.file && file.file.size > 0) {
- const range = request.headers.range as string;
- const parts = range.replace(/bytes=/, '').split('-');
- const start = parseInt(parts[0], 10);
- let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
- if (end > file.file.size) {
- end = file.file.size - 1;
- }
- const chunksize = end - start + 1;
-
- image = {
- data: fs.createReadStream(file.path, {
- start,
- end,
- }),
- ext: file.ext,
- type: file.mime,
- };
-
- reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
- reply.header('Accept-Ranges', 'bytes');
- reply.header('Content-Length', chunksize);
- reply.code(206);
- } else {
- image = {
- data: fs.createReadStream(file.path),
- ext: file.ext,
- type: file.mime,
- };
- }
- }
-
- if ('cleanup' in file) {
- if ('pipe' in image.data && typeof image.data.pipe === 'function') {
- // image.dataがstreamなら、stream終了後にcleanup
- image.data.on('end', file.cleanup);
- image.data.on('close', file.cleanup);
- } else {
- // image.dataがstreamでないなら直ちにcleanup
- file.cleanup();
- }
- }
-
- reply.header('Content-Type', image.type);
- reply.header('Cache-Control', 'max-age=31536000, immutable');
- reply.header('Content-Disposition',
- contentDisposition(
- 'inline',
- correctFilename(file.filename, image.ext),
- ),
- );
- return image.data;
- } catch (e) {
- if ('cleanup' in file) file.cleanup();
- throw e;
- }
- }
-
- @bindThis
- private async getStreamAndTypeFromUrl(url: string): Promise<
- { state: 'remote'; fileRole?: 'thumbnail' | 'webpublic' | 'original'; file?: MiDriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; }
- | { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; mime: string; ext: string | null; path: string; }
- | '404'
- | '204'
- > {
- if (url.startsWith(`${this.config.url}/files/`)) {
- const key = url.replace(`${this.config.url}/files/`, '').split('/').shift();
- if (!key) throw new StatusError('Invalid File Key', 400, 'Invalid File Key');
-
- return await this.getFileFromKey(key);
- }
-
- return await this.downloadAndDetectTypeFromUrl(url);
- }
-
- @bindThis
- private async downloadAndDetectTypeFromUrl(url: string): Promise<
- { state: 'remote'; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; }
- > {
- const [path, cleanup] = await createTemp();
- try {
- const { filename } = await this.downloadService.downloadUrl(url, path);
-
- const { mime, ext } = await this.fileInfoService.detectType(path);
-
- return {
- state: 'remote',
- mime, ext,
- path, cleanup,
- filename,
- };
- } catch (e) {
- cleanup();
- throw e;
- }
- }
-
- @bindThis
- private async getFileFromKey(key: string): Promise<
- { state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; url: string; mime: string; ext: string | null; path: string; cleanup: () => void; }
- | { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; mime: string; ext: string | null; path: string; }
- | '404'
- | '204'
- > {
- // Fetch drive file
- const file = await this.driveFilesRepository.createQueryBuilder('file')
- .where('file.accessKey = :accessKey', { accessKey: key })
- .orWhere('file.thumbnailAccessKey = :thumbnailAccessKey', { thumbnailAccessKey: key })
- .orWhere('file.webpublicAccessKey = :webpublicAccessKey', { webpublicAccessKey: key })
- .getOne();
-
- if (file == null) return '404';
-
- const isThumbnail = file.thumbnailAccessKey === key;
- const isWebpublic = file.webpublicAccessKey === key;
-
- if (!file.storedInternal) {
- if (!(file.isLink && file.uri)) return '204';
- const result = await this.downloadAndDetectTypeFromUrl(file.uri);
- file.size = (await fs.promises.stat(result.path)).size; // DB file.sizeは正確とは限らないので
- return {
- ...result,
- url: file.uri,
- fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original',
- file,
- filename: file.name,
- };
- }
-
- const path = this.internalStorageService.resolvePath(key);
-
- if (isThumbnail || isWebpublic) {
- const { mime, ext } = await this.fileInfoService.detectType(path);
- return {
- state: 'stored_internal',
- fileRole: isThumbnail ? 'thumbnail' : 'webpublic',
- file,
- filename: file.name,
- mime, ext,
- path,
- };
- }
-
- return {
- state: 'stored_internal',
- fileRole: 'original',
- file,
- filename: file.name,
- // 古いファイルは修正前のmimeを持っているのでできるだけ修正してあげる
- mime: this.fileInfoService.fixMime(file.type),
- ext: null,
- path,
- };
- }
}
diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts
index 239ef82dec..93c36f5365 100644
--- a/packages/backend/src/server/NodeinfoServerService.ts
+++ b/packages/backend/src/server/NodeinfoServerService.ts
@@ -48,8 +48,6 @@ export class NodeinfoServerService {
@bindThis
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
const nodeinfo2 = async (version: number) => {
- const now = Date.now();
-
const notesChart = await this.notesChart.getChart('hour', 1, null);
const localPosts = notesChart.local.total[0];
diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts
index 0223650329..8259a2a9e4 100644
--- a/packages/backend/src/server/ServerModule.ts
+++ b/packages/backend/src/server/ServerModule.ts
@@ -13,7 +13,6 @@ import { NodeinfoServerService } from './NodeinfoServerService.js';
import { ServerService } from './ServerService.js';
import { WellKnownServerService } from './WellKnownServerService.js';
import { GetterService } from './api/GetterService.js';
-import { ChannelsService } from './api/stream/ChannelsService.js';
import { ActivityPubServerService } from './ActivityPubServerService.js';
import { ApiLoggerService } from './api/ApiLoggerService.js';
import { ApiServerService } from './api/ApiServerService.js';
@@ -25,29 +24,31 @@ import { SignupApiService } from './api/SignupApiService.js';
import { StreamingApiServerService } from './api/StreamingApiServerService.js';
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
import { ClientServerService } from './web/ClientServerService.js';
+import { HtmlTemplateService } from './web/HtmlTemplateService.js';
import { FeedService } from './web/FeedService.js';
import { UrlPreviewService } from './web/UrlPreviewService.js';
import { ClientLoggerService } from './web/ClientLoggerService.js';
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
-import { MainChannelService } from './api/stream/channels/main.js';
-import { AdminChannelService } from './api/stream/channels/admin.js';
-import { AntennaChannelService } from './api/stream/channels/antenna.js';
-import { ChannelChannelService } from './api/stream/channels/channel.js';
-import { DriveChannelService } from './api/stream/channels/drive.js';
-import { GlobalTimelineChannelService } from './api/stream/channels/global-timeline.js';
-import { HashtagChannelService } from './api/stream/channels/hashtag.js';
-import { HomeTimelineChannelService } from './api/stream/channels/home-timeline.js';
-import { HybridTimelineChannelService } from './api/stream/channels/hybrid-timeline.js';
-import { LocalTimelineChannelService } from './api/stream/channels/local-timeline.js';
-import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js';
-import { ServerStatsChannelService } from './api/stream/channels/server-stats.js';
-import { UserListChannelService } from './api/stream/channels/user-list.js';
-import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js';
-import { ChatUserChannelService } from './api/stream/channels/chat-user.js';
-import { ChatRoomChannelService } from './api/stream/channels/chat-room.js';
-import { ReversiChannelService } from './api/stream/channels/reversi.js';
-import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js';
+import MainStreamConnection from '@/server/api/stream/Connection.js';
+import { MainChannel } from './api/stream/channels/main.js';
+import { AdminChannel } from './api/stream/channels/admin.js';
+import { AntennaChannel } from './api/stream/channels/antenna.js';
+import { ChannelChannel } from './api/stream/channels/channel.js';
+import { DriveChannel } from './api/stream/channels/drive.js';
+import { GlobalTimelineChannel } from './api/stream/channels/global-timeline.js';
+import { HashtagChannel } from './api/stream/channels/hashtag.js';
+import { HomeTimelineChannel } from './api/stream/channels/home-timeline.js';
+import { HybridTimelineChannel } from './api/stream/channels/hybrid-timeline.js';
+import { LocalTimelineChannel } from './api/stream/channels/local-timeline.js';
+import { QueueStatsChannel } from './api/stream/channels/queue-stats.js';
+import { ServerStatsChannel } from './api/stream/channels/server-stats.js';
+import { UserListChannel } from './api/stream/channels/user-list.js';
+import { RoleTimelineChannel } from './api/stream/channels/role-timeline.js';
+import { ChatUserChannel } from './api/stream/channels/chat-user.js';
+import { ChatRoomChannel } from './api/stream/channels/chat-room.js';
+import { ReversiChannel } from './api/stream/channels/reversi.js';
+import { ReversiGameChannel } from './api/stream/channels/reversi-game.js';
import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
@Module({
@@ -58,6 +59,7 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
providers: [
ClientServerService,
ClientLoggerService,
+ HtmlTemplateService,
FeedService,
HealthServerService,
UrlPreviewService,
@@ -67,7 +69,7 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
ServerService,
WellKnownServerService,
GetterService,
- ChannelsService,
+ MainStreamConnection,
ApiCallService,
ApiLoggerService,
ApiServerService,
@@ -78,24 +80,24 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
SigninService,
SignupApiService,
StreamingApiServerService,
- MainChannelService,
- AdminChannelService,
- AntennaChannelService,
- ChannelChannelService,
- DriveChannelService,
- GlobalTimelineChannelService,
- HashtagChannelService,
- RoleTimelineChannelService,
- ChatUserChannelService,
- ChatRoomChannelService,
- ReversiChannelService,
- ReversiGameChannelService,
- HomeTimelineChannelService,
- HybridTimelineChannelService,
- LocalTimelineChannelService,
- QueueStatsChannelService,
- ServerStatsChannelService,
- UserListChannelService,
+ MainChannel,
+ AdminChannel,
+ AntennaChannel,
+ ChannelChannel,
+ DriveChannel,
+ GlobalTimelineChannel,
+ HashtagChannel,
+ RoleTimelineChannel,
+ ChatUserChannel,
+ ChatRoomChannel,
+ ReversiChannel,
+ ReversiGameChannel,
+ HomeTimelineChannel,
+ HybridTimelineChannel,
+ LocalTimelineChannel,
+ QueueStatsChannel,
+ ServerStatsChannel,
+ UserListChannel,
OpenApiServerService,
OAuth2ProviderService,
],
diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts
index 23c085ee27..ef9ac81f95 100644
--- a/packages/backend/src/server/ServerService.ts
+++ b/packages/backend/src/server/ServerService.ts
@@ -75,7 +75,7 @@ export class ServerService implements OnApplicationShutdown {
@bindThis
public async launch(): Promise {
const fastify = Fastify({
- trustProxy: true,
+ trustProxy: this.config.trustProxy,
logger: false,
});
this.#fastify = fastify;
@@ -238,30 +238,6 @@ export class ServerService implements OnApplicationShutdown {
}
});
- fastify.get<{ Params: { code: string } }>('/verify-email/:code', async (request, reply) => {
- const profile = await this.userProfilesRepository.findOneBy({
- emailVerifyCode: request.params.code,
- });
-
- if (profile != null) {
- await this.userProfilesRepository.update({ userId: profile.userId }, {
- emailVerified: true,
- emailVerifyCode: null,
- });
-
- this.globalEventService.publishMainStream(profile.userId, 'meUpdated', await this.userEntityService.pack(profile.userId, { id: profile.userId }, {
- schema: 'MeDetailed',
- includeSecrets: true,
- }));
-
- reply.code(200).send('Verification succeeded! メールアドレスの認証に成功しました。');
- return;
- } else {
- reply.code(404).send('Verification failed. Please try again. メールアドレスの認証に失敗しました。もう一度お試しください');
- return;
- }
- });
-
fastify.register(this.clientServerService.createServer);
this.streamingApiServerService.attach(fastify.server);
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index 7a4af407a3..0ccb3df631 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -7,7 +7,6 @@ import { randomUUID } from 'node:crypto';
import * as fs from 'node:fs';
import * as stream from 'node:stream/promises';
import { Inject, Injectable } from '@nestjs/common';
-import * as Sentry from '@sentry/node';
import { DI } from '@/di-symbols.js';
import { getIpHash } from '@/misc/get-ip-hash.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
@@ -37,6 +36,7 @@ export class ApiCallService implements OnApplicationShutdown {
private logger: Logger;
private userIpHistories: Map>;
private userIpHistoriesClearIntervalId: NodeJS.Timeout;
+ private Sentry: typeof import('@sentry/node') | null = null;
constructor(
@Inject(DI.meta)
@@ -59,6 +59,12 @@ export class ApiCallService implements OnApplicationShutdown {
this.userIpHistoriesClearIntervalId = setInterval(() => {
this.userIpHistories.clear();
}, 1000 * 60 * 60);
+
+ if (this.config.sentryForBackend) {
+ import('@sentry/node').then((Sentry) => {
+ this.Sentry = Sentry;
+ });
+ }
}
#sendApiError(reply: FastifyReply, err: ApiError): void {
@@ -120,8 +126,8 @@ export class ApiCallService implements OnApplicationShutdown {
},
});
- if (this.config.sentryForBackend) {
- Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, {
+ if (this.Sentry != null) {
+ this.Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, {
level: 'error',
user: {
id: userId,
@@ -307,11 +313,14 @@ export class ApiCallService implements OnApplicationShutdown {
}
if (ep.meta.limit) {
- // koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
- let limitActor: string;
+ let limitActor: string | null = null;
if (user) {
limitActor = user.id;
- } else {
+ } else if (this.config.enableIpRateLimit) {
+ if (process.env.NODE_ENV === 'production' && (request.ip === '::1' || request.ip === '127.0.0.1')) {
+ this.logger.warn('Recieved API request from localhost IP address for rate limiting in production environment. This is likely due to an improper trustProxy setting in the config file.');
+ }
+
limitActor = getIpHash(request.ip);
}
@@ -324,7 +333,7 @@ export class ApiCallService implements OnApplicationShutdown {
// TODO: 毎リクエスト計算するのもあれだしキャッシュしたい
const factor = user ? (await this.roleService.getUserPolicies(user.id)).rateLimitFactor : 1;
- if (factor > 0) {
+ if (limitActor != null && factor > 0) {
// Rate limit
const rateLimit = await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable }, limitActor, factor);
if (rateLimit != null) {
@@ -417,7 +426,7 @@ export class ApiCallService implements OnApplicationShutdown {
if (['boolean', 'number', 'integer'].includes(param.type ?? '') && typeof data[k] === 'string') {
try {
data[k] = JSON.parse(data[k]);
- } catch (e) {
+ } catch (_) {
throw new ApiError({
message: 'Invalid param.',
code: 'INVALID_PARAM',
@@ -432,8 +441,8 @@ export class ApiCallService implements OnApplicationShutdown {
}
// API invoking
- if (this.config.sentryForBackend) {
- return await Sentry.startSpan({
+ if (this.Sentry != null) {
+ return await this.Sentry.startSpan({
name: 'API: ' + ep.name,
}, () => ep.exec(data, user, token, file, request.ip, request.headers)
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id)));
diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts
index 32818003ad..57d74ef2b1 100644
--- a/packages/backend/src/server/api/ApiServerService.ts
+++ b/packages/backend/src/server/api/ApiServerService.ts
@@ -176,6 +176,17 @@ export class ApiServerService {
}
});
+ fastify.all('/clear-browser-cache', (request, reply) => {
+ if (['GET', 'POST'].includes(request.method)) {
+ reply.header('Clear-Site-Data', '"cache", "prefetchCache", "prerenderCache", "executionContexts"');
+ reply.code(204);
+ reply.send();
+ } else {
+ reply.code(405);
+ reply.send();
+ }
+ });
+
// Make sure any unknown path under /api returns HTTP 404 Not Found,
// because otherwise ClientServerService will return the base client HTML
// page with HTTP 200.
diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts
index 3e889372d8..5c9d16a95a 100644
--- a/packages/backend/src/server/api/SigninApiService.ts
+++ b/packages/backend/src/server/api/SigninApiService.ts
@@ -15,6 +15,7 @@ import type {
UserSecurityKeysRepository,
UsersRepository,
} from '@/models/_.js';
+import type Logger from '@/logger.js';
import type { Config } from '@/config.js';
import { getIpHash } from '@/misc/get-ip-hash.js';
import type { MiLocalUser } from '@/models/User.js';
@@ -23,6 +24,7 @@ import { bindThis } from '@/decorators.js';
import { WebAuthnService } from '@/core/WebAuthnService.js';
import { UserAuthService } from '@/core/UserAuthService.js';
import { CaptchaService } from '@/core/CaptchaService.js';
+import { LoggerService } from '@/core/LoggerService.js';
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { RateLimiterService } from './RateLimiterService.js';
import { SigninService } from './SigninService.js';
@@ -31,6 +33,8 @@ import type { FastifyReply, FastifyRequest } from 'fastify';
@Injectable()
export class SigninApiService {
+ private logger: Logger;
+
constructor(
@Inject(DI.config)
private config: Config,
@@ -50,6 +54,7 @@ export class SigninApiService {
@Inject(DI.signinsRepository)
private signinsRepository: SigninsRepository,
+ private loggerService: LoggerService,
private idService: IdService,
private rateLimiterService: RateLimiterService,
private signinService: SigninService,
@@ -57,6 +62,7 @@ export class SigninApiService {
private webAuthnService: WebAuthnService,
private captchaService: CaptchaService,
) {
+ this.logger = this.loggerService.getLogger('Signin');
}
@bindThis
@@ -90,16 +96,21 @@ export class SigninApiService {
}
// not more than 1 attempt per second and not more than 10 attempts per hour
- const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
- if (rateLimit != null) {
- reply.code(429);
- return {
- error: {
- message: 'Too many failed attempts to sign in. Try again later.',
- code: 'TOO_MANY_AUTHENTICATION_FAILURES',
- id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
- },
- };
+ if (this.config.enableIpRateLimit) {
+ if (process.env.NODE_ENV === 'production' && (request.ip === '::1' || request.ip === '127.0.0.1')) {
+ this.logger.warn('Recieved signin request from localhost IP address for rate limiting in production environment. This is likely due to an improper trustProxy setting in the config file.');
+ }
+ const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
+ if (rateLimit != null) {
+ reply.code(429);
+ return {
+ error: {
+ message: 'Too many failed attempts to sign in. Try again later.',
+ code: 'TOO_MANY_AUTHENTICATION_FAILURES',
+ id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
+ },
+ };
+ }
}
if (typeof username !== 'string') {
@@ -220,7 +231,7 @@ export class SigninApiService {
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
- } catch (e) {
+ } catch (_) {
return await fail(403, {
id: 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f',
});
diff --git a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts
index 9ba23c54e2..6feb4c3afa 100644
--- a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts
+++ b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts
@@ -84,19 +84,25 @@ export class SigninWithPasskeyApiService {
return error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' });
};
- try {
+ if (this.config.enableIpRateLimit) {
+ if (process.env.NODE_ENV === 'production' && (request.ip === '::1' || request.ip === '127.0.0.1')) {
+ this.logger.warn('Recieved signin with passkey request from localhost IP address for rate limiting in production environment. This is likely due to an improper trustProxy setting in the config file.');
+ }
+
+ try {
// Not more than 1 API call per 250ms and not more than 100 attempts per 30min
// NOTE: 1 Sign-in require 2 API calls
- await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 30 * 1000, max: 200, minInterval: 250 }, getIpHash(request.ip));
- } catch (err) {
- reply.code(429);
- return {
- error: {
- message: 'Too many failed attempts to sign in. Try again later.',
- code: 'TOO_MANY_AUTHENTICATION_FAILURES',
- id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
- },
- };
+ await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 30 * 1000, max: 200, minInterval: 250 }, getIpHash(request.ip));
+ } catch (_) {
+ reply.code(429);
+ return {
+ error: {
+ message: 'Too many failed attempts to sign in. Try again later.',
+ code: 'TOO_MANY_AUTHENTICATION_FAILURES',
+ id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
+ },
+ };
+ }
}
// Initiate Passkey Auth challenge with context
diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts
index 53336a087d..b419c51ef1 100644
--- a/packages/backend/src/server/api/SignupApiService.ts
+++ b/packages/backend/src/server/api/SignupApiService.ts
@@ -255,7 +255,7 @@ export class SignupApiService {
throw new FastifyReplyError(400, 'EXPIRED');
}
- const { account, secret } = await this.signupService.signup({
+ const { account } = await this.signupService.signup({
username: pendingUser.username,
passwordHash: pendingUser.password,
});
diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts
index 2a4e1fc574..8a317bdc4e 100644
--- a/packages/backend/src/server/api/StreamingApiServerService.ts
+++ b/packages/backend/src/server/api/StreamingApiServerService.ts
@@ -8,17 +8,14 @@ import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import * as WebSocket from 'ws';
import { DI } from '@/di-symbols.js';
-import type { UsersRepository, MiAccessToken } from '@/models/_.js';
-import { NotificationService } from '@/core/NotificationService.js';
+import type { MiAccessToken } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
-import { CacheService } from '@/core/CacheService.js';
import { MiLocalUser } from '@/models/User.js';
import { UserService } from '@/core/UserService.js';
-import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
-import MainStreamConnection from './stream/Connection.js';
-import { ChannelsService } from './stream/ChannelsService.js';
+import MainStreamConnection, { ConnectionRequest } from './stream/Connection.js';
import type * as http from 'node:http';
+import { ContextIdFactory, ModuleRef } from '@nestjs/core';
@Injectable()
export class StreamingApiServerService {
@@ -30,15 +27,9 @@ export class StreamingApiServerService {
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
- @Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
-
- private cacheService: CacheService,
+ private moduleRef: ModuleRef,
private authenticateService: AuthenticateService,
- private channelsService: ChannelsService,
- private notificationService: NotificationService,
private usersService: UserService,
- private channelFollowingService: ChannelFollowingService,
) {
}
@@ -92,13 +83,12 @@ export class StreamingApiServerService {
return;
}
- const stream = new MainStreamConnection(
- this.channelsService,
- this.notificationService,
- this.cacheService,
- this.channelFollowingService,
- user, app,
- );
+ const contextId = ContextIdFactory.create();
+ this.moduleRef.registerRequestByContextId({
+ user,
+ token: app,
+ }, contextId);
+ const stream = await this.moduleRef.create(MainStreamConnection, contextId);
await stream.init();
@@ -121,7 +111,7 @@ export class StreamingApiServerService {
user: MiLocalUser | null;
app: MiAccessToken | null
}) => {
- const { stream, user, app } = ctx;
+ const { stream, user } = ctx;
const ev = new EventEmitter();
diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts
index c0c43dd5c9..9311c80eaa 100644
--- a/packages/backend/src/server/api/endpoint-list.ts
+++ b/packages/backend/src/server/api/endpoint-list.ts
@@ -143,6 +143,9 @@ export * as 'channels/timeline' from './endpoints/channels/timeline.js';
export * as 'channels/unfavorite' from './endpoints/channels/unfavorite.js';
export * as 'channels/unfollow' from './endpoints/channels/unfollow.js';
export * as 'channels/update' from './endpoints/channels/update.js';
+export * as 'channels/mute/create' from './endpoints/channels/mute/create.js';
+export * as 'channels/mute/delete' from './endpoints/channels/mute/delete.js';
+export * as 'channels/mute/list' from './endpoints/channels/mute/list.js';
export * as 'charts/active-users' from './endpoints/charts/active-users.js';
export * as 'charts/ap-request' from './endpoints/charts/ap-request.js';
export * as 'charts/drive' from './endpoints/charts/drive.js';
@@ -388,6 +391,7 @@ export * as 'users/featured-notes' from './endpoints/users/featured-notes.js';
export * as 'users/flashs' from './endpoints/users/flashs.js';
export * as 'users/followers' from './endpoints/users/followers.js';
export * as 'users/following' from './endpoints/users/following.js';
+export * as 'users/get-following-birthday-users' from './endpoints/users/get-following-birthday-users.js';
export * as 'users/gallery/posts' from './endpoints/users/gallery/posts.js';
export * as 'users/get-frequently-replied-users' from './endpoints/users/get-frequently-replied-users.js';
export * as 'users/lists/create' from './endpoints/users/lists/create.js';
@@ -412,6 +416,7 @@ export * as 'users/search' from './endpoints/users/search.js';
export * as 'users/search-by-username-and-host' from './endpoints/users/search-by-username-and-host.js';
export * as 'users/show' from './endpoints/users/show.js';
export * as 'users/update-memo' from './endpoints/users/update-memo.js';
+export * as 'verify-email' from './endpoints/verify-email.js';
export * as 'chat/messages/create-to-user' from './endpoints/chat/messages/create-to-user.js';
export * as 'chat/messages/create-to-room' from './endpoints/chat/messages/create-to-room.js';
export * as 'chat/messages/delete' from './endpoints/chat/messages/delete.js';
diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
index 06047b58a6..6606202118 100644
--- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
@@ -34,13 +34,22 @@ export const meta = {
res: {
type: 'object',
optional: false, nullable: false,
- ref: 'MeDetailed',
- properties: {
- token: {
- type: 'string',
- optional: false, nullable: false,
+ allOf: [
+ {
+ type: 'object',
+ ref: 'MeDetailed',
},
- },
+ {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ token: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ },
+ }
+ ],
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/admin/ad/create.ts b/packages/backend/src/server/api/endpoints/admin/ad/create.ts
index 955154f4fb..01697ae185 100644
--- a/packages/backend/src/server/api/endpoints/admin/ad/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/ad/create.ts
@@ -36,6 +36,7 @@ export const paramDef = {
startsAt: { type: 'integer' },
imageUrl: { type: 'string', minLength: 1 },
dayOfWeek: { type: 'integer' },
+ isSensitive: { type: 'boolean' },
},
required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'imageUrl', 'dayOfWeek'],
} as const;
@@ -55,6 +56,7 @@ export default class extends Endpoint { // eslint-
expiresAt: new Date(ps.expiresAt),
startsAt: new Date(ps.startsAt),
dayOfWeek: ps.dayOfWeek,
+ isSensitive: ps.isSensitive,
url: ps.url,
imageUrl: ps.imageUrl,
priority: ps.priority,
@@ -73,6 +75,7 @@ export default class extends Endpoint { // eslint-
expiresAt: ad.expiresAt.toISOString(),
startsAt: ad.startsAt.toISOString(),
dayOfWeek: ad.dayOfWeek,
+ isSensitive: ad.isSensitive,
url: ad.url,
imageUrl: ad.imageUrl,
priority: ad.priority,
diff --git a/packages/backend/src/server/api/endpoints/admin/ad/list.ts b/packages/backend/src/server/api/endpoints/admin/ad/list.ts
index 4f897d98e4..f67cad5bd2 100644
--- a/packages/backend/src/server/api/endpoints/admin/ad/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/ad/list.ts
@@ -63,6 +63,7 @@ export default class extends Endpoint { // eslint-
expiresAt: ad.expiresAt.toISOString(),
startsAt: ad.startsAt.toISOString(),
dayOfWeek: ad.dayOfWeek,
+ isSensitive: ad.isSensitive,
url: ad.url,
imageUrl: ad.imageUrl,
memo: ad.memo,
diff --git a/packages/backend/src/server/api/endpoints/admin/ad/update.ts b/packages/backend/src/server/api/endpoints/admin/ad/update.ts
index 4e3d731aca..a3d9aaddc6 100644
--- a/packages/backend/src/server/api/endpoints/admin/ad/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/ad/update.ts
@@ -39,6 +39,7 @@ export const paramDef = {
expiresAt: { type: 'integer' },
startsAt: { type: 'integer' },
dayOfWeek: { type: 'integer' },
+ isSensitive: { type: 'boolean' },
},
required: ['id'],
} as const;
@@ -66,6 +67,7 @@ export default class extends Endpoint { // eslint-
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : undefined,
startsAt: ps.startsAt ? new Date(ps.startsAt) : undefined,
dayOfWeek: ps.dayOfWeek,
+ isSensitive: ps.isSensitive,
});
const updatedAd = await this.adsRepository.findOneByOrFail({ id: ad.id });
diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts
index b8bfda73a4..74462b302a 100644
--- a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts
@@ -72,7 +72,7 @@ export default class extends Endpoint { // eslint-
private announcementService: AnnouncementService,
) {
super(meta, paramDef, async (ps, me) => {
- const { raw, packed } = await this.announcementService.create({
+ const { packed } = await this.announcementService.create({
updatedAt: null,
title: ps.title,
text: ps.text,
diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts
index 81a788de2b..aeebceed5a 100644
--- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts
@@ -49,6 +49,36 @@ export const meta = {
type: 'string',
optional: false, nullable: false,
},
+ icon: {
+ type: 'string',
+ optional: false, nullable: false,
+ enum: ['info', 'warning', 'error', 'success'],
+ },
+ display: {
+ type: 'string',
+ optional: false, nullable: false,
+ enum: ['normal', 'banner', 'dialog'],
+ },
+ isActive: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ forExistingUsers: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ silence: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ needConfirmationToRead: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ userId: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
imageUrl: {
type: 'string',
optional: false, nullable: true,
diff --git a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
index b84a5c73f9..e7a70d0762 100644
--- a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
+++ b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
@@ -157,6 +157,22 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
+ maybeSensitive: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ maybePorn: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ requestIp: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
+ requestHeaders: {
+ type: 'object',
+ optional: false, nullable: true,
+ },
},
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
index cf03859ce5..d4305e7d7c 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
@@ -76,7 +76,7 @@ export default class extends Endpoint { // eslint-
try {
// Create file
driveFile = await this.driveService.uploadFromUrl({ url: emoji.originalUrl, user: null, force: true });
- } catch (e) {
+ } catch (_) {
// TODO: need to return Drive Error
throw new ApiError();
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
index 660aa55bf8..b9448b4bc2 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
@@ -24,39 +24,7 @@ export const meta = {
optional: false, nullable: false,
items: {
type: 'object',
- optional: false, nullable: false,
- properties: {
- id: {
- type: 'string',
- optional: false, nullable: false,
- format: 'id',
- },
- aliases: {
- type: 'array',
- optional: false, nullable: false,
- items: {
- type: 'string',
- optional: false, nullable: false,
- },
- },
- name: {
- type: 'string',
- optional: false, nullable: false,
- },
- category: {
- type: 'string',
- optional: false, nullable: true,
- },
- host: {
- type: 'string',
- optional: false, nullable: true,
- description: 'The local host is represented with `null`.',
- },
- url: {
- type: 'string',
- optional: false, nullable: false,
- },
- },
+ ref: 'EmojiDetailed',
},
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
index 34d200455e..658367409c 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
@@ -24,39 +24,7 @@ export const meta = {
optional: false, nullable: false,
items: {
type: 'object',
- optional: false, nullable: false,
- properties: {
- id: {
- type: 'string',
- optional: false, nullable: false,
- format: 'id',
- },
- aliases: {
- type: 'array',
- optional: false, nullable: false,
- items: {
- type: 'string',
- optional: false, nullable: false,
- },
- },
- name: {
- type: 'string',
- optional: false, nullable: false,
- },
- category: {
- type: 'string',
- optional: false, nullable: true,
- },
- host: {
- type: 'string',
- optional: false, nullable: true,
- description: 'The local host is represented with `null`. The field exists for compatibility with other API endpoints that return files.',
- },
- url: {
- type: 'string',
- optional: false, nullable: false,
- },
- },
+ ref: 'EmojiDetailed',
},
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
index 7bde10af46..e20bc21f6b 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
@@ -117,7 +117,7 @@ export default class extends Endpoint { // eslint-
case 'SAME_NAME_EMOJI_EXISTS': throw new ApiError(meta.errors.sameNameEmojiExists);
}
// 網羅性チェック
- const mustBeNever: never = error;
+ const _mustBeNever: never = error;
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index 6ec908d5bf..5beed3a7e8 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -223,10 +223,12 @@ export const meta = {
sensitiveMediaDetection: {
type: 'string',
optional: false, nullable: false,
+ enum: ['none', 'all', 'local', 'remote'],
},
sensitiveMediaDetectionSensitivity: {
type: 'string',
optional: false, nullable: false,
+ enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'],
},
setSensitiveFlagAutomatically: {
type: 'boolean',
@@ -426,8 +428,7 @@ export const meta = {
optional: false, nullable: true,
},
clientOptions: {
- type: 'object',
- optional: false, nullable: false,
+ ref: 'MetaClientOptions',
},
description: {
type: 'string',
@@ -473,6 +474,10 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
+ feedbackUrl: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
summalyProxy: {
type: 'string',
optional: false, nullable: true,
@@ -587,6 +592,10 @@ export const meta = {
type: 'number',
optional: false, nullable: false,
},
+ showRoleBadgesOfRemoteUsers: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
},
},
} as const;
@@ -742,6 +751,7 @@ export default class extends Endpoint { // eslint-
enableRemoteNotesCleaning: instance.enableRemoteNotesCleaning,
remoteNotesCleaningExpiryDaysForEachNotes: instance.remoteNotesCleaningExpiryDaysForEachNotes,
remoteNotesCleaningMaxProcessingDurationInMinutes: instance.remoteNotesCleaningMaxProcessingDurationInMinutes,
+ showRoleBadgesOfRemoteUsers: instance.showRoleBadgesOfRemoteUsers,
};
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts
index f3e440b4cb..86158d7e22 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts
@@ -52,18 +52,14 @@ export default class extends Endpoint { // eslint-
super(meta, paramDef, async (ps, me) => {
const jobs = await this.deliverQueue.getJobs(['delayed']);
- const res = [] as [string, number][];
+ const counts = new Map();
for (const job of jobs) {
const host = new URL(job.data.to).host;
- if (res.find(x => x[0] === host)) {
- res.find(x => x[0] === host)![1]++;
- } else {
- res.push([host, 1]);
- }
+ counts.set(host, (counts.get(host) ?? 0) + 1);
}
- res.sort((a, b) => b[1] - a[1]);
+ const res = [...counts.entries()].sort((a, b) => b[1] - a[1]);
return res;
});
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts
index e7589cba81..ad6a823b8f 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts
@@ -52,18 +52,14 @@ export default class extends Endpoint { // eslint-
super(meta, paramDef, async (ps, me) => {
const jobs = await this.inboxQueue.getJobs(['delayed']);
- const res = [] as [string, number][];
+ const counts = new Map();
for (const job of jobs) {
const host = new URL(job.data.signature.keyId).host;
- if (res.find(x => x[0] === host)) {
- res.find(x => x[0] === host)![1]++;
- } else {
- res.push([host, 1]);
- }
+ counts.set(host, (counts.get(host) ?? 0) + 1);
}
- res.sort((a, b) => b[1] - a[1]);
+ const res = [...counts.entries()].sort((a, b) => b[1] - a[1]);
return res;
});
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts
index d7f9e4eaa3..b69699c338 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts
@@ -5,7 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js';
+import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, PostScheduledNoteQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js';
export const meta = {
tags: ['admin'],
@@ -49,6 +49,7 @@ export default class extends Endpoint { // eslint-
constructor(
@Inject('queue:system') public systemQueue: SystemQueue,
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
+ @Inject('queue:postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue,
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,
diff --git a/packages/backend/src/server/api/endpoints/admin/server-info.ts b/packages/backend/src/server/api/endpoints/admin/server-info.ts
index 80b6a4d32e..603be514c8 100644
--- a/packages/backend/src/server/api/endpoints/admin/server-info.ts
+++ b/packages/backend/src/server/api/endpoints/admin/server-info.ts
@@ -4,7 +4,6 @@
*/
import * as os from 'node:os';
-import si from 'systeminformation';
import { Inject, Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import * as Redis from 'ioredis';
@@ -112,6 +111,8 @@ export default class extends Endpoint { // eslint-
) {
super(meta, paramDef, async () => {
+ const si = await import('systeminformation');
+
const memStats = await si.mem();
const fsStats = await si.fsSize();
const netInterface = await si.networkInterfaceDefault();
diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts
index 1ba6853dbe..2fd7ab8ca2 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts
@@ -103,6 +103,8 @@ export const meta = {
quote: { optional: true, ...notificationRecieveConfig },
reaction: { optional: true, ...notificationRecieveConfig },
pollEnded: { optional: true, ...notificationRecieveConfig },
+ scheduledNotePosted: { optional: true, ...notificationRecieveConfig },
+ scheduledNotePostFailed: { optional: true, ...notificationRecieveConfig },
receiveFollowRequest: { optional: true, ...notificationRecieveConfig },
followRequestAccepted: { optional: true, ...notificationRecieveConfig },
roleAssigned: { optional: true, ...notificationRecieveConfig },
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index a1a2a99d6e..7a8dfc4555 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -3,7 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Injectable } from '@nestjs/common';
+import { Injectable, Inject } from '@nestjs/common';
+import { DI } from '@/di-symbols.js';
import type { MiMeta } from '@/models/Meta.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
@@ -67,7 +68,14 @@ export const paramDef = {
description: { type: 'string', nullable: true },
defaultLightTheme: { type: 'string', nullable: true },
defaultDarkTheme: { type: 'string', nullable: true },
- clientOptions: { type: 'object', nullable: false },
+ clientOptions: {
+ type: 'object', nullable: false,
+ properties: {
+ entrancePageStyle: { type: 'string', nullable: false, enum: ['classic', 'simple'] },
+ showTimelineForVisitor: { type: 'boolean', nullable: false },
+ showActivitiesForVisitor: { type: 'boolean', nullable: false },
+ },
+ },
cacheRemoteFiles: { type: 'boolean' },
cacheRemoteSensitiveFiles: { type: 'boolean' },
emailRequiredForSignup: { type: 'boolean' },
@@ -209,6 +217,7 @@ export const paramDef = {
enableRemoteNotesCleaning: { type: 'boolean' },
remoteNotesCleaningExpiryDaysForEachNotes: { type: 'number' },
remoteNotesCleaningMaxProcessingDurationInMinutes: { type: 'number' },
+ showRoleBadgesOfRemoteUsers: { type: 'boolean' },
},
required: [],
} as const;
@@ -216,6 +225,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
private metaService: MetaService,
private moderationLogService: ModerationLogService,
) {
@@ -328,7 +340,10 @@ export default class extends Endpoint { // eslint-
}
if (ps.clientOptions !== undefined) {
- set.clientOptions = ps.clientOptions;
+ set.clientOptions = {
+ ...serverSettings.clientOptions,
+ ...ps.clientOptions,
+ };
}
if (ps.cacheRemoteFiles !== undefined) {
@@ -743,6 +758,10 @@ export default class extends Endpoint { // eslint-
set.remoteNotesCleaningMaxProcessingDurationInMinutes = ps.remoteNotesCleaningMaxProcessingDurationInMinutes;
}
+ if (ps.showRoleBadgesOfRemoteUsers !== undefined) {
+ set.showRoleBadgesOfRemoteUsers = ps.showRoleBadgesOfRemoteUsers;
+ }
+
const before = await this.metaService.fetch(true);
await this.metaService.update(set);
diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts
index b2d9cea03c..c59479d370 100644
--- a/packages/backend/src/server/api/endpoints/antennas/notes.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts
@@ -5,6 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
+import { Brackets } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { NotesRepository, AntennasRepository } from '@/models/_.js';
import { QueryService } from '@/core/QueryService.js';
@@ -14,6 +15,7 @@ import { IdService } from '@/core/IdService.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { trackPromise } from '@/misc/promise-tracker.js';
+import { ChannelMutingService } from '@/core/ChannelMutingService.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -69,6 +71,7 @@ export default class extends Endpoint { // eslint-
private queryService: QueryService,
private fanoutTimelineService: FanoutTimelineService,
private globalEventService: GlobalEventService,
+ private channelMutingService: ChannelMutingService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@@ -108,6 +111,21 @@ export default class extends Endpoint { // eslint-
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
+ // -- ミュートされたチャンネル対策
+ const mutingChannelIds = await this.channelMutingService
+ .list({ requestUserId: me.id }, { idOnly: true })
+ .then(x => x.map(x => x.id));
+ if (mutingChannelIds.length > 0) {
+ query.andWhere(new Brackets(qb => {
+ qb.orWhere('note.channelId IS NULL');
+ qb.orWhere('note.channelId NOT IN (:...mutingChannelIds)', { mutingChannelIds });
+ }));
+ query.andWhere(new Brackets(qb => {
+ qb.orWhere('note.renoteChannelId IS NULL');
+ qb.orWhere('note.renoteChannelId NOT IN (:...mutingChannelIds)', { mutingChannelIds });
+ }));
+ }
+
// NOTE: センシティブ除外の設定はこのエンドポイントでは無視する。
// https://github.com/misskey-dev/misskey/pull/15346#discussion_r1929950255
diff --git a/packages/backend/src/server/api/endpoints/ap/get.ts b/packages/backend/src/server/api/endpoints/ap/get.ts
index 14286bc23e..ff03fce72b 100644
--- a/packages/backend/src/server/api/endpoints/ap/get.ts
+++ b/packages/backend/src/server/api/endpoints/ap/get.ts
@@ -43,7 +43,7 @@ export default class extends Endpoint { // eslint-
private apResolverService: ApResolverService,
) {
super(meta, paramDef, async (ps, me) => {
- const resolver = this.apResolverService.createResolver();
+ const resolver = await this.apResolverService.createResolver();
const object = await resolver.resolve(ps.uri);
return object;
});
diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts
index 4afed7dc5c..47da6b4fbd 100644
--- a/packages/backend/src/server/api/endpoints/ap/show.ts
+++ b/packages/backend/src/server/api/endpoints/ap/show.ts
@@ -18,9 +18,9 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
-import { ApiError } from '../../error.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
+import { ApiError } from '../../error.js';
export const meta = {
tags: ['federation'],
@@ -148,7 +148,7 @@ export default class extends Endpoint { // eslint-
if (this.utilityService.isSelfHost(host)) return null;
// リモートから一旦オブジェクトフェッチ
- const resolver = this.apResolverService.createResolver();
+ const resolver = await this.apResolverService.createResolver();
// allow ap/show exclusively to lookup URLs that are cross-origin or non-canonical (like https://alice.example.com/@bob@bob.example.com -> https://bob.example.com/@bob)
const object = await resolver.resolve(uri, FetchAllowSoftFailMask.CrossOrigin | FetchAllowSoftFailMask.NonCanonicalId).catch((err) => {
if (err instanceof IdentifiableError) {
@@ -215,7 +215,7 @@ export default class extends Endpoint { // eslint-
type: 'Note',
object,
};
- } catch (e) {
+ } catch (_) {
return null;
}
}
diff --git a/packages/backend/src/server/api/endpoints/channels/create.ts b/packages/backend/src/server/api/endpoints/channels/create.ts
index e3a6d2d670..8d49b6fd0f 100644
--- a/packages/backend/src/server/api/endpoints/channels/create.ts
+++ b/packages/backend/src/server/api/endpoints/channels/create.ts
@@ -46,7 +46,7 @@ export const paramDef = {
type: 'object',
properties: {
name: { type: 'string', minLength: 1, maxLength: 128 },
- description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
+ description: { type: 'string', nullable: true, maxLength: 2048 },
bannerId: { type: 'string', format: 'misskey:id', nullable: true },
color: { type: 'string', minLength: 1, maxLength: 16 },
isSensitive: { type: 'boolean', nullable: true },
diff --git a/packages/backend/src/server/api/endpoints/channels/mute/create.ts b/packages/backend/src/server/api/endpoints/channels/mute/create.ts
new file mode 100644
index 0000000000..26ce707c7a
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/channels/mute/create.ts
@@ -0,0 +1,90 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { ChannelsRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { ChannelMutingService } from '@/core/ChannelMutingService.js';
+
+export const meta = {
+ tags: ['channels', 'mute'],
+
+ requireCredential: true,
+ prohibitMoved: true,
+
+ kind: 'write:channels',
+
+ errors: {
+ noSuchChannel: {
+ message: 'No such Channel.',
+ code: 'NO_SUCH_CHANNEL',
+ id: '7174361e-d58f-31d6-2e7c-6fb830786a3f',
+ },
+
+ alreadyMuting: {
+ message: 'You are already muting that user.',
+ code: 'ALREADY_MUTING_CHANNEL',
+ id: '5a251978-769a-da44-3e89-3931e43bb592',
+ },
+
+ expiresAtIsPast: {
+ message: 'Cannot set past date to "expiresAt".',
+ code: 'EXPIRES_AT_IS_PAST',
+ id: '42b32236-df2c-a45f-fdbf-def67268f749',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ channelId: { type: 'string', format: 'misskey:id' },
+ expiresAt: {
+ type: 'integer',
+ nullable: true,
+ description: 'A Unix Epoch timestamp that must lie in the future. `null` means an indefinite mute.',
+ },
+ },
+ required: ['channelId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.channelsRepository)
+ private channelsRepository: ChannelsRepository,
+ private channelMutingService: ChannelMutingService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ // Check if exists the channel
+ const targetChannel = await this.channelsRepository.findOneBy({ id: ps.channelId });
+ if (!targetChannel) {
+ throw new ApiError(meta.errors.noSuchChannel);
+ }
+
+ // Check if already muting
+ const exist = await this.channelMutingService.isMuted({
+ requestUserId: me.id,
+ targetChannelId: targetChannel.id,
+ });
+ if (exist) {
+ throw new ApiError(meta.errors.alreadyMuting);
+ }
+
+ // Check if expiresAt is past
+ if (ps.expiresAt && ps.expiresAt <= Date.now()) {
+ throw new ApiError(meta.errors.expiresAtIsPast);
+ }
+
+ await this.channelMutingService.mute({
+ requestUserId: me.id,
+ targetChannelId: targetChannel.id,
+ expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null,
+ });
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/channels/mute/delete.ts b/packages/backend/src/server/api/endpoints/channels/mute/delete.ts
new file mode 100644
index 0000000000..79abeebe99
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/channels/mute/delete.ts
@@ -0,0 +1,73 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { ChannelsRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { ChannelMutingService } from '@/core/ChannelMutingService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+ tags: ['channels', 'mute'],
+
+ requireCredential: true,
+ prohibitMoved: true,
+
+ kind: 'write:channels',
+
+ errors: {
+ noSuchChannel: {
+ message: 'No such Channel.',
+ code: 'NO_SUCH_CHANNEL',
+ id: 'e7998769-6e94-d9c2-6b8f-94a527314aba',
+ },
+
+ notMuting: {
+ message: 'You are not muting that channel.',
+ code: 'NOT_MUTING_CHANNEL',
+ id: '14d55962-6ea8-d990-1333-d6bef78dc2ab',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ channelId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['channelId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.channelsRepository)
+ private channelsRepository: ChannelsRepository,
+ private channelMutingService: ChannelMutingService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ // Check if exists the channel
+ const targetChannel = await this.channelsRepository.findOneBy({ id: ps.channelId });
+ if (!targetChannel) {
+ throw new ApiError(meta.errors.noSuchChannel);
+ }
+
+ // Check muting
+ const exist = await this.channelMutingService.isMuted({
+ requestUserId: me.id,
+ targetChannelId: targetChannel.id,
+ });
+ if (!exist) {
+ throw new ApiError(meta.errors.notMuting);
+ }
+
+ await this.channelMutingService.unmute({
+ requestUserId: me.id,
+ targetChannelId: targetChannel.id,
+ });
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/channels/mute/list.ts b/packages/backend/src/server/api/endpoints/channels/mute/list.ts
new file mode 100644
index 0000000000..74338eea38
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/channels/mute/list.ts
@@ -0,0 +1,49 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { ChannelMutingService } from '@/core/ChannelMutingService.js';
+import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
+
+export const meta = {
+ tags: ['channels', 'mute'],
+
+ requireCredential: true,
+ prohibitMoved: true,
+
+ kind: 'read:channels',
+
+ res: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'Channel',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {},
+ required: [],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private channelMutingService: ChannelMutingService,
+ private channelEntityService: ChannelEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const mutings = await this.channelMutingService.list({
+ requestUserId: me.id,
+ });
+ return await this.channelEntityService.packMany(mutings, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts
index 46b050d4b4..4f56bc2110 100644
--- a/packages/backend/src/server/api/endpoints/channels/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts
@@ -13,6 +13,7 @@ import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { MiLocalUser } from '@/models/User.js';
+import { ChannelMutingService } from '@/core/ChannelMutingService.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -70,6 +71,7 @@ export default class extends Endpoint { // eslint-
private queryService: QueryService,
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
private activeUsersChart: ActiveUsersChart,
+ private channelMutingService: ChannelMutingService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@@ -98,6 +100,7 @@ export default class extends Endpoint { // eslint-
useDbFallback: true,
redisTimelines: [`channelTimeline:${channel.id}`],
excludePureRenotes: false,
+ ignoreAuthorChannelFromMute: true,
dbFallback: async (untilId, sinceId, limit) => {
return await this.getFromDb({ untilId, sinceId, limit, channelId: channel.id }, me);
},
@@ -122,6 +125,16 @@ export default class extends Endpoint { // eslint-
.leftJoinAndSelect('note.channel', 'channel');
this.queryService.generateBaseNoteFilteringQuery(query, me);
+
+ if (me) {
+ const mutingChannelIds = await this.channelMutingService
+ .list({ requestUserId: me.id }, { idOnly: true })
+ .then(x => x.map(x => x.id).filter(x => x !== ps.channelId));
+ if (mutingChannelIds.length > 0) {
+ query.andWhere('note.channelId NOT IN (:...mutingChannelIds)', { mutingChannelIds });
+ query.andWhere('note.renoteChannelId NOT IN (:...mutingChannelIds)', { mutingChannelIds });
+ }
+ }
//#endregion
return await query.limit(ps.limit).getMany();
diff --git a/packages/backend/src/server/api/endpoints/channels/update.ts b/packages/backend/src/server/api/endpoints/channels/update.ts
index dba2938b39..5ec55896e4 100644
--- a/packages/backend/src/server/api/endpoints/channels/update.ts
+++ b/packages/backend/src/server/api/endpoints/channels/update.ts
@@ -50,7 +50,7 @@ export const paramDef = {
properties: {
channelId: { type: 'string', format: 'misskey:id' },
name: { type: 'string', minLength: 1, maxLength: 128 },
- description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
+ description: { type: 'string', nullable: true, maxLength: 2048 },
bannerId: { type: 'string', format: 'misskey:id', nullable: true },
isArchived: { type: 'boolean', nullable: true },
pinnedNoteIds: {
diff --git a/packages/backend/src/server/api/endpoints/clips/list.ts b/packages/backend/src/server/api/endpoints/clips/list.ts
index 2e4a3ff820..af20ea9f8d 100644
--- a/packages/backend/src/server/api/endpoints/clips/list.ts
+++ b/packages/backend/src/server/api/endpoints/clips/list.ts
@@ -5,6 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
+import { QueryService } from '@/core/QueryService.js';
import type { ClipsRepository } from '@/models/_.js';
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
import { DI } from '@/di-symbols.js';
@@ -29,7 +30,13 @@ export const meta = {
export const paramDef = {
type: 'object',
- properties: {},
+ properties: {
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ sinceId: { type: 'string', format: 'misskey:id' },
+ untilId: { type: 'string', format: 'misskey:id' },
+ sinceDate: { type: 'integer' },
+ untilDate: { type: 'integer' },
+ },
required: [],
} as const;
@@ -39,12 +46,14 @@ export default class extends Endpoint { // eslint-
@Inject(DI.clipsRepository)
private clipsRepository: ClipsRepository,
+ private queryService: QueryService,
private clipEntityService: ClipEntityService,
) {
super(meta, paramDef, async (ps, me) => {
- const clips = await this.clipsRepository.findBy({
- userId: me.id,
- });
+ const query = this.queryService.makePaginationQuery(this.clipsRepository.createQueryBuilder('clip'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
+ .andWhere('clip.userId = :userId', { userId: me.id });
+
+ const clips = await query.limit(ps.limit).getMany();
return await this.clipEntityService.packMany(clips, me);
});
diff --git a/packages/backend/src/server/api/endpoints/flash/update.ts b/packages/backend/src/server/api/endpoints/flash/update.ts
index e378669f0a..8696c6f6e8 100644
--- a/packages/backend/src/server/api/endpoints/flash/update.ts
+++ b/packages/backend/src/server/api/endpoints/flash/update.ts
@@ -73,8 +73,8 @@ export default class extends Endpoint { // eslint-
updatedAt: new Date(),
...Object.fromEntries(
Object.entries(ps).filter(
- ([key, val]) => (key !== 'flashId') && Object.hasOwn(paramDef.properties, key)
- )
+ ([key, val]) => (key !== 'flashId') && Object.hasOwn(paramDef.properties, key),
+ ),
),
});
});
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
index 65eece5b97..8dc5cafb56 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
@@ -81,7 +81,7 @@ export default class extends Endpoint {
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
- } catch (e) {
+ } catch (_) {
throw new Error('authentication failed');
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
index 9391aee5e0..050dbaf49e 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
@@ -212,7 +212,7 @@ export default class extends Endpoint {
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
- } catch (e) {
+ } catch (_) {
throw new Error('authentication failed');
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
index a54c598213..b6c837eda7 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
@@ -72,7 +72,7 @@ export default class extends Endpoint { // eslint-
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
- } catch (e) {
+ } catch (_) {
throw new Error('authentication failed');
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
index c350136eae..6e5d9943de 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
@@ -61,7 +61,7 @@ export default class extends Endpoint { // eslint-
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
- } catch (e) {
+ } catch (_) {
throw new Error('authentication failed');
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
index b5a53cc889..23b577dc18 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
@@ -57,7 +57,7 @@ export default class extends Endpoint { // eslint-
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
- } catch (e) {
+ } catch (_) {
throw new Error('authentication failed');
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/apps.ts b/packages/backend/src/server/api/endpoints/i/apps.ts
index 055b5cc061..523d81ac73 100644
--- a/packages/backend/src/server/api/endpoints/i/apps.ts
+++ b/packages/backend/src/server/api/endpoints/i/apps.ts
@@ -46,6 +46,14 @@ export const meta = {
type: 'string',
},
},
+ iconUrl: {
+ type: 'string',
+ optional: true, nullable: true,
+ },
+ description: {
+ type: 'string',
+ optional: true, nullable: true,
+ },
},
},
},
@@ -88,6 +96,8 @@ export default class extends Endpoint { // eslint-
createdAt: this.idService.parse(token.id).date.toISOString(),
lastUsedAt: token.lastUsedAt?.toISOString(),
permission: token.app ? token.app.permission : token.permission,
+ iconUrl: token.iconUrl,
+ description: token.description ?? token.app?.description ?? null,
})));
});
}
diff --git a/packages/backend/src/server/api/endpoints/i/change-password.ts b/packages/backend/src/server/api/endpoints/i/change-password.ts
index bb78d47149..19ea187ee8 100644
--- a/packages/backend/src/server/api/endpoints/i/change-password.ts
+++ b/packages/backend/src/server/api/endpoints/i/change-password.ts
@@ -45,7 +45,7 @@ export default class extends Endpoint { // eslint-
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
- } catch (e) {
+ } catch (_) {
throw new Error('authentication failed');
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts
index bfa0b4605d..42324c7778 100644
--- a/packages/backend/src/server/api/endpoints/i/delete-account.ts
+++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts
@@ -49,7 +49,7 @@ export default class extends Endpoint { // eslint-
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
- } catch (e) {
+ } catch (_) {
throw new Error('authentication failed');
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts
index f933eaab00..4fe39bb8e8 100644
--- a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts
+++ b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts
@@ -71,7 +71,6 @@ export default class extends Endpoint { // eslint-
private notificationService: NotificationService,
) {
super(meta, paramDef, async (ps, me) => {
- const EXTRA_LIMIT = 100;
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : undefined);
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : undefined);
diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts
index da1faee30d..c2f4281f36 100644
--- a/packages/backend/src/server/api/endpoints/i/update-email.ts
+++ b/packages/backend/src/server/api/endpoints/i/update-email.ts
@@ -91,7 +91,7 @@ export default class extends Endpoint { // eslint-
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
- } catch (e) {
+ } catch (_) {
throw new Error('authentication failed');
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 082d97f5d4..5207d9f2b0 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -7,7 +7,7 @@ import RE2 from 're2';
import * as mfm from 'mfm-js';
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
-import { JSDOM } from 'jsdom';
+import * as htmlParser from 'node-html-parser';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
import { extractHashtags } from '@/misc/extract-hashtags.js';
import * as Acct from '@/misc/acct.js';
@@ -209,6 +209,8 @@ export const paramDef = {
quote: notificationRecieveConfig,
reaction: notificationRecieveConfig,
pollEnded: notificationRecieveConfig,
+ scheduledNotePosted: notificationRecieveConfig,
+ scheduledNotePostFailed: notificationRecieveConfig,
receiveFollowRequest: notificationRecieveConfig,
followRequestAccepted: notificationRecieveConfig,
roleAssigned: notificationRecieveConfig,
@@ -293,8 +295,20 @@ export default class extends Endpoint { // eslint-
if (ps.chatScope !== undefined) updates.chatScope = ps.chatScope;
function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) {
- // TODO: ちゃんと数える
- const length = JSON.stringify(mutedWords).length;
+ const count = (arr: (string[] | string)[]) => {
+ let length = 0;
+ for (const item of arr) {
+ if (typeof item === 'string') {
+ length += item.length;
+ } else if (Array.isArray(item)) {
+ for (const subItem of item) {
+ length += subItem.length;
+ }
+ }
+ }
+ return length;
+ };
+ const length = count(mutedWords);
if (length > limit) {
throw new ApiError(meta.errors.tooManyMutedWords);
}
@@ -309,7 +323,7 @@ export default class extends Endpoint