Compare commits
	
		
			7 Commits
		
	
	
		
			2024.9.0-a
			...
			multiple-r
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					0ddd4bc545 | ||
| 
						 | 
					cb6a1c773e | ||
| 
						 | 
					9df887ba93 | ||
| 
						 | 
					a2769d0733 | ||
| 
						 | 
					036f90133c | ||
| 
						 | 
					f9bfff604d | ||
| 
						 | 
					fd0e840138 | 
@@ -1,211 +0,0 @@
 | 
			
		||||
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 | 
			
		||||
# Misskey configuration
 | 
			
		||||
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 | 
			
		||||
 | 
			
		||||
#   ┌─────┐
 | 
			
		||||
#───┘ URL └─────────────────────────────────────────────────────
 | 
			
		||||
 | 
			
		||||
# Final accessible URL seen by a user.
 | 
			
		||||
url: 'http://misskey.local'
 | 
			
		||||
 | 
			
		||||
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
 | 
			
		||||
# URL SETTINGS AFTER THAT!
 | 
			
		||||
 | 
			
		||||
#   ┌───────────────────────┐
 | 
			
		||||
#───┘ Port and TLS settings └───────────────────────────────────
 | 
			
		||||
 | 
			
		||||
#
 | 
			
		||||
# Misskey requires a reverse proxy to support HTTPS connections.
 | 
			
		||||
#
 | 
			
		||||
#                 +----- https://example.tld/ ------------+
 | 
			
		||||
#   +------+      |+-------------+      +----------------+|
 | 
			
		||||
#   | User | ---> || Proxy (443) | ---> | Misskey (3000) ||
 | 
			
		||||
#   +------+      |+-------------+      +----------------+|
 | 
			
		||||
#                 +---------------------------------------+
 | 
			
		||||
#
 | 
			
		||||
#   You need to set up a reverse proxy. (e.g. nginx)
 | 
			
		||||
#   An encrypted connection with HTTPS is highly recommended
 | 
			
		||||
#   because tokens may be transferred in GET requests.
 | 
			
		||||
 | 
			
		||||
# The port that your Misskey server should listen on.
 | 
			
		||||
port: 61812
 | 
			
		||||
 | 
			
		||||
#   ┌──────────────────────────┐
 | 
			
		||||
#───┘ PostgreSQL configuration └────────────────────────────────
 | 
			
		||||
 | 
			
		||||
db:
 | 
			
		||||
  host: db
 | 
			
		||||
  port: 5432
 | 
			
		||||
 | 
			
		||||
  # Database name
 | 
			
		||||
  db: misskey
 | 
			
		||||
 | 
			
		||||
  # Auth
 | 
			
		||||
  user: postgres
 | 
			
		||||
  pass: postgres
 | 
			
		||||
 | 
			
		||||
  # Whether disable Caching queries
 | 
			
		||||
  #disableCache: true
 | 
			
		||||
 | 
			
		||||
  # Extra Connection options
 | 
			
		||||
  #extra:
 | 
			
		||||
  #  ssl: true
 | 
			
		||||
 | 
			
		||||
dbReplications: false
 | 
			
		||||
 | 
			
		||||
# You can configure any number of replicas here
 | 
			
		||||
#dbSlaves:
 | 
			
		||||
#  -
 | 
			
		||||
#    host:
 | 
			
		||||
#    port:
 | 
			
		||||
#    db:
 | 
			
		||||
#    user:
 | 
			
		||||
#    pass:
 | 
			
		||||
#  -
 | 
			
		||||
#    host:
 | 
			
		||||
#    port:
 | 
			
		||||
#    db:
 | 
			
		||||
#    user:
 | 
			
		||||
#    pass:
 | 
			
		||||
 | 
			
		||||
#   ┌─────────────────────┐
 | 
			
		||||
#───┘ Redis configuration └─────────────────────────────────────
 | 
			
		||||
 | 
			
		||||
redis:
 | 
			
		||||
  host: redis
 | 
			
		||||
  port: 6379
 | 
			
		||||
  #family: 0  # 0=Both, 4=IPv4, 6=IPv6
 | 
			
		||||
  #pass: example-pass
 | 
			
		||||
  #prefix: example-prefix
 | 
			
		||||
  #db: 1
 | 
			
		||||
 | 
			
		||||
#redisForPubsub:
 | 
			
		||||
#  host: redis
 | 
			
		||||
#  port: 6379
 | 
			
		||||
#  #family: 0  # 0=Both, 4=IPv4, 6=IPv6
 | 
			
		||||
#  #pass: example-pass
 | 
			
		||||
#  #prefix: example-prefix
 | 
			
		||||
#  #db: 1
 | 
			
		||||
 | 
			
		||||
#redisForJobQueue:
 | 
			
		||||
#  host: redis
 | 
			
		||||
#  port: 6379
 | 
			
		||||
#  #family: 0  # 0=Both, 4=IPv4, 6=IPv6
 | 
			
		||||
#  #pass: example-pass
 | 
			
		||||
#  #prefix: example-prefix
 | 
			
		||||
#  #db: 1
 | 
			
		||||
 | 
			
		||||
#redisForTimelines:
 | 
			
		||||
#  host: redis
 | 
			
		||||
#  port: 6379
 | 
			
		||||
#  #family: 0  # 0=Both, 4=IPv4, 6=IPv6
 | 
			
		||||
#  #pass: example-pass
 | 
			
		||||
#  #prefix: example-prefix
 | 
			
		||||
#  #db: 1
 | 
			
		||||
 | 
			
		||||
#redisForReactions:
 | 
			
		||||
#  host: redis
 | 
			
		||||
#  port: 6379
 | 
			
		||||
#  #family: 0  # 0=Both, 4=IPv4, 6=IPv6
 | 
			
		||||
#  #pass: example-pass
 | 
			
		||||
#  #prefix: example-prefix
 | 
			
		||||
#  #db: 1
 | 
			
		||||
 | 
			
		||||
#   ┌───────────────────────────┐
 | 
			
		||||
#───┘ MeiliSearch configuration └─────────────────────────────
 | 
			
		||||
 | 
			
		||||
#meilisearch:
 | 
			
		||||
#  host: meilisearch
 | 
			
		||||
#  port: 7700
 | 
			
		||||
#  apiKey: ''
 | 
			
		||||
#  ssl: true
 | 
			
		||||
#  index: ''
 | 
			
		||||
 | 
			
		||||
#   ┌───────────────┐
 | 
			
		||||
#───┘ ID generation └───────────────────────────────────────────
 | 
			
		||||
 | 
			
		||||
# You can select the ID generation method.
 | 
			
		||||
# You don't usually need to change this setting, but you can
 | 
			
		||||
# change it according to your preferences.
 | 
			
		||||
 | 
			
		||||
# Available methods:
 | 
			
		||||
# aid ... Short, Millisecond accuracy
 | 
			
		||||
# aidx ... Millisecond accuracy
 | 
			
		||||
# meid ... Similar to ObjectID, Millisecond accuracy
 | 
			
		||||
# ulid ... Millisecond accuracy
 | 
			
		||||
# objectid ... This is left for backward compatibility
 | 
			
		||||
 | 
			
		||||
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
 | 
			
		||||
# ID SETTINGS AFTER THAT!
 | 
			
		||||
 | 
			
		||||
id: 'aidx'
 | 
			
		||||
 | 
			
		||||
#   ┌────────────────┐
 | 
			
		||||
#───┘ Error tracking └──────────────────────────────────────────
 | 
			
		||||
 | 
			
		||||
# Sentry is available for error tracking.
 | 
			
		||||
# See the Sentry documentation for more details on options.
 | 
			
		||||
 | 
			
		||||
#sentryForBackend:
 | 
			
		||||
#  enableNodeProfiling: true
 | 
			
		||||
#  options:
 | 
			
		||||
#    dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
 | 
			
		||||
 | 
			
		||||
#sentryForFrontend:
 | 
			
		||||
#  options:
 | 
			
		||||
#    dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
 | 
			
		||||
 | 
			
		||||
#   ┌─────────────────────┐
 | 
			
		||||
#───┘ Other configuration └─────────────────────────────────────
 | 
			
		||||
 | 
			
		||||
# Whether disable HSTS
 | 
			
		||||
#disableHsts: true
 | 
			
		||||
 | 
			
		||||
# Number of worker processes
 | 
			
		||||
#clusterLimit: 1
 | 
			
		||||
 | 
			
		||||
# Job concurrency per worker
 | 
			
		||||
# deliverJobConcurrency: 128
 | 
			
		||||
# inboxJobConcurrency: 16
 | 
			
		||||
 | 
			
		||||
# Job rate limiter
 | 
			
		||||
# deliverJobPerSec: 128
 | 
			
		||||
# inboxJobPerSec: 32
 | 
			
		||||
 | 
			
		||||
# Job attempts
 | 
			
		||||
# deliverJobMaxAttempts: 12
 | 
			
		||||
# inboxJobMaxAttempts: 8
 | 
			
		||||
 | 
			
		||||
# IP address family used for outgoing request (ipv4, ipv6 or dual)
 | 
			
		||||
#outgoingAddressFamily: ipv4
 | 
			
		||||
 | 
			
		||||
# Proxy for HTTP/HTTPS
 | 
			
		||||
#proxy: http://127.0.0.1:3128
 | 
			
		||||
 | 
			
		||||
proxyBypassHosts:
 | 
			
		||||
  - api.deepl.com
 | 
			
		||||
  - api-free.deepl.com
 | 
			
		||||
  - www.recaptcha.net
 | 
			
		||||
  - hcaptcha.com
 | 
			
		||||
  - challenges.cloudflare.com
 | 
			
		||||
 | 
			
		||||
# Proxy for SMTP/SMTPS
 | 
			
		||||
#proxySmtp: http://127.0.0.1:3128   # use HTTP/1.1 CONNECT
 | 
			
		||||
#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4
 | 
			
		||||
#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5
 | 
			
		||||
 | 
			
		||||
# Media Proxy
 | 
			
		||||
#mediaProxy: https://example.com/proxy
 | 
			
		||||
 | 
			
		||||
# Proxy remote files (default: true)
 | 
			
		||||
proxyRemoteFiles: true
 | 
			
		||||
 | 
			
		||||
# Sign to ActivityPub GET request (default: true)
 | 
			
		||||
signToActivityPubGet: true
 | 
			
		||||
 | 
			
		||||
allowedPrivateNetworks: [
 | 
			
		||||
  '127.0.0.1/32'
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
# Upload or download file size limits (bytes)
 | 
			
		||||
#maxFileSize: 262144000
 | 
			
		||||
@@ -106,14 +106,6 @@ redis:
 | 
			
		||||
#  #prefix: example-prefix
 | 
			
		||||
#  #db: 1
 | 
			
		||||
 | 
			
		||||
#redisForReactions:
 | 
			
		||||
#  host: redis
 | 
			
		||||
#  port: 6379
 | 
			
		||||
#  #family: 0  # 0=Both, 4=IPv4, 6=IPv6
 | 
			
		||||
#  #pass: example-pass
 | 
			
		||||
#  #prefix: example-prefix
 | 
			
		||||
#  #db: 1
 | 
			
		||||
 | 
			
		||||
#   ┌───────────────────────────┐
 | 
			
		||||
#───┘ MeiliSearch configuration └─────────────────────────────
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -172,16 +172,6 @@ redis:
 | 
			
		||||
#  # You can specify more ioredis options...
 | 
			
		||||
#  #username: example-username
 | 
			
		||||
 | 
			
		||||
#redisForReactions:
 | 
			
		||||
#  host: localhost
 | 
			
		||||
#  port: 6379
 | 
			
		||||
#  #family: 0  # 0=Both, 4=IPv4, 6=IPv6
 | 
			
		||||
#  #pass: example-pass
 | 
			
		||||
#  #prefix: example-prefix
 | 
			
		||||
#  #db: 1
 | 
			
		||||
#  # You can specify more ioredis options...
 | 
			
		||||
#  #username: example-username
 | 
			
		||||
 | 
			
		||||
#   ┌───────────────────────────┐
 | 
			
		||||
#───┘ MeiliSearch configuration └─────────────────────────────
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -103,14 +103,6 @@ redis:
 | 
			
		||||
#  #prefix: example-prefix
 | 
			
		||||
#  #db: 1
 | 
			
		||||
 | 
			
		||||
#redisForReactions:
 | 
			
		||||
#  host: redis
 | 
			
		||||
#  port: 6379
 | 
			
		||||
#  #family: 0  # 0=Both, 4=IPv4, 6=IPv6
 | 
			
		||||
#  #pass: example-pass
 | 
			
		||||
#  #prefix: example-prefix
 | 
			
		||||
#  #db: 1
 | 
			
		||||
 | 
			
		||||
#   ┌───────────────────────────┐
 | 
			
		||||
#───┘ MeiliSearch configuration └─────────────────────────────
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,8 +3,6 @@
 | 
			
		||||
set -xe
 | 
			
		||||
 | 
			
		||||
sudo chown node node_modules
 | 
			
		||||
sudo apt-get update
 | 
			
		||||
sudo apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2 libxtst6 xauth xvfb
 | 
			
		||||
git config --global --add safe.directory /workspace
 | 
			
		||||
git submodule update --init
 | 
			
		||||
corepack install
 | 
			
		||||
@@ -14,4 +12,3 @@ pnpm install --frozen-lockfile
 | 
			
		||||
cp .devcontainer/devcontainer.yml .config/default.yml
 | 
			
		||||
pnpm build
 | 
			
		||||
pnpm migrate
 | 
			
		||||
pnpm exec cypress install
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/api-misskey-js.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/api-misskey-js.yml
									
									
									
									
										vendored
									
									
								
							@@ -21,7 +21,7 @@ jobs:
 | 
			
		||||
      - run: corepack enable
 | 
			
		||||
 | 
			
		||||
      - name: Setup Node.js
 | 
			
		||||
        uses: actions/setup-node@v4.0.4
 | 
			
		||||
        uses: actions/setup-node@v4.0.3
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: '.node-version'
 | 
			
		||||
          cache: 'pnpm'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/changelog-check.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/changelog-check.yml
									
									
									
									
										vendored
									
									
								
							@@ -14,7 +14,7 @@ jobs:
 | 
			
		||||
      - name: Checkout head
 | 
			
		||||
        uses: actions/checkout@v4.1.1
 | 
			
		||||
      - name: Setup Node.js
 | 
			
		||||
        uses: actions/setup-node@v4.0.4
 | 
			
		||||
        uses: actions/setup-node@v4.0.3
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: '.node-version'
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      - name: setup node
 | 
			
		||||
        id: setup-node
 | 
			
		||||
        uses: actions/setup-node@v4.0.4
 | 
			
		||||
        uses: actions/setup-node@v4.0.3
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: '.node-version'
 | 
			
		||||
          cache: pnpm
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/check-spdx-license-id.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/check-spdx-license-id.yml
									
									
									
									
										vendored
									
									
								
							@@ -48,16 +48,12 @@ jobs:
 | 
			
		||||
            "packages/backend/migration"
 | 
			
		||||
            "packages/backend/src"
 | 
			
		||||
            "packages/backend/test"
 | 
			
		||||
            "packages/frontend-shared/@types"
 | 
			
		||||
            "packages/frontend-shared/js"
 | 
			
		||||
            "packages/frontend/.storybook"
 | 
			
		||||
            "packages/frontend/@types"
 | 
			
		||||
            "packages/frontend/lib"
 | 
			
		||||
            "packages/frontend/public"
 | 
			
		||||
            "packages/frontend/src"
 | 
			
		||||
            "packages/frontend/test"
 | 
			
		||||
            "packages/frontend-embed/@types"
 | 
			
		||||
            "packages/frontend-embed/src"
 | 
			
		||||
            "packages/misskey-bubble-game/src"
 | 
			
		||||
            "packages/misskey-reversi/src"
 | 
			
		||||
            "packages/sw/src"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/get-api-diff.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/get-api-diff.yml
									
									
									
									
										vendored
									
									
								
							@@ -33,7 +33,7 @@ jobs:
 | 
			
		||||
    - name: Install pnpm
 | 
			
		||||
      uses: pnpm/action-setup@v4
 | 
			
		||||
    - name: Use Node.js ${{ matrix.node-version }}
 | 
			
		||||
      uses: actions/setup-node@v4.0.4
 | 
			
		||||
      uses: actions/setup-node@v4.0.3
 | 
			
		||||
      with:
 | 
			
		||||
        node-version: ${{ matrix.node-version }}
 | 
			
		||||
        cache: 'pnpm'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										29
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										29
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
								
							@@ -8,8 +8,6 @@ on:
 | 
			
		||||
    paths:
 | 
			
		||||
      - packages/backend/**
 | 
			
		||||
      - packages/frontend/**
 | 
			
		||||
      - packages/frontend-shared/**
 | 
			
		||||
      - packages/frontend-embed/**
 | 
			
		||||
      - packages/sw/**
 | 
			
		||||
      - packages/misskey-js/**
 | 
			
		||||
      - packages/shared/eslint.config.js
 | 
			
		||||
@@ -18,8 +16,6 @@ on:
 | 
			
		||||
    paths:
 | 
			
		||||
      - packages/backend/**
 | 
			
		||||
      - packages/frontend/**
 | 
			
		||||
      - packages/frontend-shared/**
 | 
			
		||||
      - packages/frontend-embed/**
 | 
			
		||||
      - packages/sw/**
 | 
			
		||||
      - packages/misskey-js/**
 | 
			
		||||
      - packages/shared/eslint.config.js
 | 
			
		||||
@@ -33,7 +29,7 @@ jobs:
 | 
			
		||||
        fetch-depth: 0
 | 
			
		||||
        submodules: true
 | 
			
		||||
    - uses: pnpm/action-setup@v4
 | 
			
		||||
    - uses: actions/setup-node@v4.0.4
 | 
			
		||||
    - uses: actions/setup-node@v4.0.3
 | 
			
		||||
      with:
 | 
			
		||||
        node-version-file: '.node-version'
 | 
			
		||||
        cache: 'pnpm'
 | 
			
		||||
@@ -44,25 +40,22 @@ jobs:
 | 
			
		||||
    needs: [pnpm_install]
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    continue-on-error: true
 | 
			
		||||
    env:
 | 
			
		||||
      eslint-cache-version: v1
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        workspace:
 | 
			
		||||
        - backend
 | 
			
		||||
        - frontend
 | 
			
		||||
        - frontend-shared
 | 
			
		||||
        - frontend-embed
 | 
			
		||||
        - sw
 | 
			
		||||
        - misskey-js
 | 
			
		||||
    env:
 | 
			
		||||
      eslint-cache-version: v1
 | 
			
		||||
      eslint-cache-path: ${{ github.workspace }}/node_modules/.cache/eslint-${{ matrix.workspace }}
 | 
			
		||||
    steps:
 | 
			
		||||
    - uses: actions/checkout@v4.1.1
 | 
			
		||||
      with:
 | 
			
		||||
        fetch-depth: 0
 | 
			
		||||
        submodules: true
 | 
			
		||||
    - uses: pnpm/action-setup@v4
 | 
			
		||||
    - uses: actions/setup-node@v4.0.4
 | 
			
		||||
    - uses: actions/setup-node@v4.0.3
 | 
			
		||||
      with:
 | 
			
		||||
        node-version-file: '.node-version'
 | 
			
		||||
        cache: 'pnpm'
 | 
			
		||||
@@ -71,10 +64,11 @@ jobs:
 | 
			
		||||
    - name: Restore eslint cache
 | 
			
		||||
      uses: actions/cache@v4.0.2
 | 
			
		||||
      with:
 | 
			
		||||
        path: ${{ env.eslint-cache-path }}
 | 
			
		||||
        key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}
 | 
			
		||||
        restore-keys: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
 | 
			
		||||
    - run: pnpm --filter ${{ matrix.workspace }} run eslint --cache --cache-location ${{ env.eslint-cache-path }} --cache-strategy content
 | 
			
		||||
        path: node_modules/.cache/eslint
 | 
			
		||||
        key: eslint-${{ env.eslint-cache-version }}-${{ hashFiles('/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}
 | 
			
		||||
        restore-keys: |
 | 
			
		||||
          eslint-${{ env.eslint-cache-version }}-${{ hashFiles('/pnpm-lock.yaml') }}-
 | 
			
		||||
    - run: pnpm --filter ${{ matrix.workspace }} run eslint --cache --cache-location node_modules/.cache/eslint --cache-strategy content
 | 
			
		||||
 | 
			
		||||
  typecheck:
 | 
			
		||||
    needs: [pnpm_install]
 | 
			
		||||
@@ -84,7 +78,6 @@ jobs:
 | 
			
		||||
      matrix:
 | 
			
		||||
        workspace:
 | 
			
		||||
        - backend
 | 
			
		||||
        - sw
 | 
			
		||||
        - misskey-js
 | 
			
		||||
    steps:
 | 
			
		||||
    - uses: actions/checkout@v4.1.1
 | 
			
		||||
@@ -92,14 +85,14 @@ jobs:
 | 
			
		||||
        fetch-depth: 0
 | 
			
		||||
        submodules: true
 | 
			
		||||
    - uses: pnpm/action-setup@v4
 | 
			
		||||
    - uses: actions/setup-node@v4.0.4
 | 
			
		||||
    - uses: actions/setup-node@v4.0.3
 | 
			
		||||
      with:
 | 
			
		||||
        node-version-file: '.node-version'
 | 
			
		||||
        cache: 'pnpm'
 | 
			
		||||
    - run: corepack enable
 | 
			
		||||
    - run: pnpm i --frozen-lockfile
 | 
			
		||||
    - run: pnpm --filter misskey-js run build
 | 
			
		||||
      if: ${{ matrix.workspace == 'backend' || matrix.workspace == 'sw' }}
 | 
			
		||||
      if: ${{ matrix.workspace == 'backend' }}
 | 
			
		||||
    - run: pnpm --filter misskey-reversi run build
 | 
			
		||||
      if: ${{ matrix.workspace == 'backend' }}
 | 
			
		||||
    - run: pnpm --filter ${{ matrix.workspace }} run typecheck
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/locale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/locale.yml
									
									
									
									
										vendored
									
									
								
							@@ -19,7 +19,7 @@ jobs:
 | 
			
		||||
        fetch-depth: 0
 | 
			
		||||
        submodules: true
 | 
			
		||||
    - uses: pnpm/action-setup@v4
 | 
			
		||||
    - uses: actions/setup-node@v4.0.4
 | 
			
		||||
    - uses: actions/setup-node@v4.0.3
 | 
			
		||||
      with:
 | 
			
		||||
        node-version-file: '.node-version'
 | 
			
		||||
        cache: 'pnpm'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/on-release-created.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/on-release-created.yml
									
									
									
									
										vendored
									
									
								
							@@ -26,7 +26,7 @@ jobs:
 | 
			
		||||
      - name: Install pnpm
 | 
			
		||||
        uses: pnpm/action-setup@v4
 | 
			
		||||
      - name: Use Node.js ${{ matrix.node-version }}
 | 
			
		||||
        uses: actions/setup-node@v4.0.4
 | 
			
		||||
        uses: actions/setup-node@v4.0.3
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: ${{ matrix.node-version }}
 | 
			
		||||
          cache: 'pnpm'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										17
									
								
								.github/workflows/report-api-diff.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										17
									
								
								.github/workflows/report-api-diff.yml
									
									
									
									
										vendored
									
									
								
							@@ -70,16 +70,8 @@ jobs:
 | 
			
		||||
      - id: out-diff
 | 
			
		||||
        name: Build diff Comment
 | 
			
		||||
        run: |
 | 
			
		||||
          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
 | 
			
		||||
            cat <<- EOF >> ./output.md
 | 
			
		||||
          cat <<- EOF > ./output.md
 | 
			
		||||
          このPRによるapi.jsonの差分
 | 
			
		||||
          <details>
 | 
			
		||||
          <summary>差分はこちら</summary>
 | 
			
		||||
 | 
			
		||||
@@ -87,10 +79,9 @@ jobs:
 | 
			
		||||
          $(cat ./api.json.diff)
 | 
			
		||||
          \`\`\`
 | 
			
		||||
          </details>
 | 
			
		||||
            EOF
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
          echo "$FOOTER" >> ./output.md
 | 
			
		||||
          [Get diff files from Workflow Page](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})
 | 
			
		||||
          EOF
 | 
			
		||||
      - uses: thollander/actions-comment-pull-request@v2
 | 
			
		||||
        with:
 | 
			
		||||
          pr_number: ${{ steps.load-pr-num.outputs.pr-number }}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/storybook.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/storybook.yml
									
									
									
									
										vendored
									
									
								
							@@ -41,7 +41,7 @@ jobs:
 | 
			
		||||
    - name: Install pnpm
 | 
			
		||||
      uses: pnpm/action-setup@v4
 | 
			
		||||
    - name: Use Node.js 20.x
 | 
			
		||||
      uses: actions/setup-node@v4.0.4
 | 
			
		||||
      uses: actions/setup-node@v4.0.3
 | 
			
		||||
      with:
 | 
			
		||||
        node-version-file: '.node-version'
 | 
			
		||||
        cache: 'pnpm'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/test-backend.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/test-backend.yml
									
									
									
									
										vendored
									
									
								
							@@ -46,7 +46,7 @@ jobs:
 | 
			
		||||
    - name: Install FFmpeg
 | 
			
		||||
      uses: FedericoCarboni/setup-ffmpeg@v3
 | 
			
		||||
    - name: Use Node.js ${{ matrix.node-version }}
 | 
			
		||||
      uses: actions/setup-node@v4.0.4
 | 
			
		||||
      uses: actions/setup-node@v4.0.3
 | 
			
		||||
      with:
 | 
			
		||||
        node-version: ${{ matrix.node-version }}
 | 
			
		||||
        cache: 'pnpm'
 | 
			
		||||
@@ -93,7 +93,7 @@ jobs:
 | 
			
		||||
      - name: Install pnpm
 | 
			
		||||
        uses: pnpm/action-setup@v4
 | 
			
		||||
      - name: Use Node.js ${{ matrix.node-version }}
 | 
			
		||||
        uses: actions/setup-node@v4.0.4
 | 
			
		||||
        uses: actions/setup-node@v4.0.3
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: ${{ matrix.node-version }}
 | 
			
		||||
          cache: 'pnpm'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/test-frontend.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/test-frontend.yml
									
									
									
									
										vendored
									
									
								
							@@ -35,7 +35,7 @@ jobs:
 | 
			
		||||
    - name: Install pnpm
 | 
			
		||||
      uses: pnpm/action-setup@v4
 | 
			
		||||
    - name: Use Node.js ${{ matrix.node-version }}
 | 
			
		||||
      uses: actions/setup-node@v4.0.4
 | 
			
		||||
      uses: actions/setup-node@v4.0.3
 | 
			
		||||
      with:
 | 
			
		||||
        node-version: ${{ matrix.node-version }}
 | 
			
		||||
        cache: 'pnpm'
 | 
			
		||||
@@ -90,7 +90,7 @@ jobs:
 | 
			
		||||
    - name: Install pnpm
 | 
			
		||||
      uses: pnpm/action-setup@v4
 | 
			
		||||
    - name: Use Node.js ${{ matrix.node-version }}
 | 
			
		||||
      uses: actions/setup-node@v4.0.4
 | 
			
		||||
      uses: actions/setup-node@v4.0.3
 | 
			
		||||
      with:
 | 
			
		||||
        node-version: ${{ matrix.node-version }}
 | 
			
		||||
        cache: 'pnpm'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/test-misskey-js.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/test-misskey-js.yml
									
									
									
									
										vendored
									
									
								
							@@ -31,7 +31,7 @@ jobs:
 | 
			
		||||
      - run: corepack enable
 | 
			
		||||
 | 
			
		||||
      - name: Setup Node.js ${{ matrix.node-version }}
 | 
			
		||||
        uses: actions/setup-node@v4.0.4
 | 
			
		||||
        uses: actions/setup-node@v4.0.3
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: ${{ matrix.node-version }}
 | 
			
		||||
          cache: 'pnpm'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/test-production.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/test-production.yml
									
									
									
									
										vendored
									
									
								
							@@ -25,7 +25,7 @@ jobs:
 | 
			
		||||
    - name: Install pnpm
 | 
			
		||||
      uses: pnpm/action-setup@v4
 | 
			
		||||
    - name: Use Node.js ${{ matrix.node-version }}
 | 
			
		||||
      uses: actions/setup-node@v4.0.4
 | 
			
		||||
      uses: actions/setup-node@v4.0.3
 | 
			
		||||
      with:
 | 
			
		||||
        node-version: ${{ matrix.node-version }}
 | 
			
		||||
        cache: 'pnpm'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/validate-api-json.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/validate-api-json.yml
									
									
									
									
										vendored
									
									
								
							@@ -27,7 +27,7 @@ jobs:
 | 
			
		||||
    - name: Install pnpm
 | 
			
		||||
      uses: pnpm/action-setup@v4
 | 
			
		||||
    - name: Use Node.js ${{ matrix.node-version }}
 | 
			
		||||
      uses: actions/setup-node@v4.0.4
 | 
			
		||||
      uses: actions/setup-node@v4.0.3
 | 
			
		||||
      with:
 | 
			
		||||
        node-version: ${{ matrix.node-version }}
 | 
			
		||||
        cache: 'pnpm'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -35,7 +35,6 @@ coverage
 | 
			
		||||
!/.config/example.yml
 | 
			
		||||
!/.config/docker_example.yml
 | 
			
		||||
!/.config/docker_example.env
 | 
			
		||||
!/.config/cypress-devcontainer.yml
 | 
			
		||||
docker-compose.yml
 | 
			
		||||
compose.yml
 | 
			
		||||
.devcontainer/compose.yml
 | 
			
		||||
@@ -45,7 +44,6 @@ compose.yml
 | 
			
		||||
/build
 | 
			
		||||
built
 | 
			
		||||
built-test
 | 
			
		||||
js-built
 | 
			
		||||
/data
 | 
			
		||||
/.cache-loader
 | 
			
		||||
/db
 | 
			
		||||
@@ -65,10 +63,6 @@ temp
 | 
			
		||||
tsdoc-metadata.json
 | 
			
		||||
misskey-assets
 | 
			
		||||
 | 
			
		||||
# Vite temporary files
 | 
			
		||||
vite.config.js.timestamp-*
 | 
			
		||||
vite.config.ts.timestamp-*
 | 
			
		||||
 | 
			
		||||
# blender backups
 | 
			
		||||
*.blend1
 | 
			
		||||
*.blend2
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										33
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -1,34 +1,3 @@
 | 
			
		||||
## 2024.9.0
 | 
			
		||||
 | 
			
		||||
### General
 | 
			
		||||
- Feat: UserWebhookとSystemWebhookのテスト送信機能を追加 (#14445)
 | 
			
		||||
- Enhance: ユーザーによるコンテンツインポートの可否をロールポリシーで制御できるように
 | 
			
		||||
 | 
			
		||||
### Client
 | 
			
		||||
- Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能
 | 
			
		||||
  - 埋め込みコードやウェブサイトへの実装方法の詳細は https://misskey-hub.net/docs/for-users/features/embed/ をご覧ください
 | 
			
		||||
- Enhance: サイズ制限を超過するファイルをアップロードしようとした際にエラーを出すように
 | 
			
		||||
- Enhance: アイコンデコレーション管理画面にプレビューを追加
 | 
			
		||||
- Enhance: コントロールパネル内のファイル一覧でセンシティブなファイルを区別しやすく
 | 
			
		||||
- Enhance: ScratchpadにUIインスペクターを追加
 | 
			
		||||
- Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正
 | 
			
		||||
- Fix: 月の違う同じ日はセパレータが表示されないのを修正
 | 
			
		||||
- Fix: 縦横比が極端なカスタム絵文字を表示する際にレイアウトが崩れる箇所があるのを修正  
 | 
			
		||||
  (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/725)
 | 
			
		||||
- Fix: 設定変更時のリロード確認ダイアログが複数個表示されることがある問題を修正
 | 
			
		||||
- Fix: ファイルの詳細ページのファイルの説明で改行が正しく表示されない問題を修正  
 | 
			
		||||
  (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/bde6bb0bd2e8b0d027e724d2acdb8ae0585a8110)
 | 
			
		||||
 | 
			
		||||
### Server
 | 
			
		||||
- Feat: Misskey® Reactions Buffering Technology™ (RBT)により、リアクションの作成負荷を低減することが可能に
 | 
			
		||||
- Fix: アンテナの書き込み時にキーワードが与えられなかった場合のエラーをApiErrorとして投げるように
 | 
			
		||||
  - この変更により、公式フロントエンドでは入力の不備が内部エラーとして報告される代わりに一般的なエラーダイアログで報告されます
 | 
			
		||||
- Fix: ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正
 | 
			
		||||
- Fix: 外部ページを解析する際に、ページに紐づけられた関連リソースも読み込まれてしまう問題を修正  
 | 
			
		||||
  (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/26e0412fbb91447c37e8fb06ffb0487346063bb8)
 | 
			
		||||
- Fix: `Retry-After`ヘッダーが送信されなかった問題を修正
 | 
			
		||||
  (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/8a982c61c01909e7540ff1be9f019df07c3f0624)
 | 
			
		||||
 | 
			
		||||
## 2024.8.0
 | 
			
		||||
 | 
			
		||||
### General
 | 
			
		||||
@@ -64,8 +33,6 @@
 | 
			
		||||
- Fix: 無制限にストリーミングのチャンネルに接続できる問題を修正
 | 
			
		||||
- Fix: ベースロールのポリシーを変更した際にモデログに記録されないのを修正  
 | 
			
		||||
  (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/700)
 | 
			
		||||
- Fix: Prevent memory leak from memory caches (#14310)
 | 
			
		||||
- Fix: More reliable memory cache eviction (#14311)
 | 
			
		||||
 | 
			
		||||
## 2024.7.0
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,9 +21,7 @@ WORKDIR /misskey
 | 
			
		||||
COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"]
 | 
			
		||||
COPY --link ["scripts", "./scripts"]
 | 
			
		||||
COPY --link ["packages/backend/package.json", "./packages/backend/"]
 | 
			
		||||
COPY --link ["packages/frontend-shared/package.json", "./packages/frontend-shared/"]
 | 
			
		||||
COPY --link ["packages/frontend/package.json", "./packages/frontend/"]
 | 
			
		||||
COPY --link ["packages/frontend-embed/package.json", "./packages/frontend-embed/"]
 | 
			
		||||
COPY --link ["packages/sw/package.json", "./packages/sw/"]
 | 
			
		||||
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
 | 
			
		||||
COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"]
 | 
			
		||||
 
 | 
			
		||||
@@ -124,14 +124,6 @@ redis:
 | 
			
		||||
#  #prefix: example-prefix
 | 
			
		||||
#  #db: 1
 | 
			
		||||
 | 
			
		||||
#redisForReactions:
 | 
			
		||||
#  host: redis
 | 
			
		||||
#  port: 6379
 | 
			
		||||
#  #family: 0  # 0=Both, 4=IPv4, 6=IPv6
 | 
			
		||||
#  #pass: example-pass
 | 
			
		||||
#  #prefix: example-prefix
 | 
			
		||||
#  #db: 1
 | 
			
		||||
 | 
			
		||||
#   ┌───────────────────────────┐
 | 
			
		||||
#───┘ MeiliSearch configuration └─────────────────────────────
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2316,7 +2316,6 @@ _pages:
 | 
			
		||||
  eyeCatchingImageSet: "Set thumbnail"
 | 
			
		||||
  eyeCatchingImageRemove: "Delete thumbnail"
 | 
			
		||||
  chooseBlock: "Add a block"
 | 
			
		||||
  enterSectionTitle: "Enter a section title"
 | 
			
		||||
  selectType: "Select a type"
 | 
			
		||||
  contentBlocks: "Content"
 | 
			
		||||
  inputBlocks: "Input"
 | 
			
		||||
@@ -2500,10 +2499,7 @@ _moderationLogTypes:
 | 
			
		||||
  createAbuseReportNotificationRecipient: "Create a recipient for abuse reports"
 | 
			
		||||
  updateAbuseReportNotificationRecipient: "Update recipients for abuse reports"
 | 
			
		||||
  deleteAbuseReportNotificationRecipient: "Delete a recipient for abuse reports"
 | 
			
		||||
  deleteAccount: "Delete the account"
 | 
			
		||||
  deletePage: "Delete the page"
 | 
			
		||||
  deleteFlash: "Delete Play"
 | 
			
		||||
  deleteGalleryPost: "Delete the gallery post"
 | 
			
		||||
_fileViewer:
 | 
			
		||||
  title: "File details"
 | 
			
		||||
  type: "File type"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										116
									
								
								locales/index.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										116
									
								
								locales/index.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -2384,14 +2384,6 @@ export interface Locale extends ILocale {
 | 
			
		||||
     * スクラッチパッドは、AiScriptの実験環境を提供します。Misskeyと対話するコードの記述、実行、結果の確認ができます。
 | 
			
		||||
     */
 | 
			
		||||
    "scratchpadDescription": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * UIインスペクター
 | 
			
		||||
     */
 | 
			
		||||
    "uiInspector": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * メモリ上に存在しているUIコンポーネントのインスタンスの一覧を見ることができます。UIコンポーネントはUi:C:系関数により生成されます。
 | 
			
		||||
     */
 | 
			
		||||
    "uiInspectorDescription": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * 出力
 | 
			
		||||
     */
 | 
			
		||||
@@ -3129,7 +3121,7 @@ export interface Locale extends ILocale {
 | 
			
		||||
     */
 | 
			
		||||
    "narrow": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * 設定はページリロード後に反映されます。
 | 
			
		||||
     * 設定はページリロード後に反映されます。今すぐリロードしますか?
 | 
			
		||||
     */
 | 
			
		||||
    "reloadToApplySetting": string;
 | 
			
		||||
    /**
 | 
			
		||||
@@ -5076,26 +5068,6 @@ export interface Locale extends ILocale {
 | 
			
		||||
     * 作成したアンテナ
 | 
			
		||||
     */
 | 
			
		||||
    "createdAntennas": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * {x}から
 | 
			
		||||
     */
 | 
			
		||||
    "fromX": ParameterizedString<"x">;
 | 
			
		||||
    /**
 | 
			
		||||
     * 埋め込みコードを生成
 | 
			
		||||
     */
 | 
			
		||||
    "genEmbedCode": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * このユーザーのノート一覧
 | 
			
		||||
     */
 | 
			
		||||
    "noteOfThisUser": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * これ以上このクリップにノートを追加できません。
 | 
			
		||||
     */
 | 
			
		||||
    "clipNoteLimitExceeded": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * パフォーマンス
 | 
			
		||||
     */
 | 
			
		||||
    "performance": string;
 | 
			
		||||
    "_delivery": {
 | 
			
		||||
        /**
 | 
			
		||||
         * 配信状態
 | 
			
		||||
@@ -5587,10 +5559,6 @@ export interface Locale extends ILocale {
 | 
			
		||||
         * 有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。
 | 
			
		||||
         */
 | 
			
		||||
        "fanoutTimelineDbFallbackDescription": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。
 | 
			
		||||
         */
 | 
			
		||||
        "reactionsBufferingDescription": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 問い合わせ先URL
 | 
			
		||||
         */
 | 
			
		||||
@@ -6718,6 +6686,10 @@ export interface Locale extends ILocale {
 | 
			
		||||
             * ノートのピン留めの最大数
 | 
			
		||||
             */
 | 
			
		||||
            "pinMax": string;
 | 
			
		||||
            /**
 | 
			
		||||
             * 一つのノートに対する最大リアクション数
 | 
			
		||||
             */
 | 
			
		||||
            "reactionsPerNoteLimit": string;
 | 
			
		||||
            /**
 | 
			
		||||
             * アンテナの作成可能数
 | 
			
		||||
             */
 | 
			
		||||
@@ -6770,26 +6742,6 @@ export interface Locale extends ILocale {
 | 
			
		||||
             * アイコンデコレーションの最大取付個数
 | 
			
		||||
             */
 | 
			
		||||
            "avatarDecorationLimit": string;
 | 
			
		||||
            /**
 | 
			
		||||
             * アンテナのインポートを許可
 | 
			
		||||
             */
 | 
			
		||||
            "canImportAntennas": string;
 | 
			
		||||
            /**
 | 
			
		||||
             * ブロックのインポートを許可
 | 
			
		||||
             */
 | 
			
		||||
            "canImportBlocking": string;
 | 
			
		||||
            /**
 | 
			
		||||
             * フォローのインポートを許可
 | 
			
		||||
             */
 | 
			
		||||
            "canImportFollowing": string;
 | 
			
		||||
            /**
 | 
			
		||||
             * ミュートのインポートを許可
 | 
			
		||||
             */
 | 
			
		||||
            "canImportMuting": string;
 | 
			
		||||
            /**
 | 
			
		||||
             * リストのインポートを許可
 | 
			
		||||
             */
 | 
			
		||||
            "canImportUserLists": string;
 | 
			
		||||
        };
 | 
			
		||||
        "_condition": {
 | 
			
		||||
            /**
 | 
			
		||||
@@ -9513,10 +9465,6 @@ export interface Locale extends ILocale {
 | 
			
		||||
         * Webhookを削除しますか?
 | 
			
		||||
         */
 | 
			
		||||
        "deleteConfirm": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * スイッチの右にあるボタンをクリックするとダミーのデータを使用したテスト用Webhookを送信できます。
 | 
			
		||||
         */
 | 
			
		||||
        "testRemarks": string;
 | 
			
		||||
    };
 | 
			
		||||
    "_abuseReport": {
 | 
			
		||||
        "_notificationRecipient": {
 | 
			
		||||
@@ -10248,60 +10196,6 @@ export interface Locale extends ILocale {
 | 
			
		||||
         */
 | 
			
		||||
        "native": string;
 | 
			
		||||
    };
 | 
			
		||||
    "_embedCodeGen": {
 | 
			
		||||
        /**
 | 
			
		||||
         * 埋め込みコードをカスタマイズ
 | 
			
		||||
         */
 | 
			
		||||
        "title": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * ヘッダーを表示
 | 
			
		||||
         */
 | 
			
		||||
        "header": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 自動で続きを読み込む(非推奨)
 | 
			
		||||
         */
 | 
			
		||||
        "autoload": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 高さの最大値
 | 
			
		||||
         */
 | 
			
		||||
        "maxHeight": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 0で最大値の設定が無効になります。ウィジェットが縦に伸び続けるのを防ぐために、何らかの値に指定してください。
 | 
			
		||||
         */
 | 
			
		||||
        "maxHeightDescription": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 高さの最大値制限が無効(0)になっています。これが意図した変更ではない場合は、高さの最大値を何らかの値に設定してください。
 | 
			
		||||
         */
 | 
			
		||||
        "maxHeightWarn": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * プレビュー画面で表示可能な範囲を超えたため、実際に埋め込んだ際とは表示が異なります。
 | 
			
		||||
         */
 | 
			
		||||
        "previewIsNotActual": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 角丸にする
 | 
			
		||||
         */
 | 
			
		||||
        "rounded": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 外枠に枠線をつける
 | 
			
		||||
         */
 | 
			
		||||
        "border": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * プレビューに反映
 | 
			
		||||
         */
 | 
			
		||||
        "applyToPreview": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 埋め込みコードを作成
 | 
			
		||||
         */
 | 
			
		||||
        "generateCode": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * コードが生成されました
 | 
			
		||||
         */
 | 
			
		||||
        "codeGenerated": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 生成されたコードをウェブサイトに貼り付けてご利用ください。
 | 
			
		||||
         */
 | 
			
		||||
        "codeGeneratedDescription": string;
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
declare const locales: {
 | 
			
		||||
    [lang: string]: Locale;
 | 
			
		||||
 
 | 
			
		||||
@@ -592,8 +592,6 @@ ascendingOrder: "昇順"
 | 
			
		||||
descendingOrder: "降順"
 | 
			
		||||
scratchpad: "スクラッチパッド"
 | 
			
		||||
scratchpadDescription: "スクラッチパッドは、AiScriptの実験環境を提供します。Misskeyと対話するコードの記述、実行、結果の確認ができます。"
 | 
			
		||||
uiInspector: "UIインスペクター"
 | 
			
		||||
uiInspectorDescription: "メモリ上に存在しているUIコンポーネントのインスタンスの一覧を見ることができます。UIコンポーネントはUi:C:系関数により生成されます。"
 | 
			
		||||
output: "出力"
 | 
			
		||||
script: "スクリプト"
 | 
			
		||||
disablePagesScript: "Pagesのスクリプトを無効にする"
 | 
			
		||||
@@ -778,7 +776,7 @@ left: "左"
 | 
			
		||||
center: "中央"
 | 
			
		||||
wide: "広い"
 | 
			
		||||
narrow: "狭い"
 | 
			
		||||
reloadToApplySetting: "設定はページリロード後に反映されます。"
 | 
			
		||||
reloadToApplySetting: "設定はページリロード後に反映されます。今すぐリロードしますか?"
 | 
			
		||||
needReloadToApply: "反映には再起動が必要です。"
 | 
			
		||||
showTitlebar: "タイトルバーを表示する"
 | 
			
		||||
clearCache: "キャッシュをクリア"
 | 
			
		||||
@@ -1265,11 +1263,6 @@ confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示
 | 
			
		||||
sensitiveMediaRevealConfirm: "センシティブなメディアです。表示しますか?"
 | 
			
		||||
createdLists: "作成したリスト"
 | 
			
		||||
createdAntennas: "作成したアンテナ"
 | 
			
		||||
fromX: "{x}から"
 | 
			
		||||
genEmbedCode: "埋め込みコードを生成"
 | 
			
		||||
noteOfThisUser: "このユーザーのノート一覧"
 | 
			
		||||
clipNoteLimitExceeded: "これ以上このクリップにノートを追加できません。"
 | 
			
		||||
performance: "パフォーマンス"
 | 
			
		||||
 | 
			
		||||
_delivery:
 | 
			
		||||
  status: "配信状態"
 | 
			
		||||
@@ -1412,7 +1405,6 @@ _serverSettings:
 | 
			
		||||
  fanoutTimelineDescription: "有効にすると、各種タイムラインを取得する際のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。サーバーのメモリ容量が少ない場合、または動作が不安定な場合は無効にすることができます。"
 | 
			
		||||
  fanoutTimelineDbFallback: "データベースへのフォールバック"
 | 
			
		||||
  fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。"
 | 
			
		||||
  reactionsBufferingDescription: "有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。"
 | 
			
		||||
  inquiryUrl: "問い合わせ先URL"
 | 
			
		||||
  inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。"
 | 
			
		||||
 | 
			
		||||
@@ -1736,6 +1728,7 @@ _role:
 | 
			
		||||
    alwaysMarkNsfw: "ファイルにNSFWを常に付与"
 | 
			
		||||
    canUpdateBioMedia: "アイコンとバナーの更新を許可"
 | 
			
		||||
    pinMax: "ノートのピン留めの最大数"
 | 
			
		||||
    reactionsPerNoteLimit: "一つのノートに対する最大リアクション数"
 | 
			
		||||
    antennaMax: "アンテナの作成可能数"
 | 
			
		||||
    wordMuteMax: "ワードミュートの最大文字数"
 | 
			
		||||
    webhookMax: "Webhookの作成可能数"
 | 
			
		||||
@@ -1749,11 +1742,6 @@ _role:
 | 
			
		||||
    canSearchNotes: "ノート検索の利用"
 | 
			
		||||
    canUseTranslator: "翻訳機能の利用"
 | 
			
		||||
    avatarDecorationLimit: "アイコンデコレーションの最大取付個数"
 | 
			
		||||
    canImportAntennas: "アンテナのインポートを許可"
 | 
			
		||||
    canImportBlocking: "ブロックのインポートを許可"
 | 
			
		||||
    canImportFollowing: "フォローのインポートを許可"
 | 
			
		||||
    canImportMuting: "ミュートのインポートを許可"
 | 
			
		||||
    canImportUserLists: "リストのインポートを許可"
 | 
			
		||||
  _condition:
 | 
			
		||||
    roleAssignedTo: "マニュアルロールにアサイン済み"
 | 
			
		||||
    isLocal: "ローカルユーザー"
 | 
			
		||||
@@ -2521,7 +2509,6 @@ _webhookSettings:
 | 
			
		||||
    abuseReportResolved: "ユーザーからの通報を処理したとき"
 | 
			
		||||
    userCreated: "ユーザーが作成されたとき"
 | 
			
		||||
  deleteConfirm: "Webhookを削除しますか?"
 | 
			
		||||
  testRemarks: "スイッチの右にあるボタンをクリックするとダミーのデータを使用したテスト用Webhookを送信できます。"
 | 
			
		||||
 | 
			
		||||
_abuseReport:
 | 
			
		||||
  _notificationRecipient:
 | 
			
		||||
@@ -2731,18 +2718,3 @@ _contextMenu:
 | 
			
		||||
  app: "アプリケーション"
 | 
			
		||||
  appWithShift: "Shiftキーでアプリケーション"
 | 
			
		||||
  native: "ブラウザのUI"
 | 
			
		||||
 | 
			
		||||
_embedCodeGen:
 | 
			
		||||
  title: "埋め込みコードをカスタマイズ"
 | 
			
		||||
  header: "ヘッダーを表示"
 | 
			
		||||
  autoload: "自動で続きを読み込む(非推奨)"
 | 
			
		||||
  maxHeight: "高さの最大値"
 | 
			
		||||
  maxHeightDescription: "0で最大値の設定が無効になります。ウィジェットが縦に伸び続けるのを防ぐために、何らかの値に指定してください。"
 | 
			
		||||
  maxHeightWarn: "高さの最大値制限が無効(0)になっています。これが意図した変更ではない場合は、高さの最大値を何らかの値に設定してください。"
 | 
			
		||||
  previewIsNotActual: "プレビュー画面で表示可能な範囲を超えたため、実際に埋め込んだ際とは表示が異なります。"
 | 
			
		||||
  rounded: "角丸にする"
 | 
			
		||||
  border: "外枠に枠線をつける"
 | 
			
		||||
  applyToPreview: "プレビューに反映"
 | 
			
		||||
  generateCode: "埋め込みコードを作成"
 | 
			
		||||
  codeGenerated: "コードが生成されました"
 | 
			
		||||
  codeGeneratedDescription: "生成されたコードをウェブサイトに貼り付けてご利用ください。"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								package.json
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
	"name": "misskey",
 | 
			
		||||
	"version": "2024.9.0-alpha.4",
 | 
			
		||||
	"version": "2024.8.0-rc.3",
 | 
			
		||||
	"codename": "nasubi",
 | 
			
		||||
	"repository": {
 | 
			
		||||
		"type": "git",
 | 
			
		||||
@@ -8,9 +8,7 @@
 | 
			
		||||
	},
 | 
			
		||||
	"packageManager": "pnpm@9.6.0",
 | 
			
		||||
	"workspaces": [
 | 
			
		||||
		"packages/frontend-shared",
 | 
			
		||||
		"packages/frontend",
 | 
			
		||||
		"packages/frontend-embed",
 | 
			
		||||
		"packages/backend",
 | 
			
		||||
		"packages/sw",
 | 
			
		||||
		"packages/misskey-js",
 | 
			
		||||
@@ -37,7 +35,6 @@
 | 
			
		||||
		"cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts",
 | 
			
		||||
		"cy:run": "pnpm cypress run",
 | 
			
		||||
		"e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run",
 | 
			
		||||
		"e2e-dev-container": "cp ./.config/cypress-devcontainer.yml ./.config/test.yml && pnpm start-server-and-test start:test http://localhost:61812 cy:run",
 | 
			
		||||
		"jest": "cd packages/backend && pnpm jest",
 | 
			
		||||
		"jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage",
 | 
			
		||||
		"test": "pnpm -r test",
 | 
			
		||||
@@ -56,11 +53,11 @@
 | 
			
		||||
		"fast-glob": "3.3.2",
 | 
			
		||||
		"ignore-walk": "6.0.5",
 | 
			
		||||
		"js-yaml": "4.1.0",
 | 
			
		||||
		"postcss": "8.4.47",
 | 
			
		||||
		"postcss": "8.4.40",
 | 
			
		||||
		"tar": "6.2.1",
 | 
			
		||||
		"terser": "5.33.0",
 | 
			
		||||
		"typescript": "5.6.2",
 | 
			
		||||
		"esbuild": "0.23.1",
 | 
			
		||||
		"terser": "5.31.3",
 | 
			
		||||
		"typescript": "5.5.4",
 | 
			
		||||
		"esbuild": "0.23.0",
 | 
			
		||||
		"glob": "11.0.0"
 | 
			
		||||
	},
 | 
			
		||||
	"devDependencies": {
 | 
			
		||||
@@ -69,11 +66,11 @@
 | 
			
		||||
		"@typescript-eslint/eslint-plugin": "7.17.0",
 | 
			
		||||
		"@typescript-eslint/parser": "7.17.0",
 | 
			
		||||
		"cross-env": "7.0.3",
 | 
			
		||||
		"cypress": "13.14.2",
 | 
			
		||||
		"cypress": "13.13.1",
 | 
			
		||||
		"eslint": "9.8.0",
 | 
			
		||||
		"globals": "15.9.0",
 | 
			
		||||
		"globals": "15.8.0",
 | 
			
		||||
		"ncp": "2.0.0",
 | 
			
		||||
		"start-server-and-test": "2.0.8"
 | 
			
		||||
		"start-server-and-test": "2.0.4"
 | 
			
		||||
	},
 | 
			
		||||
	"optionalDependencies": {
 | 
			
		||||
		"@tensorflow/tfjs-core": "4.4.0"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,31 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: MIT
 | 
			
		||||
 */
 | 
			
		||||
//@ts-check
 | 
			
		||||
(() => {
 | 
			
		||||
	/** @type {NodeListOf<HTMLIFrameElement>} */
 | 
			
		||||
	const els = document.querySelectorAll('iframe[data-misskey-embed-id]');
 | 
			
		||||
 | 
			
		||||
	window.addEventListener('message', function (event) {
 | 
			
		||||
		els.forEach((el) => {
 | 
			
		||||
			if (event.source !== el.contentWindow) {
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const id = el.dataset.misskeyEmbedId;
 | 
			
		||||
 | 
			
		||||
			if (event.data.type === 'misskey:embed:ready') {
 | 
			
		||||
				el.contentWindow?.postMessage({
 | 
			
		||||
					type: 'misskey:embedParent:registerIframeId',
 | 
			
		||||
					payload: {
 | 
			
		||||
						iframeId: id,
 | 
			
		||||
					}
 | 
			
		||||
				}, '*');
 | 
			
		||||
			}
 | 
			
		||||
			if (event.data.type === 'misskey:embed:changeHeight' && event.data.iframeId === id) {
 | 
			
		||||
				el.style.height = event.data.payload.height + 'px';
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
})();
 | 
			
		||||
@@ -0,0 +1,20 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export class MultipleReactions1721117896543 {
 | 
			
		||||
	name = 'MultipleReactions1721117896543';
 | 
			
		||||
 | 
			
		||||
	async up(queryRunner) {
 | 
			
		||||
		await queryRunner.query('DROP INDEX "public"."IDX_ad0c221b25672daf2df320a817"');
 | 
			
		||||
		await queryRunner.query('CREATE UNIQUE INDEX "IDX_a7751b74317122d11575bff31c" ON "note_reaction" ("userId", "noteId", "reaction") ');
 | 
			
		||||
		await queryRunner.query('CREATE INDEX "IDX_ad0c221b25672daf2df320a817" ON "note_reaction" ("userId", "noteId") ');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async down(queryRunner) {
 | 
			
		||||
		await queryRunner.query('DROP INDEX "public"."IDX_ad0c221b25672daf2df320a817"');
 | 
			
		||||
		await queryRunner.query('DROP INDEX "public"."IDX_a7751b74317122d11575bff31c"');
 | 
			
		||||
		await queryRunner.query('CREATE UNIQUE INDEX "IDX_ad0c221b25672daf2df320a817" ON "note_reaction" ("userId", "noteId") ');
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,16 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export class ReactionsBuffering1726804538569 {
 | 
			
		||||
    name = 'ReactionsBuffering1726804538569'
 | 
			
		||||
 | 
			
		||||
    async up(queryRunner) {
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "meta" ADD "enableReactionsBuffering" boolean NOT NULL DEFAULT false`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async down(queryRunner) {
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableReactionsBuffering"`);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -67,24 +67,24 @@
 | 
			
		||||
	"dependencies": {
 | 
			
		||||
		"@aws-sdk/client-s3": "3.620.0",
 | 
			
		||||
		"@aws-sdk/lib-storage": "3.620.0",
 | 
			
		||||
		"@bull-board/api": "5.23.0",
 | 
			
		||||
		"@bull-board/fastify": "5.23.0",
 | 
			
		||||
		"@bull-board/ui": "5.23.0",
 | 
			
		||||
		"@discordapp/twemoji": "15.1.0",
 | 
			
		||||
		"@fastify/accepts": "5.0.0",
 | 
			
		||||
		"@fastify/cookie": "10.0.0",
 | 
			
		||||
		"@fastify/cors": "10.0.0",
 | 
			
		||||
		"@fastify/express": "4.0.0",
 | 
			
		||||
		"@fastify/http-proxy": "10.0.0",
 | 
			
		||||
		"@fastify/multipart": "9.0.0",
 | 
			
		||||
		"@fastify/static": "8.0.0",
 | 
			
		||||
		"@fastify/view": "10.0.0",
 | 
			
		||||
		"@bull-board/api": "5.21.1",
 | 
			
		||||
		"@bull-board/fastify": "5.21.1",
 | 
			
		||||
		"@bull-board/ui": "5.21.1",
 | 
			
		||||
		"@discordapp/twemoji": "15.0.3",
 | 
			
		||||
		"@fastify/accepts": "4.3.0",
 | 
			
		||||
		"@fastify/cookie": "9.3.1",
 | 
			
		||||
		"@fastify/cors": "9.0.1",
 | 
			
		||||
		"@fastify/express": "3.0.0",
 | 
			
		||||
		"@fastify/http-proxy": "9.5.0",
 | 
			
		||||
		"@fastify/multipart": "8.3.0",
 | 
			
		||||
		"@fastify/static": "7.0.4",
 | 
			
		||||
		"@fastify/view": "9.1.0",
 | 
			
		||||
		"@misskey-dev/sharp-read-bmp": "1.2.0",
 | 
			
		||||
		"@misskey-dev/summaly": "5.1.0",
 | 
			
		||||
		"@napi-rs/canvas": "0.1.56",
 | 
			
		||||
		"@nestjs/common": "10.4.3",
 | 
			
		||||
		"@nestjs/core": "10.4.3",
 | 
			
		||||
		"@nestjs/testing": "10.4.3",
 | 
			
		||||
		"@napi-rs/canvas": "^0.1.53",
 | 
			
		||||
		"@nestjs/common": "10.3.10",
 | 
			
		||||
		"@nestjs/core": "10.3.10",
 | 
			
		||||
		"@nestjs/testing": "10.3.10",
 | 
			
		||||
		"@peertube/http-signature": "1.7.0",
 | 
			
		||||
		"@sentry/node": "8.20.0",
 | 
			
		||||
		"@sentry/profiling-node": "8.20.0",
 | 
			
		||||
@@ -100,8 +100,8 @@
 | 
			
		||||
		"async-mutex": "0.5.0",
 | 
			
		||||
		"bcryptjs": "2.4.3",
 | 
			
		||||
		"blurhash": "2.0.5",
 | 
			
		||||
		"body-parser": "1.20.3",
 | 
			
		||||
		"bullmq": "5.13.2",
 | 
			
		||||
		"body-parser": "1.20.2",
 | 
			
		||||
		"bullmq": "5.10.4",
 | 
			
		||||
		"cacheable-lookup": "7.0.0",
 | 
			
		||||
		"cbor": "9.0.2",
 | 
			
		||||
		"chalk": "5.3.0",
 | 
			
		||||
@@ -112,28 +112,27 @@
 | 
			
		||||
		"content-disposition": "0.5.4",
 | 
			
		||||
		"date-fns": "2.30.0",
 | 
			
		||||
		"deep-email-validator": "0.1.21",
 | 
			
		||||
		"fastify": "5.0.0",
 | 
			
		||||
		"fastify-raw-body": "5.0.0",
 | 
			
		||||
		"fastify": "4.28.1",
 | 
			
		||||
		"fastify-raw-body": "4.3.0",
 | 
			
		||||
		"feed": "4.2.2",
 | 
			
		||||
		"file-type": "19.5.0",
 | 
			
		||||
		"file-type": "19.3.0",
 | 
			
		||||
		"fluent-ffmpeg": "2.1.3",
 | 
			
		||||
		"form-data": "4.0.0",
 | 
			
		||||
		"got": "14.4.2",
 | 
			
		||||
		"happy-dom": "15.7.4",
 | 
			
		||||
		"happy-dom": "10.0.3",
 | 
			
		||||
		"hpagent": "1.2.0",
 | 
			
		||||
		"htmlescape": "1.1.1",
 | 
			
		||||
		"http-link-header": "1.1.3",
 | 
			
		||||
		"ioredis": "5.4.1",
 | 
			
		||||
		"ip-cidr": "4.0.2",
 | 
			
		||||
		"ip-cidr": "4.0.1",
 | 
			
		||||
		"ipaddr.js": "2.2.0",
 | 
			
		||||
		"is-svg": "5.1.0",
 | 
			
		||||
		"is-svg": "5.0.1",
 | 
			
		||||
		"js-yaml": "4.1.0",
 | 
			
		||||
		"jsdom": "24.1.1",
 | 
			
		||||
		"json5": "2.2.3",
 | 
			
		||||
		"jsonld": "8.3.2",
 | 
			
		||||
		"jsrsasign": "11.1.0",
 | 
			
		||||
		"meilisearch": "0.42.0",
 | 
			
		||||
		"juice": "11.0.0",
 | 
			
		||||
		"meilisearch": "0.41.0",
 | 
			
		||||
		"mfm-js": "0.24.0",
 | 
			
		||||
		"microformats-parser": "2.0.2",
 | 
			
		||||
		"mime-types": "2.1.35",
 | 
			
		||||
@@ -143,24 +142,24 @@
 | 
			
		||||
		"nanoid": "5.0.7",
 | 
			
		||||
		"nested-property": "4.0.0",
 | 
			
		||||
		"node-fetch": "3.3.2",
 | 
			
		||||
		"nodemailer": "6.9.15",
 | 
			
		||||
		"nodemailer": "6.9.14",
 | 
			
		||||
		"nsfwjs": "2.4.2",
 | 
			
		||||
		"oauth": "0.10.0",
 | 
			
		||||
		"oauth2orize": "1.12.0",
 | 
			
		||||
		"oauth2orize-pkce": "0.1.2",
 | 
			
		||||
		"os-utils": "0.0.14",
 | 
			
		||||
		"otpauth": "9.3.2",
 | 
			
		||||
		"otpauth": "9.3.1",
 | 
			
		||||
		"parse5": "7.1.2",
 | 
			
		||||
		"pg": "8.13.0",
 | 
			
		||||
		"pg": "8.12.0",
 | 
			
		||||
		"pkce-challenge": "4.1.0",
 | 
			
		||||
		"probe-image-size": "7.2.3",
 | 
			
		||||
		"promise-limit": "2.7.0",
 | 
			
		||||
		"pug": "3.0.3",
 | 
			
		||||
		"punycode": "2.3.1",
 | 
			
		||||
		"qrcode": "1.5.4",
 | 
			
		||||
		"qrcode": "1.5.3",
 | 
			
		||||
		"random-seed": "0.3.0",
 | 
			
		||||
		"ratelimiter": "3.4.1",
 | 
			
		||||
		"re2": "1.21.4",
 | 
			
		||||
		"re2": "1.21.3",
 | 
			
		||||
		"redis-lock": "0.1.4",
 | 
			
		||||
		"reflect-metadata": "0.2.2",
 | 
			
		||||
		"rename": "1.0.4",
 | 
			
		||||
@@ -168,17 +167,17 @@
 | 
			
		||||
		"rxjs": "7.8.1",
 | 
			
		||||
		"sanitize-html": "2.13.0",
 | 
			
		||||
		"secure-json-parse": "2.7.0",
 | 
			
		||||
		"sharp": "0.33.5",
 | 
			
		||||
		"sharp": "0.33.4",
 | 
			
		||||
		"slacc": "0.0.10",
 | 
			
		||||
		"strict-event-emitter-types": "2.0.0",
 | 
			
		||||
		"stringz": "2.1.0",
 | 
			
		||||
		"systeminformation": "5.23.5",
 | 
			
		||||
		"systeminformation": "5.22.11",
 | 
			
		||||
		"tinycolor2": "1.6.0",
 | 
			
		||||
		"tmp": "0.2.3",
 | 
			
		||||
		"tsc-alias": "1.8.10",
 | 
			
		||||
		"tsconfig-paths": "4.2.0",
 | 
			
		||||
		"typeorm": "0.3.20",
 | 
			
		||||
		"typescript": "5.6.2",
 | 
			
		||||
		"typescript": "5.5.4",
 | 
			
		||||
		"ulid": "2.3.0",
 | 
			
		||||
		"vary": "1.1.2",
 | 
			
		||||
		"web-push": "3.6.7",
 | 
			
		||||
@@ -187,7 +186,7 @@
 | 
			
		||||
	},
 | 
			
		||||
	"devDependencies": {
 | 
			
		||||
		"@jest/globals": "29.7.0",
 | 
			
		||||
		"@nestjs/platform-express": "10.4.3",
 | 
			
		||||
		"@nestjs/platform-express": "10.3.10",
 | 
			
		||||
		"@simplewebauthn/types": "10.0.0",
 | 
			
		||||
		"@swc/jest": "0.2.36",
 | 
			
		||||
		"@types/accepts": "1.3.7",
 | 
			
		||||
@@ -196,10 +195,10 @@
 | 
			
		||||
		"@types/body-parser": "1.19.5",
 | 
			
		||||
		"@types/color-convert": "2.0.3",
 | 
			
		||||
		"@types/content-disposition": "0.5.8",
 | 
			
		||||
		"@types/fluent-ffmpeg": "2.1.26",
 | 
			
		||||
		"@types/fluent-ffmpeg": "2.1.24",
 | 
			
		||||
		"@types/htmlescape": "1.1.3",
 | 
			
		||||
		"@types/http-link-header": "1.0.7",
 | 
			
		||||
		"@types/jest": "29.5.13",
 | 
			
		||||
		"@types/jest": "29.5.12",
 | 
			
		||||
		"@types/js-yaml": "4.0.9",
 | 
			
		||||
		"@types/jsdom": "21.1.7",
 | 
			
		||||
		"@types/jsonld": "1.5.15",
 | 
			
		||||
@@ -207,18 +206,18 @@
 | 
			
		||||
		"@types/mime-types": "2.1.4",
 | 
			
		||||
		"@types/ms": "0.7.34",
 | 
			
		||||
		"@types/node": "20.14.12",
 | 
			
		||||
		"@types/nodemailer": "6.4.16",
 | 
			
		||||
		"@types/nodemailer": "6.4.15",
 | 
			
		||||
		"@types/oauth": "0.9.5",
 | 
			
		||||
		"@types/oauth2orize": "1.11.5",
 | 
			
		||||
		"@types/oauth2orize-pkce": "0.1.2",
 | 
			
		||||
		"@types/pg": "8.11.10",
 | 
			
		||||
		"@types/pg": "8.11.6",
 | 
			
		||||
		"@types/pug": "2.0.10",
 | 
			
		||||
		"@types/punycode": "2.1.4",
 | 
			
		||||
		"@types/qrcode": "1.5.5",
 | 
			
		||||
		"@types/random-seed": "0.3.5",
 | 
			
		||||
		"@types/ratelimiter": "3.4.6",
 | 
			
		||||
		"@types/rename": "1.0.7",
 | 
			
		||||
		"@types/sanitize-html": "2.13.0",
 | 
			
		||||
		"@types/sanitize-html": "2.11.0",
 | 
			
		||||
		"@types/semver": "7.5.8",
 | 
			
		||||
		"@types/simple-oauth2": "5.0.7",
 | 
			
		||||
		"@types/sinonjs__fake-timers": "8.1.5",
 | 
			
		||||
@@ -226,17 +225,17 @@
 | 
			
		||||
		"@types/tmp": "0.2.6",
 | 
			
		||||
		"@types/vary": "1.1.3",
 | 
			
		||||
		"@types/web-push": "3.6.3",
 | 
			
		||||
		"@types/ws": "8.5.12",
 | 
			
		||||
		"@types/ws": "8.5.11",
 | 
			
		||||
		"@typescript-eslint/eslint-plugin": "7.17.0",
 | 
			
		||||
		"@typescript-eslint/parser": "7.17.0",
 | 
			
		||||
		"aws-sdk-client-mock": "4.0.1",
 | 
			
		||||
		"cross-env": "7.0.3",
 | 
			
		||||
		"eslint-plugin-import": "2.30.0",
 | 
			
		||||
		"execa": "9.4.0",
 | 
			
		||||
		"eslint-plugin-import": "2.29.1",
 | 
			
		||||
		"execa": "9.3.0",
 | 
			
		||||
		"fkill": "9.0.0",
 | 
			
		||||
		"jest": "29.7.0",
 | 
			
		||||
		"jest-mock": "29.7.0",
 | 
			
		||||
		"nodemon": "3.1.7",
 | 
			
		||||
		"nodemon": "3.1.4",
 | 
			
		||||
		"pid-port": "1.0.0",
 | 
			
		||||
		"simple-oauth2": "5.1.0"
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,8 +13,6 @@ import { createPostgresDataSource } from './postgres.js';
 | 
			
		||||
import { RepositoryModule } from './models/RepositoryModule.js';
 | 
			
		||||
import { allSettled } from './misc/promise-tracker.js';
 | 
			
		||||
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
 | 
			
		||||
import { MiMeta } from '@/models/Meta.js';
 | 
			
		||||
import { GlobalEvents } from './core/GlobalEventService.js';
 | 
			
		||||
 | 
			
		||||
const $config: Provider = {
 | 
			
		||||
	provide: DI.config,
 | 
			
		||||
@@ -80,76 +78,11 @@ const $redisForTimelines: Provider = {
 | 
			
		||||
	inject: [DI.config],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const $redisForReactions: Provider = {
 | 
			
		||||
	provide: DI.redisForReactions,
 | 
			
		||||
	useFactory: (config: Config) => {
 | 
			
		||||
		return new Redis.Redis(config.redisForReactions);
 | 
			
		||||
	},
 | 
			
		||||
	inject: [DI.config],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const $meta: Provider = {
 | 
			
		||||
	provide: DI.meta,
 | 
			
		||||
	useFactory: async (db: DataSource, redisForSub: Redis.Redis) => {
 | 
			
		||||
		const meta = await db.transaction(async transactionalEntityManager => {
 | 
			
		||||
			// 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する
 | 
			
		||||
			const metas = await transactionalEntityManager.find(MiMeta, {
 | 
			
		||||
				order: {
 | 
			
		||||
					id: 'DESC',
 | 
			
		||||
				},
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			const meta = metas[0];
 | 
			
		||||
 | 
			
		||||
			if (meta) {
 | 
			
		||||
				return meta;
 | 
			
		||||
			} else {
 | 
			
		||||
				// metaが空のときfetchMetaが同時に呼ばれるとここが同時に呼ばれてしまうことがあるのでフェイルセーフなupsertを使う
 | 
			
		||||
				const saved = await transactionalEntityManager
 | 
			
		||||
					.upsert(
 | 
			
		||||
						MiMeta,
 | 
			
		||||
						{
 | 
			
		||||
							id: 'x',
 | 
			
		||||
						},
 | 
			
		||||
						['id'],
 | 
			
		||||
					)
 | 
			
		||||
					.then((x) => transactionalEntityManager.findOneByOrFail(MiMeta, x.identifiers[0]));
 | 
			
		||||
 | 
			
		||||
				return saved;
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		async function onMessage(_: string, data: string): Promise<void> {
 | 
			
		||||
			const obj = JSON.parse(data);
 | 
			
		||||
 | 
			
		||||
			if (obj.channel === 'internal') {
 | 
			
		||||
				const { type, body } = obj.message as GlobalEvents['internal']['payload'];
 | 
			
		||||
				switch (type) {
 | 
			
		||||
					case 'metaUpdated': {
 | 
			
		||||
						for (const key in body) {
 | 
			
		||||
							(meta as any)[key] = (body as any)[key];
 | 
			
		||||
						}
 | 
			
		||||
						meta.proxyAccount = null; // joinなカラムは通常取ってこないので
 | 
			
		||||
						break;
 | 
			
		||||
					}
 | 
			
		||||
					default:
 | 
			
		||||
						break;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		redisForSub.on('message', onMessage);
 | 
			
		||||
 | 
			
		||||
		return meta;
 | 
			
		||||
	},
 | 
			
		||||
	inject: [DI.db, DI.redisForSub],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@Global()
 | 
			
		||||
@Module({
 | 
			
		||||
	imports: [RepositoryModule],
 | 
			
		||||
	providers: [$config, $db, $meta, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions],
 | 
			
		||||
	exports: [$config, $db, $meta, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions, RepositoryModule],
 | 
			
		||||
	providers: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines],
 | 
			
		||||
	exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, RepositoryModule],
 | 
			
		||||
})
 | 
			
		||||
export class GlobalModule implements OnApplicationShutdown {
 | 
			
		||||
	constructor(
 | 
			
		||||
@@ -158,7 +91,6 @@ export class GlobalModule implements OnApplicationShutdown {
 | 
			
		||||
		@Inject(DI.redisForPub) private redisForPub: Redis.Redis,
 | 
			
		||||
		@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
 | 
			
		||||
		@Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis,
 | 
			
		||||
		@Inject(DI.redisForReactions) private redisForReactions: Redis.Redis,
 | 
			
		||||
	) { }
 | 
			
		||||
 | 
			
		||||
	public async dispose(): Promise<void> {
 | 
			
		||||
@@ -171,7 +103,6 @@ export class GlobalModule implements OnApplicationShutdown {
 | 
			
		||||
			this.redisForPub.disconnect(),
 | 
			
		||||
			this.redisForSub.disconnect(),
 | 
			
		||||
			this.redisForTimelines.disconnect(),
 | 
			
		||||
			this.redisForReactions.disconnect(),
 | 
			
		||||
		]);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -49,7 +49,6 @@ type Source = {
 | 
			
		||||
	redisForPubsub?: RedisOptionsSource;
 | 
			
		||||
	redisForJobQueue?: RedisOptionsSource;
 | 
			
		||||
	redisForTimelines?: RedisOptionsSource;
 | 
			
		||||
	redisForReactions?: RedisOptionsSource;
 | 
			
		||||
	meilisearch?: {
 | 
			
		||||
		host: string;
 | 
			
		||||
		port: string;
 | 
			
		||||
@@ -134,7 +133,7 @@ export type Config = {
 | 
			
		||||
	proxySmtp: string | undefined;
 | 
			
		||||
	proxyBypassHosts: string[] | undefined;
 | 
			
		||||
	allowedPrivateNetworks: string[] | undefined;
 | 
			
		||||
	maxFileSize: number;
 | 
			
		||||
	maxFileSize: number | undefined;
 | 
			
		||||
	clusterLimit: number | undefined;
 | 
			
		||||
	id: string;
 | 
			
		||||
	outgoingAddress: string | undefined;
 | 
			
		||||
@@ -161,10 +160,8 @@ export type Config = {
 | 
			
		||||
	authUrl: string;
 | 
			
		||||
	driveUrl: string;
 | 
			
		||||
	userAgent: string;
 | 
			
		||||
	frontendEntry: string;
 | 
			
		||||
	frontendManifestExists: boolean;
 | 
			
		||||
	frontendEmbedEntry: string;
 | 
			
		||||
	frontendEmbedManifestExists: boolean;
 | 
			
		||||
	clientEntry: string;
 | 
			
		||||
	clientManifestExists: boolean;
 | 
			
		||||
	mediaProxy: string;
 | 
			
		||||
	externalMediaProxyEnabled: boolean;
 | 
			
		||||
	videoThumbnailGenerator: string | null;
 | 
			
		||||
@@ -172,7 +169,6 @@ export type Config = {
 | 
			
		||||
	redisForPubsub: RedisOptions & RedisOptionsSource;
 | 
			
		||||
	redisForJobQueue: RedisOptions & RedisOptionsSource;
 | 
			
		||||
	redisForTimelines: RedisOptions & RedisOptionsSource;
 | 
			
		||||
	redisForReactions: RedisOptions & RedisOptionsSource;
 | 
			
		||||
	sentryForBackend: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; } | undefined;
 | 
			
		||||
	sentryForFrontend: { options: Partial<Sentry.NodeOptions> } | undefined;
 | 
			
		||||
	perChannelMaxNoteCacheCount: number;
 | 
			
		||||
@@ -200,16 +196,10 @@ const path = process.env.MISSKEY_CONFIG_YML
 | 
			
		||||
 | 
			
		||||
export function loadConfig(): Config {
 | 
			
		||||
	const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8'));
 | 
			
		||||
 | 
			
		||||
	const frontendManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_vite_/manifest.json');
 | 
			
		||||
	const frontendEmbedManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_embed_vite_/manifest.json');
 | 
			
		||||
	const frontendManifest = frontendManifestExists ?
 | 
			
		||||
		JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_vite_/manifest.json`, 'utf-8'))
 | 
			
		||||
	const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json');
 | 
			
		||||
	const clientManifest = clientManifestExists ?
 | 
			
		||||
		JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8'))
 | 
			
		||||
		: { 'src/_boot_.ts': { file: 'src/_boot_.ts' } };
 | 
			
		||||
	const frontendEmbedManifest = frontendEmbedManifestExists ?
 | 
			
		||||
		JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_embed_vite_/manifest.json`, 'utf-8'))
 | 
			
		||||
		: { 'src/boot.ts': { file: 'src/boot.ts' } };
 | 
			
		||||
 | 
			
		||||
	const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
 | 
			
		||||
 | 
			
		||||
	const url = tryCreateUrl(config.url ?? process.env.MISSKEY_URL ?? '');
 | 
			
		||||
@@ -253,7 +243,6 @@ export function loadConfig(): Config {
 | 
			
		||||
		redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis,
 | 
			
		||||
		redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis,
 | 
			
		||||
		redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,
 | 
			
		||||
		redisForReactions: config.redisForReactions ? convertRedisOptions(config.redisForReactions, host) : redis,
 | 
			
		||||
		sentryForBackend: config.sentryForBackend,
 | 
			
		||||
		sentryForFrontend: config.sentryForFrontend,
 | 
			
		||||
		id: config.id,
 | 
			
		||||
@@ -261,7 +250,7 @@ export function loadConfig(): Config {
 | 
			
		||||
		proxySmtp: config.proxySmtp,
 | 
			
		||||
		proxyBypassHosts: config.proxyBypassHosts,
 | 
			
		||||
		allowedPrivateNetworks: config.allowedPrivateNetworks,
 | 
			
		||||
		maxFileSize: config.maxFileSize ?? 262144000,
 | 
			
		||||
		maxFileSize: config.maxFileSize,
 | 
			
		||||
		clusterLimit: config.clusterLimit,
 | 
			
		||||
		outgoingAddress: config.outgoingAddress,
 | 
			
		||||
		outgoingAddressFamily: config.outgoingAddressFamily,
 | 
			
		||||
@@ -281,10 +270,8 @@ export function loadConfig(): Config {
 | 
			
		||||
			config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator
 | 
			
		||||
			: null,
 | 
			
		||||
		userAgent: `Misskey/${version} (${config.url})`,
 | 
			
		||||
		frontendEntry: frontendManifest['src/_boot_.ts'],
 | 
			
		||||
		frontendManifestExists: frontendManifestExists,
 | 
			
		||||
		frontendEmbedEntry: frontendEmbedManifest['src/boot.ts'],
 | 
			
		||||
		frontendEmbedManifestExists: frontendEmbedManifestExists,
 | 
			
		||||
		clientEntry: clientManifest['src/_boot_.ts'],
 | 
			
		||||
		clientManifestExists: clientManifestExists,
 | 
			
		||||
		perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000,
 | 
			
		||||
		perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500,
 | 
			
		||||
		deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,6 @@ export const MAX_NOTE_TEXT_LENGTH = 3000;
 | 
			
		||||
export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min
 | 
			
		||||
export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days
 | 
			
		||||
 | 
			
		||||
export const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
 | 
			
		||||
 | 
			
		||||
//#region hard limits
 | 
			
		||||
// If you change DB_* values, you must also change the DB schema.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -14,10 +14,10 @@ import type {
 | 
			
		||||
	AbuseReportNotificationRecipientRepository,
 | 
			
		||||
	MiAbuseReportNotificationRecipient,
 | 
			
		||||
	MiAbuseUserReport,
 | 
			
		||||
	MiMeta,
 | 
			
		||||
	MiUser,
 | 
			
		||||
} from '@/models/_.js';
 | 
			
		||||
import { EmailService } from '@/core/EmailService.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { RoleService } from '@/core/RoleService.js';
 | 
			
		||||
import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
 | 
			
		||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
 | 
			
		||||
@@ -27,19 +27,15 @@ import { IdService } from './IdService.js';
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class AbuseReportNotificationService implements OnApplicationShutdown {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.abuseReportNotificationRecipientRepository)
 | 
			
		||||
		private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.redisForSub)
 | 
			
		||||
		private redisForSub: Redis.Redis,
 | 
			
		||||
 | 
			
		||||
		private idService: IdService,
 | 
			
		||||
		private roleService: RoleService,
 | 
			
		||||
		private systemWebhookService: SystemWebhookService,
 | 
			
		||||
		private emailService: EmailService,
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
		private moderationLogService: ModerationLogService,
 | 
			
		||||
		private globalEventService: GlobalEventService,
 | 
			
		||||
	) {
 | 
			
		||||
@@ -97,8 +93,10 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
 | 
			
		||||
			.filter(x => x != null),
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		// 送信先の鮮度を保つため、毎回取得する
 | 
			
		||||
		const meta = await this.metaService.fetch(true);
 | 
			
		||||
		recipientEMailAddresses.push(
 | 
			
		||||
			...(this.meta.email ? [this.meta.email] : []),
 | 
			
		||||
			...(meta.email ? [meta.email] : []),
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		if (recipientEMailAddresses.length <= 0) {
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ import { IsNull, In, MoreThan, Not } from 'typeorm';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
 | 
			
		||||
import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MiMeta, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js';
 | 
			
		||||
import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js';
 | 
			
		||||
import type { RelationshipJobData, ThinUser } from '@/queue/types.js';
 | 
			
		||||
 | 
			
		||||
import { IdService } from '@/core/IdService.js';
 | 
			
		||||
@@ -22,15 +22,13 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
 | 
			
		||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
			
		||||
import { ProxyAccountService } from '@/core/ProxyAccountService.js';
 | 
			
		||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import InstanceChart from '@/core/chart/charts/instance.js';
 | 
			
		||||
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class AccountMoveService {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.usersRepository)
 | 
			
		||||
		private usersRepository: UsersRepository,
 | 
			
		||||
 | 
			
		||||
@@ -59,6 +57,7 @@ export class AccountMoveService {
 | 
			
		||||
		private perUserFollowingChart: PerUserFollowingChart,
 | 
			
		||||
		private federatedInstanceService: FederatedInstanceService,
 | 
			
		||||
		private instanceChart: InstanceChart,
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
		private relayService: RelayService,
 | 
			
		||||
		private queueService: QueueService,
 | 
			
		||||
	) {
 | 
			
		||||
@@ -277,7 +276,7 @@ export class AccountMoveService {
 | 
			
		||||
		if (this.userEntityService.isRemoteUser(oldAccount)) {
 | 
			
		||||
			this.federatedInstanceService.fetch(oldAccount.host).then(async i => {
 | 
			
		||||
				this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length);
 | 
			
		||||
				if (this.meta.enableChartsForFederatedInstances) {
 | 
			
		||||
				if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
 | 
			
		||||
					this.instanceChart.updateFollowers(i.host, false);
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
 
 | 
			
		||||
@@ -123,14 +123,11 @@ export class AntennaService implements OnApplicationShutdown {
 | 
			
		||||
		if (antenna.src === 'home') {
 | 
			
		||||
			// TODO
 | 
			
		||||
		} else if (antenna.src === 'list') {
 | 
			
		||||
			if (antenna.userListId == null) return false;
 | 
			
		||||
			const exists = await this.userListMembershipsRepository.exists({
 | 
			
		||||
				where: {
 | 
			
		||||
					userListId: antenna.userListId,
 | 
			
		||||
					userId: note.userId,
 | 
			
		||||
				},
 | 
			
		||||
			});
 | 
			
		||||
			if (!exists) return false;
 | 
			
		||||
			const listUsers = (await this.userListMembershipsRepository.findBy({
 | 
			
		||||
				userListId: antenna.userListId!,
 | 
			
		||||
			})).map(x => x.userId);
 | 
			
		||||
 | 
			
		||||
			if (!listUsers.includes(note.userId)) return false;
 | 
			
		||||
		} else if (antenna.src === 'users') {
 | 
			
		||||
			const accts = antenna.users.map(x => {
 | 
			
		||||
				const { username, host } = Acct.parse(x);
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,7 @@ export class AvatarDecorationService implements OnApplicationShutdown {
 | 
			
		||||
		private moderationLogService: ModerationLogService,
 | 
			
		||||
		private globalEventService: GlobalEventService,
 | 
			
		||||
	) {
 | 
			
		||||
		this.cache = new MemorySingleCache<MiAvatarDecoration[]>(1000 * 60 * 30); // 30s
 | 
			
		||||
		this.cache = new MemorySingleCache<MiAvatarDecoration[]>(1000 * 60 * 30);
 | 
			
		||||
 | 
			
		||||
		this.redisForSub.on('message', this.onMessage);
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -56,10 +56,10 @@ export class CacheService implements OnApplicationShutdown {
 | 
			
		||||
	) {
 | 
			
		||||
		//this.onMessage = this.onMessage.bind(this);
 | 
			
		||||
 | 
			
		||||
		this.userByIdCache = new MemoryKVCache<MiUser>(1000 * 60 * 5); // 5m
 | 
			
		||||
		this.localUserByNativeTokenCache = new MemoryKVCache<MiLocalUser | null>(1000 * 60 * 5); // 5m
 | 
			
		||||
		this.localUserByIdCache = new MemoryKVCache<MiLocalUser>(1000 * 60 * 5); // 5m
 | 
			
		||||
		this.uriPersonCache = new MemoryKVCache<MiUser | null>(1000 * 60 * 5); // 5m
 | 
			
		||||
		this.userByIdCache = new MemoryKVCache<MiUser>(Infinity);
 | 
			
		||||
		this.localUserByNativeTokenCache = new MemoryKVCache<MiLocalUser | null>(Infinity);
 | 
			
		||||
		this.localUserByIdCache = new MemoryKVCache<MiLocalUser>(Infinity);
 | 
			
		||||
		this.uriPersonCache = new MemoryKVCache<MiUser | null>(Infinity);
 | 
			
		||||
 | 
			
		||||
		this.userProfileCache = new RedisKVCache<MiUserProfile>(this.redisClient, 'userProfile', {
 | 
			
		||||
			lifetime: 1000 * 60 * 30, // 30m
 | 
			
		||||
@@ -135,14 +135,14 @@ export class CacheService implements OnApplicationShutdown {
 | 
			
		||||
					if (user == null) {
 | 
			
		||||
						this.userByIdCache.delete(body.id);
 | 
			
		||||
						this.localUserByIdCache.delete(body.id);
 | 
			
		||||
						for (const [k, v] of this.uriPersonCache.entries) {
 | 
			
		||||
						for (const [k, v] of this.uriPersonCache.cache.entries()) {
 | 
			
		||||
							if (v.value?.id === body.id) {
 | 
			
		||||
								this.uriPersonCache.delete(k);
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					} else {
 | 
			
		||||
						this.userByIdCache.set(user.id, user);
 | 
			
		||||
						for (const [k, v] of this.uriPersonCache.entries) {
 | 
			
		||||
						for (const [k, v] of this.uriPersonCache.cache.entries()) {
 | 
			
		||||
							if (v.value?.id === user.id) {
 | 
			
		||||
								this.uriPersonCache.set(k, user);
 | 
			
		||||
							}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,6 @@ import {
 | 
			
		||||
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
 | 
			
		||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
 | 
			
		||||
import { UserSearchService } from '@/core/UserSearchService.js';
 | 
			
		||||
import { WebhookTestService } from '@/core/WebhookTestService.js';
 | 
			
		||||
import { AccountMoveService } from './AccountMoveService.js';
 | 
			
		||||
import { AccountUpdateService } from './AccountUpdateService.js';
 | 
			
		||||
import { AiService } from './AiService.js';
 | 
			
		||||
@@ -50,7 +49,6 @@ import { PollService } from './PollService.js';
 | 
			
		||||
import { PushNotificationService } from './PushNotificationService.js';
 | 
			
		||||
import { QueryService } from './QueryService.js';
 | 
			
		||||
import { ReactionService } from './ReactionService.js';
 | 
			
		||||
import { ReactionsBufferingService } from './ReactionsBufferingService.js';
 | 
			
		||||
import { RelayService } from './RelayService.js';
 | 
			
		||||
import { RoleService } from './RoleService.js';
 | 
			
		||||
import { S3Service } from './S3Service.js';
 | 
			
		||||
@@ -194,7 +192,6 @@ const $ProxyAccountService: Provider = { provide: 'ProxyAccountService', useExis
 | 
			
		||||
const $PushNotificationService: Provider = { provide: 'PushNotificationService', useExisting: PushNotificationService };
 | 
			
		||||
const $QueryService: Provider = { provide: 'QueryService', useExisting: QueryService };
 | 
			
		||||
const $ReactionService: Provider = { provide: 'ReactionService', useExisting: ReactionService };
 | 
			
		||||
const $ReactionsBufferingService: Provider = { provide: 'ReactionsBufferingService', useExisting: ReactionsBufferingService };
 | 
			
		||||
const $RelayService: Provider = { provide: 'RelayService', useExisting: RelayService };
 | 
			
		||||
const $RoleService: Provider = { provide: 'RoleService', useExisting: RoleService };
 | 
			
		||||
const $S3Service: Provider = { provide: 'S3Service', useExisting: S3Service };
 | 
			
		||||
@@ -214,7 +211,6 @@ const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: Us
 | 
			
		||||
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
 | 
			
		||||
const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
 | 
			
		||||
const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
 | 
			
		||||
const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService };
 | 
			
		||||
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
 | 
			
		||||
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
 | 
			
		||||
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
 | 
			
		||||
@@ -344,7 +340,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		PushNotificationService,
 | 
			
		||||
		QueryService,
 | 
			
		||||
		ReactionService,
 | 
			
		||||
		ReactionsBufferingService,
 | 
			
		||||
		RelayService,
 | 
			
		||||
		RoleService,
 | 
			
		||||
		S3Service,
 | 
			
		||||
@@ -364,7 +359,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		VideoProcessingService,
 | 
			
		||||
		UserWebhookService,
 | 
			
		||||
		SystemWebhookService,
 | 
			
		||||
		WebhookTestService,
 | 
			
		||||
		UtilityService,
 | 
			
		||||
		FileInfoService,
 | 
			
		||||
		SearchService,
 | 
			
		||||
@@ -490,7 +484,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		$PushNotificationService,
 | 
			
		||||
		$QueryService,
 | 
			
		||||
		$ReactionService,
 | 
			
		||||
		$ReactionsBufferingService,
 | 
			
		||||
		$RelayService,
 | 
			
		||||
		$RoleService,
 | 
			
		||||
		$S3Service,
 | 
			
		||||
@@ -510,7 +503,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		$VideoProcessingService,
 | 
			
		||||
		$UserWebhookService,
 | 
			
		||||
		$SystemWebhookService,
 | 
			
		||||
		$WebhookTestService,
 | 
			
		||||
		$UtilityService,
 | 
			
		||||
		$FileInfoService,
 | 
			
		||||
		$SearchService,
 | 
			
		||||
@@ -637,7 +629,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		PushNotificationService,
 | 
			
		||||
		QueryService,
 | 
			
		||||
		ReactionService,
 | 
			
		||||
		ReactionsBufferingService,
 | 
			
		||||
		RelayService,
 | 
			
		||||
		RoleService,
 | 
			
		||||
		S3Service,
 | 
			
		||||
@@ -657,7 +648,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		VideoProcessingService,
 | 
			
		||||
		UserWebhookService,
 | 
			
		||||
		SystemWebhookService,
 | 
			
		||||
		WebhookTestService,
 | 
			
		||||
		UtilityService,
 | 
			
		||||
		FileInfoService,
 | 
			
		||||
		SearchService,
 | 
			
		||||
@@ -782,7 +772,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		$PushNotificationService,
 | 
			
		||||
		$QueryService,
 | 
			
		||||
		$ReactionService,
 | 
			
		||||
		$ReactionsBufferingService,
 | 
			
		||||
		$RelayService,
 | 
			
		||||
		$RoleService,
 | 
			
		||||
		$S3Service,
 | 
			
		||||
@@ -802,7 +791,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		$VideoProcessingService,
 | 
			
		||||
		$UserWebhookService,
 | 
			
		||||
		$SystemWebhookService,
 | 
			
		||||
		$WebhookTestService,
 | 
			
		||||
		$UtilityService,
 | 
			
		||||
		$FileInfoService,
 | 
			
		||||
		$SearchService,
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ const parseEmojiStrRegexp = /^([-\w]+)(?:@([\w.-]+))?$/;
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class CustomEmojiService implements OnApplicationShutdown {
 | 
			
		||||
	private emojisCache: MemoryKVCache<MiEmoji | null>;
 | 
			
		||||
	private cache: MemoryKVCache<MiEmoji | null>;
 | 
			
		||||
	public localEmojisCache: RedisSingleCache<Map<string, MiEmoji>>;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
@@ -40,7 +40,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
 | 
			
		||||
		private moderationLogService: ModerationLogService,
 | 
			
		||||
		private globalEventService: GlobalEventService,
 | 
			
		||||
	) {
 | 
			
		||||
		this.emojisCache = new MemoryKVCache<MiEmoji | null>(1000 * 60 * 60 * 12); // 12h
 | 
			
		||||
		this.cache = new MemoryKVCache<MiEmoji | null>(1000 * 60 * 60 * 12);
 | 
			
		||||
 | 
			
		||||
		this.localEmojisCache = new RedisSingleCache<Map<string, MiEmoji>>(this.redisClient, 'localEmojis', {
 | 
			
		||||
			lifetime: 1000 * 60 * 30, // 30m
 | 
			
		||||
@@ -334,7 +334,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
 | 
			
		||||
			host,
 | 
			
		||||
		})) ?? null;
 | 
			
		||||
 | 
			
		||||
		const emoji = await this.emojisCache.fetch(`${name} ${host}`, queryOrNull);
 | 
			
		||||
		const emoji = await this.cache.fetch(`${name} ${host}`, queryOrNull);
 | 
			
		||||
 | 
			
		||||
		if (emoji == null) return null;
 | 
			
		||||
		return emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
 | 
			
		||||
@@ -361,7 +361,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
 | 
			
		||||
	 */
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async prefetchEmojis(emojis: { name: string; host: string | null; }[]): Promise<void> {
 | 
			
		||||
		const notCachedEmojis = emojis.filter(emoji => this.emojisCache.get(`${emoji.name} ${emoji.host}`) == null);
 | 
			
		||||
		const notCachedEmojis = emojis.filter(emoji => this.cache.get(`${emoji.name} ${emoji.host}`) == null);
 | 
			
		||||
		const emojisQuery: any[] = [];
 | 
			
		||||
		const hosts = new Set(notCachedEmojis.map(e => e.host));
 | 
			
		||||
		for (const host of hosts) {
 | 
			
		||||
@@ -376,7 +376,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
 | 
			
		||||
			select: ['name', 'host', 'originalUrl', 'publicUrl'],
 | 
			
		||||
		}) : [];
 | 
			
		||||
		for (const emoji of _emojis) {
 | 
			
		||||
			this.emojisCache.set(`${emoji.name} ${emoji.host}`, emoji);
 | 
			
		||||
			this.cache.set(`${emoji.name} ${emoji.host}`, emoji);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -401,7 +401,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public dispose(): void {
 | 
			
		||||
		this.emojisCache.dispose();
 | 
			
		||||
		this.cache.dispose();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
 
 | 
			
		||||
@@ -42,7 +42,7 @@ export class DownloadService {
 | 
			
		||||
 | 
			
		||||
		const timeout = 30 * 1000;
 | 
			
		||||
		const operationTimeout = 60 * 1000;
 | 
			
		||||
		const maxSize = this.config.maxFileSize;
 | 
			
		||||
		const maxSize = this.config.maxFileSize ?? 262144000;
 | 
			
		||||
 | 
			
		||||
		const urlObj = new URL(url);
 | 
			
		||||
		let filename = urlObj.pathname.split('/').pop() ?? 'untitled';
 | 
			
		||||
 
 | 
			
		||||
@@ -11,10 +11,11 @@ import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
 | 
			
		||||
import { IsNull } from 'typeorm';
 | 
			
		||||
import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository, MiMeta } from '@/models/_.js';
 | 
			
		||||
import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/_.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
import Logger from '@/logger.js';
 | 
			
		||||
import type { MiRemoteUser, MiUser } from '@/models/User.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { MiDriveFile } from '@/models/DriveFile.js';
 | 
			
		||||
import { IdService } from '@/core/IdService.js';
 | 
			
		||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
 | 
			
		||||
@@ -98,9 +99,6 @@ export class DriveService {
 | 
			
		||||
		@Inject(DI.config)
 | 
			
		||||
		private config: Config,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.usersRepository)
 | 
			
		||||
		private usersRepository: UsersRepository,
 | 
			
		||||
 | 
			
		||||
@@ -117,6 +115,7 @@ export class DriveService {
 | 
			
		||||
		private userEntityService: UserEntityService,
 | 
			
		||||
		private driveFileEntityService: DriveFileEntityService,
 | 
			
		||||
		private idService: IdService,
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
		private downloadService: DownloadService,
 | 
			
		||||
		private internalStorageService: InternalStorageService,
 | 
			
		||||
		private s3Service: S3Service,
 | 
			
		||||
@@ -150,7 +149,9 @@ export class DriveService {
 | 
			
		||||
	// thunbnail, webpublic を必要なら生成
 | 
			
		||||
		const alts = await this.generateAlts(path, type, !file.uri);
 | 
			
		||||
 | 
			
		||||
		if (this.meta.useObjectStorage) {
 | 
			
		||||
		const meta = await this.metaService.fetch();
 | 
			
		||||
 | 
			
		||||
		if (meta.useObjectStorage) {
 | 
			
		||||
		//#region ObjectStorage params
 | 
			
		||||
			let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) ?? ['']);
 | 
			
		||||
 | 
			
		||||
@@ -169,11 +170,11 @@ export class DriveService {
 | 
			
		||||
				ext = '';
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const baseUrl = this.meta.objectStorageBaseUrl
 | 
			
		||||
				?? `${ this.meta.objectStorageUseSSL ? 'https' : 'http' }://${ this.meta.objectStorageEndpoint }${ this.meta.objectStoragePort ? `:${this.meta.objectStoragePort}` : '' }/${ this.meta.objectStorageBucket }`;
 | 
			
		||||
			const baseUrl = meta.objectStorageBaseUrl
 | 
			
		||||
				?? `${ meta.objectStorageUseSSL ? 'https' : 'http' }://${ meta.objectStorageEndpoint }${ meta.objectStoragePort ? `:${meta.objectStoragePort}` : '' }/${ meta.objectStorageBucket }`;
 | 
			
		||||
 | 
			
		||||
			// for original
 | 
			
		||||
			const key = `${this.meta.objectStoragePrefix}/${randomUUID()}${ext}`;
 | 
			
		||||
			const key = `${meta.objectStoragePrefix}/${randomUUID()}${ext}`;
 | 
			
		||||
			const url = `${ baseUrl }/${ key }`;
 | 
			
		||||
 | 
			
		||||
			// for alts
 | 
			
		||||
@@ -190,7 +191,7 @@ export class DriveService {
 | 
			
		||||
			];
 | 
			
		||||
 | 
			
		||||
			if (alts.webpublic) {
 | 
			
		||||
				webpublicKey = `${this.meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`;
 | 
			
		||||
				webpublicKey = `${meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`;
 | 
			
		||||
				webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
 | 
			
		||||
 | 
			
		||||
				this.registerLogger.info(`uploading webpublic: ${webpublicKey}`);
 | 
			
		||||
@@ -198,7 +199,7 @@ export class DriveService {
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (alts.thumbnail) {
 | 
			
		||||
				thumbnailKey = `${this.meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`;
 | 
			
		||||
				thumbnailKey = `${meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`;
 | 
			
		||||
				thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
 | 
			
		||||
 | 
			
		||||
				this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`);
 | 
			
		||||
@@ -375,8 +376,10 @@ export class DriveService {
 | 
			
		||||
		if (type === 'image/apng') type = 'image/png';
 | 
			
		||||
		if (!FILE_TYPE_BROWSERSAFE.includes(type)) type = 'application/octet-stream';
 | 
			
		||||
 | 
			
		||||
		const meta = await this.metaService.fetch();
 | 
			
		||||
 | 
			
		||||
		const params = {
 | 
			
		||||
			Bucket: this.meta.objectStorageBucket,
 | 
			
		||||
			Bucket: meta.objectStorageBucket,
 | 
			
		||||
			Key: key,
 | 
			
		||||
			Body: stream,
 | 
			
		||||
			ContentType: type,
 | 
			
		||||
@@ -389,9 +392,9 @@ export class DriveService {
 | 
			
		||||
			// 許可されているファイル形式でしか拡張子をつけない
 | 
			
		||||
			ext ? correctFilename(filename, ext) : filename,
 | 
			
		||||
		);
 | 
			
		||||
		if (this.meta.objectStorageSetPublicRead) params.ACL = 'public-read';
 | 
			
		||||
		if (meta.objectStorageSetPublicRead) params.ACL = 'public-read';
 | 
			
		||||
 | 
			
		||||
		await this.s3Service.upload(this.meta, params)
 | 
			
		||||
		await this.s3Service.upload(meta, params)
 | 
			
		||||
			.then(
 | 
			
		||||
				result => {
 | 
			
		||||
					if ('Bucket' in result) { // CompleteMultipartUploadCommandOutput
 | 
			
		||||
@@ -457,31 +460,32 @@ export class DriveService {
 | 
			
		||||
		ext = null,
 | 
			
		||||
	}: AddFileArgs): Promise<MiDriveFile> {
 | 
			
		||||
		let skipNsfwCheck = false;
 | 
			
		||||
		const instance = await this.metaService.fetch();
 | 
			
		||||
		const userRoleNSFW = user && (await this.roleService.getUserPolicies(user.id)).alwaysMarkNsfw;
 | 
			
		||||
		if (user == null) {
 | 
			
		||||
			skipNsfwCheck = true;
 | 
			
		||||
		} else if (userRoleNSFW) {
 | 
			
		||||
			skipNsfwCheck = true;
 | 
			
		||||
		}
 | 
			
		||||
		if (this.meta.sensitiveMediaDetection === 'none') skipNsfwCheck = true;
 | 
			
		||||
		if (user && this.meta.sensitiveMediaDetection === 'local' && this.userEntityService.isRemoteUser(user)) skipNsfwCheck = true;
 | 
			
		||||
		if (user && this.meta.sensitiveMediaDetection === 'remote' && this.userEntityService.isLocalUser(user)) skipNsfwCheck = true;
 | 
			
		||||
		if (instance.sensitiveMediaDetection === 'none') skipNsfwCheck = true;
 | 
			
		||||
		if (user && instance.sensitiveMediaDetection === 'local' && this.userEntityService.isRemoteUser(user)) skipNsfwCheck = true;
 | 
			
		||||
		if (user && instance.sensitiveMediaDetection === 'remote' && this.userEntityService.isLocalUser(user)) skipNsfwCheck = true;
 | 
			
		||||
 | 
			
		||||
		const info = await this.fileInfoService.getFileInfo(path, {
 | 
			
		||||
			skipSensitiveDetection: skipNsfwCheck,
 | 
			
		||||
			sensitiveThreshold: // 感度が高いほどしきい値は低くすることになる
 | 
			
		||||
			this.meta.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 :
 | 
			
		||||
			this.meta.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 :
 | 
			
		||||
			this.meta.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 :
 | 
			
		||||
			this.meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 :
 | 
			
		||||
			instance.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 :
 | 
			
		||||
			instance.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 :
 | 
			
		||||
			instance.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 :
 | 
			
		||||
			instance.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 :
 | 
			
		||||
			0.5,
 | 
			
		||||
			sensitiveThresholdForPorn: 0.75,
 | 
			
		||||
			enableSensitiveMediaDetectionForVideos: this.meta.enableSensitiveMediaDetectionForVideos,
 | 
			
		||||
			enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos,
 | 
			
		||||
		});
 | 
			
		||||
		this.registerLogger.info(`${JSON.stringify(info)}`);
 | 
			
		||||
 | 
			
		||||
		// 現状 false positive が多すぎて実用に耐えない
 | 
			
		||||
		//if (info.porn && this.meta.disallowUploadWhenPredictedAsPorn) {
 | 
			
		||||
		//if (info.porn && instance.disallowUploadWhenPredictedAsPorn) {
 | 
			
		||||
		//	throw new IdentifiableError('282f77bf-5816-4f72-9264-aa14d8261a21', 'Detected as porn.');
 | 
			
		||||
		//}
 | 
			
		||||
 | 
			
		||||
@@ -585,9 +589,9 @@ export class DriveService {
 | 
			
		||||
			sensitive ?? false
 | 
			
		||||
			: false;
 | 
			
		||||
 | 
			
		||||
		if (user && this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) file.isSensitive = true;
 | 
			
		||||
		if (user && this.utilityService.isMediaSilencedHost(instance.mediaSilencedHosts, user.host)) file.isSensitive = true;
 | 
			
		||||
		if (info.sensitive && profile!.autoSensitive) file.isSensitive = true;
 | 
			
		||||
		if (info.sensitive && this.meta.setSensitiveFlagAutomatically) file.isSensitive = true;
 | 
			
		||||
		if (info.sensitive && instance.setSensitiveFlagAutomatically) file.isSensitive = true;
 | 
			
		||||
		if (userRoleNSFW) file.isSensitive = true;
 | 
			
		||||
 | 
			
		||||
		if (url !== null) {
 | 
			
		||||
@@ -648,7 +652,7 @@ export class DriveService {
 | 
			
		||||
			// ローカルユーザーのみ
 | 
			
		||||
			this.perUserDriveChart.update(file, true);
 | 
			
		||||
		} else {
 | 
			
		||||
			if (this.meta.enableChartsForFederatedInstances) {
 | 
			
		||||
			if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
 | 
			
		||||
				this.instanceChart.updateDrive(file, true);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
@@ -794,7 +798,7 @@ export class DriveService {
 | 
			
		||||
			// ローカルユーザーのみ
 | 
			
		||||
			this.perUserDriveChart.update(file, false);
 | 
			
		||||
		} else {
 | 
			
		||||
			if (this.meta.enableChartsForFederatedInstances) {
 | 
			
		||||
			if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
 | 
			
		||||
				this.instanceChart.updateDrive(file, false);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
@@ -816,13 +820,14 @@ export class DriveService {
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async deleteObjectStorageFile(key: string) {
 | 
			
		||||
		const meta = await this.metaService.fetch();
 | 
			
		||||
		try {
 | 
			
		||||
			const param = {
 | 
			
		||||
				Bucket: this.meta.objectStorageBucket,
 | 
			
		||||
				Bucket: meta.objectStorageBucket,
 | 
			
		||||
				Key: key,
 | 
			
		||||
			} as DeleteObjectCommandInput;
 | 
			
		||||
 | 
			
		||||
			await this.s3Service.delete(this.meta, param);
 | 
			
		||||
			await this.s3Service.delete(meta, param);
 | 
			
		||||
		} catch (err: any) {
 | 
			
		||||
			if (err.name === 'NoSuchKey') {
 | 
			
		||||
				this.deleteLogger.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err as Error);
 | 
			
		||||
 
 | 
			
		||||
@@ -5,17 +5,18 @@
 | 
			
		||||
 | 
			
		||||
import { URLSearchParams } from 'node:url';
 | 
			
		||||
import * as nodemailer from 'nodemailer';
 | 
			
		||||
import juice from 'juice';
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { validate as validateEmail } from 'deep-email-validator';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { UtilityService } from '@/core/UtilityService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
import type Logger from '@/logger.js';
 | 
			
		||||
import type { MiMeta, UserProfilesRepository } from '@/models/_.js';
 | 
			
		||||
import type { UserProfilesRepository } from '@/models/_.js';
 | 
			
		||||
import { LoggerService } from '@/core/LoggerService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
 | 
			
		||||
import { QueueService } from '@/core/QueueService.js';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class EmailService {
 | 
			
		||||
@@ -25,41 +26,49 @@ export class EmailService {
 | 
			
		||||
		@Inject(DI.config)
 | 
			
		||||
		private config: Config,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.userProfilesRepository)
 | 
			
		||||
		private userProfilesRepository: UserProfilesRepository,
 | 
			
		||||
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
		private loggerService: LoggerService,
 | 
			
		||||
		private utilityService: UtilityService,
 | 
			
		||||
		private httpRequestService: HttpRequestService,
 | 
			
		||||
		private queueService: QueueService,
 | 
			
		||||
	) {
 | 
			
		||||
		this.logger = this.loggerService.getLogger('email');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async sendEmail(to: string, subject: string, html: string, text: string) {
 | 
			
		||||
		if (!this.meta.enableEmail) return;
 | 
			
		||||
		const meta = await this.metaService.fetch(true);
 | 
			
		||||
 | 
			
		||||
		if (!meta.enableEmail) return;
 | 
			
		||||
 | 
			
		||||
		const iconUrl = `${this.config.url}/static-assets/mi-white.png`;
 | 
			
		||||
		const emailSettingUrl = `${this.config.url}/settings/email`;
 | 
			
		||||
 | 
			
		||||
		const enableAuth = this.meta.smtpUser != null && this.meta.smtpUser !== '';
 | 
			
		||||
		const enableAuth = meta.smtpUser != null && meta.smtpUser !== '';
 | 
			
		||||
 | 
			
		||||
		const transporter = nodemailer.createTransport({
 | 
			
		||||
			host: this.meta.smtpHost,
 | 
			
		||||
			port: this.meta.smtpPort,
 | 
			
		||||
			secure: this.meta.smtpSecure,
 | 
			
		||||
			host: meta.smtpHost,
 | 
			
		||||
			port: meta.smtpPort,
 | 
			
		||||
			secure: meta.smtpSecure,
 | 
			
		||||
			ignoreTLS: !enableAuth,
 | 
			
		||||
			proxy: this.config.proxySmtp,
 | 
			
		||||
			auth: enableAuth ? {
 | 
			
		||||
				user: this.meta.smtpUser,
 | 
			
		||||
				pass: this.meta.smtpPass,
 | 
			
		||||
				user: meta.smtpUser,
 | 
			
		||||
				pass: meta.smtpPass,
 | 
			
		||||
			} : undefined,
 | 
			
		||||
		} as any);
 | 
			
		||||
 | 
			
		||||
		const htmlContent = `<!doctype html>
 | 
			
		||||
		try {
 | 
			
		||||
			// TODO: htmlサニタイズ
 | 
			
		||||
			const info = await transporter.sendMail({
 | 
			
		||||
				from: meta.email!,
 | 
			
		||||
				to: to,
 | 
			
		||||
				subject: subject,
 | 
			
		||||
				text: text,
 | 
			
		||||
				html: `<!doctype html>
 | 
			
		||||
<html>
 | 
			
		||||
	<head>
 | 
			
		||||
		<meta charset="utf-8">
 | 
			
		||||
@@ -124,7 +133,7 @@ export class EmailService {
 | 
			
		||||
	<body>
 | 
			
		||||
		<main>
 | 
			
		||||
			<header>
 | 
			
		||||
				<img src="${ this.meta.logoImageUrl ?? this.meta.iconUrl ?? iconUrl }"/>
 | 
			
		||||
				<img src="${ meta.logoImageUrl ?? meta.iconUrl ?? iconUrl }"/>
 | 
			
		||||
			</header>
 | 
			
		||||
			<article>
 | 
			
		||||
				<h1>${ subject }</h1>
 | 
			
		||||
@@ -138,18 +147,7 @@ export class EmailService {
 | 
			
		||||
			<a href="${ this.config.url }">${ this.config.host }</a>
 | 
			
		||||
		</nav>
 | 
			
		||||
	</body>
 | 
			
		||||
</html>`;
 | 
			
		||||
 | 
			
		||||
		const inlinedHtml = juice(htmlContent);
 | 
			
		||||
 | 
			
		||||
		try {
 | 
			
		||||
			// TODO: htmlサニタイズ
 | 
			
		||||
			const info = await transporter.sendMail({
 | 
			
		||||
				from: this.meta.email!,
 | 
			
		||||
				to: to,
 | 
			
		||||
				subject: subject,
 | 
			
		||||
				text: text,
 | 
			
		||||
				html: inlinedHtml,
 | 
			
		||||
</html>`,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			this.logger.info(`Message sent: ${info.messageId}`);
 | 
			
		||||
@@ -164,6 +162,8 @@ export class EmailService {
 | 
			
		||||
		available: boolean;
 | 
			
		||||
		reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned' | 'network' | 'blacklist';
 | 
			
		||||
	}> {
 | 
			
		||||
		const meta = await this.metaService.fetch();
 | 
			
		||||
 | 
			
		||||
		const exist = await this.userProfilesRepository.countBy({
 | 
			
		||||
			emailVerified: true,
 | 
			
		||||
			email: emailAddress,
 | 
			
		||||
@@ -181,11 +181,11 @@ export class EmailService {
 | 
			
		||||
			reason?: string | null,
 | 
			
		||||
		} = { valid: true, reason: null };
 | 
			
		||||
 | 
			
		||||
		if (this.meta.enableActiveEmailValidation) {
 | 
			
		||||
			if (this.meta.enableVerifymailApi && this.meta.verifymailAuthKey != null) {
 | 
			
		||||
				validated = await this.verifyMail(emailAddress, this.meta.verifymailAuthKey);
 | 
			
		||||
			} else if (this.meta.enableTruemailApi && this.meta.truemailInstance && this.meta.truemailAuthKey != null) {
 | 
			
		||||
				validated = await this.trueMail(this.meta.truemailInstance, emailAddress, this.meta.truemailAuthKey);
 | 
			
		||||
		if (meta.enableActiveEmailValidation) {
 | 
			
		||||
			if (meta.enableVerifymailApi && meta.verifymailAuthKey != null) {
 | 
			
		||||
				validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey);
 | 
			
		||||
			} else if (meta.enableTruemailApi && meta.truemailInstance && meta.truemailAuthKey != null) {
 | 
			
		||||
				validated = await this.trueMail(meta.truemailInstance, emailAddress, meta.truemailAuthKey);
 | 
			
		||||
			} else {
 | 
			
		||||
				validated = await validateEmail({
 | 
			
		||||
					email: emailAddress,
 | 
			
		||||
@@ -215,7 +215,7 @@ export class EmailService {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const emailDomain: string = emailAddress.split('@')[1];
 | 
			
		||||
		const isBanned = this.utilityService.isBlockedHost(this.meta.bannedEmailDomains, emailDomain);
 | 
			
		||||
		const isBanned = this.utilityService.isBlockedHost(meta.bannedEmailDomains, emailDomain);
 | 
			
		||||
 | 
			
		||||
		if (isBanned) {
 | 
			
		||||
			return {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,18 +10,16 @@ import type { MiUser } from '@/models/User.js';
 | 
			
		||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
 | 
			
		||||
import { IdService } from '@/core/IdService.js';
 | 
			
		||||
import type { MiHashtag } from '@/models/Hashtag.js';
 | 
			
		||||
import type { HashtagsRepository, MiMeta } from '@/models/_.js';
 | 
			
		||||
import type { HashtagsRepository } from '@/models/_.js';
 | 
			
		||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { FeaturedService } from '@/core/FeaturedService.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { UtilityService } from '@/core/UtilityService.js';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class HashtagService {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.redis)
 | 
			
		||||
		private redisClient: Redis.Redis, // TODO: 専用のRedisサーバーを設定できるようにする
 | 
			
		||||
 | 
			
		||||
@@ -31,6 +29,7 @@ export class HashtagService {
 | 
			
		||||
		private userEntityService: UserEntityService,
 | 
			
		||||
		private featuredService: FeaturedService,
 | 
			
		||||
		private idService: IdService,
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
		private utilityService: UtilityService,
 | 
			
		||||
	) {
 | 
			
		||||
	}
 | 
			
		||||
@@ -161,9 +160,10 @@ export class HashtagService {
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async updateHashtagsRanking(hashtag: string, userId: MiUser['id']): Promise<void> {
 | 
			
		||||
		const hiddenTags = this.meta.hiddenTags.map(t => normalizeForSearch(t));
 | 
			
		||||
		const instance = await this.metaService.fetch();
 | 
			
		||||
		const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t));
 | 
			
		||||
		if (hiddenTags.includes(hashtag)) return;
 | 
			
		||||
		if (this.utilityService.isKeyWordIncluded(hashtag, this.meta.sensitiveWords)) return;
 | 
			
		||||
		if (this.utilityService.isKeyWordIncluded(hashtag, instance.sensitiveWords)) return;
 | 
			
		||||
 | 
			
		||||
		// YYYYMMDDHHmm (10分間隔)
 | 
			
		||||
		const now = new Date();
 | 
			
		||||
 
 | 
			
		||||
@@ -8,12 +8,13 @@ import * as mfm from 'mfm-js';
 | 
			
		||||
import { In, DataSource, IsNull, LessThan } from 'typeorm';
 | 
			
		||||
import * as Redis from 'ioredis';
 | 
			
		||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
 | 
			
		||||
import RE2 from 're2';
 | 
			
		||||
import { extractMentions } from '@/misc/extract-mentions.js';
 | 
			
		||||
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
 | 
			
		||||
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 { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, 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';
 | 
			
		||||
@@ -22,8 +23,11 @@ import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
 | 
			
		||||
import type { IPoll } from '@/models/Poll.js';
 | 
			
		||||
import { MiPoll } from '@/models/Poll.js';
 | 
			
		||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
 | 
			
		||||
import { checkWordMute } from '@/misc/check-word-mute.js';
 | 
			
		||||
import type { MiChannel } from '@/models/Channel.js';
 | 
			
		||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
 | 
			
		||||
import { MemorySingleCache } from '@/misc/cache.js';
 | 
			
		||||
import type { MiUserProfile } from '@/models/UserProfile.js';
 | 
			
		||||
import { RelayService } from '@/core/RelayService.js';
 | 
			
		||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
@@ -47,6 +51,7 @@ import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
 | 
			
		||||
import { RoleService } from '@/core/RoleService.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { SearchService } from '@/core/SearchService.js';
 | 
			
		||||
import { FeaturedService } from '@/core/FeaturedService.js';
 | 
			
		||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
 | 
			
		||||
@@ -151,9 +156,6 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
			
		||||
		@Inject(DI.config)
 | 
			
		||||
		private config: Config,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.db)
 | 
			
		||||
		private db: DataSource,
 | 
			
		||||
 | 
			
		||||
@@ -208,6 +210,7 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
			
		||||
		private apDeliverManagerService: ApDeliverManagerService,
 | 
			
		||||
		private apRendererService: ApRendererService,
 | 
			
		||||
		private roleService: RoleService,
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
		private searchService: SearchService,
 | 
			
		||||
		private notesChart: NotesChart,
 | 
			
		||||
		private perUserNotesChart: PerUserNotesChart,
 | 
			
		||||
@@ -248,8 +251,10 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
			
		||||
		if (data.channel != null) data.visibleUsers = [];
 | 
			
		||||
		if (data.channel != null) data.localOnly = true;
 | 
			
		||||
 | 
			
		||||
		const meta = await this.metaService.fetch();
 | 
			
		||||
 | 
			
		||||
		if (data.visibility === 'public' && data.channel == null) {
 | 
			
		||||
			const sensitiveWords = this.meta.sensitiveWords;
 | 
			
		||||
			const sensitiveWords = meta.sensitiveWords;
 | 
			
		||||
			if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) {
 | 
			
		||||
				data.visibility = 'home';
 | 
			
		||||
			} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
 | 
			
		||||
@@ -257,17 +262,17 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const hasProhibitedWords = this.checkProhibitedWordsContain({
 | 
			
		||||
		const hasProhibitedWords = await this.checkProhibitedWordsContain({
 | 
			
		||||
			cw: data.cw,
 | 
			
		||||
			text: data.text,
 | 
			
		||||
			pollChoices: data.poll?.choices,
 | 
			
		||||
		}, this.meta.prohibitedWords);
 | 
			
		||||
		}, meta.prohibitedWords);
 | 
			
		||||
 | 
			
		||||
		if (hasProhibitedWords) {
 | 
			
		||||
			throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const inSilencedInstance = this.utilityService.isSilencedHost(this.meta.silencedHosts, user.host);
 | 
			
		||||
		const inSilencedInstance = this.utilityService.isSilencedHost(meta.silencedHosts, user.host);
 | 
			
		||||
 | 
			
		||||
		if (data.visibility === 'public' && inSilencedInstance && user.host !== null) {
 | 
			
		||||
			data.visibility = 'home';
 | 
			
		||||
@@ -360,7 +365,7 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// if the host is media-silenced, custom emojis are not allowed
 | 
			
		||||
		if (this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) emojis = [];
 | 
			
		||||
		if (this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, user.host)) emojis = [];
 | 
			
		||||
 | 
			
		||||
		tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32);
 | 
			
		||||
 | 
			
		||||
@@ -501,8 +506,10 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
			
		||||
		host: MiUser['host'];
 | 
			
		||||
		isBot: MiUser['isBot'];
 | 
			
		||||
	}, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) {
 | 
			
		||||
		const meta = await this.metaService.fetch();
 | 
			
		||||
 | 
			
		||||
		this.notesChart.update(note, true);
 | 
			
		||||
		if (note.visibility !== 'specified' && (this.meta.enableChartsForRemoteUser || (user.host == null))) {
 | 
			
		||||
		if (note.visibility !== 'specified' && (meta.enableChartsForRemoteUser || (user.host == null))) {
 | 
			
		||||
			this.perUserNotesChart.update(user, note, true);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -510,7 +517,7 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
			
		||||
		if (this.userEntityService.isRemoteUser(user)) {
 | 
			
		||||
			this.federatedInstanceService.fetch(user.host).then(async i => {
 | 
			
		||||
				this.instancesRepository.increment({ id: i.id }, 'notesCount', 1);
 | 
			
		||||
				if (this.meta.enableChartsForFederatedInstances) {
 | 
			
		||||
				if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
 | 
			
		||||
					this.instanceChart.updateNote(i.host, note, true);
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
@@ -846,14 +853,15 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) {
 | 
			
		||||
		if (!this.meta.enableFanoutTimeline) return;
 | 
			
		||||
		const meta = await this.metaService.fetch();
 | 
			
		||||
		if (!meta.enableFanoutTimeline) return;
 | 
			
		||||
 | 
			
		||||
		const r = this.redisForTimelines.pipeline();
 | 
			
		||||
 | 
			
		||||
		if (note.channelId) {
 | 
			
		||||
			this.fanoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r);
 | 
			
		||||
 | 
			
		||||
			this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r);
 | 
			
		||||
			this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
 | 
			
		||||
 | 
			
		||||
			const channelFollowings = await this.channelFollowingsRepository.find({
 | 
			
		||||
				where: {
 | 
			
		||||
@@ -863,9 +871,9 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			for (const channelFollowing of channelFollowings) {
 | 
			
		||||
				this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax, r);
 | 
			
		||||
				this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
 | 
			
		||||
				if (note.fileIds.length > 0) {
 | 
			
		||||
					this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r);
 | 
			
		||||
					this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
@@ -903,9 +911,9 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
			
		||||
					if (!following.withReplies) continue;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax, r);
 | 
			
		||||
				this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
 | 
			
		||||
				if (note.fileIds.length > 0) {
 | 
			
		||||
					this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r);
 | 
			
		||||
					this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
@@ -922,25 +930,25 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
			
		||||
					if (!userListMembership.withReplies) continue;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, this.meta.perUserListTimelineCacheMax, r);
 | 
			
		||||
				this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r);
 | 
			
		||||
				if (note.fileIds.length > 0) {
 | 
			
		||||
					this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, this.meta.perUserListTimelineCacheMax / 2, r);
 | 
			
		||||
					this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 自分自身のHTL
 | 
			
		||||
			if (note.userHost == null) {
 | 
			
		||||
				if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) {
 | 
			
		||||
					this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, this.meta.perUserHomeTimelineCacheMax, r);
 | 
			
		||||
					this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
 | 
			
		||||
					if (note.fileIds.length > 0) {
 | 
			
		||||
						this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r);
 | 
			
		||||
						this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 自分自身以外への返信
 | 
			
		||||
			if (isReply(note)) {
 | 
			
		||||
				this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r);
 | 
			
		||||
				this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
 | 
			
		||||
 | 
			
		||||
				if (note.visibility === 'public' && note.userHost == null) {
 | 
			
		||||
					this.fanoutTimelineService.push('localTimelineWithReplies', note.id, 300, r);
 | 
			
		||||
@@ -949,9 +957,9 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r);
 | 
			
		||||
				this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
 | 
			
		||||
				if (note.fileIds.length > 0) {
 | 
			
		||||
					this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax / 2 : this.meta.perRemoteUserUserTimelineCacheMax / 2, r);
 | 
			
		||||
					this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if (note.visibility === 'public' && note.userHost == null) {
 | 
			
		||||
@@ -1010,9 +1018,9 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public checkProhibitedWordsContain(content: Parameters<UtilityService['concatNoteContentsForKeyWordCheck']>[0], prohibitedWords?: string[]) {
 | 
			
		||||
	public async checkProhibitedWordsContain(content: Parameters<UtilityService['concatNoteContentsForKeyWordCheck']>[0], prohibitedWords?: string[]) {
 | 
			
		||||
		if (prohibitedWords == null) {
 | 
			
		||||
			prohibitedWords = this.meta.prohibitedWords;
 | 
			
		||||
			prohibitedWords = (await this.metaService.fetch()).prohibitedWords;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ import { Brackets, In } from 'typeorm';
 | 
			
		||||
import { Injectable, Inject } from '@nestjs/common';
 | 
			
		||||
import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
 | 
			
		||||
import type { MiNote, IMentionedRemoteUsers } from '@/models/Note.js';
 | 
			
		||||
import type { InstancesRepository, MiMeta, NotesRepository, UsersRepository } from '@/models/_.js';
 | 
			
		||||
import type { InstancesRepository, NotesRepository, UsersRepository } from '@/models/_.js';
 | 
			
		||||
import { RelayService } from '@/core/RelayService.js';
 | 
			
		||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
@@ -19,7 +19,9 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
			
		||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
 | 
			
		||||
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
 | 
			
		||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
			
		||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { SearchService } from '@/core/SearchService.js';
 | 
			
		||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
 | 
			
		||||
import { isQuote, isRenote } from '@/misc/is-renote.js';
 | 
			
		||||
@@ -30,9 +32,6 @@ export class NoteDeleteService {
 | 
			
		||||
		@Inject(DI.config)
 | 
			
		||||
		private config: Config,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.usersRepository)
 | 
			
		||||
		private usersRepository: UsersRepository,
 | 
			
		||||
 | 
			
		||||
@@ -43,11 +42,13 @@ export class NoteDeleteService {
 | 
			
		||||
		private instancesRepository: InstancesRepository,
 | 
			
		||||
 | 
			
		||||
		private userEntityService: UserEntityService,
 | 
			
		||||
		private noteEntityService: NoteEntityService,
 | 
			
		||||
		private globalEventService: GlobalEventService,
 | 
			
		||||
		private relayService: RelayService,
 | 
			
		||||
		private federatedInstanceService: FederatedInstanceService,
 | 
			
		||||
		private apRendererService: ApRendererService,
 | 
			
		||||
		private apDeliverManagerService: ApDeliverManagerService,
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
		private searchService: SearchService,
 | 
			
		||||
		private moderationLogService: ModerationLogService,
 | 
			
		||||
		private notesChart: NotesChart,
 | 
			
		||||
@@ -101,15 +102,17 @@ export class NoteDeleteService {
 | 
			
		||||
			}
 | 
			
		||||
			//#endregion
 | 
			
		||||
 | 
			
		||||
			const meta = await this.metaService.fetch();
 | 
			
		||||
 | 
			
		||||
			this.notesChart.update(note, false);
 | 
			
		||||
			if (this.meta.enableChartsForRemoteUser || (user.host == null)) {
 | 
			
		||||
			if (meta.enableChartsForRemoteUser || (user.host == null)) {
 | 
			
		||||
				this.perUserNotesChart.update(user, note, false);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (this.userEntityService.isRemoteUser(user)) {
 | 
			
		||||
				this.federatedInstanceService.fetch(user.host).then(async i => {
 | 
			
		||||
					this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1);
 | 
			
		||||
					if (this.meta.enableChartsForFederatedInstances) {
 | 
			
		||||
					if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
 | 
			
		||||
						this.instanceChart.updateNote(i.host, note, false);
 | 
			
		||||
					}
 | 
			
		||||
				});
 | 
			
		||||
 
 | 
			
		||||
@@ -4,25 +4,26 @@
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import type { MiMeta, UsersRepository } from '@/models/_.js';
 | 
			
		||||
import type { UsersRepository } from '@/models/_.js';
 | 
			
		||||
import type { MiLocalUser } from '@/models/User.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class ProxyAccountService {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.usersRepository)
 | 
			
		||||
		private usersRepository: UsersRepository,
 | 
			
		||||
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
	) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async fetch(): Promise<MiLocalUser | null> {
 | 
			
		||||
		if (this.meta.proxyAccountId == null) return null;
 | 
			
		||||
		return await this.usersRepository.findOneByOrFail({ id: this.meta.proxyAccountId }) as MiLocalUser;
 | 
			
		||||
		const meta = await this.metaService.fetch();
 | 
			
		||||
		if (meta.proxyAccountId == null) return null;
 | 
			
		||||
		return await this.usersRepository.findOneByOrFail({ id: meta.proxyAccountId }) as MiLocalUser;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,8 @@ import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
import type { Packed } from '@/misc/json-schema.js';
 | 
			
		||||
import { getNoteSummary } from '@/misc/get-note-summary.js';
 | 
			
		||||
import type { MiMeta, MiSwSubscription, SwSubscriptionsRepository } from '@/models/_.js';
 | 
			
		||||
import type { MiSwSubscription, SwSubscriptionsRepository } from '@/models/_.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { RedisKVCache } from '@/misc/cache.js';
 | 
			
		||||
 | 
			
		||||
@@ -53,14 +54,13 @@ export class PushNotificationService implements OnApplicationShutdown {
 | 
			
		||||
		@Inject(DI.config)
 | 
			
		||||
		private config: Config,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.redis)
 | 
			
		||||
		private redisClient: Redis.Redis,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.swSubscriptionsRepository)
 | 
			
		||||
		private swSubscriptionsRepository: SwSubscriptionsRepository,
 | 
			
		||||
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
	) {
 | 
			
		||||
		this.subscriptionsCache = new RedisKVCache<MiSwSubscription[]>(this.redisClient, 'userSwSubscriptions', {
 | 
			
		||||
			lifetime: 1000 * 60 * 60 * 1, // 1h
 | 
			
		||||
@@ -73,12 +73,14 @@ export class PushNotificationService implements OnApplicationShutdown {
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async pushNotification<T extends keyof PushNotificationsTypes>(userId: string, type: T, body: PushNotificationsTypes[T]) {
 | 
			
		||||
		if (!this.meta.enableServiceWorker || this.meta.swPublicKey == null || this.meta.swPrivateKey == null) return;
 | 
			
		||||
		const meta = await this.metaService.fetch();
 | 
			
		||||
 | 
			
		||||
		if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return;
 | 
			
		||||
 | 
			
		||||
		// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録
 | 
			
		||||
		push.setVapidDetails(this.config.url,
 | 
			
		||||
			this.meta.swPublicKey,
 | 
			
		||||
			this.meta.swPrivateKey);
 | 
			
		||||
			meta.swPublicKey,
 | 
			
		||||
			meta.swPrivateKey);
 | 
			
		||||
 | 
			
		||||
		const subscriptions = await this.subscriptionsCache.fetch(userId);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -87,12 +87,6 @@ export class QueueService {
 | 
			
		||||
			repeat: { pattern: '*/5 * * * *' },
 | 
			
		||||
			removeOnComplete: true,
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		this.systemQueue.add('bakeBufferedReactions', {
 | 
			
		||||
		}, {
 | 
			
		||||
			repeat: { pattern: '0 0 * * *' },
 | 
			
		||||
			removeOnComplete: true,
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
@@ -458,15 +452,10 @@ export class QueueService {
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @see UserWebhookDeliverJobData
 | 
			
		||||
	 * @see UserWebhookDeliverProcessorService
 | 
			
		||||
	 * @see WebhookDeliverProcessorService
 | 
			
		||||
	 */
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public userWebhookDeliver(
 | 
			
		||||
		webhook: MiWebhook,
 | 
			
		||||
		type: typeof webhookEventTypes[number],
 | 
			
		||||
		content: unknown,
 | 
			
		||||
		opts?: { attempts?: number },
 | 
			
		||||
	) {
 | 
			
		||||
	public userWebhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
 | 
			
		||||
		const data: UserWebhookDeliverJobData = {
 | 
			
		||||
			type,
 | 
			
		||||
			content,
 | 
			
		||||
@@ -479,7 +468,7 @@ export class QueueService {
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		return this.userWebhookDeliverQueue.add(webhook.id, data, {
 | 
			
		||||
			attempts: opts?.attempts ?? 4,
 | 
			
		||||
			attempts: 4,
 | 
			
		||||
			backoff: {
 | 
			
		||||
				type: 'custom',
 | 
			
		||||
			},
 | 
			
		||||
@@ -490,15 +479,10 @@ export class QueueService {
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @see SystemWebhookDeliverJobData
 | 
			
		||||
	 * @see SystemWebhookDeliverProcessorService
 | 
			
		||||
	 * @see WebhookDeliverProcessorService
 | 
			
		||||
	 */
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public systemWebhookDeliver(
 | 
			
		||||
		webhook: MiSystemWebhook,
 | 
			
		||||
		type: SystemWebhookEventType,
 | 
			
		||||
		content: unknown,
 | 
			
		||||
		opts?: { attempts?: number },
 | 
			
		||||
	) {
 | 
			
		||||
	public systemWebhookDeliver(webhook: MiSystemWebhook, type: SystemWebhookEventType, content: unknown) {
 | 
			
		||||
		const data: SystemWebhookDeliverJobData = {
 | 
			
		||||
			type,
 | 
			
		||||
			content,
 | 
			
		||||
@@ -510,7 +494,7 @@ export class QueueService {
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		return this.systemWebhookDeliverQueue.add(webhook.id, data, {
 | 
			
		||||
			attempts: opts?.attempts ?? 4,
 | 
			
		||||
			attempts: 4,
 | 
			
		||||
			backoff: {
 | 
			
		||||
				type: 'custom',
 | 
			
		||||
			},
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,9 @@
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import * as Redis from 'ioredis';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository, MiMeta } from '@/models/_.js';
 | 
			
		||||
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/_.js';
 | 
			
		||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
 | 
			
		||||
import type { MiRemoteUser, MiUser } from '@/models/User.js';
 | 
			
		||||
import type { MiNote } from '@/models/Note.js';
 | 
			
		||||
@@ -20,6 +21,7 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ
 | 
			
		||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 | 
			
		||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
			
		||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { UtilityService } from '@/core/UtilityService.js';
 | 
			
		||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
 | 
			
		||||
@@ -28,10 +30,9 @@ import { RoleService } from '@/core/RoleService.js';
 | 
			
		||||
import { FeaturedService } from '@/core/FeaturedService.js';
 | 
			
		||||
import { trackPromise } from '@/misc/promise-tracker.js';
 | 
			
		||||
import { isQuote, isRenote } from '@/misc/is-renote.js';
 | 
			
		||||
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
 | 
			
		||||
import { PER_NOTE_REACTION_USER_PAIR_CACHE_MAX } from '@/const.js';
 | 
			
		||||
 | 
			
		||||
const FALLBACK = '\u2764';
 | 
			
		||||
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
 | 
			
		||||
 | 
			
		||||
const legacies: Record<string, string> = {
 | 
			
		||||
	'like': '👍',
 | 
			
		||||
@@ -70,8 +71,8 @@ const decodeCustomEmojiRegexp = /^:([\w+-]+)(?:@([\w.-]+))?:$/;
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class ReactionService {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
		@Inject(DI.redis)
 | 
			
		||||
		private redisClient: Redis.Redis,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.usersRepository)
 | 
			
		||||
		private usersRepository: UsersRepository,
 | 
			
		||||
@@ -86,12 +87,12 @@ export class ReactionService {
 | 
			
		||||
		private emojisRepository: EmojisRepository,
 | 
			
		||||
 | 
			
		||||
		private utilityService: UtilityService,
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
		private customEmojiService: CustomEmojiService,
 | 
			
		||||
		private roleService: RoleService,
 | 
			
		||||
		private userEntityService: UserEntityService,
 | 
			
		||||
		private noteEntityService: NoteEntityService,
 | 
			
		||||
		private userBlockingService: UserBlockingService,
 | 
			
		||||
		private reactionsBufferingService: ReactionsBufferingService,
 | 
			
		||||
		private idService: IdService,
 | 
			
		||||
		private featuredService: FeaturedService,
 | 
			
		||||
		private globalEventService: GlobalEventService,
 | 
			
		||||
@@ -104,6 +105,8 @@ export class ReactionService {
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async create(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot'] }, note: MiNote, _reaction?: string | null) {
 | 
			
		||||
		const meta = await this.metaService.fetch();
 | 
			
		||||
 | 
			
		||||
		// Check blocking
 | 
			
		||||
		if (note.userId !== user.id) {
 | 
			
		||||
			const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
 | 
			
		||||
@@ -149,7 +152,7 @@ export class ReactionService {
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						// for media silenced host, custom emoji reactions are not allowed
 | 
			
		||||
						if (reacterHost != null && this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, reacterHost)) {
 | 
			
		||||
						if (reacterHost != null && this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, reacterHost)) {
 | 
			
		||||
							reaction = FALLBACK;
 | 
			
		||||
						}
 | 
			
		||||
					} else {
 | 
			
		||||
@@ -175,28 +178,14 @@ export class ReactionService {
 | 
			
		||||
			await this.noteReactionsRepository.insert(record);
 | 
			
		||||
		} catch (e) {
 | 
			
		||||
			if (isDuplicateKeyValueError(e)) {
 | 
			
		||||
				const exists = await this.noteReactionsRepository.findOneByOrFail({
 | 
			
		||||
					noteId: note.id,
 | 
			
		||||
					userId: user.id,
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				if (exists.reaction !== reaction) {
 | 
			
		||||
					// 別のリアクションがすでにされていたら置き換える
 | 
			
		||||
					await this.delete(user, note);
 | 
			
		||||
					await this.noteReactionsRepository.insert(record);
 | 
			
		||||
				} else {
 | 
			
		||||
				// 同じリアクションがすでにされていたらエラー
 | 
			
		||||
				throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298');
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				throw e;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Increment reactions count
 | 
			
		||||
		if (this.meta.enableReactionsBuffering) {
 | 
			
		||||
			await this.reactionsBufferingService.create(note.id, user.id, reaction, note.reactionAndUserPairCache);
 | 
			
		||||
		} else {
 | 
			
		||||
		const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;
 | 
			
		||||
		await this.notesRepository.createQueryBuilder().update()
 | 
			
		||||
			.set({
 | 
			
		||||
@@ -207,7 +196,6 @@ export class ReactionService {
 | 
			
		||||
			})
 | 
			
		||||
			.where('id = :id', { id: note.id })
 | 
			
		||||
			.execute();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 30%の確率、セルフではない、3日以内に投稿されたノートの場合ハイライト用ランキング更新
 | 
			
		||||
		if (
 | 
			
		||||
@@ -227,7 +215,7 @@ export class ReactionService {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (this.meta.enableChartsForRemoteUser || (user.host == null)) {
 | 
			
		||||
		if (meta.enableChartsForRemoteUser || (user.host == null)) {
 | 
			
		||||
			this.perUserReactionsChart.update(user, note);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -286,11 +274,12 @@ export class ReactionService {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async delete(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote) {
 | 
			
		||||
	public async delete(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, _reaction?: string | null) {
 | 
			
		||||
		// if already unreacted
 | 
			
		||||
		const exist = await this.noteReactionsRepository.findOneBy({
 | 
			
		||||
			noteId: note.id,
 | 
			
		||||
			userId: user.id,
 | 
			
		||||
			reaction: _reaction ?? FALLBACK,
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (exist == null) {
 | 
			
		||||
@@ -305,9 +294,6 @@ export class ReactionService {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Decrement reactions count
 | 
			
		||||
		if (this.meta.enableReactionsBuffering) {
 | 
			
		||||
			await this.reactionsBufferingService.delete(note.id, user.id, exist.reaction);
 | 
			
		||||
		} else {
 | 
			
		||||
		const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`;
 | 
			
		||||
		await this.notesRepository.createQueryBuilder().update()
 | 
			
		||||
			.set({
 | 
			
		||||
@@ -316,7 +302,6 @@ export class ReactionService {
 | 
			
		||||
			})
 | 
			
		||||
			.where('id = :id', { id: note.id })
 | 
			
		||||
			.execute();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		this.globalEventService.publishNoteStream(note.id, 'unreacted', {
 | 
			
		||||
			reaction: this.decodeReaction(exist.reaction).reaction,
 | 
			
		||||
@@ -337,7 +322,6 @@ export class ReactionService {
 | 
			
		||||
		//#endregion
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO: 廃止
 | 
			
		||||
	/**
 | 
			
		||||
	 * 文字列タイプのレガシーな形式のリアクションを現在の形式に変換しつつ、
 | 
			
		||||
	 * データベース上には存在する「0個のリアクションがついている」という情報を削除する。
 | 
			
		||||
 
 | 
			
		||||
@@ -1,162 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import * as Redis from 'ioredis';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { MiNote } from '@/models/Note.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import type { MiUser, NotesRepository } from '@/models/_.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
import { PER_NOTE_REACTION_USER_PAIR_CACHE_MAX } from '@/const.js';
 | 
			
		||||
 | 
			
		||||
const REDIS_DELTA_PREFIX = 'reactionsBufferDeltas';
 | 
			
		||||
const REDIS_PAIR_PREFIX = 'reactionsBufferPairs';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class ReactionsBufferingService {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.config)
 | 
			
		||||
		private config: Config,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.redisForReactions)
 | 
			
		||||
		private redisForReactions: Redis.Redis, // TODO: 専用のRedisインスタンスにする
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.notesRepository)
 | 
			
		||||
		private notesRepository: NotesRepository,
 | 
			
		||||
	) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async create(noteId: MiNote['id'], userId: MiUser['id'], reaction: string, currentPairs: string[]): Promise<void> {
 | 
			
		||||
		const pipeline = this.redisForReactions.pipeline();
 | 
			
		||||
		pipeline.hincrby(`${REDIS_DELTA_PREFIX}:${noteId}`, reaction, 1);
 | 
			
		||||
		for (let i = 0; i < currentPairs.length; i++) {
 | 
			
		||||
			pipeline.zadd(`${REDIS_PAIR_PREFIX}:${noteId}`, i, currentPairs[i]);
 | 
			
		||||
		}
 | 
			
		||||
		pipeline.zadd(`${REDIS_PAIR_PREFIX}:${noteId}`, Date.now(), `${userId}/${reaction}`);
 | 
			
		||||
		pipeline.zremrangebyrank(`${REDIS_PAIR_PREFIX}:${noteId}`, 0, -(PER_NOTE_REACTION_USER_PAIR_CACHE_MAX + 1));
 | 
			
		||||
		await pipeline.exec();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async delete(noteId: MiNote['id'], userId: MiUser['id'], reaction: string): Promise<void> {
 | 
			
		||||
		const pipeline = this.redisForReactions.pipeline();
 | 
			
		||||
		pipeline.hincrby(`${REDIS_DELTA_PREFIX}:${noteId}`, reaction, -1);
 | 
			
		||||
		pipeline.zrem(`${REDIS_PAIR_PREFIX}:${noteId}`, `${userId}/${reaction}`);
 | 
			
		||||
		// TODO: 「消した要素一覧」も持っておかないとcreateされた時に上書きされて復活する
 | 
			
		||||
		await pipeline.exec();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async get(noteId: MiNote['id']): Promise<{
 | 
			
		||||
		deltas: Record<string, number>;
 | 
			
		||||
		pairs: ([MiUser['id'], string])[];
 | 
			
		||||
	}> {
 | 
			
		||||
		const pipeline = this.redisForReactions.pipeline();
 | 
			
		||||
		pipeline.hgetall(`${REDIS_DELTA_PREFIX}:${noteId}`);
 | 
			
		||||
		pipeline.zrange(`${REDIS_PAIR_PREFIX}:${noteId}`, 0, -1);
 | 
			
		||||
		const results = await pipeline.exec();
 | 
			
		||||
 | 
			
		||||
		const resultDeltas = results![0][1] as Record<string, string>;
 | 
			
		||||
		const resultPairs = results![1][1] as string[];
 | 
			
		||||
 | 
			
		||||
		const deltas = {} as Record<string, number>;
 | 
			
		||||
		for (const [name, count] of Object.entries(resultDeltas)) {
 | 
			
		||||
			deltas[name] = parseInt(count);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const pairs = resultPairs.map(x => x.split('/') as [MiUser['id'], string]);
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			deltas,
 | 
			
		||||
			pairs,
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async getMany(noteIds: MiNote['id'][]): Promise<Map<MiNote['id'], {
 | 
			
		||||
		deltas: Record<string, number>;
 | 
			
		||||
		pairs: ([MiUser['id'], string])[];
 | 
			
		||||
	}>> {
 | 
			
		||||
		const map = new Map<MiNote['id'], {
 | 
			
		||||
			deltas: Record<string, number>;
 | 
			
		||||
			pairs: ([MiUser['id'], string])[];
 | 
			
		||||
		}>();
 | 
			
		||||
 | 
			
		||||
		const pipeline = this.redisForReactions.pipeline();
 | 
			
		||||
		for (const noteId of noteIds) {
 | 
			
		||||
			pipeline.hgetall(`${REDIS_DELTA_PREFIX}:${noteId}`);
 | 
			
		||||
			pipeline.zrange(`${REDIS_PAIR_PREFIX}:${noteId}`, 0, -1);
 | 
			
		||||
		}
 | 
			
		||||
		const results = await pipeline.exec();
 | 
			
		||||
 | 
			
		||||
		const opsForEachNotes = 2;
 | 
			
		||||
		for (let i = 0; i < noteIds.length; i++) {
 | 
			
		||||
			const noteId = noteIds[i];
 | 
			
		||||
			const resultDeltas = results![i * opsForEachNotes][1] as Record<string, string>;
 | 
			
		||||
			const resultPairs = results![i * opsForEachNotes + 1][1] as string[];
 | 
			
		||||
 | 
			
		||||
			const deltas = {} as Record<string, number>;
 | 
			
		||||
			for (const [name, count] of Object.entries(resultDeltas)) {
 | 
			
		||||
				deltas[name] = parseInt(count);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const pairs = resultPairs.map(x => x.split('/') as [MiUser['id'], string]);
 | 
			
		||||
 | 
			
		||||
			map.set(noteId, {
 | 
			
		||||
				deltas,
 | 
			
		||||
				pairs,
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return map;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO: scanは重い可能性があるので、別途 bufferedNoteIds を直接Redis上に持っておいてもいいかもしれない
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async bake(): Promise<void> {
 | 
			
		||||
		const bufferedNoteIds = [];
 | 
			
		||||
		let cursor = '0';
 | 
			
		||||
		do {
 | 
			
		||||
			// https://github.com/redis/ioredis#transparent-key-prefixing
 | 
			
		||||
			const result = await this.redisForReactions.scan(
 | 
			
		||||
				cursor,
 | 
			
		||||
				'MATCH',
 | 
			
		||||
				`${this.config.redis.prefix}:${REDIS_DELTA_PREFIX}:*`,
 | 
			
		||||
				'COUNT',
 | 
			
		||||
				'1000');
 | 
			
		||||
 | 
			
		||||
			cursor = result[0];
 | 
			
		||||
			bufferedNoteIds.push(...result[1].map(x => x.replace(`${this.config.redis.prefix}:${REDIS_DELTA_PREFIX}:`, '')));
 | 
			
		||||
		} while (cursor !== '0');
 | 
			
		||||
 | 
			
		||||
		const bufferedMap = await this.getMany(bufferedNoteIds);
 | 
			
		||||
 | 
			
		||||
		// clear
 | 
			
		||||
		const pipeline = this.redisForReactions.pipeline();
 | 
			
		||||
		for (const noteId of bufferedNoteIds) {
 | 
			
		||||
			pipeline.del(`${REDIS_DELTA_PREFIX}:${noteId}`);
 | 
			
		||||
			pipeline.del(`${REDIS_PAIR_PREFIX}:${noteId}`);
 | 
			
		||||
		}
 | 
			
		||||
		await pipeline.exec();
 | 
			
		||||
 | 
			
		||||
		// TODO: SQL一個にまとめたい
 | 
			
		||||
		for (const [noteId, buffered] of bufferedMap) {
 | 
			
		||||
			const sql = Object.entries(buffered.deltas)
 | 
			
		||||
				.map(([reaction, count]) =>
 | 
			
		||||
					`jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + ${count})::text::jsonb)`)
 | 
			
		||||
				.join(' || ');
 | 
			
		||||
 | 
			
		||||
			this.notesRepository.createQueryBuilder().update()
 | 
			
		||||
				.set({
 | 
			
		||||
					reactions: () => sql,
 | 
			
		||||
					reactionAndUserPairCache: buffered.pairs.map(x => x.join('/')),
 | 
			
		||||
				})
 | 
			
		||||
				.where('id = :id', { id: noteId })
 | 
			
		||||
				.execute();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -35,7 +35,7 @@ export class RelayService {
 | 
			
		||||
		private createSystemUserService: CreateSystemUserService,
 | 
			
		||||
		private apRendererService: ApRendererService,
 | 
			
		||||
	) {
 | 
			
		||||
		this.relaysCache = new MemorySingleCache<MiRelay[]>(1000 * 60 * 10); // 10m
 | 
			
		||||
		this.relaysCache = new MemorySingleCache<MiRelay[]>(1000 * 60 * 10);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,6 @@ import * as Redis from 'ioredis';
 | 
			
		||||
import { In } from 'typeorm';
 | 
			
		||||
import { ModuleRef } from '@nestjs/core';
 | 
			
		||||
import type {
 | 
			
		||||
	MiMeta,
 | 
			
		||||
	MiRole,
 | 
			
		||||
	MiRoleAssignment,
 | 
			
		||||
	RoleAssignmentsRepository,
 | 
			
		||||
@@ -19,6 +18,7 @@ import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js';
 | 
			
		||||
import type { MiUser } from '@/models/User.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { CacheService } from '@/core/CacheService.js';
 | 
			
		||||
import type { RoleCondFormulaValue } from '@/models/Role.js';
 | 
			
		||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
			
		||||
@@ -49,6 +49,7 @@ export type RolePolicies = {
 | 
			
		||||
	alwaysMarkNsfw: boolean;
 | 
			
		||||
	canUpdateBioMedia: boolean;
 | 
			
		||||
	pinLimit: number;
 | 
			
		||||
	reactionsPerNoteLimit: number;
 | 
			
		||||
	antennaLimit: number;
 | 
			
		||||
	wordMuteLimit: number;
 | 
			
		||||
	webhookLimit: number;
 | 
			
		||||
@@ -58,11 +59,6 @@ export type RolePolicies = {
 | 
			
		||||
	userEachUserListsLimit: number;
 | 
			
		||||
	rateLimitFactor: number;
 | 
			
		||||
	avatarDecorationLimit: number;
 | 
			
		||||
	canImportAntennas: boolean;
 | 
			
		||||
	canImportBlocking: boolean;
 | 
			
		||||
	canImportFollowing: boolean;
 | 
			
		||||
	canImportMuting: boolean;
 | 
			
		||||
	canImportUserLists: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const DEFAULT_POLICIES: RolePolicies = {
 | 
			
		||||
@@ -83,6 +79,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
 | 
			
		||||
	alwaysMarkNsfw: false,
 | 
			
		||||
	canUpdateBioMedia: true,
 | 
			
		||||
	pinLimit: 5,
 | 
			
		||||
	reactionsPerNoteLimit: 1,
 | 
			
		||||
	antennaLimit: 5,
 | 
			
		||||
	wordMuteLimit: 200,
 | 
			
		||||
	webhookLimit: 3,
 | 
			
		||||
@@ -92,11 +89,6 @@ export const DEFAULT_POLICIES: RolePolicies = {
 | 
			
		||||
	userEachUserListsLimit: 50,
 | 
			
		||||
	rateLimitFactor: 1,
 | 
			
		||||
	avatarDecorationLimit: 1,
 | 
			
		||||
	canImportAntennas: true,
 | 
			
		||||
	canImportBlocking: true,
 | 
			
		||||
	canImportFollowing: true,
 | 
			
		||||
	canImportMuting: true,
 | 
			
		||||
	canImportUserLists: true,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
@@ -111,8 +103,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 | 
			
		||||
	constructor(
 | 
			
		||||
		private moduleRef: ModuleRef,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
		@Inject(DI.redis)
 | 
			
		||||
		private redisClient: Redis.Redis,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.redisForTimelines)
 | 
			
		||||
		private redisForTimelines: Redis.Redis,
 | 
			
		||||
@@ -129,6 +121,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 | 
			
		||||
		@Inject(DI.roleAssignmentsRepository)
 | 
			
		||||
		private roleAssignmentsRepository: RoleAssignmentsRepository,
 | 
			
		||||
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
		private cacheService: CacheService,
 | 
			
		||||
		private userEntityService: UserEntityService,
 | 
			
		||||
		private globalEventService: GlobalEventService,
 | 
			
		||||
@@ -136,8 +129,10 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 | 
			
		||||
		private moderationLogService: ModerationLogService,
 | 
			
		||||
		private fanoutTimelineService: FanoutTimelineService,
 | 
			
		||||
	) {
 | 
			
		||||
		this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60); // 1h
 | 
			
		||||
		this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 5); // 5m
 | 
			
		||||
		//this.onMessage = this.onMessage.bind(this);
 | 
			
		||||
 | 
			
		||||
		this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60 * 1);
 | 
			
		||||
		this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 60 * 1);
 | 
			
		||||
 | 
			
		||||
		this.redisForSub.on('message', this.onMessage);
 | 
			
		||||
	}
 | 
			
		||||
@@ -348,7 +343,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async getUserPolicies(userId: MiUser['id'] | null): Promise<RolePolicies> {
 | 
			
		||||
		const basePolicies = { ...DEFAULT_POLICIES, ...this.meta.policies };
 | 
			
		||||
		const meta = await this.metaService.fetch();
 | 
			
		||||
		const basePolicies = { ...DEFAULT_POLICIES, ...meta.policies };
 | 
			
		||||
 | 
			
		||||
		if (userId == null) return basePolicies;
 | 
			
		||||
 | 
			
		||||
@@ -386,6 +382,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 | 
			
		||||
			alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
 | 
			
		||||
			canUpdateBioMedia: calc('canUpdateBioMedia', vs => vs.some(v => v === true)),
 | 
			
		||||
			pinLimit: calc('pinLimit', vs => Math.max(...vs)),
 | 
			
		||||
			reactionsPerNoteLimit: calc('reactionsPerNoteLimit', vs => Math.max(...vs)),
 | 
			
		||||
			antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
 | 
			
		||||
			wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)),
 | 
			
		||||
			webhookLimit: calc('webhookLimit', vs => Math.max(...vs)),
 | 
			
		||||
@@ -395,11 +392,6 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 | 
			
		||||
			userEachUserListsLimit: calc('userEachUserListsLimit', vs => Math.max(...vs)),
 | 
			
		||||
			rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)),
 | 
			
		||||
			avatarDecorationLimit: calc('avatarDecorationLimit', vs => Math.max(...vs)),
 | 
			
		||||
			canImportAntennas: calc('canImportAntennas', vs => vs.some(v => v === true)),
 | 
			
		||||
			canImportBlocking: calc('canImportBlocking', vs => vs.some(v => v === true)),
 | 
			
		||||
			canImportFollowing: calc('canImportFollowing', vs => vs.some(v => v === true)),
 | 
			
		||||
			canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)),
 | 
			
		||||
			canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)),
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import bcrypt from 'bcryptjs';
 | 
			
		||||
import { DataSource, IsNull } from 'typeorm';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { MiMeta, UsedUsernamesRepository, UsersRepository } from '@/models/_.js';
 | 
			
		||||
import type { UsedUsernamesRepository, UsersRepository } from '@/models/_.js';
 | 
			
		||||
import { MiUser } from '@/models/User.js';
 | 
			
		||||
import { MiUserProfile } from '@/models/UserProfile.js';
 | 
			
		||||
import { IdService } from '@/core/IdService.js';
 | 
			
		||||
@@ -20,6 +20,7 @@ import { InstanceActorService } from '@/core/InstanceActorService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import UsersChart from '@/core/chart/charts/users.js';
 | 
			
		||||
import { UtilityService } from '@/core/UtilityService.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { UserService } from '@/core/UserService.js';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
@@ -28,9 +29,6 @@ export class SignupService {
 | 
			
		||||
		@Inject(DI.db)
 | 
			
		||||
		private db: DataSource,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.usersRepository)
 | 
			
		||||
		private usersRepository: UsersRepository,
 | 
			
		||||
 | 
			
		||||
@@ -41,6 +39,7 @@ export class SignupService {
 | 
			
		||||
		private userService: UserService,
 | 
			
		||||
		private userEntityService: UserEntityService,
 | 
			
		||||
		private idService: IdService,
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
		private instanceActorService: InstanceActorService,
 | 
			
		||||
		private usersChart: UsersChart,
 | 
			
		||||
	) {
 | 
			
		||||
@@ -89,7 +88,8 @@ export class SignupService {
 | 
			
		||||
		const isTheFirstUser = !await this.instanceActorService.realLocalUsersPresent();
 | 
			
		||||
 | 
			
		||||
		if (!opts.ignorePreservedUsernames && !isTheFirstUser) {
 | 
			
		||||
			const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
 | 
			
		||||
			const instance = await this.metaService.fetch(true);
 | 
			
		||||
			const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
 | 
			
		||||
			if (isPreserved) {
 | 
			
		||||
				throw new Error('USED_USERNAME');
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
@@ -54,7 +54,7 @@ export class SystemWebhookService implements OnApplicationShutdown {
 | 
			
		||||
	 * SystemWebhook の一覧を取得する.
 | 
			
		||||
	 */
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public fetchSystemWebhooks(params?: {
 | 
			
		||||
	public async fetchSystemWebhooks(params?: {
 | 
			
		||||
		ids?: MiSystemWebhook['id'][];
 | 
			
		||||
		isActive?: MiSystemWebhook['isActive'];
 | 
			
		||||
		on?: MiSystemWebhook['on'];
 | 
			
		||||
@@ -165,24 +165,19 @@ export class SystemWebhookService implements OnApplicationShutdown {
 | 
			
		||||
	/**
 | 
			
		||||
	 * SystemWebhook をWebhook配送キューに追加する
 | 
			
		||||
	 * @see QueueService.systemWebhookDeliver
 | 
			
		||||
	 * // TODO: contentの型を厳格化する
 | 
			
		||||
	 */
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async enqueueSystemWebhook<T extends SystemWebhookEventType>(
 | 
			
		||||
		webhook: MiSystemWebhook | MiSystemWebhook['id'],
 | 
			
		||||
		type: T,
 | 
			
		||||
		content: unknown,
 | 
			
		||||
	) {
 | 
			
		||||
	public async enqueueSystemWebhook(webhook: MiSystemWebhook | MiSystemWebhook['id'], type: SystemWebhookEventType, content: unknown) {
 | 
			
		||||
		const webhookEntity = typeof webhook === 'string'
 | 
			
		||||
			? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)
 | 
			
		||||
			: webhook;
 | 
			
		||||
		if (!webhookEntity || !webhookEntity.isActive) {
 | 
			
		||||
			this.logger.info(`SystemWebhook is not active or not found : ${webhook}`);
 | 
			
		||||
			this.logger.info(`Webhook is not active or not found : ${webhook}`);
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (!webhookEntity.on.includes(type)) {
 | 
			
		||||
			this.logger.info(`SystemWebhook ${webhookEntity.id} is not listening to ${type}`);
 | 
			
		||||
			this.logger.info(`Webhook ${webhookEntity.id} is not listening to ${type}`);
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -13,20 +13,23 @@ import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
 | 
			
		||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
			
		||||
import { IdService } from '@/core/IdService.js';
 | 
			
		||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
 | 
			
		||||
import type { Packed } from '@/misc/json-schema.js';
 | 
			
		||||
import InstanceChart from '@/core/chart/charts/instance.js';
 | 
			
		||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
 | 
			
		||||
import { UserWebhookService } from '@/core/UserWebhookService.js';
 | 
			
		||||
import { NotificationService } from '@/core/NotificationService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UsersRepository } from '@/models/_.js';
 | 
			
		||||
import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
 | 
			
		||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
			
		||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { CacheService } from '@/core/CacheService.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
import { AccountMoveService } from '@/core/AccountMoveService.js';
 | 
			
		||||
import { UtilityService } from '@/core/UtilityService.js';
 | 
			
		||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
 | 
			
		||||
import type { ThinUser } from '@/queue/types.js';
 | 
			
		||||
import Logger from '../logger.js';
 | 
			
		||||
 | 
			
		||||
@@ -55,9 +58,6 @@ export class UserFollowingService implements OnModuleInit {
 | 
			
		||||
		@Inject(DI.config)
 | 
			
		||||
		private config: Config,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.usersRepository)
 | 
			
		||||
		private usersRepository: UsersRepository,
 | 
			
		||||
 | 
			
		||||
@@ -79,11 +79,13 @@ export class UserFollowingService implements OnModuleInit {
 | 
			
		||||
		private idService: IdService,
 | 
			
		||||
		private queueService: QueueService,
 | 
			
		||||
		private globalEventService: GlobalEventService,
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
		private notificationService: NotificationService,
 | 
			
		||||
		private federatedInstanceService: FederatedInstanceService,
 | 
			
		||||
		private webhookService: UserWebhookService,
 | 
			
		||||
		private apRendererService: ApRendererService,
 | 
			
		||||
		private accountMoveService: AccountMoveService,
 | 
			
		||||
		private fanoutTimelineService: FanoutTimelineService,
 | 
			
		||||
		private perUserFollowingChart: PerUserFollowingChart,
 | 
			
		||||
		private instanceChart: InstanceChart,
 | 
			
		||||
	) {
 | 
			
		||||
@@ -170,7 +172,7 @@ export class UserFollowingService implements OnModuleInit {
 | 
			
		||||
			followee.isLocked ||
 | 
			
		||||
			(followeeProfile.carefulBot && follower.isBot) ||
 | 
			
		||||
			(this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee) && process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING !== 'true') ||
 | 
			
		||||
			(this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower) && this.utilityService.isSilencedHost(this.meta.silencedHosts, follower.host))
 | 
			
		||||
			(this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower) && this.utilityService.isSilencedHost((await this.metaService.fetch()).silencedHosts, follower.host))
 | 
			
		||||
		) {
 | 
			
		||||
			let autoAccept = false;
 | 
			
		||||
 | 
			
		||||
@@ -305,14 +307,14 @@ export class UserFollowingService implements OnModuleInit {
 | 
			
		||||
			if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
 | 
			
		||||
				this.federatedInstanceService.fetch(follower.host).then(async i => {
 | 
			
		||||
					this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
 | 
			
		||||
					if (this.meta.enableChartsForFederatedInstances) {
 | 
			
		||||
					if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
 | 
			
		||||
						this.instanceChart.updateFollowing(i.host, true);
 | 
			
		||||
					}
 | 
			
		||||
				});
 | 
			
		||||
			} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
 | 
			
		||||
				this.federatedInstanceService.fetch(followee.host).then(async i => {
 | 
			
		||||
					this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
 | 
			
		||||
					if (this.meta.enableChartsForFederatedInstances) {
 | 
			
		||||
					if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
 | 
			
		||||
						this.instanceChart.updateFollowers(i.host, true);
 | 
			
		||||
					}
 | 
			
		||||
				});
 | 
			
		||||
@@ -437,14 +439,14 @@ export class UserFollowingService implements OnModuleInit {
 | 
			
		||||
			if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
 | 
			
		||||
				this.federatedInstanceService.fetch(follower.host).then(async i => {
 | 
			
		||||
					this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
 | 
			
		||||
					if (this.meta.enableChartsForFederatedInstances) {
 | 
			
		||||
					if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
 | 
			
		||||
						this.instanceChart.updateFollowing(i.host, false);
 | 
			
		||||
					}
 | 
			
		||||
				});
 | 
			
		||||
			} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
 | 
			
		||||
				this.federatedInstanceService.fetch(followee.host).then(async i => {
 | 
			
		||||
					this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
 | 
			
		||||
					if (this.meta.enableChartsForFederatedInstances) {
 | 
			
		||||
					if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
 | 
			
		||||
						this.instanceChart.updateFollowers(i.host, false);
 | 
			
		||||
					}
 | 
			
		||||
				});
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ export class UserKeypairService implements OnApplicationShutdown {
 | 
			
		||||
	) {
 | 
			
		||||
		this.cache = new RedisKVCache<MiUserKeypair>(this.redisClient, 'userKeypair', {
 | 
			
		||||
			lifetime: 1000 * 60 * 60 * 24, // 24h
 | 
			
		||||
			memoryCacheLifetime: 1000 * 60 * 60, // 1h
 | 
			
		||||
			memoryCacheLifetime: Infinity,
 | 
			
		||||
			fetcher: (key) => this.userKeypairsRepository.findOneByOrFail({ userId: key }),
 | 
			
		||||
			toRedisConverter: (value) => JSON.stringify(value),
 | 
			
		||||
			fromRedisConverter: (value) => JSON.parse(value),
 | 
			
		||||
 
 | 
			
		||||
@@ -5,8 +5,8 @@
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import * as Redis from 'ioredis';
 | 
			
		||||
import { type WebhooksRepository } from '@/models/_.js';
 | 
			
		||||
import { MiWebhook } from '@/models/Webhook.js';
 | 
			
		||||
import type { WebhooksRepository } from '@/models/_.js';
 | 
			
		||||
import type { MiWebhook } from '@/models/Webhook.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { GlobalEvents } from '@/core/GlobalEventService.js';
 | 
			
		||||
@@ -38,31 +38,6 @@ export class UserWebhookService implements OnApplicationShutdown {
 | 
			
		||||
		return this.activeWebhooks;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * UserWebhook の一覧を取得する.
 | 
			
		||||
	 */
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public fetchWebhooks(params?: {
 | 
			
		||||
		ids?: MiWebhook['id'][];
 | 
			
		||||
		isActive?: MiWebhook['active'];
 | 
			
		||||
		on?: MiWebhook['on'];
 | 
			
		||||
	}): Promise<MiWebhook[]> {
 | 
			
		||||
		const query = this.webhooksRepository.createQueryBuilder('webhook');
 | 
			
		||||
		if (params) {
 | 
			
		||||
			if (params.ids && params.ids.length > 0) {
 | 
			
		||||
				query.andWhere('webhook.id IN (:...ids)', { ids: params.ids });
 | 
			
		||||
			}
 | 
			
		||||
			if (params.isActive !== undefined) {
 | 
			
		||||
				query.andWhere('webhook.active = :isActive', { isActive: params.isActive });
 | 
			
		||||
			}
 | 
			
		||||
			if (params.on && params.on.length > 0) {
 | 
			
		||||
				query.andWhere(':on <@ webhook.on', { on: params.on });
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return query.getMany();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	private async onMessage(_: string, data: string): Promise<void> {
 | 
			
		||||
		const obj = JSON.parse(data);
 | 
			
		||||
 
 | 
			
		||||
@@ -12,9 +12,10 @@ import {
 | 
			
		||||
} from '@simplewebauthn/server';
 | 
			
		||||
import { AttestationFormat, isoCBOR, isoUint8Array } from '@simplewebauthn/server/helpers';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { MiMeta, UserSecurityKeysRepository } from '@/models/_.js';
 | 
			
		||||
import type { UserSecurityKeysRepository } from '@/models/_.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { MiUser } from '@/models/_.js';
 | 
			
		||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
 | 
			
		||||
import type {
 | 
			
		||||
@@ -22,6 +23,7 @@ import type {
 | 
			
		||||
	AuthenticatorTransportFuture,
 | 
			
		||||
	CredentialDeviceType,
 | 
			
		||||
	PublicKeyCredentialCreationOptionsJSON,
 | 
			
		||||
	PublicKeyCredentialDescriptorFuture,
 | 
			
		||||
	PublicKeyCredentialRequestOptionsJSON,
 | 
			
		||||
	RegistrationResponseJSON,
 | 
			
		||||
} from '@simplewebauthn/types';
 | 
			
		||||
@@ -29,33 +31,33 @@ import type {
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class WebAuthnService {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.config)
 | 
			
		||||
		private config: Config,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.redis)
 | 
			
		||||
		private redisClient: Redis.Redis,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.config)
 | 
			
		||||
		private config: Config,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.userSecurityKeysRepository)
 | 
			
		||||
		private userSecurityKeysRepository: UserSecurityKeysRepository,
 | 
			
		||||
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
	) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public getRelyingParty(): { origin: string; rpId: string; rpName: string; rpIcon?: string; } {
 | 
			
		||||
	public async getRelyingParty(): Promise<{ origin: string; rpId: string; rpName: string; rpIcon?: string; }> {
 | 
			
		||||
		const instance = await this.metaService.fetch();
 | 
			
		||||
		return {
 | 
			
		||||
			origin: this.config.url,
 | 
			
		||||
			rpId: this.config.hostname,
 | 
			
		||||
			rpName: this.meta.name ?? this.config.host,
 | 
			
		||||
			rpIcon: this.meta.iconUrl ?? undefined,
 | 
			
		||||
			rpName: instance.name ?? this.config.host,
 | 
			
		||||
			rpIcon: instance.iconUrl ?? undefined,
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async initiateRegistration(userId: MiUser['id'], userName: string, userDisplayName?: string): Promise<PublicKeyCredentialCreationOptionsJSON> {
 | 
			
		||||
		const relyingParty = this.getRelyingParty();
 | 
			
		||||
		const relyingParty = await this.getRelyingParty();
 | 
			
		||||
		const keys = await this.userSecurityKeysRepository.findBy({
 | 
			
		||||
			userId: userId,
 | 
			
		||||
		});
 | 
			
		||||
@@ -102,7 +104,7 @@ export class WebAuthnService {
 | 
			
		||||
 | 
			
		||||
		await this.redisClient.del(`webauthn:challenge:${userId}`);
 | 
			
		||||
 | 
			
		||||
		const relyingParty = this.getRelyingParty();
 | 
			
		||||
		const relyingParty = await this.getRelyingParty();
 | 
			
		||||
 | 
			
		||||
		let verification;
 | 
			
		||||
		try {
 | 
			
		||||
@@ -141,7 +143,7 @@ export class WebAuthnService {
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async initiateAuthentication(userId: MiUser['id']): Promise<PublicKeyCredentialRequestOptionsJSON> {
 | 
			
		||||
		const relyingParty = this.getRelyingParty();
 | 
			
		||||
		const relyingParty = await this.getRelyingParty();
 | 
			
		||||
		const keys = await this.userSecurityKeysRepository.findBy({
 | 
			
		||||
			userId: userId,
 | 
			
		||||
		});
 | 
			
		||||
@@ -207,7 +209,7 @@ export class WebAuthnService {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const relyingParty = this.getRelyingParty();
 | 
			
		||||
		const relyingParty = await this.getRelyingParty();
 | 
			
		||||
 | 
			
		||||
		let verification;
 | 
			
		||||
		try {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,434 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { MiAbuseUserReport, MiNote, MiUser, MiWebhook } from '@/models/_.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
 | 
			
		||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
 | 
			
		||||
import { Packed } from '@/misc/json-schema.js';
 | 
			
		||||
import { type WebhookEventTypes } from '@/models/Webhook.js';
 | 
			
		||||
import { UserWebhookService } from '@/core/UserWebhookService.js';
 | 
			
		||||
import { QueueService } from '@/core/QueueService.js';
 | 
			
		||||
 | 
			
		||||
const oneDayMillis = 24 * 60 * 60 * 1000;
 | 
			
		||||
 | 
			
		||||
function generateAbuseReport(override?: Partial<MiAbuseUserReport>): MiAbuseUserReport {
 | 
			
		||||
	return {
 | 
			
		||||
		id: 'dummy-abuse-report1',
 | 
			
		||||
		targetUserId: 'dummy-target-user',
 | 
			
		||||
		targetUser: null,
 | 
			
		||||
		reporterId: 'dummy-reporter-user',
 | 
			
		||||
		reporter: null,
 | 
			
		||||
		assigneeId: null,
 | 
			
		||||
		assignee: null,
 | 
			
		||||
		resolved: false,
 | 
			
		||||
		forwarded: false,
 | 
			
		||||
		comment: 'This is a dummy report for testing purposes.',
 | 
			
		||||
		targetUserHost: null,
 | 
			
		||||
		reporterHost: null,
 | 
			
		||||
		...override,
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function generateDummyUser(override?: Partial<MiUser>): MiUser {
 | 
			
		||||
	return {
 | 
			
		||||
		id: 'dummy-user-1',
 | 
			
		||||
		updatedAt: new Date(Date.now() - oneDayMillis * 7),
 | 
			
		||||
		lastFetchedAt: new Date(Date.now() - oneDayMillis * 5),
 | 
			
		||||
		lastActiveDate: new Date(Date.now() - oneDayMillis * 3),
 | 
			
		||||
		hideOnlineStatus: false,
 | 
			
		||||
		username: 'dummy1',
 | 
			
		||||
		usernameLower: 'dummy1',
 | 
			
		||||
		name: 'DummyUser1',
 | 
			
		||||
		followersCount: 10,
 | 
			
		||||
		followingCount: 5,
 | 
			
		||||
		movedToUri: null,
 | 
			
		||||
		movedAt: null,
 | 
			
		||||
		alsoKnownAs: null,
 | 
			
		||||
		notesCount: 30,
 | 
			
		||||
		avatarId: null,
 | 
			
		||||
		avatar: null,
 | 
			
		||||
		bannerId: null,
 | 
			
		||||
		banner: null,
 | 
			
		||||
		avatarUrl: null,
 | 
			
		||||
		bannerUrl: null,
 | 
			
		||||
		avatarBlurhash: null,
 | 
			
		||||
		bannerBlurhash: null,
 | 
			
		||||
		avatarDecorations: [],
 | 
			
		||||
		tags: [],
 | 
			
		||||
		isSuspended: false,
 | 
			
		||||
		isLocked: false,
 | 
			
		||||
		isBot: false,
 | 
			
		||||
		isCat: true,
 | 
			
		||||
		isRoot: false,
 | 
			
		||||
		isExplorable: true,
 | 
			
		||||
		isHibernated: false,
 | 
			
		||||
		isDeleted: false,
 | 
			
		||||
		emojis: [],
 | 
			
		||||
		host: null,
 | 
			
		||||
		inbox: null,
 | 
			
		||||
		sharedInbox: null,
 | 
			
		||||
		featured: null,
 | 
			
		||||
		uri: null,
 | 
			
		||||
		followersUri: null,
 | 
			
		||||
		token: null,
 | 
			
		||||
		...override,
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function generateDummyNote(override?: Partial<MiNote>): MiNote {
 | 
			
		||||
	return {
 | 
			
		||||
		id: 'dummy-note-1',
 | 
			
		||||
		replyId: null,
 | 
			
		||||
		reply: null,
 | 
			
		||||
		renoteId: null,
 | 
			
		||||
		renote: null,
 | 
			
		||||
		threadId: null,
 | 
			
		||||
		text: 'This is a dummy note for testing purposes.',
 | 
			
		||||
		name: null,
 | 
			
		||||
		cw: null,
 | 
			
		||||
		userId: 'dummy-user-1',
 | 
			
		||||
		user: null,
 | 
			
		||||
		localOnly: true,
 | 
			
		||||
		reactionAcceptance: 'likeOnly',
 | 
			
		||||
		renoteCount: 10,
 | 
			
		||||
		repliesCount: 5,
 | 
			
		||||
		clippedCount: 0,
 | 
			
		||||
		reactions: {},
 | 
			
		||||
		visibility: 'public',
 | 
			
		||||
		uri: null,
 | 
			
		||||
		url: null,
 | 
			
		||||
		fileIds: [],
 | 
			
		||||
		attachedFileTypes: [],
 | 
			
		||||
		visibleUserIds: [],
 | 
			
		||||
		mentions: [],
 | 
			
		||||
		mentionedRemoteUsers: '[]',
 | 
			
		||||
		reactionAndUserPairCache: [],
 | 
			
		||||
		emojis: [],
 | 
			
		||||
		tags: [],
 | 
			
		||||
		hasPoll: false,
 | 
			
		||||
		channelId: null,
 | 
			
		||||
		channel: null,
 | 
			
		||||
		userHost: null,
 | 
			
		||||
		replyUserId: null,
 | 
			
		||||
		replyUserHost: null,
 | 
			
		||||
		renoteUserId: null,
 | 
			
		||||
		renoteUserHost: null,
 | 
			
		||||
		...override,
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function toPackedNote(note: MiNote, detail = true, override?: Packed<'Note'>): Packed<'Note'> {
 | 
			
		||||
	return {
 | 
			
		||||
		id: note.id,
 | 
			
		||||
		createdAt: new Date().toISOString(),
 | 
			
		||||
		deletedAt: null,
 | 
			
		||||
		text: note.text,
 | 
			
		||||
		cw: note.cw,
 | 
			
		||||
		userId: note.userId,
 | 
			
		||||
		user: toPackedUserLite(note.user ?? generateDummyUser()),
 | 
			
		||||
		replyId: note.replyId,
 | 
			
		||||
		renoteId: note.renoteId,
 | 
			
		||||
		isHidden: false,
 | 
			
		||||
		visibility: note.visibility,
 | 
			
		||||
		mentions: note.mentions,
 | 
			
		||||
		visibleUserIds: note.visibleUserIds,
 | 
			
		||||
		fileIds: note.fileIds,
 | 
			
		||||
		files: [],
 | 
			
		||||
		tags: note.tags,
 | 
			
		||||
		poll: null,
 | 
			
		||||
		emojis: note.emojis,
 | 
			
		||||
		channelId: note.channelId,
 | 
			
		||||
		channel: note.channel,
 | 
			
		||||
		localOnly: note.localOnly,
 | 
			
		||||
		reactionAcceptance: note.reactionAcceptance,
 | 
			
		||||
		reactionEmojis: {},
 | 
			
		||||
		reactions: {},
 | 
			
		||||
		reactionCount: 0,
 | 
			
		||||
		renoteCount: note.renoteCount,
 | 
			
		||||
		repliesCount: note.repliesCount,
 | 
			
		||||
		uri: note.uri ?? undefined,
 | 
			
		||||
		url: note.url ?? undefined,
 | 
			
		||||
		reactionAndUserPairCache: note.reactionAndUserPairCache,
 | 
			
		||||
		...(detail ? {
 | 
			
		||||
			clippedCount: note.clippedCount,
 | 
			
		||||
			reply: note.reply ? toPackedNote(note.reply, false) : null,
 | 
			
		||||
			renote: note.renote ? toPackedNote(note.renote, true) : null,
 | 
			
		||||
			myReaction: null,
 | 
			
		||||
		} : {}),
 | 
			
		||||
		...override,
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function toPackedUserLite(user: MiUser, override?: Packed<'UserLite'>): Packed<'UserLite'> {
 | 
			
		||||
	return {
 | 
			
		||||
		id: user.id,
 | 
			
		||||
		name: user.name,
 | 
			
		||||
		username: user.username,
 | 
			
		||||
		host: user.host,
 | 
			
		||||
		avatarUrl: user.avatarUrl,
 | 
			
		||||
		avatarBlurhash: user.avatarBlurhash,
 | 
			
		||||
		avatarDecorations: user.avatarDecorations.map(it => ({
 | 
			
		||||
			id: it.id,
 | 
			
		||||
			angle: it.angle,
 | 
			
		||||
			flipH: it.flipH,
 | 
			
		||||
			url: 'https://example.com/dummy-image001.png',
 | 
			
		||||
			offsetX: it.offsetX,
 | 
			
		||||
			offsetY: it.offsetY,
 | 
			
		||||
		})),
 | 
			
		||||
		isBot: user.isBot,
 | 
			
		||||
		isCat: user.isCat,
 | 
			
		||||
		emojis: user.emojis,
 | 
			
		||||
		onlineStatus: 'active',
 | 
			
		||||
		badgeRoles: [],
 | 
			
		||||
		...override,
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function toPackedUserDetailedNotMe(user: MiUser, override?: Packed<'UserDetailedNotMe'>): Packed<'UserDetailedNotMe'> {
 | 
			
		||||
	return {
 | 
			
		||||
		...toPackedUserLite(user),
 | 
			
		||||
		url: null,
 | 
			
		||||
		uri: null,
 | 
			
		||||
		movedTo: null,
 | 
			
		||||
		alsoKnownAs: [],
 | 
			
		||||
		createdAt: new Date().toISOString(),
 | 
			
		||||
		updatedAt: user.updatedAt?.toISOString() ?? null,
 | 
			
		||||
		lastFetchedAt: user.lastFetchedAt?.toISOString() ?? null,
 | 
			
		||||
		bannerUrl: user.bannerUrl,
 | 
			
		||||
		bannerBlurhash: user.bannerBlurhash,
 | 
			
		||||
		isLocked: user.isLocked,
 | 
			
		||||
		isSilenced: false,
 | 
			
		||||
		isSuspended: user.isSuspended,
 | 
			
		||||
		description: null,
 | 
			
		||||
		location: null,
 | 
			
		||||
		birthday: null,
 | 
			
		||||
		lang: null,
 | 
			
		||||
		fields: [],
 | 
			
		||||
		verifiedLinks: [],
 | 
			
		||||
		followersCount: user.followersCount,
 | 
			
		||||
		followingCount: user.followingCount,
 | 
			
		||||
		notesCount: user.notesCount,
 | 
			
		||||
		pinnedNoteIds: [],
 | 
			
		||||
		pinnedNotes: [],
 | 
			
		||||
		pinnedPageId: null,
 | 
			
		||||
		pinnedPage: null,
 | 
			
		||||
		publicReactions: true,
 | 
			
		||||
		followersVisibility: 'public',
 | 
			
		||||
		followingVisibility: 'public',
 | 
			
		||||
		twoFactorEnabled: false,
 | 
			
		||||
		usePasswordLessLogin: false,
 | 
			
		||||
		securityKeys: false,
 | 
			
		||||
		roles: [],
 | 
			
		||||
		memo: null,
 | 
			
		||||
		moderationNote: undefined,
 | 
			
		||||
		isFollowing: false,
 | 
			
		||||
		isFollowed: false,
 | 
			
		||||
		hasPendingFollowRequestFromYou: false,
 | 
			
		||||
		hasPendingFollowRequestToYou: false,
 | 
			
		||||
		isBlocking: false,
 | 
			
		||||
		isBlocked: false,
 | 
			
		||||
		isMuted: false,
 | 
			
		||||
		isRenoteMuted: false,
 | 
			
		||||
		notify: 'none',
 | 
			
		||||
		withReplies: true,
 | 
			
		||||
		...override,
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const dummyUser1 = generateDummyUser();
 | 
			
		||||
const dummyUser2 = generateDummyUser({
 | 
			
		||||
	id: 'dummy-user-2',
 | 
			
		||||
	updatedAt: new Date(Date.now() - oneDayMillis * 30),
 | 
			
		||||
	lastFetchedAt: new Date(Date.now() - oneDayMillis),
 | 
			
		||||
	lastActiveDate: new Date(Date.now() - oneDayMillis),
 | 
			
		||||
	username: 'dummy2',
 | 
			
		||||
	usernameLower: 'dummy2',
 | 
			
		||||
	name: 'DummyUser2',
 | 
			
		||||
	followersCount: 40,
 | 
			
		||||
	followingCount: 50,
 | 
			
		||||
	notesCount: 900,
 | 
			
		||||
});
 | 
			
		||||
const dummyUser3 = generateDummyUser({
 | 
			
		||||
	id: 'dummy-user-3',
 | 
			
		||||
	updatedAt: new Date(Date.now() - oneDayMillis * 15),
 | 
			
		||||
	lastFetchedAt: new Date(Date.now() - oneDayMillis * 2),
 | 
			
		||||
	lastActiveDate: new Date(Date.now() - oneDayMillis * 2),
 | 
			
		||||
	username: 'dummy3',
 | 
			
		||||
	usernameLower: 'dummy3',
 | 
			
		||||
	name: 'DummyUser3',
 | 
			
		||||
	followersCount: 60,
 | 
			
		||||
	followingCount: 70,
 | 
			
		||||
	notesCount: 15900,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class WebhookTestService {
 | 
			
		||||
	public static NoSuchWebhookError = class extends Error {};
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		private userWebhookService: UserWebhookService,
 | 
			
		||||
		private systemWebhookService: SystemWebhookService,
 | 
			
		||||
		private queueService: QueueService,
 | 
			
		||||
	) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * UserWebhookのテスト送信を行う.
 | 
			
		||||
	 * 送信されるペイロードはいずれもダミーの値で、実際にはデータベース上に存在しない.
 | 
			
		||||
	 *
 | 
			
		||||
	 * また、この関数経由で送信されるWebhookは以下の設定を無視する.
 | 
			
		||||
	 * - Webhookそのものの有効・無効設定(active)
 | 
			
		||||
	 * - 送信対象イベント(on)に関する設定
 | 
			
		||||
	 */
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async testUserWebhook(
 | 
			
		||||
		params: {
 | 
			
		||||
			webhookId: MiWebhook['id'],
 | 
			
		||||
			type: WebhookEventTypes,
 | 
			
		||||
			override?: Partial<Omit<MiWebhook, 'id'>>,
 | 
			
		||||
		},
 | 
			
		||||
		sender: MiUser | null,
 | 
			
		||||
	) {
 | 
			
		||||
		const webhooks = await this.userWebhookService.fetchWebhooks({ ids: [params.webhookId] })
 | 
			
		||||
			.then(it => it.filter(it => it.userId === sender?.id));
 | 
			
		||||
		if (webhooks.length === 0) {
 | 
			
		||||
			throw new WebhookTestService.NoSuchWebhookError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const webhook = webhooks[0];
 | 
			
		||||
		const send = (contents: unknown) => {
 | 
			
		||||
			const merged = {
 | 
			
		||||
				...webhook,
 | 
			
		||||
				...params.override,
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			// テスト目的なのでUserWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図).
 | 
			
		||||
			// また、Jobの試行回数も1回だけ.
 | 
			
		||||
			this.queueService.userWebhookDeliver(merged, params.type, contents, { attempts: 1 });
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		const dummyNote1 = generateDummyNote({
 | 
			
		||||
			userId: dummyUser1.id,
 | 
			
		||||
			user: dummyUser1,
 | 
			
		||||
		});
 | 
			
		||||
		const dummyReply1 = generateDummyNote({
 | 
			
		||||
			id: 'dummy-reply-1',
 | 
			
		||||
			replyId: dummyNote1.id,
 | 
			
		||||
			reply: dummyNote1,
 | 
			
		||||
			userId: dummyUser1.id,
 | 
			
		||||
			user: dummyUser1,
 | 
			
		||||
		});
 | 
			
		||||
		const dummyRenote1 = generateDummyNote({
 | 
			
		||||
			id: 'dummy-renote-1',
 | 
			
		||||
			renoteId: dummyNote1.id,
 | 
			
		||||
			renote: dummyNote1,
 | 
			
		||||
			userId: dummyUser2.id,
 | 
			
		||||
			user: dummyUser2,
 | 
			
		||||
			text: null,
 | 
			
		||||
		});
 | 
			
		||||
		const dummyMention1 = generateDummyNote({
 | 
			
		||||
			id: 'dummy-mention-1',
 | 
			
		||||
			userId: dummyUser1.id,
 | 
			
		||||
			user: dummyUser1,
 | 
			
		||||
			text: `@${dummyUser2.username} This is a mention to you.`,
 | 
			
		||||
			mentions: [dummyUser2.id],
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		switch (params.type) {
 | 
			
		||||
			case 'note': {
 | 
			
		||||
				send(toPackedNote(dummyNote1));
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
			case 'reply': {
 | 
			
		||||
				send(toPackedNote(dummyReply1));
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
			case 'renote': {
 | 
			
		||||
				send(toPackedNote(dummyRenote1));
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
			case 'mention': {
 | 
			
		||||
				send(toPackedNote(dummyMention1));
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
			case 'follow': {
 | 
			
		||||
				send(toPackedUserDetailedNotMe(dummyUser1));
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
			case 'followed': {
 | 
			
		||||
				send(toPackedUserLite(dummyUser2));
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
			case 'unfollow': {
 | 
			
		||||
				send(toPackedUserDetailedNotMe(dummyUser3));
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * SystemWebhookのテスト送信を行う.
 | 
			
		||||
	 * 送信されるペイロードはいずれもダミーの値で、実際にはデータベース上に存在しない.
 | 
			
		||||
	 *
 | 
			
		||||
	 * また、この関数経由で送信されるWebhookは以下の設定を無視する.
 | 
			
		||||
	 * - Webhookそのものの有効・無効設定(isActive)
 | 
			
		||||
	 * - 送信対象イベント(on)に関する設定
 | 
			
		||||
	 */
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async testSystemWebhook(
 | 
			
		||||
		params: {
 | 
			
		||||
			webhookId: MiSystemWebhook['id'],
 | 
			
		||||
			type: SystemWebhookEventType,
 | 
			
		||||
			override?: Partial<Omit<MiSystemWebhook, 'id'>>,
 | 
			
		||||
		},
 | 
			
		||||
	) {
 | 
			
		||||
		const webhooks = await this.systemWebhookService.fetchSystemWebhooks({ ids: [params.webhookId] });
 | 
			
		||||
		if (webhooks.length === 0) {
 | 
			
		||||
			throw new WebhookTestService.NoSuchWebhookError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const webhook = webhooks[0];
 | 
			
		||||
		const send = (contents: unknown) => {
 | 
			
		||||
			const merged = {
 | 
			
		||||
				...webhook,
 | 
			
		||||
				...params.override,
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			// テスト目的なのでSystemWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図).
 | 
			
		||||
			// また、Jobの試行回数も1回だけ.
 | 
			
		||||
			this.queueService.systemWebhookDeliver(merged, params.type, contents, { attempts: 1 });
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		switch (params.type) {
 | 
			
		||||
			case 'abuseReport': {
 | 
			
		||||
				send(generateAbuseReport({
 | 
			
		||||
					targetUserId: dummyUser1.id,
 | 
			
		||||
					targetUser: dummyUser1,
 | 
			
		||||
					reporterId: dummyUser2.id,
 | 
			
		||||
					reporter: dummyUser2,
 | 
			
		||||
				}));
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
			case 'abuseReportResolved': {
 | 
			
		||||
				send(generateAbuseReport({
 | 
			
		||||
					targetUserId: dummyUser1.id,
 | 
			
		||||
					targetUser: dummyUser1,
 | 
			
		||||
					reporterId: dummyUser2.id,
 | 
			
		||||
					reporter: dummyUser2,
 | 
			
		||||
					assigneeId: dummyUser3.id,
 | 
			
		||||
					assignee: dummyUser3,
 | 
			
		||||
					resolved: true,
 | 
			
		||||
				}));
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
			case 'userCreated': {
 | 
			
		||||
				send(toPackedUserLite(dummyUser1));
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -54,8 +54,8 @@ export class ApDbResolverService implements OnApplicationShutdown {
 | 
			
		||||
		private cacheService: CacheService,
 | 
			
		||||
		private apPersonService: ApPersonService,
 | 
			
		||||
	) {
 | 
			
		||||
		this.publicKeyCache = new MemoryKVCache<MiUserPublickey | null>(1000 * 60 * 60 * 12); // 12h
 | 
			
		||||
		this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey | null>(1000 * 60 * 60 * 12); // 12h
 | 
			
		||||
		this.publicKeyCache = new MemoryKVCache<MiUserPublickey | null>(Infinity);
 | 
			
		||||
		this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey | null>(Infinity);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
 
 | 
			
		||||
@@ -17,13 +17,14 @@ import { NoteCreateService } from '@/core/NoteCreateService.js';
 | 
			
		||||
import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js';
 | 
			
		||||
import { AppLockService } from '@/core/AppLockService.js';
 | 
			
		||||
import type Logger from '@/logger.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { IdService } from '@/core/IdService.js';
 | 
			
		||||
import { StatusError } from '@/misc/status-error.js';
 | 
			
		||||
import { UtilityService } from '@/core/UtilityService.js';
 | 
			
		||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 | 
			
		||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
			
		||||
import { QueueService } from '@/core/QueueService.js';
 | 
			
		||||
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js';
 | 
			
		||||
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import type { MiRemoteUser } from '@/models/User.js';
 | 
			
		||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
			
		||||
@@ -47,9 +48,6 @@ export class ApInboxService {
 | 
			
		||||
		@Inject(DI.config)
 | 
			
		||||
		private config: Config,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.usersRepository)
 | 
			
		||||
		private usersRepository: UsersRepository,
 | 
			
		||||
 | 
			
		||||
@@ -66,6 +64,7 @@ export class ApInboxService {
 | 
			
		||||
		private noteEntityService: NoteEntityService,
 | 
			
		||||
		private utilityService: UtilityService,
 | 
			
		||||
		private idService: IdService,
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
		private abuseReportService: AbuseReportService,
 | 
			
		||||
		private userFollowingService: UserFollowingService,
 | 
			
		||||
		private apAudienceService: ApAudienceService,
 | 
			
		||||
@@ -291,7 +290,8 @@ export class ApInboxService {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// アナウンス先をブロックしてたら中断
 | 
			
		||||
		if (this.utilityService.isBlockedHost(this.meta.blockedHosts, this.utilityService.extractDbHost(uri))) return;
 | 
			
		||||
		const meta = await this.metaService.fetch();
 | 
			
		||||
		if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) return;
 | 
			
		||||
 | 
			
		||||
		const unlock = await this.appLockService.getApLock(uri);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -207,28 +207,8 @@ export class ApRequestService {
 | 
			
		||||
 | 
			
		||||
		if ((contentType ?? '').split(';')[0].trimEnd().toLowerCase() === 'text/html' && _followAlternate === true) {
 | 
			
		||||
			const html = await res.text();
 | 
			
		||||
			const window = 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 window = new Window();
 | 
			
		||||
			const document = window.document;
 | 
			
		||||
			try {
 | 
			
		||||
			document.documentElement.innerHTML = html;
 | 
			
		||||
 | 
			
		||||
			const alternate = document.querySelector('head > link[rel="alternate"][type="application/activity+json"]');
 | 
			
		||||
@@ -238,11 +218,6 @@ export class ApRequestService {
 | 
			
		||||
					return await this.signedGet(href, user, false);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			} catch (e) {
 | 
			
		||||
				// something went wrong parsing the HTML, ignore the whole thing
 | 
			
		||||
			} finally {
 | 
			
		||||
				window.close();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		//#endregion
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,8 +7,9 @@ import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { IsNull, Not } from 'typeorm';
 | 
			
		||||
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
 | 
			
		||||
import { InstanceActorService } from '@/core/InstanceActorService.js';
 | 
			
		||||
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js';
 | 
			
		||||
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { UtilityService } from '@/core/UtilityService.js';
 | 
			
		||||
@@ -28,7 +29,6 @@ export class Resolver {
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		private config: Config,
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
		private usersRepository: UsersRepository,
 | 
			
		||||
		private notesRepository: NotesRepository,
 | 
			
		||||
		private pollsRepository: PollsRepository,
 | 
			
		||||
@@ -36,6 +36,7 @@ export class Resolver {
 | 
			
		||||
		private followRequestsRepository: FollowRequestsRepository,
 | 
			
		||||
		private utilityService: UtilityService,
 | 
			
		||||
		private instanceActorService: InstanceActorService,
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
		private apRequestService: ApRequestService,
 | 
			
		||||
		private httpRequestService: HttpRequestService,
 | 
			
		||||
		private apRendererService: ApRendererService,
 | 
			
		||||
@@ -93,7 +94,8 @@ export class Resolver {
 | 
			
		||||
			return await this.resolveLocal(value);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (this.utilityService.isBlockedHost(this.meta.blockedHosts, host)) {
 | 
			
		||||
		const meta = await this.metaService.fetch();
 | 
			
		||||
		if (this.utilityService.isBlockedHost(meta.blockedHosts, host)) {
 | 
			
		||||
			throw new Error('Instance is blocked');
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -176,9 +178,6 @@ export class ApResolverService {
 | 
			
		||||
		@Inject(DI.config)
 | 
			
		||||
		private config: Config,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.usersRepository)
 | 
			
		||||
		private usersRepository: UsersRepository,
 | 
			
		||||
 | 
			
		||||
@@ -196,6 +195,7 @@ export class ApResolverService {
 | 
			
		||||
 | 
			
		||||
		private utilityService: UtilityService,
 | 
			
		||||
		private instanceActorService: InstanceActorService,
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
		private apRequestService: ApRequestService,
 | 
			
		||||
		private httpRequestService: HttpRequestService,
 | 
			
		||||
		private apRendererService: ApRendererService,
 | 
			
		||||
@@ -208,7 +208,6 @@ export class ApResolverService {
 | 
			
		||||
	public createResolver(): Resolver {
 | 
			
		||||
		return new Resolver(
 | 
			
		||||
			this.config,
 | 
			
		||||
			this.meta,
 | 
			
		||||
			this.usersRepository,
 | 
			
		||||
			this.notesRepository,
 | 
			
		||||
			this.pollsRepository,
 | 
			
		||||
@@ -216,6 +215,7 @@ export class ApResolverService {
 | 
			
		||||
			this.followRequestsRepository,
 | 
			
		||||
			this.utilityService,
 | 
			
		||||
			this.instanceActorService,
 | 
			
		||||
			this.metaService,
 | 
			
		||||
			this.apRequestService,
 | 
			
		||||
			this.httpRequestService,
 | 
			
		||||
			this.apRendererService,
 | 
			
		||||
 
 | 
			
		||||
@@ -5,9 +5,10 @@
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { DriveFilesRepository, MiMeta } from '@/models/_.js';
 | 
			
		||||
import type { DriveFilesRepository } from '@/models/_.js';
 | 
			
		||||
import type { MiRemoteUser } from '@/models/User.js';
 | 
			
		||||
import type { MiDriveFile } from '@/models/DriveFile.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { truncate } from '@/misc/truncate.js';
 | 
			
		||||
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
 | 
			
		||||
import { DriveService } from '@/core/DriveService.js';
 | 
			
		||||
@@ -23,12 +24,10 @@ export class ApImageService {
 | 
			
		||||
	private logger: Logger;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.driveFilesRepository)
 | 
			
		||||
		private driveFilesRepository: DriveFilesRepository,
 | 
			
		||||
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
		private apResolverService: ApResolverService,
 | 
			
		||||
		private driveService: DriveService,
 | 
			
		||||
		private apLoggerService: ApLoggerService,
 | 
			
		||||
@@ -64,10 +63,12 @@ export class ApImageService {
 | 
			
		||||
 | 
			
		||||
		this.logger.info(`Creating the Image: ${image.url}`);
 | 
			
		||||
 | 
			
		||||
		const instance = await this.metaService.fetch();
 | 
			
		||||
 | 
			
		||||
		// Cache if remote file cache is on AND either
 | 
			
		||||
		// 1. remote sensitive file is also on
 | 
			
		||||
		// 2. or the image is not sensitive
 | 
			
		||||
		const shouldBeCached = this.meta.cacheRemoteFiles && (this.meta.cacheRemoteSensitiveFiles || !image.sensitive);
 | 
			
		||||
		const shouldBeCached = instance.cacheRemoteFiles && (instance.cacheRemoteSensitiveFiles || !image.sensitive);
 | 
			
		||||
 | 
			
		||||
		const file = await this.driveService.uploadFromUrl({
 | 
			
		||||
			url: image.url,
 | 
			
		||||
 
 | 
			
		||||
@@ -6,12 +6,13 @@
 | 
			
		||||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { In } from 'typeorm';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { PollsRepository, EmojisRepository, MiMeta } from '@/models/_.js';
 | 
			
		||||
import type { PollsRepository, EmojisRepository } from '@/models/_.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
import type { MiRemoteUser } from '@/models/User.js';
 | 
			
		||||
import type { MiNote } from '@/models/Note.js';
 | 
			
		||||
import { toArray, toSingle, unique } from '@/misc/prelude/array.js';
 | 
			
		||||
import type { MiEmoji } from '@/models/Emoji.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { AppLockService } from '@/core/AppLockService.js';
 | 
			
		||||
import type { MiDriveFile } from '@/models/DriveFile.js';
 | 
			
		||||
import { NoteCreateService } from '@/core/NoteCreateService.js';
 | 
			
		||||
@@ -45,9 +46,6 @@ export class ApNoteService {
 | 
			
		||||
		@Inject(DI.config)
 | 
			
		||||
		private config: Config,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.pollsRepository)
 | 
			
		||||
		private pollsRepository: PollsRepository,
 | 
			
		||||
 | 
			
		||||
@@ -67,6 +65,7 @@ export class ApNoteService {
 | 
			
		||||
		private apMentionService: ApMentionService,
 | 
			
		||||
		private apImageService: ApImageService,
 | 
			
		||||
		private apQuestionService: ApQuestionService,
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
		private appLockService: AppLockService,
 | 
			
		||||
		private pollService: PollService,
 | 
			
		||||
		private noteCreateService: NoteCreateService,
 | 
			
		||||
@@ -183,7 +182,7 @@ export class ApNoteService {
 | 
			
		||||
		/**
 | 
			
		||||
		 * 禁止ワードチェック
 | 
			
		||||
		 */
 | 
			
		||||
		const hasProhibitedWords = this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices });
 | 
			
		||||
		const hasProhibitedWords = await this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices });
 | 
			
		||||
		if (hasProhibitedWords) {
 | 
			
		||||
			throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
 | 
			
		||||
		}
 | 
			
		||||
@@ -337,7 +336,8 @@ export class ApNoteService {
 | 
			
		||||
		const uri = getApId(value);
 | 
			
		||||
 | 
			
		||||
		// ブロックしていたら中断
 | 
			
		||||
		if (this.utilityService.isBlockedHost(this.meta.blockedHosts, this.utilityService.extractDbHost(uri))) {
 | 
			
		||||
		const meta = await this.metaService.fetch();
 | 
			
		||||
		if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) {
 | 
			
		||||
			throw new StatusError('blocked host', 451);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ import promiseLimit from 'promise-limit';
 | 
			
		||||
import { DataSource } from 'typeorm';
 | 
			
		||||
import { ModuleRef } from '@nestjs/core';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { FollowingsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js';
 | 
			
		||||
import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
 | 
			
		||||
import { MiUser } from '@/models/User.js';
 | 
			
		||||
@@ -35,6 +35,7 @@ import type { UtilityService } from '@/core/UtilityService.js';
 | 
			
		||||
import type { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { RoleService } from '@/core/RoleService.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
 | 
			
		||||
import type { AccountMoveService } from '@/core/AccountMoveService.js';
 | 
			
		||||
import { checkHttps } from '@/misc/check-https.js';
 | 
			
		||||
@@ -61,6 +62,7 @@ export class ApPersonService implements OnModuleInit {
 | 
			
		||||
	private driveFileEntityService: DriveFileEntityService;
 | 
			
		||||
	private idService: IdService;
 | 
			
		||||
	private globalEventService: GlobalEventService;
 | 
			
		||||
	private metaService: MetaService;
 | 
			
		||||
	private federatedInstanceService: FederatedInstanceService;
 | 
			
		||||
	private fetchInstanceMetadataService: FetchInstanceMetadataService;
 | 
			
		||||
	private cacheService: CacheService;
 | 
			
		||||
@@ -82,9 +84,6 @@ export class ApPersonService implements OnModuleInit {
 | 
			
		||||
		@Inject(DI.config)
 | 
			
		||||
		private config: Config,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.db)
 | 
			
		||||
		private db: DataSource,
 | 
			
		||||
 | 
			
		||||
@@ -113,6 +112,7 @@ export class ApPersonService implements OnModuleInit {
 | 
			
		||||
		this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
 | 
			
		||||
		this.idService = this.moduleRef.get('IdService');
 | 
			
		||||
		this.globalEventService = this.moduleRef.get('GlobalEventService');
 | 
			
		||||
		this.metaService = this.moduleRef.get('MetaService');
 | 
			
		||||
		this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
 | 
			
		||||
		this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService');
 | 
			
		||||
		this.cacheService = this.moduleRef.get('CacheService');
 | 
			
		||||
@@ -407,10 +407,10 @@ export class ApPersonService implements OnModuleInit {
 | 
			
		||||
		this.cacheService.uriPersonCache.set(user.uri, user);
 | 
			
		||||
 | 
			
		||||
		// Register host
 | 
			
		||||
		this.federatedInstanceService.fetch(host).then(i => {
 | 
			
		||||
		this.federatedInstanceService.fetch(host).then(async i => {
 | 
			
		||||
			this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
 | 
			
		||||
			this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
 | 
			
		||||
			if (this.meta.enableChartsForFederatedInstances) {
 | 
			
		||||
			if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
 | 
			
		||||
				this.instanceChart.newUser(i.host);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 
 | 
			
		||||
@@ -5,9 +5,10 @@
 | 
			
		||||
 | 
			
		||||
import { Injectable, Inject } from '@nestjs/common';
 | 
			
		||||
import { DataSource } from 'typeorm';
 | 
			
		||||
import type { FollowingsRepository, InstancesRepository, MiMeta } from '@/models/_.js';
 | 
			
		||||
import type { FollowingsRepository, InstancesRepository } from '@/models/_.js';
 | 
			
		||||
import { AppLockService } from '@/core/AppLockService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import Chart from '../core.js';
 | 
			
		||||
import { ChartLoggerService } from '../ChartLoggerService.js';
 | 
			
		||||
@@ -23,15 +24,13 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
 | 
			
		||||
		@Inject(DI.db)
 | 
			
		||||
		private db: DataSource,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.followingsRepository)
 | 
			
		||||
		private followingsRepository: FollowingsRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.instancesRepository)
 | 
			
		||||
		private instancesRepository: InstancesRepository,
 | 
			
		||||
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
		private appLockService: AppLockService,
 | 
			
		||||
		private chartLoggerService: ChartLoggerService,
 | 
			
		||||
	) {
 | 
			
		||||
@@ -44,6 +43,8 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
 | 
			
		||||
		const meta = await this.metaService.fetch();
 | 
			
		||||
 | 
			
		||||
		const suspendedInstancesQuery = this.instancesRepository.createQueryBuilder('instance')
 | 
			
		||||
			.select('instance.host')
 | 
			
		||||
			.where('instance.suspensionState != \'none\'');
 | 
			
		||||
@@ -64,21 +65,21 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
 | 
			
		||||
			this.followingsRepository.createQueryBuilder('following')
 | 
			
		||||
				.select('COUNT(DISTINCT following.followeeHost)')
 | 
			
		||||
				.where('following.followeeHost IS NOT NULL')
 | 
			
		||||
				.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
 | 
			
		||||
				.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
 | 
			
		||||
				.andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
 | 
			
		||||
				.getRawOne()
 | 
			
		||||
				.then(x => parseInt(x.count, 10)),
 | 
			
		||||
			this.followingsRepository.createQueryBuilder('following')
 | 
			
		||||
				.select('COUNT(DISTINCT following.followerHost)')
 | 
			
		||||
				.where('following.followerHost IS NOT NULL')
 | 
			
		||||
				.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
 | 
			
		||||
				.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
 | 
			
		||||
				.andWhere(`following.followerHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
 | 
			
		||||
				.getRawOne()
 | 
			
		||||
				.then(x => parseInt(x.count, 10)),
 | 
			
		||||
			this.followingsRepository.createQueryBuilder('following')
 | 
			
		||||
				.select('COUNT(DISTINCT following.followeeHost)')
 | 
			
		||||
				.where('following.followeeHost IS NOT NULL')
 | 
			
		||||
				.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
 | 
			
		||||
				.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
 | 
			
		||||
				.andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
 | 
			
		||||
				.andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`)
 | 
			
		||||
				.setParameters(pubsubSubQuery.getParameters())
 | 
			
		||||
@@ -87,7 +88,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
 | 
			
		||||
			this.instancesRepository.createQueryBuilder('instance')
 | 
			
		||||
				.select('COUNT(instance.id)')
 | 
			
		||||
				.where(`instance.host IN (${ subInstancesQuery.getQuery() })`)
 | 
			
		||||
				.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
 | 
			
		||||
				.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
 | 
			
		||||
				.andWhere('instance.suspensionState = \'none\'')
 | 
			
		||||
				.andWhere('instance.isNotResponding = false')
 | 
			
		||||
				.getRawOne()
 | 
			
		||||
@@ -95,7 +96,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
 | 
			
		||||
			this.instancesRepository.createQueryBuilder('instance')
 | 
			
		||||
				.select('COUNT(instance.id)')
 | 
			
		||||
				.where(`instance.host IN (${ pubInstancesQuery.getQuery() })`)
 | 
			
		||||
				.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
 | 
			
		||||
				.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
 | 
			
		||||
				.andWhere('instance.suspensionState = \'none\'')
 | 
			
		||||
				.andWhere('instance.isNotResponding = false')
 | 
			
		||||
				.getRawOne()
 | 
			
		||||
 
 | 
			
		||||
@@ -3,22 +3,19 @@
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import type { Packed } from '@/misc/json-schema.js';
 | 
			
		||||
import type { MiInstance } from '@/models/Instance.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { UtilityService } from '@/core/UtilityService.js';
 | 
			
		||||
import { RoleService } from '@/core/RoleService.js';
 | 
			
		||||
import { MiUser } from '@/models/User.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { MiMeta } from '@/models/_.js';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class InstanceEntityService {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
		private roleService: RoleService,
 | 
			
		||||
 | 
			
		||||
		private utilityService: UtilityService,
 | 
			
		||||
@@ -30,6 +27,7 @@ export class InstanceEntityService {
 | 
			
		||||
		instance: MiInstance,
 | 
			
		||||
		me?: { id: MiUser['id']; } | null | undefined,
 | 
			
		||||
	): Promise<Packed<'FederationInstance'>> {
 | 
			
		||||
		const meta = await this.metaService.fetch();
 | 
			
		||||
		const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
@@ -43,7 +41,7 @@ export class InstanceEntityService {
 | 
			
		||||
			isNotResponding: instance.isNotResponding,
 | 
			
		||||
			isSuspended: instance.suspensionState !== 'none',
 | 
			
		||||
			suspensionState: instance.suspensionState,
 | 
			
		||||
			isBlocked: this.utilityService.isBlockedHost(this.meta.blockedHosts, instance.host),
 | 
			
		||||
			isBlocked: this.utilityService.isBlockedHost(meta.blockedHosts, instance.host),
 | 
			
		||||
			softwareName: instance.softwareName,
 | 
			
		||||
			softwareVersion: instance.softwareVersion,
 | 
			
		||||
			openRegistrations: instance.openRegistrations,
 | 
			
		||||
@@ -51,8 +49,8 @@ export class InstanceEntityService {
 | 
			
		||||
			description: instance.description,
 | 
			
		||||
			maintainerName: instance.maintainerName,
 | 
			
		||||
			maintainerEmail: instance.maintainerEmail,
 | 
			
		||||
			isSilenced: this.utilityService.isSilencedHost(this.meta.silencedHosts, instance.host),
 | 
			
		||||
			isMediaSilenced: this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, instance.host),
 | 
			
		||||
			isSilenced: this.utilityService.isSilencedHost(meta.silencedHosts, instance.host),
 | 
			
		||||
			isMediaSilenced: this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, instance.host),
 | 
			
		||||
			iconUrl: instance.iconUrl,
 | 
			
		||||
			faviconUrl: instance.faviconUrl,
 | 
			
		||||
			themeColor: instance.themeColor,
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ import type { Packed } from '@/misc/json-schema.js';
 | 
			
		||||
import type { MiMeta } from '@/models/Meta.js';
 | 
			
		||||
import type { AdsRepository } from '@/models/_.js';
 | 
			
		||||
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
			
		||||
import { InstanceActorService } from '@/core/InstanceActorService.js';
 | 
			
		||||
@@ -23,13 +24,11 @@ export class MetaEntityService {
 | 
			
		||||
		@Inject(DI.config)
 | 
			
		||||
		private config: Config,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.adsRepository)
 | 
			
		||||
		private adsRepository: AdsRepository,
 | 
			
		||||
 | 
			
		||||
		private userEntityService: UserEntityService,
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
		private instanceActorService: InstanceActorService,
 | 
			
		||||
	) { }
 | 
			
		||||
 | 
			
		||||
@@ -38,7 +37,7 @@ export class MetaEntityService {
 | 
			
		||||
		let instance = meta;
 | 
			
		||||
 | 
			
		||||
		if (!instance) {
 | 
			
		||||
			instance = this.meta;
 | 
			
		||||
			instance = await this.metaService.fetch();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const ads = await this.adsRepository.createQueryBuilder('ads')
 | 
			
		||||
@@ -130,7 +129,6 @@ export class MetaEntityService {
 | 
			
		||||
			mediaProxy: this.config.mediaProxy,
 | 
			
		||||
			enableUrlPreview: instance.urlPreviewEnabled,
 | 
			
		||||
			noteSearchableScope: (this.config.meilisearch == null || this.config.meilisearch.scope !== 'local') ? 'global' : 'local',
 | 
			
		||||
			maxFileSize: this.config.maxFileSize,
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		return packed;
 | 
			
		||||
@@ -141,7 +139,7 @@ export class MetaEntityService {
 | 
			
		||||
		let instance = meta;
 | 
			
		||||
 | 
			
		||||
		if (!instance) {
 | 
			
		||||
			instance = this.meta;
 | 
			
		||||
			instance = await this.metaService.fetch();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const packed = await this.pack(instance);
 | 
			
		||||
 
 | 
			
		||||
@@ -11,46 +11,29 @@ import type { Packed } from '@/misc/json-schema.js';
 | 
			
		||||
import { awaitAll } from '@/misc/prelude/await-all.js';
 | 
			
		||||
import type { MiUser } from '@/models/User.js';
 | 
			
		||||
import type { MiNote } from '@/models/Note.js';
 | 
			
		||||
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, MiMeta } from '@/models/_.js';
 | 
			
		||||
import type { MiNoteReaction } from '@/models/NoteReaction.js';
 | 
			
		||||
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository } from '@/models/_.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { DebounceLoader } from '@/misc/loader.js';
 | 
			
		||||
import { IdService } from '@/core/IdService.js';
 | 
			
		||||
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import type { OnModuleInit } from '@nestjs/common';
 | 
			
		||||
import type { CustomEmojiService } from '../CustomEmojiService.js';
 | 
			
		||||
import type { ReactionService } from '../ReactionService.js';
 | 
			
		||||
import type { UserEntityService } from './UserEntityService.js';
 | 
			
		||||
import type { DriveFileEntityService } from './DriveFileEntityService.js';
 | 
			
		||||
 | 
			
		||||
function mergeReactions(src: Record<string, number>, delta: Record<string, number>) {
 | 
			
		||||
	const reactions = { ...src };
 | 
			
		||||
	for (const [name, count] of Object.entries(delta)) {
 | 
			
		||||
		if (reactions[name] != null) {
 | 
			
		||||
			reactions[name] += count;
 | 
			
		||||
		} else {
 | 
			
		||||
			reactions[name] = count;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return reactions;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class NoteEntityService implements OnModuleInit {
 | 
			
		||||
	private userEntityService: UserEntityService;
 | 
			
		||||
	private driveFileEntityService: DriveFileEntityService;
 | 
			
		||||
	private customEmojiService: CustomEmojiService;
 | 
			
		||||
	private reactionService: ReactionService;
 | 
			
		||||
	private reactionsBufferingService: ReactionsBufferingService;
 | 
			
		||||
	private idService: IdService;
 | 
			
		||||
	private noteLoader = new DebounceLoader(this.findNoteOrFail);
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		private moduleRef: ModuleRef,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.usersRepository)
 | 
			
		||||
		private usersRepository: UsersRepository,
 | 
			
		||||
 | 
			
		||||
@@ -76,8 +59,6 @@ export class NoteEntityService implements OnModuleInit {
 | 
			
		||||
		//private driveFileEntityService: DriveFileEntityService,
 | 
			
		||||
		//private customEmojiService: CustomEmojiService,
 | 
			
		||||
		//private reactionService: ReactionService,
 | 
			
		||||
		//private reactionsBufferingService: ReactionsBufferingService,
 | 
			
		||||
		//private idService: IdService,
 | 
			
		||||
	) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -86,7 +67,6 @@ export class NoteEntityService implements OnModuleInit {
 | 
			
		||||
		this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
 | 
			
		||||
		this.customEmojiService = this.moduleRef.get('CustomEmojiService');
 | 
			
		||||
		this.reactionService = this.moduleRef.get('ReactionService');
 | 
			
		||||
		this.reactionsBufferingService = this.moduleRef.get('ReactionsBufferingService');
 | 
			
		||||
		this.idService = this.moduleRef.get('IdService');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -190,10 +170,10 @@ export class NoteEntityService implements OnModuleInit {
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async populateMyReaction(note: { id: MiNote['id']; reactions: MiNote['reactions']; reactionAndUserPairCache?: MiNote['reactionAndUserPairCache']; }, meId: MiUser['id'], _hint_?: {
 | 
			
		||||
		myReactions: Map<MiNote['id'], string | null>;
 | 
			
		||||
		myReactionsMap: Map<MiNote['id'], string | null>;
 | 
			
		||||
	}) {
 | 
			
		||||
		if (_hint_?.myReactions) {
 | 
			
		||||
			const reaction = _hint_.myReactions.get(note.id);
 | 
			
		||||
		if (_hint_?.myReactionsMap) {
 | 
			
		||||
			const reaction = _hint_.myReactionsMap.get(note.id);
 | 
			
		||||
			if (reaction) {
 | 
			
		||||
				return this.reactionService.convertLegacyReaction(reaction);
 | 
			
		||||
			} else {
 | 
			
		||||
@@ -307,7 +287,6 @@ export class NoteEntityService implements OnModuleInit {
 | 
			
		||||
			skipHide?: boolean;
 | 
			
		||||
			withReactionAndUserPairCache?: boolean;
 | 
			
		||||
			_hint_?: {
 | 
			
		||||
				bufferdReactions: Map<MiNote['id'], { deltas: Record<string, number>; pairs: ([MiUser['id'], string])[] }> | null;
 | 
			
		||||
				myReactions: Map<MiNote['id'], string | null>;
 | 
			
		||||
				packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
 | 
			
		||||
				packedUsers: Map<MiUser['id'], Packed<'UserLite'>>
 | 
			
		||||
@@ -324,20 +303,6 @@ export class NoteEntityService implements OnModuleInit {
 | 
			
		||||
		const note = typeof src === 'object' ? src : await this.noteLoader.load(src);
 | 
			
		||||
		const host = note.userHost;
 | 
			
		||||
 | 
			
		||||
		const bufferdReactions = opts._hint_?.bufferdReactions != null
 | 
			
		||||
			? (opts._hint_.bufferdReactions.get(note.id) ?? { deltas: {}, pairs: [] })
 | 
			
		||||
			: this.meta.enableReactionsBuffering
 | 
			
		||||
				? await this.reactionsBufferingService.get(note.id)
 | 
			
		||||
				: { deltas: {}, pairs: [] };
 | 
			
		||||
		const reactions = mergeReactions(this.reactionService.convertLegacyReactions(note.reactions), bufferdReactions.deltas ?? {});
 | 
			
		||||
		for (const [name, count] of Object.entries(reactions)) {
 | 
			
		||||
			if (count <= 0) {
 | 
			
		||||
				delete reactions[name];
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const reactionAndUserPairCache = note.reactionAndUserPairCache.concat(bufferdReactions.pairs.map(x => x.join('/')));
 | 
			
		||||
 | 
			
		||||
		let text = note.text;
 | 
			
		||||
 | 
			
		||||
		if (note.name && (note.url ?? note.uri)) {
 | 
			
		||||
@@ -350,7 +315,7 @@ export class NoteEntityService implements OnModuleInit {
 | 
			
		||||
				: await this.channelsRepository.findOneBy({ id: note.channelId })
 | 
			
		||||
			: null;
 | 
			
		||||
 | 
			
		||||
		const reactionEmojiNames = Object.keys(reactions)
 | 
			
		||||
		const reactionEmojiNames = Object.keys(note.reactions)
 | 
			
		||||
			.filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ
 | 
			
		||||
			.map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', ''));
 | 
			
		||||
		const packedFiles = options?._hint_?.packedFiles;
 | 
			
		||||
@@ -369,10 +334,10 @@ export class NoteEntityService implements OnModuleInit {
 | 
			
		||||
			visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined,
 | 
			
		||||
			renoteCount: note.renoteCount,
 | 
			
		||||
			repliesCount: note.repliesCount,
 | 
			
		||||
			reactionCount: Object.values(reactions).reduce((a, b) => a + b, 0),
 | 
			
		||||
			reactions: reactions,
 | 
			
		||||
			reactionCount: Object.values(note.reactions).reduce((a, b) => a + b, 0),
 | 
			
		||||
			reactions: this.reactionService.convertLegacyReactions(note.reactions),
 | 
			
		||||
			reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host),
 | 
			
		||||
			reactionAndUserPairCache: opts.withReactionAndUserPairCache ? reactionAndUserPairCache : undefined,
 | 
			
		||||
			reactionAndUserPairCache: opts.withReactionAndUserPairCache ? note.reactionAndUserPairCache : undefined,
 | 
			
		||||
			emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined,
 | 
			
		||||
			tags: note.tags.length > 0 ? note.tags : undefined,
 | 
			
		||||
			fileIds: note.fileIds,
 | 
			
		||||
@@ -411,12 +376,8 @@ export class NoteEntityService implements OnModuleInit {
 | 
			
		||||
 | 
			
		||||
				poll: note.hasPoll ? this.populatePoll(note, meId) : undefined,
 | 
			
		||||
 | 
			
		||||
				...(meId && Object.keys(reactions).length > 0 ? {
 | 
			
		||||
					myReaction: this.populateMyReaction({
 | 
			
		||||
						id: note.id,
 | 
			
		||||
						reactions: reactions,
 | 
			
		||||
						reactionAndUserPairCache: reactionAndUserPairCache,
 | 
			
		||||
					}, meId, options?._hint_),
 | 
			
		||||
				...(meId && Object.keys(note.reactions).length > 0 ? {
 | 
			
		||||
					myReaction: this.populateMyReaction(note, meId, options?._hint_),
 | 
			
		||||
				} : {}),
 | 
			
		||||
			} : {}),
 | 
			
		||||
		});
 | 
			
		||||
@@ -439,8 +400,6 @@ export class NoteEntityService implements OnModuleInit {
 | 
			
		||||
	) {
 | 
			
		||||
		if (notes.length === 0) return [];
 | 
			
		||||
 | 
			
		||||
		const bufferdReactions = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany(notes.map(x => x.id)) : null;
 | 
			
		||||
 | 
			
		||||
		const meId = me ? me.id : null;
 | 
			
		||||
		const myReactionsMap = new Map<MiNote['id'], string | null>();
 | 
			
		||||
		if (meId) {
 | 
			
		||||
@@ -451,33 +410,23 @@ export class NoteEntityService implements OnModuleInit {
 | 
			
		||||
 | 
			
		||||
			for (const note of notes) {
 | 
			
		||||
				if (note.renote && (note.text == null && note.fileIds.length === 0)) { // pure renote
 | 
			
		||||
					const reactionsCount = Object.values(mergeReactions(note.renote.reactions, bufferdReactions?.get(note.renote.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
 | 
			
		||||
					const reactionsCount = Object.values(note.renote.reactions).reduce((a, b) => a + b, 0);
 | 
			
		||||
					if (reactionsCount === 0) {
 | 
			
		||||
						myReactionsMap.set(note.renote.id, null);
 | 
			
		||||
					} else if (reactionsCount <= note.renote.reactionAndUserPairCache.length + (bufferdReactions?.get(note.renote.id)?.pairs.length ?? 0)) {
 | 
			
		||||
						const pairInBuffer = bufferdReactions?.get(note.renote.id)?.pairs.find(p => p[0] === meId);
 | 
			
		||||
						if (pairInBuffer) {
 | 
			
		||||
							myReactionsMap.set(note.renote.id, pairInBuffer[1]);
 | 
			
		||||
						} else {
 | 
			
		||||
					} else if (reactionsCount <= note.renote.reactionAndUserPairCache.length) {
 | 
			
		||||
						const pair = note.renote.reactionAndUserPairCache.find(p => p.startsWith(meId));
 | 
			
		||||
						myReactionsMap.set(note.renote.id, pair ? pair.split('/')[1] : null);
 | 
			
		||||
						}
 | 
			
		||||
					} else {
 | 
			
		||||
						idsNeedFetchMyReaction.add(note.renote.id);
 | 
			
		||||
					}
 | 
			
		||||
				} else {
 | 
			
		||||
					if (note.id < oldId) {
 | 
			
		||||
						const reactionsCount = Object.values(mergeReactions(note.reactions, bufferdReactions?.get(note.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
 | 
			
		||||
						const reactionsCount = Object.values(note.reactions).reduce((a, b) => a + b, 0);
 | 
			
		||||
						if (reactionsCount === 0) {
 | 
			
		||||
							myReactionsMap.set(note.id, null);
 | 
			
		||||
						} else if (reactionsCount <= note.reactionAndUserPairCache.length + (bufferdReactions?.get(note.id)?.pairs.length ?? 0)) {
 | 
			
		||||
							const pairInBuffer = bufferdReactions?.get(note.id)?.pairs.find(p => p[0] === meId);
 | 
			
		||||
							if (pairInBuffer) {
 | 
			
		||||
								myReactionsMap.set(note.id, pairInBuffer[1]);
 | 
			
		||||
							} else {
 | 
			
		||||
						} else if (reactionsCount <= note.reactionAndUserPairCache.length) {
 | 
			
		||||
							const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
 | 
			
		||||
							myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null);
 | 
			
		||||
							}
 | 
			
		||||
						} else {
 | 
			
		||||
							idsNeedFetchMyReaction.add(note.id);
 | 
			
		||||
						}
 | 
			
		||||
@@ -512,7 +461,6 @@ export class NoteEntityService implements OnModuleInit {
 | 
			
		||||
		return await Promise.all(notes.map(n => this.pack(n, me, {
 | 
			
		||||
			...options,
 | 
			
		||||
			_hint_: {
 | 
			
		||||
				bufferdReactions,
 | 
			
		||||
				myReactions: myReactionsMap,
 | 
			
		||||
				packedFiles,
 | 
			
		||||
				packedUsers,
 | 
			
		||||
 
 | 
			
		||||
@@ -3,14 +3,13 @@
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import si from 'systeminformation';
 | 
			
		||||
import Xev from 'xev';
 | 
			
		||||
import * as osUtils from 'os-utils';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import type { OnApplicationShutdown } from '@nestjs/common';
 | 
			
		||||
import { MiMeta } from '@/models/_.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
 | 
			
		||||
const ev = new Xev();
 | 
			
		||||
 | 
			
		||||
@@ -24,8 +23,7 @@ export class ServerStatsService implements OnApplicationShutdown {
 | 
			
		||||
	private intervalId: NodeJS.Timeout | null = null;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
	) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -34,7 +32,7 @@ export class ServerStatsService implements OnApplicationShutdown {
 | 
			
		||||
	 */
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async start(): Promise<void> {
 | 
			
		||||
		if (!this.meta.enableServerMachineStats) return;
 | 
			
		||||
		if (!(await this.metaService.fetch(true)).enableServerMachineStats) return;
 | 
			
		||||
 | 
			
		||||
		const log = [] as any[];
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,9 +10,8 @@
 | 
			
		||||
 * The getter will return a .bind version of the function
 | 
			
		||||
 * and memoize the result against a symbol on the instance
 | 
			
		||||
 */
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
export function bindThis(target: any, key: string, descriptor: any) {
 | 
			
		||||
	const fn = descriptor.value;
 | 
			
		||||
	let fn = descriptor.value;
 | 
			
		||||
 | 
			
		||||
	if (typeof fn !== 'function') {
 | 
			
		||||
		throw new TypeError(`@bindThis decorator can only be applied to methods not: ${typeof fn}`);
 | 
			
		||||
@@ -22,18 +21,26 @@ export function bindThis(target: any, key: string, descriptor: any) {
 | 
			
		||||
		configurable: true,
 | 
			
		||||
		get() {
 | 
			
		||||
			// eslint-disable-next-line no-prototype-builtins
 | 
			
		||||
			if (this === target.prototype || this.hasOwnProperty(key)) {
 | 
			
		||||
			if (this === target.prototype || this.hasOwnProperty(key) ||
 | 
			
		||||
        typeof fn !== 'function') {
 | 
			
		||||
				return fn;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const boundFn = fn.bind(this);
 | 
			
		||||
			Reflect.defineProperty(this, key, {
 | 
			
		||||
				value: boundFn,
 | 
			
		||||
			Object.defineProperty(this, key, {
 | 
			
		||||
				configurable: true,
 | 
			
		||||
				writable: true,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
				get() {
 | 
			
		||||
					return boundFn;
 | 
			
		||||
				},
 | 
			
		||||
				set(value) {
 | 
			
		||||
					fn = value;
 | 
			
		||||
					delete this[key];
 | 
			
		||||
				},
 | 
			
		||||
			});
 | 
			
		||||
			return boundFn;
 | 
			
		||||
		},
 | 
			
		||||
		set(value: any) {
 | 
			
		||||
			fn = value;
 | 
			
		||||
		},
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,13 +6,11 @@
 | 
			
		||||
export const DI = {
 | 
			
		||||
	config: Symbol('config'),
 | 
			
		||||
	db: Symbol('db'),
 | 
			
		||||
	meta: Symbol('meta'),
 | 
			
		||||
	meilisearch: Symbol('meilisearch'),
 | 
			
		||||
	redis: Symbol('redis'),
 | 
			
		||||
	redisForPub: Symbol('redisForPub'),
 | 
			
		||||
	redisForSub: Symbol('redisForSub'),
 | 
			
		||||
	redisForTimelines: Symbol('redisForTimelines'),
 | 
			
		||||
	redisForReactions: Symbol('redisForReactions'),
 | 
			
		||||
 | 
			
		||||
	//#region Repositories
 | 
			
		||||
	usersRepository: Symbol('usersRepository'),
 | 
			
		||||
 
 | 
			
		||||
@@ -7,23 +7,23 @@ import * as Redis from 'ioredis';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
 | 
			
		||||
export class RedisKVCache<T> {
 | 
			
		||||
	private readonly lifetime: number;
 | 
			
		||||
	private readonly memoryCache: MemoryKVCache<T>;
 | 
			
		||||
	private readonly fetcher: (key: string) => Promise<T>;
 | 
			
		||||
	private readonly toRedisConverter: (value: T) => string;
 | 
			
		||||
	private readonly fromRedisConverter: (value: string) => T | undefined;
 | 
			
		||||
	private redisClient: Redis.Redis;
 | 
			
		||||
	private name: string;
 | 
			
		||||
	private lifetime: number;
 | 
			
		||||
	private memoryCache: MemoryKVCache<T>;
 | 
			
		||||
	private fetcher: (key: string) => Promise<T>;
 | 
			
		||||
	private toRedisConverter: (value: T) => string;
 | 
			
		||||
	private fromRedisConverter: (value: string) => T | undefined;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		private redisClient: Redis.Redis,
 | 
			
		||||
		private name: string,
 | 
			
		||||
		opts: {
 | 
			
		||||
	constructor(redisClient: RedisKVCache<T>['redisClient'], name: RedisKVCache<T>['name'], opts: {
 | 
			
		||||
		lifetime: RedisKVCache<T>['lifetime'];
 | 
			
		||||
		memoryCacheLifetime: number;
 | 
			
		||||
		fetcher: RedisKVCache<T>['fetcher'];
 | 
			
		||||
		toRedisConverter: RedisKVCache<T>['toRedisConverter'];
 | 
			
		||||
		fromRedisConverter: RedisKVCache<T>['fromRedisConverter'];
 | 
			
		||||
		},
 | 
			
		||||
	) {
 | 
			
		||||
	}) {
 | 
			
		||||
		this.redisClient = redisClient;
 | 
			
		||||
		this.name = name;
 | 
			
		||||
		this.lifetime = opts.lifetime;
 | 
			
		||||
		this.memoryCache = new MemoryKVCache(opts.memoryCacheLifetime);
 | 
			
		||||
		this.fetcher = opts.fetcher;
 | 
			
		||||
@@ -55,13 +55,7 @@ export class RedisKVCache<T> {
 | 
			
		||||
 | 
			
		||||
		const cached = await this.redisClient.get(`kvcache:${this.name}:${key}`);
 | 
			
		||||
		if (cached == null) return undefined;
 | 
			
		||||
 | 
			
		||||
		const value = this.fromRedisConverter(cached);
 | 
			
		||||
		if (value !== undefined) {
 | 
			
		||||
			this.memoryCache.set(key, value);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return value;
 | 
			
		||||
		return this.fromRedisConverter(cached);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
@@ -72,10 +66,6 @@ export class RedisKVCache<T> {
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
 | 
			
		||||
	 * This awaits the call to Redis to ensure that the write succeeded, which is important for a few reasons:
 | 
			
		||||
	 *   * Other code uses this to synchronize changes between worker processes. A failed write can internally de-sync the cluster.
 | 
			
		||||
	 *   * Without an `await`, consecutive calls could race. An unlucky race could result in the older write overwriting the newer value.
 | 
			
		||||
	 *   * Not awaiting here makes the entire cache non-consistent. The prevents many possible uses.
 | 
			
		||||
	 */
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async fetch(key: string): Promise<T> {
 | 
			
		||||
@@ -87,14 +77,14 @@ export class RedisKVCache<T> {
 | 
			
		||||
 | 
			
		||||
		// Cache MISS
 | 
			
		||||
		const value = await this.fetcher(key);
 | 
			
		||||
		await this.set(key, value);
 | 
			
		||||
		this.set(key, value);
 | 
			
		||||
		return value;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async refresh(key: string) {
 | 
			
		||||
		const value = await this.fetcher(key);
 | 
			
		||||
		await this.set(key, value);
 | 
			
		||||
		this.set(key, value);
 | 
			
		||||
 | 
			
		||||
		// TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする
 | 
			
		||||
	}
 | 
			
		||||
@@ -111,23 +101,23 @@ export class RedisKVCache<T> {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class RedisSingleCache<T> {
 | 
			
		||||
	private readonly lifetime: number;
 | 
			
		||||
	private readonly memoryCache: MemorySingleCache<T>;
 | 
			
		||||
	private readonly fetcher: () => Promise<T>;
 | 
			
		||||
	private readonly toRedisConverter: (value: T) => string;
 | 
			
		||||
	private readonly fromRedisConverter: (value: string) => T | undefined;
 | 
			
		||||
	private redisClient: Redis.Redis;
 | 
			
		||||
	private name: string;
 | 
			
		||||
	private lifetime: number;
 | 
			
		||||
	private memoryCache: MemorySingleCache<T>;
 | 
			
		||||
	private fetcher: () => Promise<T>;
 | 
			
		||||
	private toRedisConverter: (value: T) => string;
 | 
			
		||||
	private fromRedisConverter: (value: string) => T | undefined;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		private redisClient: Redis.Redis,
 | 
			
		||||
		private name: string,
 | 
			
		||||
		opts: {
 | 
			
		||||
			lifetime: number;
 | 
			
		||||
	constructor(redisClient: RedisSingleCache<T>['redisClient'], name: RedisSingleCache<T>['name'], opts: {
 | 
			
		||||
		lifetime: RedisSingleCache<T>['lifetime'];
 | 
			
		||||
		memoryCacheLifetime: number;
 | 
			
		||||
		fetcher: RedisSingleCache<T>['fetcher'];
 | 
			
		||||
		toRedisConverter: RedisSingleCache<T>['toRedisConverter'];
 | 
			
		||||
		fromRedisConverter: RedisSingleCache<T>['fromRedisConverter'];
 | 
			
		||||
		},
 | 
			
		||||
	) {
 | 
			
		||||
	}) {
 | 
			
		||||
		this.redisClient = redisClient;
 | 
			
		||||
		this.name = name;
 | 
			
		||||
		this.lifetime = opts.lifetime;
 | 
			
		||||
		this.memoryCache = new MemorySingleCache(opts.memoryCacheLifetime);
 | 
			
		||||
		this.fetcher = opts.fetcher;
 | 
			
		||||
@@ -159,13 +149,7 @@ export class RedisSingleCache<T> {
 | 
			
		||||
 | 
			
		||||
		const cached = await this.redisClient.get(`singlecache:${this.name}`);
 | 
			
		||||
		if (cached == null) return undefined;
 | 
			
		||||
 | 
			
		||||
		const value = this.fromRedisConverter(cached);
 | 
			
		||||
		if (value !== undefined) {
 | 
			
		||||
			this.memoryCache.set(value);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return value;
 | 
			
		||||
		return this.fromRedisConverter(cached);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
@@ -176,10 +160,6 @@ export class RedisSingleCache<T> {
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
 | 
			
		||||
	 * This awaits the call to Redis to ensure that the write succeeded, which is important for a few reasons:
 | 
			
		||||
	 *   * Other code uses this to synchronize changes between worker processes. A failed write can internally de-sync the cluster.
 | 
			
		||||
	 *   * Without an `await`, consecutive calls could race. An unlucky race could result in the older write overwriting the newer value.
 | 
			
		||||
	 *   * Not awaiting here makes the entire cache non-consistent. The prevents many possible uses.
 | 
			
		||||
	 */
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async fetch(): Promise<T> {
 | 
			
		||||
@@ -191,14 +171,14 @@ export class RedisSingleCache<T> {
 | 
			
		||||
 | 
			
		||||
		// Cache MISS
 | 
			
		||||
		const value = await this.fetcher();
 | 
			
		||||
		await this.set(value);
 | 
			
		||||
		this.set(value);
 | 
			
		||||
		return value;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async refresh() {
 | 
			
		||||
		const value = await this.fetcher();
 | 
			
		||||
		await this.set(value);
 | 
			
		||||
		this.set(value);
 | 
			
		||||
 | 
			
		||||
		// TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする
 | 
			
		||||
	}
 | 
			
		||||
@@ -207,12 +187,22 @@ export class RedisSingleCache<T> {
 | 
			
		||||
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
 | 
			
		||||
 | 
			
		||||
export class MemoryKVCache<T> {
 | 
			
		||||
	private readonly cache = new Map<string, { date: number; value: T; }>();
 | 
			
		||||
	private readonly gcIntervalHandle = setInterval(() => this.gc(), 1000 * 60 * 3); // 3m
 | 
			
		||||
	/**
 | 
			
		||||
	 * データを持つマップ
 | 
			
		||||
	 * @deprecated これを直接操作するべきではない
 | 
			
		||||
	 */
 | 
			
		||||
	public cache: Map<string, { date: number; value: T; }>;
 | 
			
		||||
	private lifetime: number;
 | 
			
		||||
	private gcIntervalHandle: NodeJS.Timeout;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		private readonly lifetime: number,
 | 
			
		||||
	) {}
 | 
			
		||||
	constructor(lifetime: MemoryKVCache<never>['lifetime']) {
 | 
			
		||||
		this.cache = new Map();
 | 
			
		||||
		this.lifetime = lifetime;
 | 
			
		||||
 | 
			
		||||
		this.gcIntervalHandle = setInterval(() => {
 | 
			
		||||
			this.gc();
 | 
			
		||||
		}, 1000 * 60 * 3);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	/**
 | 
			
		||||
@@ -297,34 +287,27 @@ export class MemoryKVCache<T> {
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public gc(): void {
 | 
			
		||||
		const now = Date.now();
 | 
			
		||||
 | 
			
		||||
		for (const [key, { date }] of this.cache.entries()) {
 | 
			
		||||
			// The map is ordered from oldest to youngest.
 | 
			
		||||
			// We can stop once we find an entry that's still active, because all following entries must *also* be active.
 | 
			
		||||
			const age = now - date;
 | 
			
		||||
			if (age < this.lifetime) break;
 | 
			
		||||
 | 
			
		||||
			if ((now - date) > this.lifetime) {
 | 
			
		||||
				this.cache.delete(key);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public dispose(): void {
 | 
			
		||||
		clearInterval(this.gcIntervalHandle);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public get entries() {
 | 
			
		||||
		return this.cache.entries();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class MemorySingleCache<T> {
 | 
			
		||||
	private cachedAt: number | null = null;
 | 
			
		||||
	private value: T | undefined;
 | 
			
		||||
	private lifetime: number;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		private lifetime: number,
 | 
			
		||||
	) {}
 | 
			
		||||
	constructor(lifetime: MemorySingleCache<never>['lifetime']) {
 | 
			
		||||
		this.lifetime = lifetime;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public set(value: T): void {
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ import type { onRequestHookHandler } from 'fastify';
 | 
			
		||||
export const handleRequestRedirectToOmitSearch: onRequestHookHandler = (request, reply, done) => {
 | 
			
		||||
	const index = request.url.indexOf('?');
 | 
			
		||||
	if (~index) {
 | 
			
		||||
		reply.redirect(request.url.slice(0, index), 301);
 | 
			
		||||
		reply.redirect(301, request.url.slice(0, index));
 | 
			
		||||
	}
 | 
			
		||||
	done();
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -144,9 +144,7 @@ export interface Schema extends OfSchema {
 | 
			
		||||
	readonly type?: TypeStringef;
 | 
			
		||||
	readonly nullable?: boolean;
 | 
			
		||||
	readonly optional?: boolean;
 | 
			
		||||
	readonly prefixItems?: ReadonlyArray<Schema>;
 | 
			
		||||
	readonly items?: Schema;
 | 
			
		||||
	readonly unevaluatedItems?: Schema | boolean;
 | 
			
		||||
	readonly properties?: Obj;
 | 
			
		||||
	readonly required?: ReadonlyArray<Extract<keyof NonNullable<this['properties']>, string>>;
 | 
			
		||||
	readonly description?: string;
 | 
			
		||||
@@ -200,7 +198,6 @@ type UnionSchemaType<a extends readonly any[], X extends Schema = a[number]> = X
 | 
			
		||||
//type UnionObjectSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? ObjectSchemaType<X> : never;
 | 
			
		||||
type UnionObjType<s extends Obj, a extends readonly any[], X extends ReadonlyArray<keyof s> = a[number]> = X extends any ? ObjType<s, X> : never;
 | 
			
		||||
type ArrayUnion<T> = T extends any ? Array<T> : never;
 | 
			
		||||
type ArrayToTuple<X extends ReadonlyArray<Schema>> = { [K in keyof X]: SchemaType<X[K]> };
 | 
			
		||||
 | 
			
		||||
type ObjectSchemaTypeDef<p extends Schema> =
 | 
			
		||||
	p['ref'] extends keyof typeof refs ? Packed<p['ref']> :
 | 
			
		||||
@@ -235,12 +232,6 @@ export type SchemaTypeDef<p extends Schema> =
 | 
			
		||||
			p['items']['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<NonNullable<p['items']['allOf']>>>[] :
 | 
			
		||||
			never
 | 
			
		||||
		) :
 | 
			
		||||
		p['prefixItems'] extends ReadonlyArray<Schema> ? (
 | 
			
		||||
			p['items'] extends NonNullable<Schema> ? [...ArrayToTuple<p['prefixItems']>, ...SchemaType<p['items']>[]] :
 | 
			
		||||
			p['items'] extends false ? ArrayToTuple<p['prefixItems']> :
 | 
			
		||||
			p['unevaluatedItems'] extends false ? ArrayToTuple<p['prefixItems']> :
 | 
			
		||||
			[...ArrayToTuple<p['prefixItems']>, ...unknown[]]
 | 
			
		||||
		) :
 | 
			
		||||
		p['items'] extends NonNullable<Schema> ? SchemaType<p['items']>[] :
 | 
			
		||||
		any[]
 | 
			
		||||
	) :
 | 
			
		||||
 
 | 
			
		||||
@@ -589,11 +589,6 @@ export class MiMeta {
 | 
			
		||||
	})
 | 
			
		||||
	public perUserListTimelineCacheMax: number;
 | 
			
		||||
 | 
			
		||||
	@Column('boolean', {
 | 
			
		||||
		default: false,
 | 
			
		||||
	})
 | 
			
		||||
	public enableReactionsBuffering: boolean;
 | 
			
		||||
 | 
			
		||||
	@Column('integer', {
 | 
			
		||||
		default: 0,
 | 
			
		||||
	})
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,8 @@ import { MiUser } from './User.js';
 | 
			
		||||
import { MiNote } from './Note.js';
 | 
			
		||||
 | 
			
		||||
@Entity('note_reaction')
 | 
			
		||||
@Index(['userId', 'noteId'], { unique: true })
 | 
			
		||||
@Index(['userId', 'noteId'])
 | 
			
		||||
@Index(['userId', 'noteId', 'reaction'], { unique: true })
 | 
			
		||||
export class MiNoteReaction {
 | 
			
		||||
	@PrimaryColumn(id())
 | 
			
		||||
	public id: string;
 | 
			
		||||
 
 | 
			
		||||
@@ -85,7 +85,7 @@ export type MiNotification = {
 | 
			
		||||
	/**
 | 
			
		||||
	 * アプリ通知のbody
 | 
			
		||||
	 */
 | 
			
		||||
	customBody: string;
 | 
			
		||||
	customBody: string | null;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * アプリ通知のheader
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,6 @@ import { id } from './util/id.js';
 | 
			
		||||
import { MiUser } from './User.js';
 | 
			
		||||
 | 
			
		||||
export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction'] as const;
 | 
			
		||||
export type WebhookEventTypes = typeof webhookEventTypes[number];
 | 
			
		||||
 | 
			
		||||
@Entity('webhook')
 | 
			
		||||
export class MiWebhook {
 | 
			
		||||
 
 | 
			
		||||
@@ -253,10 +253,6 @@ export const packedMetaLiteSchema = {
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
			default: 'local',
 | 
			
		||||
		},
 | 
			
		||||
		maxFileSize: {
 | 
			
		||||
			type: 'number',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,6 @@
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { ACHIEVEMENT_TYPES } from '@/core/AchievementService.js';
 | 
			
		||||
import { notificationTypes } from '@/types.js';
 | 
			
		||||
 | 
			
		||||
const baseSchema = {
 | 
			
		||||
@@ -295,7 +294,6 @@ export const packedNotificationSchema = {
 | 
			
		||||
			achievement: {
 | 
			
		||||
				type: 'string',
 | 
			
		||||
				optional: false, nullable: false,
 | 
			
		||||
				enum: ACHIEVEMENT_TYPES,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}, {
 | 
			
		||||
@@ -313,11 +311,11 @@ export const packedNotificationSchema = {
 | 
			
		||||
			},
 | 
			
		||||
			header: {
 | 
			
		||||
				type: 'string',
 | 
			
		||||
				optional: false, nullable: true,
 | 
			
		||||
				optional: false, nullable: false,
 | 
			
		||||
			},
 | 
			
		||||
			icon: {
 | 
			
		||||
				type: 'string',
 | 
			
		||||
				optional: false, nullable: true,
 | 
			
		||||
				optional: false, nullable: false,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}, {
 | 
			
		||||
 
 | 
			
		||||
@@ -236,6 +236,10 @@ export const packedRolePoliciesSchema = {
 | 
			
		||||
			type: 'integer',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		reactionsPerNoteLimit: {
 | 
			
		||||
			type: 'integer',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		antennaLimit: {
 | 
			
		||||
			type: 'integer',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
@@ -272,26 +276,6 @@ export const packedRolePoliciesSchema = {
 | 
			
		||||
			type: 'integer',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		canImportAntennas: {
 | 
			
		||||
			type: 'boolean',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		canImportBlocking: {
 | 
			
		||||
			type: 'boolean',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		canImportFollowing: {
 | 
			
		||||
			type: 'boolean',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		canImportMuting: {
 | 
			
		||||
			type: 'boolean',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		canImportUserLists: {
 | 
			
		||||
			type: 'boolean',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,6 @@ import { InboxProcessorService } from './processors/InboxProcessorService.js';
 | 
			
		||||
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
 | 
			
		||||
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
 | 
			
		||||
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
 | 
			
		||||
import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js';
 | 
			
		||||
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
 | 
			
		||||
import { CleanProcessorService } from './processors/CleanProcessorService.js';
 | 
			
		||||
import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
 | 
			
		||||
@@ -52,7 +51,6 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
 | 
			
		||||
		ResyncChartsProcessorService,
 | 
			
		||||
		CleanChartsProcessorService,
 | 
			
		||||
		CheckExpiredMutingsProcessorService,
 | 
			
		||||
		BakeBufferedReactionsProcessorService,
 | 
			
		||||
		CleanProcessorService,
 | 
			
		||||
		DeleteDriveFilesProcessorService,
 | 
			
		||||
		ExportCustomEmojisProcessorService,
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,6 @@ import { TickChartsProcessorService } from './processors/TickChartsProcessorServ
 | 
			
		||||
import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js';
 | 
			
		||||
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
 | 
			
		||||
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
 | 
			
		||||
import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js';
 | 
			
		||||
import { CleanProcessorService } from './processors/CleanProcessorService.js';
 | 
			
		||||
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
 | 
			
		||||
import { QueueLoggerService } from './QueueLoggerService.js';
 | 
			
		||||
@@ -119,7 +118,6 @@ export class QueueProcessorService implements OnApplicationShutdown {
 | 
			
		||||
		private cleanChartsProcessorService: CleanChartsProcessorService,
 | 
			
		||||
		private aggregateRetentionProcessorService: AggregateRetentionProcessorService,
 | 
			
		||||
		private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService,
 | 
			
		||||
		private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService,
 | 
			
		||||
		private cleanProcessorService: CleanProcessorService,
 | 
			
		||||
	) {
 | 
			
		||||
		this.logger = this.queueLoggerService.logger;
 | 
			
		||||
@@ -149,7 +147,6 @@ export class QueueProcessorService implements OnApplicationShutdown {
 | 
			
		||||
					case 'cleanCharts': return this.cleanChartsProcessorService.process();
 | 
			
		||||
					case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
 | 
			
		||||
					case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
 | 
			
		||||
					case 'bakeBufferedReactions': return this.bakeBufferedReactionsProcessorService.process();
 | 
			
		||||
					case 'clean': return this.cleanProcessorService.process();
 | 
			
		||||
					default: throw new Error(`unrecognized job type ${job.name} for system`);
 | 
			
		||||
				}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,42 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import type Logger from '@/logger.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
 | 
			
		||||
import { QueueLoggerService } from '../QueueLoggerService.js';
 | 
			
		||||
import type * as Bull from 'bullmq';
 | 
			
		||||
import { MiMeta } from '@/models/_.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class BakeBufferedReactionsProcessorService {
 | 
			
		||||
	private logger: Logger;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
 | 
			
		||||
		private reactionsBufferingService: ReactionsBufferingService,
 | 
			
		||||
		private queueLoggerService: QueueLoggerService,
 | 
			
		||||
	) {
 | 
			
		||||
		this.logger = this.queueLoggerService.logger.createSubLogger('bake-buffered-reactions');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async process(): Promise<void> {
 | 
			
		||||
		if (!this.meta.enableReactionsBuffering) {
 | 
			
		||||
			this.logger.info('Reactions buffering is disabled. Skipping...');
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		this.logger.info('Baking buffered reactions...');
 | 
			
		||||
 | 
			
		||||
		await this.reactionsBufferingService.bake();
 | 
			
		||||
 | 
			
		||||
		this.logger.succ('All buffered reactions baked.');
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -7,8 +7,9 @@ import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import * as Bull from 'bullmq';
 | 
			
		||||
import { Not } from 'typeorm';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { InstancesRepository, MiMeta } from '@/models/_.js';
 | 
			
		||||
import type { InstancesRepository } from '@/models/_.js';
 | 
			
		||||
import type Logger from '@/logger.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { ApRequestService } from '@/core/activitypub/ApRequestService.js';
 | 
			
		||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
 | 
			
		||||
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
 | 
			
		||||
@@ -30,12 +31,10 @@ export class DeliverProcessorService {
 | 
			
		||||
	private latest: string | null;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.instancesRepository)
 | 
			
		||||
		private instancesRepository: InstancesRepository,
 | 
			
		||||
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
		private utilityService: UtilityService,
 | 
			
		||||
		private federatedInstanceService: FederatedInstanceService,
 | 
			
		||||
		private fetchInstanceMetadataService: FetchInstanceMetadataService,
 | 
			
		||||
@@ -46,7 +45,7 @@ export class DeliverProcessorService {
 | 
			
		||||
		private queueLoggerService: QueueLoggerService,
 | 
			
		||||
	) {
 | 
			
		||||
		this.logger = this.queueLoggerService.logger.createSubLogger('deliver');
 | 
			
		||||
		this.suspendedHostsCache = new MemorySingleCache<MiInstance[]>(1000 * 60 * 60); // 1h
 | 
			
		||||
		this.suspendedHostsCache = new MemorySingleCache<MiInstance[]>(1000 * 60 * 60);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
@@ -54,7 +53,8 @@ export class DeliverProcessorService {
 | 
			
		||||
		const { host } = new URL(job.data.to);
 | 
			
		||||
 | 
			
		||||
		// ブロックしてたら中断
 | 
			
		||||
		if (this.utilityService.isBlockedHost(this.meta.blockedHosts, this.utilityService.toPuny(host))) {
 | 
			
		||||
		const meta = await this.metaService.fetch();
 | 
			
		||||
		if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.toPuny(host))) {
 | 
			
		||||
			return 'skip (blocked)';
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -88,7 +88,7 @@ export class DeliverProcessorService {
 | 
			
		||||
				this.apRequestChart.deliverSucc();
 | 
			
		||||
				this.federationChart.deliverd(i.host, true);
 | 
			
		||||
 | 
			
		||||
				if (this.meta.enableChartsForFederatedInstances) {
 | 
			
		||||
				if (meta.enableChartsForFederatedInstances) {
 | 
			
		||||
					this.instanceChart.requestSent(i.host, true);
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
@@ -120,7 +120,7 @@ export class DeliverProcessorService {
 | 
			
		||||
				this.apRequestChart.deliverFail();
 | 
			
		||||
				this.federationChart.deliverd(i.host, false);
 | 
			
		||||
 | 
			
		||||
				if (this.meta.enableChartsForFederatedInstances) {
 | 
			
		||||
				if (meta.enableChartsForFederatedInstances) {
 | 
			
		||||
					this.instanceChart.requestSent(i.host, false);
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
 
 | 
			
		||||
@@ -4,10 +4,11 @@
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { URL } from 'node:url';
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import httpSignature from '@peertube/http-signature';
 | 
			
		||||
import * as Bull from 'bullmq';
 | 
			
		||||
import type Logger from '@/logger.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
 | 
			
		||||
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
 | 
			
		||||
import InstanceChart from '@/core/chart/charts/instance.js';
 | 
			
		||||
@@ -27,18 +28,14 @@ import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
 | 
			
		||||
import { QueueLoggerService } from '../QueueLoggerService.js';
 | 
			
		||||
import type { InboxJobData } from '../types.js';
 | 
			
		||||
import { MiMeta } from '@/models/Meta.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class InboxProcessorService {
 | 
			
		||||
	private logger: Logger;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
 | 
			
		||||
		private utilityService: UtilityService,
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
		private apInboxService: ApInboxService,
 | 
			
		||||
		private federatedInstanceService: FederatedInstanceService,
 | 
			
		||||
		private fetchInstanceMetadataService: FetchInstanceMetadataService,
 | 
			
		||||
@@ -67,7 +64,8 @@ export class InboxProcessorService {
 | 
			
		||||
		const host = this.utilityService.toPuny(new URL(signature.keyId).hostname);
 | 
			
		||||
 | 
			
		||||
		// ブロックしてたら中断
 | 
			
		||||
		if (this.utilityService.isBlockedHost(this.meta.blockedHosts, host)) {
 | 
			
		||||
		const meta = await this.metaService.fetch();
 | 
			
		||||
		if (this.utilityService.isBlockedHost(meta.blockedHosts, host)) {
 | 
			
		||||
			return `Blocked request: ${host}`;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -168,7 +166,7 @@ export class InboxProcessorService {
 | 
			
		||||
 | 
			
		||||
				// ブロックしてたら中断
 | 
			
		||||
				const ldHost = this.utilityService.extractDbHost(authUser.user.uri);
 | 
			
		||||
				if (this.utilityService.isBlockedHost(this.meta.blockedHosts, ldHost)) {
 | 
			
		||||
				if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) {
 | 
			
		||||
					throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`);
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
@@ -199,7 +197,7 @@ export class InboxProcessorService {
 | 
			
		||||
			this.apRequestChart.inbox();
 | 
			
		||||
			this.federationChart.inbox(i.host);
 | 
			
		||||
 | 
			
		||||
			if (this.meta.enableChartsForFederatedInstances) {
 | 
			
		||||
			if (meta.enableChartsForFederatedInstances) {
 | 
			
		||||
				this.instanceChart.requestReceived(i.host);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 
 | 
			
		||||
@@ -82,7 +82,7 @@ export class FileServerService {
 | 
			
		||||
					.catch(err => this.errorHandler(request, reply, err));
 | 
			
		||||
			});
 | 
			
		||||
			fastify.get<{ Params: { key: string; } }>('/files/:key/*', async (request, reply) => {
 | 
			
		||||
				return await reply.redirect(`${this.config.url}/files/${request.params.key}`, 301);
 | 
			
		||||
				return await reply.redirect(301, `${this.config.url}/files/${request.params.key}`);
 | 
			
		||||
			});
 | 
			
		||||
			done();
 | 
			
		||||
		});
 | 
			
		||||
@@ -147,12 +147,12 @@ export class FileServerService {
 | 
			
		||||
						url.searchParams.set('static', '1');
 | 
			
		||||
 | 
			
		||||
						file.cleanup();
 | 
			
		||||
						return await reply.redirect(url.toString(), 301);
 | 
			
		||||
						return await reply.redirect(301, url.toString());
 | 
			
		||||
					} else if (file.mime.startsWith('video/')) {
 | 
			
		||||
						const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url);
 | 
			
		||||
						if (externalThumbnail) {
 | 
			
		||||
							file.cleanup();
 | 
			
		||||
							return await reply.redirect(externalThumbnail, 301);
 | 
			
		||||
							return await reply.redirect(301, externalThumbnail);
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						image = await this.videoProcessingService.generateVideoThumbnail(file.path);
 | 
			
		||||
@@ -167,7 +167,7 @@ export class FileServerService {
 | 
			
		||||
						url.searchParams.set('url', file.url);
 | 
			
		||||
 | 
			
		||||
						file.cleanup();
 | 
			
		||||
						return await reply.redirect(url.toString(), 301);
 | 
			
		||||
						return await reply.redirect(301, url.toString());
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
@@ -314,8 +314,8 @@ export class FileServerService {
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return await reply.redirect(
 | 
			
		||||
				url.toString(),
 | 
			
		||||
				301,
 | 
			
		||||
				url.toString(),
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -27,9 +27,6 @@ export class HealthServerService {
 | 
			
		||||
		@Inject(DI.redisForTimelines)
 | 
			
		||||
		private redisForTimelines: Redis.Redis,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.redisForReactions)
 | 
			
		||||
		private redisForReactions: Redis.Redis,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.db)
 | 
			
		||||
		private db: DataSource,
 | 
			
		||||
 | 
			
		||||
@@ -46,7 +43,6 @@ export class HealthServerService {
 | 
			
		||||
				this.redisForPub.ping(),
 | 
			
		||||
				this.redisForSub.ping(),
 | 
			
		||||
				this.redisForTimelines.ping(),
 | 
			
		||||
				this.redisForReactions.ping(),
 | 
			
		||||
				this.db.query('SELECT 1'),
 | 
			
		||||
				...(this.meilisearch ? [this.meilisearch.health()] : []),
 | 
			
		||||
			]).then(() => 200, () => 503));
 | 
			
		||||
 
 | 
			
		||||
@@ -134,7 +134,7 @@ export class NodeinfoServerService {
 | 
			
		||||
			return document;
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		const cache = new MemorySingleCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10); // 10m
 | 
			
		||||
		const cache = new MemorySingleCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10);
 | 
			
		||||
 | 
			
		||||
		fastify.get(nodeinfo2_1path, async (request, reply) => {
 | 
			
		||||
			const base = await cache.fetch(() => nodeinfo2(21));
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ import fastifyRawBody from 'fastify-raw-body';
 | 
			
		||||
import { IsNull } from 'typeorm';
 | 
			
		||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
import type { EmojisRepository, MiMeta, UserProfilesRepository, UsersRepository } from '@/models/_.js';
 | 
			
		||||
import type { EmojisRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type Logger from '@/logger.js';
 | 
			
		||||
import * as Acct from '@/misc/acct.js';
 | 
			
		||||
@@ -21,6 +21,7 @@ import { genIdenticon } from '@/misc/gen-identicon.js';
 | 
			
		||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
			
		||||
import { LoggerService } from '@/core/LoggerService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { ActivityPubServerService } from './ActivityPubServerService.js';
 | 
			
		||||
import { NodeinfoServerService } from './NodeinfoServerService.js';
 | 
			
		||||
import { ApiServerService } from './api/ApiServerService.js';
 | 
			
		||||
@@ -43,9 +44,6 @@ export class ServerService implements OnApplicationShutdown {
 | 
			
		||||
		@Inject(DI.config)
 | 
			
		||||
		private config: Config,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.usersRepository)
 | 
			
		||||
		private usersRepository: UsersRepository,
 | 
			
		||||
 | 
			
		||||
@@ -55,6 +53,7 @@ export class ServerService implements OnApplicationShutdown {
 | 
			
		||||
		@Inject(DI.emojisRepository)
 | 
			
		||||
		private emojisRepository: EmojisRepository,
 | 
			
		||||
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
		private userEntityService: UserEntityService,
 | 
			
		||||
		private apiServerService: ApiServerService,
 | 
			
		||||
		private openApiServerService: OpenApiServerService,
 | 
			
		||||
@@ -166,8 +165,8 @@ export class ServerService implements OnApplicationShutdown {
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return await reply.redirect(
 | 
			
		||||
				url.toString(),
 | 
			
		||||
				301,
 | 
			
		||||
				url.toString(),
 | 
			
		||||
			);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
@@ -194,7 +193,7 @@ export class ServerService implements OnApplicationShutdown {
 | 
			
		||||
			reply.header('Content-Type', 'image/png');
 | 
			
		||||
			reply.header('Cache-Control', 'public, max-age=86400');
 | 
			
		||||
 | 
			
		||||
			if (this.meta.enableIdenticonGeneration) {
 | 
			
		||||
			if ((await this.metaService.fetch()).enableIdenticonGeneration) {
 | 
			
		||||
				return await genIdenticon(request.params.x);
 | 
			
		||||
			} else {
 | 
			
		||||
				return reply.redirect('/static-assets/avatar.png');
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,8 @@ import { getIpHash } from '@/misc/get-ip-hash.js';
 | 
			
		||||
import type { MiLocalUser, MiUser } from '@/models/User.js';
 | 
			
		||||
import type { MiAccessToken } from '@/models/AccessToken.js';
 | 
			
		||||
import type Logger from '@/logger.js';
 | 
			
		||||
import type { MiMeta, UserIpsRepository } from '@/models/_.js';
 | 
			
		||||
import type { UserIpsRepository } from '@/models/_.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { createTemp } from '@/misc/create-temp.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { RoleService } from '@/core/RoleService.js';
 | 
			
		||||
@@ -39,15 +40,13 @@ export class ApiCallService implements OnApplicationShutdown {
 | 
			
		||||
	private userIpHistoriesClearIntervalId: NodeJS.Timeout;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.config)
 | 
			
		||||
		private config: Config,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.userIpsRepository)
 | 
			
		||||
		private userIpsRepository: UserIpsRepository,
 | 
			
		||||
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
		private authenticateService: AuthenticateService,
 | 
			
		||||
		private rateLimiterService: RateLimiterService,
 | 
			
		||||
		private roleService: RoleService,
 | 
			
		||||
@@ -65,6 +64,15 @@ export class ApiCallService implements OnApplicationShutdown {
 | 
			
		||||
		let statusCode = err.httpStatusCode;
 | 
			
		||||
		if (err.httpStatusCode === 401) {
 | 
			
		||||
			reply.header('WWW-Authenticate', 'Bearer realm="Misskey"');
 | 
			
		||||
		} else if (err.kind === 'client') {
 | 
			
		||||
			reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="invalid_request", error_description="${err.message}"`);
 | 
			
		||||
			statusCode = statusCode ?? 400;
 | 
			
		||||
		} else if (err.kind === 'permission') {
 | 
			
		||||
			// (ROLE_PERMISSION_DENIEDは関係ない)
 | 
			
		||||
			if (err.code === 'PERMISSION_DENIED') {
 | 
			
		||||
				reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="insufficient_scope", error_description="${err.message}"`);
 | 
			
		||||
			}
 | 
			
		||||
			statusCode = statusCode ?? 403;
 | 
			
		||||
		} else if (err.code === 'RATE_LIMIT_EXCEEDED') {
 | 
			
		||||
			const info: unknown = err.info;
 | 
			
		||||
			const unixEpochInSeconds = Date.now();
 | 
			
		||||
@@ -75,15 +83,6 @@ export class ApiCallService implements OnApplicationShutdown {
 | 
			
		||||
			} else {
 | 
			
		||||
				this.logger.warn(`rate limit information has unexpected type ${typeof(err.info?.reset)}`);
 | 
			
		||||
			}
 | 
			
		||||
		} else if (err.kind === 'client') {
 | 
			
		||||
			reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="invalid_request", error_description="${err.message}"`);
 | 
			
		||||
			statusCode = statusCode ?? 400;
 | 
			
		||||
		} else if (err.kind === 'permission') {
 | 
			
		||||
			// (ROLE_PERMISSION_DENIEDは関係ない)
 | 
			
		||||
			if (err.code === 'PERMISSION_DENIED') {
 | 
			
		||||
				reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="insufficient_scope", error_description="${err.message}"`);
 | 
			
		||||
			}
 | 
			
		||||
			statusCode = statusCode ?? 403;
 | 
			
		||||
		} else if (!statusCode) {
 | 
			
		||||
			statusCode = 500;
 | 
			
		||||
		}
 | 
			
		||||
@@ -200,18 +199,9 @@ export class ApiCallService implements OnApplicationShutdown {
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const [path, cleanup] = await createTemp();
 | 
			
		||||
		const [path] = await createTemp();
 | 
			
		||||
		await stream.pipeline(multipartData.file, fs.createWriteStream(path));
 | 
			
		||||
 | 
			
		||||
		// ファイルサイズが制限を超えていた場合
 | 
			
		||||
		// なお truncated はストリームを読み切ってからでないと機能しないため、stream.pipeline より後にある必要がある
 | 
			
		||||
		if (multipartData.file.truncated) {
 | 
			
		||||
			cleanup();
 | 
			
		||||
			reply.code(413);
 | 
			
		||||
			reply.send();
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const fields = {} as Record<string, unknown>;
 | 
			
		||||
		for (const [k, v] of Object.entries(multipartData.fields)) {
 | 
			
		||||
			fields[k] = typeof v === 'object' && 'value' in v ? v.value : undefined;
 | 
			
		||||
@@ -266,8 +256,9 @@ export class ApiCallService implements OnApplicationShutdown {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	private logIp(request: FastifyRequest, user: MiLocalUser) {
 | 
			
		||||
		if (!this.meta.enableIpLogging) return;
 | 
			
		||||
	private async logIp(request: FastifyRequest, user: MiLocalUser) {
 | 
			
		||||
		const meta = await this.metaService.fetch();
 | 
			
		||||
		if (!meta.enableIpLogging) return;
 | 
			
		||||
		const ip = request.ip;
 | 
			
		||||
		const ips = this.userIpHistories.get(user.id);
 | 
			
		||||
		if (ips == null || !ips.has(ip)) {
 | 
			
		||||
 
 | 
			
		||||
@@ -49,7 +49,7 @@ export class ApiServerService {
 | 
			
		||||
 | 
			
		||||
		fastify.register(multipart, {
 | 
			
		||||
			limits: {
 | 
			
		||||
				fileSize: this.config.maxFileSize,
 | 
			
		||||
				fileSize: this.config.maxFileSize ?? 262144000,
 | 
			
		||||
				files: 1,
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
 
 | 
			
		||||
@@ -37,7 +37,7 @@ export class AuthenticateService implements OnApplicationShutdown {
 | 
			
		||||
 | 
			
		||||
		private cacheService: CacheService,
 | 
			
		||||
	) {
 | 
			
		||||
		this.appCache = new MemoryKVCache<MiApp>(1000 * 60 * 60 * 24 * 7); // 1w
 | 
			
		||||
		this.appCache = new MemoryKVCache<MiApp>(Infinity);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
 
 | 
			
		||||
@@ -92,7 +92,6 @@ import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webho
 | 
			
		||||
import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
 | 
			
		||||
import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
 | 
			
		||||
import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
 | 
			
		||||
import * as ep___admin_systemWebhook_test from './endpoints/admin/system-webhook/test.js';
 | 
			
		||||
import * as ep___announcements from './endpoints/announcements.js';
 | 
			
		||||
import * as ep___announcements_show from './endpoints/announcements/show.js';
 | 
			
		||||
import * as ep___antennas_create from './endpoints/antennas/create.js';
 | 
			
		||||
@@ -259,7 +258,6 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
 | 
			
		||||
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
 | 
			
		||||
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
 | 
			
		||||
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
 | 
			
		||||
import * as ep___i_webhooks_test from './endpoints/i/webhooks/test.js';
 | 
			
		||||
import * as ep___invite_create from './endpoints/invite/create.js';
 | 
			
		||||
import * as ep___invite_delete from './endpoints/invite/delete.js';
 | 
			
		||||
import * as ep___invite_list from './endpoints/invite/list.js';
 | 
			
		||||
@@ -477,7 +475,6 @@ const $admin_systemWebhook_delete: Provider = { provide: 'ep:admin/system-webhoo
 | 
			
		||||
const $admin_systemWebhook_list: Provider = { provide: 'ep:admin/system-webhook/list', useClass: ep___admin_systemWebhook_list.default };
 | 
			
		||||
const $admin_systemWebhook_show: Provider = { provide: 'ep:admin/system-webhook/show', useClass: ep___admin_systemWebhook_show.default };
 | 
			
		||||
const $admin_systemWebhook_update: Provider = { provide: 'ep:admin/system-webhook/update', useClass: ep___admin_systemWebhook_update.default };
 | 
			
		||||
const $admin_systemWebhook_test: Provider = { provide: 'ep:admin/system-webhook/test', useClass: ep___admin_systemWebhook_test.default };
 | 
			
		||||
const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default };
 | 
			
		||||
const $announcements_show: Provider = { provide: 'ep:announcements/show', useClass: ep___announcements_show.default };
 | 
			
		||||
const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default };
 | 
			
		||||
@@ -644,7 +641,6 @@ const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep
 | 
			
		||||
const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default };
 | 
			
		||||
const $i_webhooks_update: Provider = { provide: 'ep:i/webhooks/update', useClass: ep___i_webhooks_update.default };
 | 
			
		||||
const $i_webhooks_delete: Provider = { provide: 'ep:i/webhooks/delete', useClass: ep___i_webhooks_delete.default };
 | 
			
		||||
const $i_webhooks_test: Provider = { provide: 'ep:i/webhooks/test', useClass: ep___i_webhooks_test.default };
 | 
			
		||||
const $invite_create: Provider = { provide: 'ep:invite/create', useClass: ep___invite_create.default };
 | 
			
		||||
const $invite_delete: Provider = { provide: 'ep:invite/delete', useClass: ep___invite_delete.default };
 | 
			
		||||
const $invite_list: Provider = { provide: 'ep:invite/list', useClass: ep___invite_list.default };
 | 
			
		||||
@@ -866,7 +862,6 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
 | 
			
		||||
		$admin_systemWebhook_list,
 | 
			
		||||
		$admin_systemWebhook_show,
 | 
			
		||||
		$admin_systemWebhook_update,
 | 
			
		||||
		$admin_systemWebhook_test,
 | 
			
		||||
		$announcements,
 | 
			
		||||
		$announcements_show,
 | 
			
		||||
		$antennas_create,
 | 
			
		||||
@@ -1033,7 +1028,6 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
 | 
			
		||||
		$i_webhooks_show,
 | 
			
		||||
		$i_webhooks_update,
 | 
			
		||||
		$i_webhooks_delete,
 | 
			
		||||
		$i_webhooks_test,
 | 
			
		||||
		$invite_create,
 | 
			
		||||
		$invite_delete,
 | 
			
		||||
		$invite_list,
 | 
			
		||||
@@ -1249,7 +1243,6 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
 | 
			
		||||
		$admin_systemWebhook_list,
 | 
			
		||||
		$admin_systemWebhook_show,
 | 
			
		||||
		$admin_systemWebhook_update,
 | 
			
		||||
		$admin_systemWebhook_test,
 | 
			
		||||
		$announcements,
 | 
			
		||||
		$announcements_show,
 | 
			
		||||
		$antennas_create,
 | 
			
		||||
@@ -1416,7 +1409,6 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
 | 
			
		||||
		$i_webhooks_show,
 | 
			
		||||
		$i_webhooks_update,
 | 
			
		||||
		$i_webhooks_delete,
 | 
			
		||||
		$i_webhooks_test,
 | 
			
		||||
		$invite_create,
 | 
			
		||||
		$invite_delete,
 | 
			
		||||
		$invite_list,
 | 
			
		||||
 
 | 
			
		||||
@@ -7,8 +7,9 @@ import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import bcrypt from 'bcryptjs';
 | 
			
		||||
import { IsNull } from 'typeorm';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository, MiRegistrationTicket, MiMeta } from '@/models/_.js';
 | 
			
		||||
import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository, MiRegistrationTicket } from '@/models/_.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { CaptchaService } from '@/core/CaptchaService.js';
 | 
			
		||||
import { IdService } from '@/core/IdService.js';
 | 
			
		||||
import { SignupService } from '@/core/SignupService.js';
 | 
			
		||||
@@ -27,9 +28,6 @@ export class SignupApiService {
 | 
			
		||||
		@Inject(DI.config)
 | 
			
		||||
		private config: Config,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.meta)
 | 
			
		||||
		private meta: MiMeta,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.usersRepository)
 | 
			
		||||
		private usersRepository: UsersRepository,
 | 
			
		||||
 | 
			
		||||
@@ -47,6 +45,7 @@ export class SignupApiService {
 | 
			
		||||
 | 
			
		||||
		private userEntityService: UserEntityService,
 | 
			
		||||
		private idService: IdService,
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
		private captchaService: CaptchaService,
 | 
			
		||||
		private signupService: SignupService,
 | 
			
		||||
		private signinService: SigninService,
 | 
			
		||||
@@ -73,29 +72,31 @@ export class SignupApiService {
 | 
			
		||||
	) {
 | 
			
		||||
		const body = request.body;
 | 
			
		||||
 | 
			
		||||
		const instance = await this.metaService.fetch(true);
 | 
			
		||||
 | 
			
		||||
		// Verify *Captcha
 | 
			
		||||
		// ただしテスト時はこの機構は障害となるため無効にする
 | 
			
		||||
		if (process.env.NODE_ENV !== 'test') {
 | 
			
		||||
			if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) {
 | 
			
		||||
				await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
 | 
			
		||||
			if (instance.enableHcaptcha && instance.hcaptchaSecretKey) {
 | 
			
		||||
				await this.captchaService.verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
 | 
			
		||||
					throw new FastifyReplyError(400, err);
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (this.meta.enableMcaptcha && this.meta.mcaptchaSecretKey && this.meta.mcaptchaSitekey && this.meta.mcaptchaInstanceUrl) {
 | 
			
		||||
				await this.captchaService.verifyMcaptcha(this.meta.mcaptchaSecretKey, this.meta.mcaptchaSitekey, this.meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
 | 
			
		||||
			if (instance.enableMcaptcha && instance.mcaptchaSecretKey && instance.mcaptchaSitekey && instance.mcaptchaInstanceUrl) {
 | 
			
		||||
				await this.captchaService.verifyMcaptcha(instance.mcaptchaSecretKey, instance.mcaptchaSitekey, instance.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
 | 
			
		||||
					throw new FastifyReplyError(400, err);
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) {
 | 
			
		||||
				await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
 | 
			
		||||
			if (instance.enableRecaptcha && instance.recaptchaSecretKey) {
 | 
			
		||||
				await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
 | 
			
		||||
					throw new FastifyReplyError(400, err);
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) {
 | 
			
		||||
				await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => {
 | 
			
		||||
			if (instance.enableTurnstile && instance.turnstileSecretKey) {
 | 
			
		||||
				await this.captchaService.verifyTurnstile(instance.turnstileSecretKey, body['turnstile-response']).catch(err => {
 | 
			
		||||
					throw new FastifyReplyError(400, err);
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
@@ -107,7 +108,7 @@ export class SignupApiService {
 | 
			
		||||
		const invitationCode = body['invitationCode'];
 | 
			
		||||
		const emailAddress = body['emailAddress'];
 | 
			
		||||
 | 
			
		||||
		if (this.meta.emailRequiredForSignup) {
 | 
			
		||||
		if (instance.emailRequiredForSignup) {
 | 
			
		||||
			if (emailAddress == null || typeof emailAddress !== 'string') {
 | 
			
		||||
				reply.code(400);
 | 
			
		||||
				return;
 | 
			
		||||
@@ -122,7 +123,7 @@ export class SignupApiService {
 | 
			
		||||
 | 
			
		||||
		let ticket: MiRegistrationTicket | null = null;
 | 
			
		||||
 | 
			
		||||
		if (this.meta.disableRegistration) {
 | 
			
		||||
		if (instance.disableRegistration) {
 | 
			
		||||
			if (invitationCode == null || typeof invitationCode !== 'string') {
 | 
			
		||||
				reply.code(400);
 | 
			
		||||
				return;
 | 
			
		||||
@@ -143,7 +144,7 @@ export class SignupApiService {
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// メアド認証が有効の場合
 | 
			
		||||
			if (this.meta.emailRequiredForSignup) {
 | 
			
		||||
			if (instance.emailRequiredForSignup) {
 | 
			
		||||
				// メアド認証済みならエラー
 | 
			
		||||
				if (ticket.usedBy) {
 | 
			
		||||
					reply.code(400);
 | 
			
		||||
@@ -161,7 +162,7 @@ export class SignupApiService {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (this.meta.emailRequiredForSignup) {
 | 
			
		||||
		if (instance.emailRequiredForSignup) {
 | 
			
		||||
			if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
 | 
			
		||||
				throw new FastifyReplyError(400, 'DUPLICATED_USERNAME');
 | 
			
		||||
			}
 | 
			
		||||
@@ -171,7 +172,7 @@ export class SignupApiService {
 | 
			
		||||
				throw new FastifyReplyError(400, 'USED_USERNAME');
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
 | 
			
		||||
			const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
 | 
			
		||||
			if (isPreserved) {
 | 
			
		||||
				throw new FastifyReplyError(400, 'DENIED_USERNAME');
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user