Compare commits
163 Commits
2024.7.0-r
...
2024.9.0-a
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1d5a3023f4 | ||
![]() |
bd5f25c678 | ||
![]() |
6ba97a7763 | ||
![]() |
d435d04eaf | ||
![]() |
01ec708020 | ||
![]() |
0e92cbf905 | ||
![]() |
8ad9f7209b | ||
![]() |
3df1bb2d71 | ||
![]() |
891bbcf475 | ||
![]() |
023fa30280 | ||
![]() |
3ad5c753fa | ||
![]() |
76408667f3 | ||
![]() |
9ac4d3da0a | ||
![]() |
67a5119072 | ||
![]() |
00ccc2251a | ||
![]() |
3d92ef193e | ||
![]() |
e9085e455f | ||
![]() |
85f46f88c6 | ||
![]() |
9cd784cdee | ||
![]() |
d4d15f338e | ||
![]() |
d3f1b0f090 | ||
![]() |
2ee19ee22e | ||
![]() |
a18a6ac264 | ||
![]() |
7e9d54fa3a | ||
![]() |
f0834ca14c | ||
![]() |
0b062f1407 | ||
![]() |
f585f70dcb | ||
![]() |
8d23122fd6 | ||
![]() |
2d0e9e0544 | ||
![]() |
f5563c8304 | ||
![]() |
4ac8aad50a | ||
![]() |
ceb4640669 | ||
![]() |
3bf63dd9c5 | ||
![]() |
ce95323e49 | ||
![]() |
daf9ae5d4a | ||
![]() |
a5e61b8c19 | ||
![]() |
cacdf9d939 | ||
![]() |
0134e6e420 | ||
![]() |
6bd6af440f | ||
![]() |
7d7a12d7d6 | ||
![]() |
887c709647 | ||
![]() |
0e4b6d1dad | ||
![]() |
07f26bc8dd | ||
![]() |
366b79e459 | ||
![]() |
6b2072f4b1 | ||
![]() |
1544ba9153 | ||
![]() |
be0906a6c7 | ||
![]() |
e0f54d6a68 | ||
![]() |
837a8e15d8 | ||
![]() |
0c2cfe31a3 | ||
![]() |
05c944c2cc | ||
![]() |
f393b6b898 | ||
![]() |
672779a15f | ||
![]() |
2cbe1d1210 | ||
![]() |
0d0cd738f8 | ||
![]() |
567acea2a3 | ||
![]() |
8d19bdbb65 | ||
![]() |
cdb0566c5b | ||
![]() |
f7398faeac | ||
![]() |
c8f49b6ae7 | ||
![]() |
74c93fcebe | ||
![]() |
8be624aa44 | ||
![]() |
3fe7e37f10 | ||
![]() |
7fe3035059 | ||
![]() |
06855f769f | ||
![]() |
3e85052754 | ||
![]() |
b6fdd71957 | ||
![]() |
36dff66883 | ||
![]() |
255c8bd1b9 | ||
![]() |
44f62160cb | ||
![]() |
8032a4e12a | ||
![]() |
2f009f7d49 | ||
![]() |
f85aa7b641 | ||
![]() |
1008fa32a0 | ||
![]() |
043ab1f69b | ||
![]() |
21a3095eb0 | ||
![]() |
1b5f0571f7 | ||
![]() |
59e83605ac | ||
![]() |
130ff361c3 | ||
![]() |
e78110a5cd | ||
![]() |
6c5593d456 | ||
![]() |
621626aad3 | ||
![]() |
f4f55ef012 | ||
![]() |
2e8a1029a4 | ||
![]() |
b53ee54e4f | ||
![]() |
b708b27bc8 | ||
![]() |
9ce44b24b8 | ||
![]() |
3cd5f86510 | ||
![]() |
9b78ce8047 | ||
![]() |
1629c0e50d | ||
![]() |
427f4a2cda | ||
![]() |
ba9c5c37b8 | ||
![]() |
e790aa0548 | ||
![]() |
bf8c42eecd | ||
![]() |
129af06198 | ||
![]() |
83c04c55ad | ||
![]() |
0b98554319 | ||
![]() |
4e0d57000c | ||
![]() |
c0de57c08d | ||
![]() |
75b0315ace | ||
![]() |
6cdecd72ee | ||
![]() |
9fbc1b7f7b | ||
![]() |
fd744f44c1 | ||
![]() |
383c41bdb6 | ||
![]() |
68ec7450af | ||
![]() |
06684fe49b | ||
![]() |
059eb6d379 | ||
![]() |
61cc3b5642 | ||
![]() |
2ab5ee81b1 | ||
![]() |
ef950a345b | ||
![]() |
bfaf938609 | ||
![]() |
d3cdc08802 | ||
![]() |
571566d476 | ||
![]() |
748a7e8f6a | ||
![]() |
6db3c50e32 | ||
![]() |
26322048db | ||
![]() |
a8810af8d9 | ||
![]() |
45d88574c3 | ||
![]() |
b68b2ee8c6 | ||
![]() |
86dd4abadc | ||
![]() |
cd210001e6 | ||
![]() |
41936c16c4 | ||
![]() |
4d757865f4 | ||
![]() |
2a2bbcd1bc | ||
![]() |
94b8c00c66 | ||
![]() |
ab7bbd4e57 | ||
![]() |
93fc06d18b | ||
![]() |
0aaf74ee22 | ||
![]() |
046f2435b2 | ||
![]() |
37c9d91ba0 | ||
![]() |
93c569c2cd | ||
![]() |
cb10156f01 | ||
![]() |
1532d5f390 | ||
![]() |
7e3dedb045 | ||
![]() |
01a815f8a7 | ||
![]() |
f50941389d | ||
![]() |
0d508db8a7 | ||
![]() |
f244d42500 | ||
![]() |
820becb4e4 | ||
![]() |
6e3e7d7df1 | ||
![]() |
008a66d73f | ||
![]() |
59e2e43a68 | ||
![]() |
1a521a44c0 | ||
![]() |
d6ba12e24c | ||
![]() |
4b04b2989b | ||
![]() |
d63b854f96 | ||
![]() |
9dacc20d67 | ||
![]() |
3137c104f2 | ||
![]() |
63f9c271ca | ||
![]() |
400ae6ef01 | ||
![]() |
8b163cd3fb | ||
![]() |
676c599e48 | ||
![]() |
fccc5b6d62 | ||
![]() |
0bb5ac0fca | ||
![]() |
c7354c5e30 | ||
![]() |
5c42a0e439 | ||
![]() |
8f40f932e4 | ||
![]() |
86b4f49880 | ||
![]() |
916ed49441 | ||
![]() |
d0b7c74fd1 | ||
![]() |
2307849c9a | ||
![]() |
8bae2ecabd | ||
![]() |
3411b9c16c |
211
.config/cypress-devcontainer.yml
Normal file
211
.config/cypress-devcontainer.yml
Normal file
@@ -0,0 +1,211 @@
|
||||
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
# 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,6 +106,14 @@ 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,6 +172,16 @@ 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,6 +103,14 @@ 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,6 +3,8 @@
|
||||
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
|
||||
@@ -12,3 +14,4 @@ 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.3
|
||||
uses: actions/setup-node@v4.0.4
|
||||
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.3
|
||||
uses: actions/setup-node@v4.0.4
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
|
||||
|
@@ -28,7 +28,7 @@ jobs:
|
||||
|
||||
- name: setup node
|
||||
id: setup-node
|
||||
uses: actions/setup-node@v4.0.3
|
||||
uses: actions/setup-node@v4.0.4
|
||||
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,12 +48,16 @@ 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.3
|
||||
uses: actions/setup-node@v4.0.4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
29
.github/workflows/lint.yml
vendored
29
.github/workflows/lint.yml
vendored
@@ -8,6 +8,8 @@ on:
|
||||
paths:
|
||||
- packages/backend/**
|
||||
- packages/frontend/**
|
||||
- packages/frontend-shared/**
|
||||
- packages/frontend-embed/**
|
||||
- packages/sw/**
|
||||
- packages/misskey-js/**
|
||||
- packages/shared/eslint.config.js
|
||||
@@ -16,6 +18,8 @@ on:
|
||||
paths:
|
||||
- packages/backend/**
|
||||
- packages/frontend/**
|
||||
- packages/frontend-shared/**
|
||||
- packages/frontend-embed/**
|
||||
- packages/sw/**
|
||||
- packages/misskey-js/**
|
||||
- packages/shared/eslint.config.js
|
||||
@@ -29,7 +33,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4.0.3
|
||||
- uses: actions/setup-node@v4.0.4
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
@@ -40,22 +44,25 @@ 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.3
|
||||
- uses: actions/setup-node@v4.0.4
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
@@ -64,11 +71,10 @@ jobs:
|
||||
- name: Restore eslint cache
|
||||
uses: actions/cache@v4.0.2
|
||||
with:
|
||||
path: node_modules/.cache/eslint
|
||||
key: eslint-${{ env.eslint-cache-version }}-${{ hashFiles('/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
eslint-${{ env.eslint-cache-version }}-${{ hashFiles('/pnpm-lock.yaml') }}-
|
||||
- run: pnpm --filter ${{ matrix.workspace }} run eslint --cache --cache-location node_modules/.cache/eslint --cache-strategy content
|
||||
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
|
||||
|
||||
typecheck:
|
||||
needs: [pnpm_install]
|
||||
@@ -78,6 +84,7 @@ jobs:
|
||||
matrix:
|
||||
workspace:
|
||||
- backend
|
||||
- sw
|
||||
- misskey-js
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
@@ -85,14 +92,14 @@ jobs:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4.0.3
|
||||
- uses: actions/setup-node@v4.0.4
|
||||
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' }}
|
||||
if: ${{ matrix.workspace == 'backend' || matrix.workspace == 'sw' }}
|
||||
- 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.3
|
||||
- uses: actions/setup-node@v4.0.4
|
||||
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.3
|
||||
uses: actions/setup-node@v4.0.4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
2
.github/workflows/release-edit-with-push.yml
vendored
2
.github/workflows/release-edit-with-push.yml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
- develop
|
||||
paths:
|
||||
- 'CHANGELOG.md'
|
||||
# - .github/workflows/release-edit-with-push.yml
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
14
.github/workflows/release-with-dispatch.yml
vendored
14
.github/workflows/release-with-dispatch.yml
vendored
@@ -17,6 +17,10 @@ on:
|
||||
type: boolean
|
||||
description: 'MERGE RELEASE BRANCH TO MAIN'
|
||||
default: false
|
||||
start-rc:
|
||||
type: boolean
|
||||
description: 'Start Release Candidate'
|
||||
default: false
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -56,13 +60,13 @@ jobs:
|
||||
|
||||
### General
|
||||
-
|
||||
|
||||
|
||||
### Client
|
||||
-
|
||||
|
||||
|
||||
### Server
|
||||
-
|
||||
|
||||
|
||||
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
|
||||
indent: ${{ vars.INDENT }}
|
||||
secrets:
|
||||
@@ -79,6 +83,9 @@ jobs:
|
||||
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
|
||||
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
|
||||
indent: ${{ vars.INDENT }}
|
||||
draft_prerelease_channel: alpha
|
||||
ready_start_prerelease_channel: beta
|
||||
prerelease_channel: ${{ inputs.start-rc && 'rc' || '' }}
|
||||
secrets:
|
||||
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
|
||||
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
||||
@@ -122,6 +129,7 @@ jobs:
|
||||
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
|
||||
indent: ${{ vars.INDENT }}
|
||||
stable_branch: ${{ vars.STABLE_BRANCH }}
|
||||
draft_prerelease_channel: alpha
|
||||
secrets:
|
||||
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
|
||||
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
||||
|
2
.github/workflows/release-with-ready.yml
vendored
2
.github/workflows/release-with-ready.yml
vendored
@@ -39,6 +39,8 @@ jobs:
|
||||
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
|
||||
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
|
||||
indent: ${{ vars.INDENT }}
|
||||
draft_prerelease_channel: alpha
|
||||
ready_start_prerelease_channel: beta
|
||||
secrets:
|
||||
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
|
||||
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
||||
|
33
.github/workflows/report-api-diff.yml
vendored
33
.github/workflows/report-api-diff.yml
vendored
@@ -70,18 +70,27 @@ jobs:
|
||||
- id: out-diff
|
||||
name: Build diff Comment
|
||||
run: |
|
||||
cat <<- EOF > ./output.md
|
||||
このPRによるapi.jsonの差分
|
||||
<details>
|
||||
<summary>差分はこちら</summary>
|
||||
|
||||
\`\`\`diff
|
||||
$(cat ./api.json.diff)
|
||||
\`\`\`
|
||||
</details>
|
||||
|
||||
[Get diff files from Workflow Page](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})
|
||||
EOF
|
||||
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
|
||||
<details>
|
||||
<summary>差分はこちら</summary>
|
||||
|
||||
\`\`\`diff
|
||||
$(cat ./api.json.diff)
|
||||
\`\`\`
|
||||
</details>
|
||||
EOF
|
||||
fi
|
||||
|
||||
echo "$FOOTER" >> ./output.md
|
||||
- uses: thollander/actions-comment-pull-request@v2
|
||||
with:
|
||||
pr_number: ${{ steps.load-pr-num.outputs.pr-number }}
|
||||
|
7
.github/workflows/storybook.yml
vendored
7
.github/workflows/storybook.yml
vendored
@@ -7,6 +7,11 @@ on:
|
||||
- develop
|
||||
- dev/storybook8 # for testing
|
||||
pull_request_target:
|
||||
branches-ignore:
|
||||
# Since pull requests targets master mostly is the "develop" branch.
|
||||
# Storybook CI is checked on the "push" event of "develop" branch so it would cause a duplicate build.
|
||||
# This is a waste of chromatic build quota, so we don't run storybook CI on pull requests targets master.
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -36,7 +41,7 @@ jobs:
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v4.0.3
|
||||
uses: actions/setup-node@v4.0.4
|
||||
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.3
|
||||
uses: actions/setup-node@v4.0.4
|
||||
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.3
|
||||
uses: actions/setup-node@v4.0.4
|
||||
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.3
|
||||
uses: actions/setup-node@v4.0.4
|
||||
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.3
|
||||
uses: actions/setup-node@v4.0.4
|
||||
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.3
|
||||
uses: actions/setup-node@v4.0.4
|
||||
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.3
|
||||
uses: actions/setup-node@v4.0.4
|
||||
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.3
|
||||
uses: actions/setup-node@v4.0.4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
8
.gitignore
vendored
8
.gitignore
vendored
@@ -35,6 +35,9 @@ coverage
|
||||
!/.config/example.yml
|
||||
!/.config/docker_example.yml
|
||||
!/.config/docker_example.env
|
||||
!/.config/cypress-devcontainer.yml
|
||||
docker-compose.yml
|
||||
compose.yml
|
||||
.devcontainer/compose.yml
|
||||
!/.devcontainer/compose.yml
|
||||
|
||||
@@ -42,6 +45,7 @@ coverage
|
||||
/build
|
||||
built
|
||||
built-test
|
||||
js-built
|
||||
/data
|
||||
/.cache-loader
|
||||
/db
|
||||
@@ -61,6 +65,10 @@ temp
|
||||
tsdoc-metadata.json
|
||||
misskey-assets
|
||||
|
||||
# Vite temporary files
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
# blender backups
|
||||
*.blend1
|
||||
*.blend2
|
||||
|
88
CHANGELOG.md
88
CHANGELOG.md
@@ -1,3 +1,72 @@
|
||||
## 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
|
||||
- Enhance: モデレーターはすべてのユーザーのフォロー・フォロワーの一覧を見られるように
|
||||
- Enhance: アカウントの削除のモデレーションログを残すように
|
||||
- Enhance: 不適切なページ、ギャラリー、Playを管理者権限で削除できるように
|
||||
- Fix: リモートユーザのフォロー・フォロワーの一覧が非公開設定の場合も表示できてしまう問題を修正
|
||||
|
||||
### Client
|
||||
- Enhance: 「自分のPlay」ページにおいてPlayが非公開かどうかが一目でわかるように
|
||||
- Enhance: 不適切なページ、ギャラリー、Playを通報できるように
|
||||
- Fix: Play編集時に公開範囲が「パブリック」にリセットされる問題を修正
|
||||
- Fix: ページ遷移に失敗することがある問題を修正
|
||||
- Fix: iOSでユーザー名などがリンクとして誤検知される現象を抑制
|
||||
- Fix: mCaptchaを使用していてもbotプロテクションに関する警告が消えないのを修正
|
||||
- Fix: ユーザーのモデレーションページにおいてユーザー名にドットが入っているとシステムアカウントとして表示されてしまう問題を修正
|
||||
- Fix: 特定の条件下でノートの削除ボタンが出ないのを修正
|
||||
|
||||
### Server
|
||||
- Enhance: 照会時にURLがhtmlかつheadタグ内に`rel="alternate"`, `type="application/activity+json"`の`link`タグがある場合に追ってリンク先を照会できるように
|
||||
- Enhance: 凍結されたアカウントのフォローリクエストを表示しないように
|
||||
- Fix: WSの`readAllNotifications` メッセージが `body` を持たない場合に動作しない問題 #14374
|
||||
- 通知ページや通知カラム(デッキ)を開いている状態において、新たに発生した通知が既読されない問題が修正されます。
|
||||
- これにより、プッシュ通知が有効な同条件下の環境において、プッシュ通知が常に発生してしまう問題も修正されます。
|
||||
- Fix: Play各種エンドポイントの返り値に`visibility`が含まれていない問題を修正
|
||||
- Fix: サーバー情報取得の際にモデレーター限定の情報が取得できないことがあるのを修正
|
||||
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/582)
|
||||
- Fix: 公開範囲がダイレクトのノートをユーザーアクティビティのチャート生成に使用しないように
|
||||
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/679)
|
||||
- Fix: ActivityPubのエンティティタイプ判定で不明なタイプを受け取った場合でも処理を継続するように
|
||||
- キュー処理のつまりが改善される可能性があります
|
||||
- Fix: リバーシの対局設定の変更が反映されないのを修正
|
||||
- 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
|
||||
|
||||
### Note
|
||||
@@ -9,10 +78,14 @@
|
||||
- Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に
|
||||
- 変更不可となっていても、設定済みのものを解除してデフォルト画像に戻すことは出来ます
|
||||
- Feat: ユーザ作成時にSystemWebhookを送信可能に #14281
|
||||
- Feat: メディアサイレンスを実装 #13842
|
||||
- メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。
|
||||
- Enhance: 管理画面でアーカイブにしたお知らせを表示・編集できるように
|
||||
- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
|
||||
- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
|
||||
- Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正
|
||||
- 翻訳の更新
|
||||
- 依存関係の更新
|
||||
|
||||
### Client
|
||||
- Feat: ユーザーページから「このユーザーのノートを検索」できるように (#14128)
|
||||
@@ -28,6 +101,10 @@
|
||||
- Enhance: AiScriptを0.19.0にアップデート
|
||||
- Enhance: Allow negative delay for MFM animation elements (`tada`, `jelly`, `twitch`, `shake`, `spin`, `jump`, `bounce`, `rainbow`)
|
||||
- Enhance: センシティブなメディアを開く際に確認ダイアログを出せるように
|
||||
- Enhance: 検索(ノート/ユーザー)で `#` から始まる文字列を入力すると、そのハッシュタグのノート/ユーザー一覧ページが表示できるように
|
||||
- Enhance: 検索(ノート/ユーザー)において、入力に空白が含まれている場合は照会を行わないように
|
||||
- Enhance: 検索(ノート/ユーザー)において、照会を行うかどうか、ハッシュタグのノート/ユーザー一覧ページを表示するかどうかの確認ダイアログを出すように
|
||||
- Enhance: 検索(ノート/ユーザー)で `@` から始まる文字列(`@user@host`など)を入力すると、そのユーザーを照会できるように
|
||||
- Enhance: ドライブのファイル・フォルダをドラッグしなくても移動できるように
|
||||
(Cherry-picked from https://github.com/nafu-at/misskey/commit/b89c2af6945c6a9f9f10e83f54d2bcf0f240b0b4, https://github.com/nafu-at/misskey/commit/8a7d710c6acb83f50c83f050bd1423c764d60a99)
|
||||
- Enhance: デッキのアンテナ・リスト選択画面からそれぞれを新規作成できるように
|
||||
@@ -55,7 +132,14 @@
|
||||
- Fix: ダイレクト投稿の"削除して編集"において、宛先が保持されていなかった問題を修正
|
||||
- Fix: 投稿フォームへのURL貼り付けによる引用が下書きに保存されていなかった問題を修正
|
||||
- Fix: "削除して編集"や下書きにおいて、リアクションの受け入れ設定が保持/保存されていなかった問題を修正
|
||||
- Fix: 照会に `#` から始まる文字列を入力してそのハッシュタグのページを表示する際、入力が `#` のみの場合に「指定されたURLに該当するページはありませんでした。」が表示されてしまう問題を修正
|
||||
- Fix: 照会に `@` から始まる文字列を入力してユーザーを照会する際、入力が `@` のみの場合に「問題が発生しました」が表示されてしまう問題を修正
|
||||
- Fix: 投稿フォームにノートのURLを貼り付けて"引用として添付"した場合、投稿文を空にすることによるRenote化が出来なかった問題を修正
|
||||
- Fix: フォロー中のユーザーに関する"TLに他の人への返信を含める"の設定が分かりづらい問題を修正
|
||||
- Fix: タイムラインページを開いた時、`TLに他の人への返信を含める`がオフのときに`ファイル付きのみ`をオンにできない問題を修正
|
||||
- Fix: deck uiでタイムラインを切り替えた際にTLの設定項目が更新されず、`TLに他の人への返信を含める`のトグルが表示されない問題を修正
|
||||
- Fix: ウィジェットのタイムライン選択欄に無効化されたタイムラインが表示される問題を修正
|
||||
- Fix: サウンドにドライブの音声を使用している際にドライブの音声が再生できなくなると設定が変更できなくなる問題を修正
|
||||
|
||||
### Server
|
||||
- Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)
|
||||
@@ -94,11 +178,15 @@
|
||||
- Fix: リノートのミュートが適用されるまでに時間がかかることがある問題を修正
|
||||
(Cherry-picked from https://github.com/Type4ny-Project/Type4ny/commit/e9601029b52e0ad43d9131b555b614e56c84ebc1)
|
||||
- Fix: Steaming APIが不正なデータを受けた場合の動作が不安定である問題 #14251
|
||||
- Fix: `users/search`において `@` から始まる文字列が与えられた際の処理が正しくなかった問題を修正
|
||||
- 名前や自己紹介に `@` から始まる文言が含まれるユーザーも検索できるようになります
|
||||
- Fix: 一部のMisskey以外のソフトウェアからファイルを受け取れない問題
|
||||
(Cherry-picked from https://github.com/Secineralyr/misskey.dream/pull/73/commits/652eaff1e8aa00b890d71d2e1e52c263c1e67c76)
|
||||
- NOTE: `drive_file`の`url`, `uri`, `src`の上限が512から1024に変更されます
|
||||
Migrationではカラム定義の変更のみが行われます。
|
||||
サーバー管理者は各サーバーの必要に応じ`drive_file` `("uri")`に対するインデックスを張りなおすことでより安定しDBの探索が行われる可能性があります。詳細 は [GitHub](https://github.com/misskey-dev/misskey/pull/14323#issuecomment-2257562228)で確認可能です
|
||||
- Fix: 自分のフォロワー限定投稿に対するリプライがホームタイムラインで見えないことが有る問題を修正
|
||||
- Fix: フォローしていないユーザによるフォロワー限定投稿に対するリプライがソーシャルタイムラインで表示されることがある問題を修正
|
||||
|
||||
### Misskey.js
|
||||
- Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応)
|
||||
|
@@ -21,7 +21,9 @@ 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,6 +124,14 @@ 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 └─────────────────────────────
|
||||
|
||||
|
@@ -2010,7 +2010,6 @@ _webhookSettings:
|
||||
createWebhook: "Vytvořit Webhook"
|
||||
name: "Jméno"
|
||||
secret: "Tajné"
|
||||
events: "Události Webhook"
|
||||
active: "Zapnuto"
|
||||
_events:
|
||||
follow: "Při sledování uživatele"
|
||||
|
@@ -2191,7 +2191,6 @@ _webhookSettings:
|
||||
createWebhook: "Webhook erstellen"
|
||||
name: "Name"
|
||||
secret: "Secret"
|
||||
events: "Webhook-Ereignisse"
|
||||
active: "Aktiviert"
|
||||
_events:
|
||||
follow: "Wenn du jemandem folgst"
|
||||
|
@@ -60,6 +60,7 @@ copyFileId: "Copy file ID"
|
||||
copyFolderId: "Copy folder ID"
|
||||
copyProfileUrl: "Copy profile URL"
|
||||
searchUser: "Search for a user"
|
||||
searchThisUsersNotes: "Search this user’s notes"
|
||||
reply: "Reply"
|
||||
loadMore: "Load more"
|
||||
showMore: "Show more"
|
||||
@@ -154,6 +155,7 @@ editList: "Edit list"
|
||||
selectChannel: "Select a channel"
|
||||
selectAntenna: "Select an antenna"
|
||||
editAntenna: "Edit antenna"
|
||||
createAntenna: "Create an antenna"
|
||||
selectWidget: "Select a widget"
|
||||
editWidgets: "Edit widgets"
|
||||
editWidgetsExit: "Done"
|
||||
@@ -165,7 +167,7 @@ emojiUrl: "Emoji URL"
|
||||
addEmoji: "Add an emoji"
|
||||
settingGuide: "Recommended settings"
|
||||
cacheRemoteFiles: "Cache remote files"
|
||||
cacheRemoteFilesDescription: "When this setting is disabled, remote files are loaded directly from the remote instance. Disabling this will decrease storage usage, but increase traffic, as thumbnails will not be generated."
|
||||
cacheRemoteFilesDescription: "When this setting is disabled, remote files are loaded directly from the remote servers. Disabling this will decrease storage usage, but increase traffic, as thumbnails will not be generated."
|
||||
youCanCleanRemoteFilesCache: "You can clear the cache by clicking the 🗑️ button in the file management view."
|
||||
cacheRemoteSensitiveFiles: "Cache sensitive remote files"
|
||||
cacheRemoteSensitiveFilesDescription: "When this setting is disabled, sensitive remote files are loaded directly from the remote instance without caching."
|
||||
@@ -180,7 +182,7 @@ addAccount: "Add account"
|
||||
reloadAccountsList: "Reload account list"
|
||||
loginFailed: "Failed to sign in"
|
||||
showOnRemote: "View on remote instance"
|
||||
continueOnRemote: "リモートで続行"
|
||||
continueOnRemote: "Continue on a remote server"
|
||||
chooseServerOnMisskeyHub: "Choose a server from the Misskey Hub"
|
||||
specifyServerHost: "Specify a server host directly"
|
||||
inputHostName: "Enter the domain"
|
||||
@@ -194,6 +196,7 @@ followConfirm: "Are you sure that you want to follow {name}?"
|
||||
proxyAccount: "Proxy account"
|
||||
proxyAccountDescription: "A proxy account is an account that acts as a remote follower for users under certain conditions. For example, when a user adds a remote user to the list, the remote user's activity will not be delivered to the instance if no local user is following that user, so the proxy account will follow instead."
|
||||
host: "Host"
|
||||
selectSelf: "Select myself"
|
||||
selectUser: "Select a user"
|
||||
recipient: "Recipient"
|
||||
annotation: "Comments"
|
||||
@@ -209,6 +212,7 @@ perDay: "Per Day"
|
||||
stopActivityDelivery: "Stop sending activities"
|
||||
blockThisInstance: "Block this instance"
|
||||
silenceThisInstance: "Silence this instance"
|
||||
mediaSilenceThisInstance: "Media-silence this server"
|
||||
operations: "Operations"
|
||||
software: "Software"
|
||||
version: "Version"
|
||||
@@ -229,7 +233,9 @@ clearCachedFilesConfirm: "Are you sure that you want to delete all cached remote
|
||||
blockedInstances: "Blocked Instances"
|
||||
blockedInstancesDescription: "List the hostnames of the instances you want to block separated by linebreaks. Listed instances will no longer be able to communicate with this instance."
|
||||
silencedInstances: "Silenced instances"
|
||||
silencedInstancesDescription: "List the hostnames of the instances that you want to silence. All accounts of the listed instances will be treated as silenced, can only make follow requests, and cannot mention local accounts if not followed. This will not affect blocked instances."
|
||||
silencedInstancesDescription: "List the host names of the servers that you want to silence, separated by a new line. All accounts belonging to the listed servers will be treated as silenced, and can only make follow requests, and cannot mention local accounts if not followed. This will not affect the blocked servers."
|
||||
mediaSilencedInstances: "Media-silenced servers"
|
||||
mediaSilencedInstancesDescription: "List the host names of the servers that you want to media-silence, separated by a new line. All accounts belonging to the listed servers will be treated as sensitive, and can't use custom emojis. This will not affect the blocked servers."
|
||||
muteAndBlock: "Mutes and Blocks"
|
||||
mutedUsers: "Muted users"
|
||||
blockedUsers: "Blocked users"
|
||||
@@ -392,7 +398,7 @@ mcaptcha: "mCaptcha"
|
||||
enableMcaptcha: "Enable mCaptcha"
|
||||
mcaptchaSiteKey: "Site key"
|
||||
mcaptchaSecretKey: "Secret key"
|
||||
mcaptchaInstanceUrl: "mCaptcha instance URL"
|
||||
mcaptchaInstanceUrl: "mCaptcha server URL"
|
||||
recaptcha: "reCAPTCHA"
|
||||
enableRecaptcha: "Enable reCAPTCHA"
|
||||
recaptchaSiteKey: "Site key"
|
||||
@@ -481,7 +487,7 @@ noMessagesYet: "No messages yet"
|
||||
newMessageExists: "There are new messages"
|
||||
onlyOneFileCanBeAttached: "You can only attach one file to a message"
|
||||
signinRequired: "Please register or sign in before continuing"
|
||||
signinOrContinueOnRemote: "To continue, you need to move your server or sign up / log in to this server."
|
||||
signinOrContinueOnRemote: "To continue, you need to move your server or sign up / log in to this server."
|
||||
invitations: "Invites"
|
||||
invitationCode: "Invitation code"
|
||||
checking: "Checking..."
|
||||
@@ -1106,6 +1112,8 @@ preservedUsernames: "Reserved usernames"
|
||||
preservedUsernamesDescription: "List usernames to reserve separated by linebreaks. These will become unable during normal account creation, but can be used by administrators to manually create accounts. Already existing accounts using these usernames will not be affected."
|
||||
createNoteFromTheFile: "Compose note from this file"
|
||||
archive: "Archive"
|
||||
archived: "Archived"
|
||||
unarchive: "Unarchive"
|
||||
channelArchiveConfirmTitle: "Really archive {name}?"
|
||||
channelArchiveConfirmDescription: "An archived channel won't appear in the channel list or search results anymore. New posts can also not be added to it anymore."
|
||||
thisChannelArchived: "This channel has been archived."
|
||||
@@ -1116,6 +1124,9 @@ preventAiLearning: "Reject usage in Machine Learning (Generative AI)"
|
||||
preventAiLearningDescription: "Requests crawlers to not use posted text or image material etc. in machine learning (Predictive / Generative AI) data sets. This is achieved by adding a \"noai\" HTML-Response flag to the respective content. A complete prevention can however not be achieved through this flag, as it may simply be ignored."
|
||||
options: "Options"
|
||||
specifyUser: "Specific user"
|
||||
lookupConfirm: "Do you want to look up?"
|
||||
openTagPageConfirm: "Do you want to open a hashtag page?"
|
||||
specifyHost: "Specify a host"
|
||||
failedToPreviewUrl: "Could not preview"
|
||||
update: "Update"
|
||||
rolesThatCanBeUsedThisEmojiAsReaction: "Roles that can use this emoji as reaction"
|
||||
@@ -1244,12 +1255,14 @@ launchApp: "Launch the app"
|
||||
useNativeUIForVideoAudioPlayer: "Use UI of browser when play video and audio"
|
||||
keepOriginalFilename: "Keep original file name"
|
||||
keepOriginalFilenameDescription: "If you turn off this setting, files names will be replaced with random string automatically when you upload files."
|
||||
noDescription: "There is not the explanation"
|
||||
noDescription: "There is no explanation"
|
||||
alwaysConfirmFollow: "Always confirm when following"
|
||||
inquiry: "Contact"
|
||||
tryAgain: "Please try again later"
|
||||
confirmWhenRevealingSensitiveMedia: "Confirm when revealing sensitive media"
|
||||
sensitiveMediaRevealConfirm: "This might be a sensitive media. Are you sure to reveal?"
|
||||
createdLists: "Created lists"
|
||||
createdAntennas: "Created antennas"
|
||||
_delivery:
|
||||
status: "Delivery status"
|
||||
stop: "Suspended"
|
||||
@@ -1352,7 +1365,7 @@ _initialTutorial:
|
||||
_exampleNote:
|
||||
cw: "This will surely make you hungry!"
|
||||
note: "Just had a chocolate-glazed donut 🍩😋"
|
||||
useCases: "This is used when following the server guidelines for necessary notes or for self-restriction of spoiler or sensitive text."
|
||||
useCases: "This is used when following the server guidelines, for necessary notes, or for self-restriction of spoiler or sensitive text."
|
||||
_howToMakeAttachmentsSensitive:
|
||||
title: "How to Mark Attachments as Sensitive?"
|
||||
description: "For attachments that are required by server guidelines or that should not be left intact, add a \"sensitive\" flag."
|
||||
@@ -1954,6 +1967,7 @@ _soundSettings:
|
||||
driveFileTypeWarnDescription: "Select an audio file"
|
||||
driveFileDurationWarn: "The audio is too long."
|
||||
driveFileDurationWarnDescription: "Long audio may disrupt using Misskey. Still continue?"
|
||||
driveFileError: "It couldn't load the sound. Please change the setting."
|
||||
_ago:
|
||||
future: "Future"
|
||||
justNow: "Just now"
|
||||
@@ -2302,6 +2316,7 @@ _pages:
|
||||
eyeCatchingImageSet: "Set thumbnail"
|
||||
eyeCatchingImageRemove: "Delete thumbnail"
|
||||
chooseBlock: "Add a block"
|
||||
enterSectionTitle: "Enter a section title"
|
||||
selectType: "Select a type"
|
||||
contentBlocks: "Content"
|
||||
inputBlocks: "Input"
|
||||
@@ -2412,7 +2427,7 @@ _webhookSettings:
|
||||
modifyWebhook: "Modify Webhook"
|
||||
name: "Name"
|
||||
secret: "Secret"
|
||||
events: "Webhook Events"
|
||||
trigger: "Trigger"
|
||||
active: "Enabled"
|
||||
_events:
|
||||
follow: "When following a user"
|
||||
@@ -2425,6 +2440,7 @@ _webhookSettings:
|
||||
_systemEvents:
|
||||
abuseReport: "When received a new abuse report"
|
||||
abuseReportResolved: "When resolved abuse reports"
|
||||
userCreated: "When user is created"
|
||||
deleteConfirm: "Are you sure you want to delete the Webhook?"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
@@ -2479,11 +2495,15 @@ _moderationLogTypes:
|
||||
unsetUserAvatar: "Unset this user's avatar"
|
||||
unsetUserBanner: "Unset this user's banner"
|
||||
createSystemWebhook: "Create SystemWebhook"
|
||||
updateSystemWebhook: "Update SystemWebHook"
|
||||
updateSystemWebhook: "Update SystemWebhook"
|
||||
deleteSystemWebhook: "Delete SystemWebhook"
|
||||
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"
|
||||
@@ -2615,3 +2635,8 @@ _mediaControls:
|
||||
pip: "Picture in Picture"
|
||||
playbackRate: "Playback Speed"
|
||||
loop: "Loop playback"
|
||||
_contextMenu:
|
||||
title: "Context menu"
|
||||
app: "Application"
|
||||
appWithShift: "Application with shift key"
|
||||
native: "Native"
|
||||
|
@@ -60,6 +60,7 @@ copyFileId: "Copiar ID del archivo"
|
||||
copyFolderId: "Copiar ID de carpeta"
|
||||
copyProfileUrl: "Copiar la URL del perfil"
|
||||
searchUser: "Buscar un usuario"
|
||||
searchThisUsersNotes: ""
|
||||
reply: "Responder"
|
||||
loadMore: "Ver más"
|
||||
showMore: "Ver más"
|
||||
@@ -2382,7 +2383,6 @@ _webhookSettings:
|
||||
createWebhook: "Crear Webhook"
|
||||
name: "Nombre"
|
||||
secret: "Secreto"
|
||||
events: "Eventos de webhook"
|
||||
active: "Activado"
|
||||
_events:
|
||||
follow: "Cuando se sigue a alguien"
|
||||
|
@@ -1094,6 +1094,8 @@ preservedUsernames: "Noms d'utilisateur·rice réservés"
|
||||
preservedUsernamesDescription: "Énumérez les noms d'utilisateur à réserver, séparés par des nouvelles lignes. Les noms d'utilisateur spécifiés ici ne seront plus utilisables lors de la création d'un compte, sauf la création manuelle par un administrateur. De plus, les comptes existants ne seront pas affectés."
|
||||
createNoteFromTheFile: "Rédiger une note de ce fichier"
|
||||
archive: "Archive"
|
||||
archived: "Archivé"
|
||||
unarchive: "Annuler l'archivage"
|
||||
channelArchiveConfirmTitle: "Voulez-vous vraiment archiver {name} ?"
|
||||
channelArchiveConfirmDescription: "Une fois archivé, le canal n'apparaîtra plus dans la liste des canaux ni dans les résultats de recherche, et la publication des nouvelles notes sera impossible."
|
||||
thisChannelArchived: "Ce canal a été archivé."
|
||||
@@ -1224,7 +1226,10 @@ enableHorizontalSwipe: "Glisser pour changer d'onglet"
|
||||
loading: "Chargement en cours"
|
||||
surrender: "Annuler"
|
||||
gameRetry: "Réessayer"
|
||||
launchApp: "Lancer l'app"
|
||||
inquiry: "Contact"
|
||||
_delivery:
|
||||
status: "Statut de la diffusion"
|
||||
stop: "Suspendu·e"
|
||||
_type:
|
||||
none: "Publié"
|
||||
|
@@ -2403,7 +2403,6 @@ _webhookSettings:
|
||||
modifyWebhook: "Sunting Webhook"
|
||||
name: "Nama"
|
||||
secret: "Secret"
|
||||
events: "Webhook Events"
|
||||
active: "Aktif"
|
||||
_events:
|
||||
follow: "Ketika mengikuti pengguna"
|
||||
|
162
locales/index.d.ts
vendored
162
locales/index.d.ts
vendored
@@ -864,6 +864,10 @@ export interface Locale extends ILocale {
|
||||
* サーバーをサイレンス
|
||||
*/
|
||||
"silenceThisInstance": string;
|
||||
/**
|
||||
* サーバーをメディアサイレンス
|
||||
*/
|
||||
"mediaSilenceThisInstance": string;
|
||||
/**
|
||||
* 操作
|
||||
*/
|
||||
@@ -948,6 +952,14 @@ export interface Locale extends ILocale {
|
||||
* サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになります。ブロックしたインスタンスには影響しません。
|
||||
*/
|
||||
"silencedInstancesDescription": string;
|
||||
/**
|
||||
* メディアサイレンスしたサーバー
|
||||
*/
|
||||
"mediaSilencedInstances": string;
|
||||
/**
|
||||
* メディアサイレンスしたいサーバーのホストを改行で区切って設定します。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。ブロックしたインスタンスには影響しません。
|
||||
*/
|
||||
"mediaSilencedInstancesDescription": string;
|
||||
/**
|
||||
* ミュートとブロック
|
||||
*/
|
||||
@@ -2372,6 +2384,14 @@ export interface Locale extends ILocale {
|
||||
* スクラッチパッドは、AiScriptの実験環境を提供します。Misskeyと対話するコードの記述、実行、結果の確認ができます。
|
||||
*/
|
||||
"scratchpadDescription": string;
|
||||
/**
|
||||
* UIインスペクター
|
||||
*/
|
||||
"uiInspector": string;
|
||||
/**
|
||||
* メモリ上に存在しているUIコンポーネントのインスタンスの一覧を見ることができます。UIコンポーネントはUi:C:系関数により生成されます。
|
||||
*/
|
||||
"uiInspectorDescription": string;
|
||||
/**
|
||||
* 出力
|
||||
*/
|
||||
@@ -2817,7 +2837,7 @@ export interface Locale extends ILocale {
|
||||
*/
|
||||
"reportAbuseOf": ParameterizedString<"name">;
|
||||
/**
|
||||
* 通報理由の詳細を記入してください。対象のノートがある場合はそのURLも記入してください。
|
||||
* 通報理由の詳細を記入してください。対象のノートやページなどがある場合はそのURLも記入してください。
|
||||
*/
|
||||
"fillAbuseReportDescription": string;
|
||||
/**
|
||||
@@ -3109,7 +3129,7 @@ export interface Locale extends ILocale {
|
||||
*/
|
||||
"narrow": string;
|
||||
/**
|
||||
* 設定はページリロード後に反映されます。今すぐリロードしますか?
|
||||
* 設定はページリロード後に反映されます。
|
||||
*/
|
||||
"reloadToApplySetting": string;
|
||||
/**
|
||||
@@ -4500,6 +4520,14 @@ export interface Locale extends ILocale {
|
||||
* ユーザー指定
|
||||
*/
|
||||
"specifyUser": string;
|
||||
/**
|
||||
* 照会しますか?
|
||||
*/
|
||||
"lookupConfirm": string;
|
||||
/**
|
||||
* ハッシュタグのページを開きますか?
|
||||
*/
|
||||
"openTagPageConfirm": string;
|
||||
/**
|
||||
* ホスト指定
|
||||
*/
|
||||
@@ -5048,6 +5076,26 @@ export interface Locale extends ILocale {
|
||||
* 作成したアンテナ
|
||||
*/
|
||||
"createdAntennas": string;
|
||||
/**
|
||||
* {x}から
|
||||
*/
|
||||
"fromX": ParameterizedString<"x">;
|
||||
/**
|
||||
* 埋め込みコードを生成
|
||||
*/
|
||||
"genEmbedCode": string;
|
||||
/**
|
||||
* このユーザーのノート一覧
|
||||
*/
|
||||
"noteOfThisUser": string;
|
||||
/**
|
||||
* これ以上このクリップにノートを追加できません。
|
||||
*/
|
||||
"clipNoteLimitExceeded": string;
|
||||
/**
|
||||
* パフォーマンス
|
||||
*/
|
||||
"performance": string;
|
||||
"_delivery": {
|
||||
/**
|
||||
* 配信状態
|
||||
@@ -5539,6 +5587,10 @@ export interface Locale extends ILocale {
|
||||
* 有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。
|
||||
*/
|
||||
"fanoutTimelineDbFallbackDescription": string;
|
||||
/**
|
||||
* 有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。
|
||||
*/
|
||||
"reactionsBufferingDescription": string;
|
||||
/**
|
||||
* 問い合わせ先URL
|
||||
*/
|
||||
@@ -6718,6 +6770,26 @@ export interface Locale extends ILocale {
|
||||
* アイコンデコレーションの最大取付個数
|
||||
*/
|
||||
"avatarDecorationLimit": string;
|
||||
/**
|
||||
* アンテナのインポートを許可
|
||||
*/
|
||||
"canImportAntennas": string;
|
||||
/**
|
||||
* ブロックのインポートを許可
|
||||
*/
|
||||
"canImportBlocking": string;
|
||||
/**
|
||||
* フォローのインポートを許可
|
||||
*/
|
||||
"canImportFollowing": string;
|
||||
/**
|
||||
* ミュートのインポートを許可
|
||||
*/
|
||||
"canImportMuting": string;
|
||||
/**
|
||||
* リストのインポートを許可
|
||||
*/
|
||||
"canImportUserLists": string;
|
||||
};
|
||||
"_condition": {
|
||||
/**
|
||||
@@ -7613,6 +7685,10 @@ export interface Locale extends ILocale {
|
||||
* 長い音声を使用するとMisskeyの使用に支障をきたす可能性があります。それでも続行しますか?
|
||||
*/
|
||||
"driveFileDurationWarnDescription": string;
|
||||
/**
|
||||
* 音声が読み込めませんでした。設定を変更してください
|
||||
*/
|
||||
"driveFileError": string;
|
||||
};
|
||||
"_ago": {
|
||||
/**
|
||||
@@ -8961,6 +9037,10 @@ export interface Locale extends ILocale {
|
||||
* ブロックを追加
|
||||
*/
|
||||
"chooseBlock": string;
|
||||
/**
|
||||
* セクションタイトルを入力
|
||||
*/
|
||||
"enterSectionTitle": string;
|
||||
/**
|
||||
* 種類を選択
|
||||
*/
|
||||
@@ -9378,9 +9458,9 @@ export interface Locale extends ILocale {
|
||||
*/
|
||||
"secret": string;
|
||||
/**
|
||||
* Webhookを実行するタイミング
|
||||
* トリガー
|
||||
*/
|
||||
"events": string;
|
||||
"trigger": string;
|
||||
/**
|
||||
* 有効
|
||||
*/
|
||||
@@ -9433,6 +9513,10 @@ export interface Locale extends ILocale {
|
||||
* Webhookを削除しますか?
|
||||
*/
|
||||
"deleteConfirm": string;
|
||||
/**
|
||||
* スイッチの右にあるボタンをクリックするとダミーのデータを使用したテスト用Webhookを送信できます。
|
||||
*/
|
||||
"testRemarks": string;
|
||||
};
|
||||
"_abuseReport": {
|
||||
"_notificationRecipient": {
|
||||
@@ -9655,6 +9739,22 @@ export interface Locale extends ILocale {
|
||||
* 通報の通知先を削除
|
||||
*/
|
||||
"deleteAbuseReportNotificationRecipient": string;
|
||||
/**
|
||||
* アカウントを削除
|
||||
*/
|
||||
"deleteAccount": string;
|
||||
/**
|
||||
* ページを削除
|
||||
*/
|
||||
"deletePage": string;
|
||||
/**
|
||||
* Playを削除
|
||||
*/
|
||||
"deleteFlash": string;
|
||||
/**
|
||||
* ギャラリーの投稿を削除
|
||||
*/
|
||||
"deleteGalleryPost": string;
|
||||
};
|
||||
"_fileViewer": {
|
||||
/**
|
||||
@@ -10148,6 +10248,60 @@ 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;
|
||||
|
@@ -2412,7 +2412,6 @@ _webhookSettings:
|
||||
modifyWebhook: "Modifica Webhook"
|
||||
name: "Nome"
|
||||
secret: "Segreto"
|
||||
events: "Quando eseguire il Webhook"
|
||||
active: "Attivo"
|
||||
_events:
|
||||
follow: "Quando segui un profilo"
|
||||
|
@@ -212,6 +212,7 @@ perDay: "1日ごと"
|
||||
stopActivityDelivery: "アクティビティの配送を停止"
|
||||
blockThisInstance: "このサーバーをブロック"
|
||||
silenceThisInstance: "サーバーをサイレンス"
|
||||
mediaSilenceThisInstance: "サーバーをメディアサイレンス"
|
||||
operations: "操作"
|
||||
software: "ソフトウェア"
|
||||
version: "バージョン"
|
||||
@@ -233,6 +234,8 @@ blockedInstances: "ブロックしたサーバー"
|
||||
blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定します。ブロックされたサーバーは、このインスタンスとやり取りできなくなります。"
|
||||
silencedInstances: "サイレンスしたサーバー"
|
||||
silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになります。ブロックしたインスタンスには影響しません。"
|
||||
mediaSilencedInstances: "メディアサイレンスしたサーバー"
|
||||
mediaSilencedInstancesDescription: "メディアサイレンスしたいサーバーのホストを改行で区切って設定します。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。ブロックしたインスタンスには影響しません。"
|
||||
muteAndBlock: "ミュートとブロック"
|
||||
mutedUsers: "ミュートしたユーザー"
|
||||
blockedUsers: "ブロックしたユーザー"
|
||||
@@ -589,6 +592,8 @@ ascendingOrder: "昇順"
|
||||
descendingOrder: "降順"
|
||||
scratchpad: "スクラッチパッド"
|
||||
scratchpadDescription: "スクラッチパッドは、AiScriptの実験環境を提供します。Misskeyと対話するコードの記述、実行、結果の確認ができます。"
|
||||
uiInspector: "UIインスペクター"
|
||||
uiInspectorDescription: "メモリ上に存在しているUIコンポーネントのインスタンスの一覧を見ることができます。UIコンポーネントはUi:C:系関数により生成されます。"
|
||||
output: "出力"
|
||||
script: "スクリプト"
|
||||
disablePagesScript: "Pagesのスクリプトを無効にする"
|
||||
@@ -700,7 +705,7 @@ abuseReports: "通報"
|
||||
reportAbuse: "通報"
|
||||
reportAbuseRenote: "リノートを通報"
|
||||
reportAbuseOf: "{name}を通報する"
|
||||
fillAbuseReportDescription: "通報理由の詳細を記入してください。対象のノートがある場合はそのURLも記入してください。"
|
||||
fillAbuseReportDescription: "通報理由の詳細を記入してください。対象のノートやページなどがある場合はそのURLも記入してください。"
|
||||
abuseReported: "内容が送信されました。ご報告ありがとうございました。"
|
||||
reporter: "通報者"
|
||||
reporteeOrigin: "通報先"
|
||||
@@ -773,7 +778,7 @@ left: "左"
|
||||
center: "中央"
|
||||
wide: "広い"
|
||||
narrow: "狭い"
|
||||
reloadToApplySetting: "設定はページリロード後に反映されます。今すぐリロードしますか?"
|
||||
reloadToApplySetting: "設定はページリロード後に反映されます。"
|
||||
needReloadToApply: "反映には再起動が必要です。"
|
||||
showTitlebar: "タイトルバーを表示する"
|
||||
clearCache: "キャッシュをクリア"
|
||||
@@ -1121,6 +1126,8 @@ preventAiLearning: "生成AIによる学習を拒否"
|
||||
preventAiLearningDescription: "外部の文章生成AIや画像生成AIに対して、投稿したノートや画像などのコンテンツを学習の対象にしないように要求します。これはnoaiフラグをHTMLレスポンスに含めることによって実現されますが、この要求に従うかはそのAI次第であるため、学習を完全に防止するものではありません。"
|
||||
options: "オプション"
|
||||
specifyUser: "ユーザー指定"
|
||||
lookupConfirm: "照会しますか?"
|
||||
openTagPageConfirm: "ハッシュタグのページを開きますか?"
|
||||
specifyHost: "ホスト指定"
|
||||
failedToPreviewUrl: "プレビューできません"
|
||||
update: "更新"
|
||||
@@ -1258,6 +1265,11 @@ confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示
|
||||
sensitiveMediaRevealConfirm: "センシティブなメディアです。表示しますか?"
|
||||
createdLists: "作成したリスト"
|
||||
createdAntennas: "作成したアンテナ"
|
||||
fromX: "{x}から"
|
||||
genEmbedCode: "埋め込みコードを生成"
|
||||
noteOfThisUser: "このユーザーのノート一覧"
|
||||
clipNoteLimitExceeded: "これ以上このクリップにノートを追加できません。"
|
||||
performance: "パフォーマンス"
|
||||
|
||||
_delivery:
|
||||
status: "配信状態"
|
||||
@@ -1400,6 +1412,7 @@ _serverSettings:
|
||||
fanoutTimelineDescription: "有効にすると、各種タイムラインを取得する際のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。サーバーのメモリ容量が少ない場合、または動作が不安定な場合は無効にすることができます。"
|
||||
fanoutTimelineDbFallback: "データベースへのフォールバック"
|
||||
fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。"
|
||||
reactionsBufferingDescription: "有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。"
|
||||
inquiryUrl: "問い合わせ先URL"
|
||||
inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。"
|
||||
|
||||
@@ -1736,6 +1749,11 @@ _role:
|
||||
canSearchNotes: "ノート検索の利用"
|
||||
canUseTranslator: "翻訳機能の利用"
|
||||
avatarDecorationLimit: "アイコンデコレーションの最大取付個数"
|
||||
canImportAntennas: "アンテナのインポートを許可"
|
||||
canImportBlocking: "ブロックのインポートを許可"
|
||||
canImportFollowing: "フォローのインポートを許可"
|
||||
canImportMuting: "ミュートのインポートを許可"
|
||||
canImportUserLists: "リストのインポートを許可"
|
||||
_condition:
|
||||
roleAssignedTo: "マニュアルロールにアサイン済み"
|
||||
isLocal: "ローカルユーザー"
|
||||
@@ -1997,6 +2015,7 @@ _soundSettings:
|
||||
driveFileTypeWarnDescription: "音声ファイルを選択してください"
|
||||
driveFileDurationWarn: "音声が長すぎます"
|
||||
driveFileDurationWarnDescription: "長い音声を使用するとMisskeyの使用に支障をきたす可能性があります。それでも続行しますか?"
|
||||
driveFileError: "音声が読み込めませんでした。設定を変更してください"
|
||||
|
||||
_ago:
|
||||
future: "未来"
|
||||
@@ -2365,6 +2384,7 @@ _pages:
|
||||
eyeCatchingImageSet: "アイキャッチ画像を設定"
|
||||
eyeCatchingImageRemove: "アイキャッチ画像を削除"
|
||||
chooseBlock: "ブロックを追加"
|
||||
enterSectionTitle: "セクションタイトルを入力"
|
||||
selectType: "種類を選択"
|
||||
contentBlocks: "コンテンツ"
|
||||
inputBlocks: "入力"
|
||||
@@ -2486,7 +2506,7 @@ _webhookSettings:
|
||||
modifyWebhook: "Webhookを編集"
|
||||
name: "名前"
|
||||
secret: "シークレット"
|
||||
events: "Webhookを実行するタイミング"
|
||||
trigger: "トリガー"
|
||||
active: "有効"
|
||||
_events:
|
||||
follow: "フォローしたとき"
|
||||
@@ -2501,6 +2521,7 @@ _webhookSettings:
|
||||
abuseReportResolved: "ユーザーからの通報を処理したとき"
|
||||
userCreated: "ユーザーが作成されたとき"
|
||||
deleteConfirm: "Webhookを削除しますか?"
|
||||
testRemarks: "スイッチの右にあるボタンをクリックするとダミーのデータを使用したテスト用Webhookを送信できます。"
|
||||
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
@@ -2561,6 +2582,10 @@ _moderationLogTypes:
|
||||
createAbuseReportNotificationRecipient: "通報の通知先を作成"
|
||||
updateAbuseReportNotificationRecipient: "通報の通知先を更新"
|
||||
deleteAbuseReportNotificationRecipient: "通報の通知先を削除"
|
||||
deleteAccount: "アカウントを削除"
|
||||
deletePage: "ページを削除"
|
||||
deleteFlash: "Playを削除"
|
||||
deleteGalleryPost: "ギャラリーの投稿を削除"
|
||||
|
||||
_fileViewer:
|
||||
title: "ファイルの詳細"
|
||||
@@ -2706,3 +2731,18 @@ _contextMenu:
|
||||
app: "アプリケーション"
|
||||
appWithShift: "Shiftキーでアプリケーション"
|
||||
native: "ブラウザのUI"
|
||||
|
||||
_embedCodeGen:
|
||||
title: "埋め込みコードをカスタマイズ"
|
||||
header: "ヘッダーを表示"
|
||||
autoload: "自動で続きを読み込む(非推奨)"
|
||||
maxHeight: "高さの最大値"
|
||||
maxHeightDescription: "0で最大値の設定が無効になります。ウィジェットが縦に伸び続けるのを防ぐために、何らかの値に指定してください。"
|
||||
maxHeightWarn: "高さの最大値制限が無効(0)になっています。これが意図した変更ではない場合は、高さの最大値を何らかの値に設定してください。"
|
||||
previewIsNotActual: "プレビュー画面で表示可能な範囲を超えたため、実際に埋め込んだ際とは表示が異なります。"
|
||||
rounded: "角丸にする"
|
||||
border: "外枠に枠線をつける"
|
||||
applyToPreview: "プレビューに反映"
|
||||
generateCode: "埋め込みコードを作成"
|
||||
codeGenerated: "コードが生成されました"
|
||||
codeGeneratedDescription: "生成されたコードをウェブサイトに貼り付けてご利用ください。"
|
||||
|
@@ -60,6 +60,7 @@ copyFileId: "ファイルIDをコピー"
|
||||
copyFolderId: "フォルダーIDをコピー"
|
||||
copyProfileUrl: "プロフィールURLをコピー"
|
||||
searchUser: "ユーザーを探す"
|
||||
searchThisUsersNotes: "ユーザーのノートを検索"
|
||||
reply: "返事"
|
||||
loadMore: "まだまだあるで!"
|
||||
showMore: "まだまだあるで!"
|
||||
@@ -114,6 +115,8 @@ cantReRenote: "リノート自体はリノートできへんで。"
|
||||
quote: "引用"
|
||||
inChannelRenote: "チャンネルの中でリノート"
|
||||
inChannelQuote: "チャンネル内引用"
|
||||
renoteToChannel: "チャンネルにリノート"
|
||||
renoteToOtherChannel: "他のチャンネルにリノート"
|
||||
pinnedNote: "ピン留めされとるノート"
|
||||
pinned: "ピン留めしとく"
|
||||
you: "あんた"
|
||||
@@ -152,6 +155,7 @@ editList: "リストいじる"
|
||||
selectChannel: "チャンネルを選ぶ"
|
||||
selectAntenna: "アンテナを選ぶ"
|
||||
editAntenna: "アンテナいじる"
|
||||
createAntenna: "アンテナを作成"
|
||||
selectWidget: "ウィジェットを選ぶ"
|
||||
editWidgets: "ウィジェットをいじる"
|
||||
editWidgetsExit: "いじるのをやめる"
|
||||
@@ -178,6 +182,10 @@ addAccount: "アカウントを追加"
|
||||
reloadAccountsList: "アカウントリストの情報を更新"
|
||||
loginFailed: "ログインに失敗してもうた…"
|
||||
showOnRemote: "リモートで見る"
|
||||
continueOnRemote: "リモートで続行"
|
||||
chooseServerOnMisskeyHub: "Misskey Hubからサーバーを選択"
|
||||
specifyServerHost: "サーバーのドメインを直接指定"
|
||||
inputHostName: "ドメインを入力せえや"
|
||||
general: "全般"
|
||||
wallpaper: "壁紙"
|
||||
setWallpaper: "壁紙を設定"
|
||||
@@ -188,6 +196,7 @@ followConfirm: "{name}をフォローしてええか?"
|
||||
proxyAccount: "プロキシアカウント"
|
||||
proxyAccountDescription: "プロキシアカウントは、代わりにフォローしてくれるアカウントや。例えば、551に豚まんが無いときやったり、ユーザーがリモートユーザーをアカウントに入れたとき、リストに入れられたユーザーが誰からもフォローされてないと寂しいやん。寂しいし、アクティビティも配達されへんから、プロキシアカウントがフォローしてくれるで。ええやつやん…"
|
||||
host: "ホスト"
|
||||
selectSelf: "自分を選択"
|
||||
selectUser: "ユーザーを選ぶ"
|
||||
recipient: "宛先"
|
||||
annotation: "注釈"
|
||||
@@ -203,6 +212,7 @@ perDay: "1日ごと"
|
||||
stopActivityDelivery: "アクティビティの配送をやめる"
|
||||
blockThisInstance: "このサーバーをブロックすんで"
|
||||
silenceThisInstance: "サーバーサイレンスすんで?"
|
||||
mediaSilenceThisInstance: "サーバーをメディアサイレンス"
|
||||
operations: "操作"
|
||||
software: "ソフトウェア"
|
||||
version: "バージョン"
|
||||
@@ -224,6 +234,8 @@ blockedInstances: "ブロックしたサーバー"
|
||||
blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定してな。ブロックされてもうたサーバーとはもう金輪際やり取りできひんくなるで。"
|
||||
silencedInstances: "サーバーサイレンスされてんねん"
|
||||
silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定すんで。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになり、フォロワーでないローカルアカウントにはメンションできなくなんねん。ブロックしたインスタンスには影響せーへんで。"
|
||||
mediaSilencedInstances: "メディアサイレンスしたサーバー"
|
||||
mediaSilencedInstancesDescription: "メディアサイレンスしたいサーバーのホストを改行で区切って設定するで。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われてな、カスタム絵文字が使えへんようになるで。ブロックしたインスタンスには影響せえへんで。"
|
||||
muteAndBlock: "ミュートとブロック"
|
||||
mutedUsers: "ミュートしとるユーザー"
|
||||
blockedUsers: "ブロックしとるユーザー"
|
||||
@@ -475,6 +487,7 @@ noMessagesYet: "まだチャットはあらへんで"
|
||||
newMessageExists: "新しいメッセージがきたで"
|
||||
onlyOneFileCanBeAttached: "ごめんな、メッセージに添付できるファイルはひとつだけなんよ。"
|
||||
signinRequired: "ログインしてくれへん?"
|
||||
signinOrContinueOnRemote: "続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があるで"
|
||||
invitations: "来てや"
|
||||
invitationCode: "招待コード"
|
||||
checking: "確認しとるで"
|
||||
@@ -1025,6 +1038,7 @@ thisPostMayBeAnnoyingHome: "ホームに投稿"
|
||||
thisPostMayBeAnnoyingCancel: "やめとく"
|
||||
thisPostMayBeAnnoyingIgnore: "このまま投稿"
|
||||
collapseRenotes: "見たことあるリノートは飛ばして表示するで"
|
||||
collapseRenotesDescription: "リアクションやリノートをしたことがあるノートをたたんで表示するで。"
|
||||
internalServerError: "サーバー内部エラー"
|
||||
internalServerErrorDescription: "サーバーでなんか変なこと起こっとるわ。"
|
||||
copyErrorInfo: "エラー情報をコピるで"
|
||||
@@ -1098,6 +1112,8 @@ preservedUsernames: "予約ユーザー名"
|
||||
preservedUsernamesDescription: "予約しとくユーザー名を行ごとに挙げるで。ここで指定されたユーザー名はアカウント作るときに使えへんくなるけど、管理者は例外や。あと、もうあるアカウントも例外やな。"
|
||||
createNoteFromTheFile: "このファイル使うてノート作るで"
|
||||
archive: "アーカイブ"
|
||||
archived: "アーカイブ済み"
|
||||
unarchive: "アーカイブ解除"
|
||||
channelArchiveConfirmTitle: "{name}をアーカイブしてええか?"
|
||||
channelArchiveConfirmDescription: "アーカイブしたら、チャンネル一覧とか検索結果からなくなるし、新しく書き込みもできへんなるで。"
|
||||
thisChannelArchived: "このチャンネル、アーカイブされとるで。"
|
||||
@@ -1108,6 +1124,9 @@ preventAiLearning: "生成AIの学習に使わんといて"
|
||||
preventAiLearningDescription: "他の文章生成AIとか画像生成AIに、投稿したノートとか画像なんかを勝手に使わんように頼むで。具体的にはnoaiフラグをHTMLレスポンスに含めるんやけど、これ聞いてくれるんはAIの気分次第やから、使われる可能性もちょっとはあるな。"
|
||||
options: "オプション"
|
||||
specifyUser: "ユーザー指定"
|
||||
lookupConfirm: "照会するけどええか?"
|
||||
openTagPageConfirm: "ハッシュタグのページを開くんか?"
|
||||
specifyHost: "ホスト指定"
|
||||
failedToPreviewUrl: "プレビューできへん"
|
||||
update: "更新"
|
||||
rolesThatCanBeUsedThisEmojiAsReaction: "ツッコミとして使えるロール"
|
||||
@@ -1239,10 +1258,20 @@ keepOriginalFilenameDescription: "この設定をオフにすると、アップ
|
||||
noDescription: "説明文はあらへんで"
|
||||
alwaysConfirmFollow: "フォローの際常に確認する"
|
||||
inquiry: "問い合わせ"
|
||||
tryAgain: "もう一度試しいや。"
|
||||
confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示するとき確認する"
|
||||
sensitiveMediaRevealConfirm: "センシティブなメディアやで。表示するんか?"
|
||||
createdLists: "作成したリスト"
|
||||
createdAntennas: "作成したアンテナ"
|
||||
_delivery:
|
||||
status: "配信状態"
|
||||
stop: "配信せぇへん"
|
||||
resume: "配信再開"
|
||||
_type:
|
||||
none: "配信しとる"
|
||||
manuallySuspended: "手動停止中"
|
||||
goneSuspended: "サーバー削除のため停止中"
|
||||
autoSuspendedForNotResponding: "サーバー応答せえへんから停止中"
|
||||
_bubbleGame:
|
||||
howToPlay: "遊び方"
|
||||
hold: "ホールド"
|
||||
@@ -1368,6 +1397,8 @@ _serverSettings:
|
||||
fanoutTimelineDescription: "入れると、おのおのタイムラインを取得するときにめちゃめちゃ動きが良うなって、データベースが軽くなるわ。でも、Redisのメモリ使う量が増えるから注意な。サーバーのメモリが足りんときとか、動きが変なときは切れるで。"
|
||||
fanoutTimelineDbFallback: "データベースにフォールバックする"
|
||||
fanoutTimelineDbFallbackDescription: "有効にしたら、タイムラインがキャッシュん中に入ってないときにDBにもっかい問い合わせるフォールバック処理ってのをやっとくで。切ったらフォールバック処理をやらんからサーバーはもっと軽くなんねんけど、タイムラインの取得範囲がちょっと減るで。"
|
||||
inquiryUrl: "問い合わせ先URL"
|
||||
inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定するで。"
|
||||
_accountMigration:
|
||||
moveFrom: "別のアカウントからこのアカウントに引っ越す"
|
||||
moveFromSub: "別のアカウントへエイリアスを作る"
|
||||
@@ -1684,6 +1715,7 @@ _role:
|
||||
canManageAvatarDecorations: "アバターを飾るモンの管理"
|
||||
driveCapacity: "ドライブ容量"
|
||||
alwaysMarkNsfw: "勝手にファイルにNSFWをくっつける"
|
||||
canUpdateBioMedia: "アイコンとバナーの更新を許可"
|
||||
pinMax: "ノートピン留めできる数"
|
||||
antennaMax: "アンテナ作れる数"
|
||||
wordMuteMax: "ワードミュートの最大文字数"
|
||||
@@ -1935,6 +1967,7 @@ _soundSettings:
|
||||
driveFileTypeWarnDescription: "音声ファイルを選びや"
|
||||
driveFileDurationWarn: "音が長すぎるわ"
|
||||
driveFileDurationWarnDescription: "長い音使うたらMisskey使うのに良うないかもしれへんで。それでもええか?"
|
||||
driveFileError: "音声が読み込めへんかったで。設定を変更せえや"
|
||||
_ago:
|
||||
future: "未来"
|
||||
justNow: "ついさっき"
|
||||
@@ -2351,6 +2384,7 @@ _deck:
|
||||
alwaysShowMainColumn: "いつもメインカラムを表示"
|
||||
columnAlign: "カラムの寄せ"
|
||||
addColumn: "カラムを追加"
|
||||
newNoteNotificationSettings: "新着ノート通知の設定"
|
||||
configureColumn: "カラムの設定"
|
||||
swapLeft: "左に移動"
|
||||
swapRight: "右に移動"
|
||||
@@ -2389,9 +2423,10 @@ _drivecleaner:
|
||||
orderByCreatedAtAsc: "追加日の古い順"
|
||||
_webhookSettings:
|
||||
createWebhook: "Webhookをつくる"
|
||||
modifyWebhook: "Webhookを編集"
|
||||
name: "名前"
|
||||
secret: "シークレット"
|
||||
events: "Webhookを投げるタイミング"
|
||||
trigger: "トリガー"
|
||||
active: "有効"
|
||||
_events:
|
||||
follow: "フォローしたとき~!"
|
||||
@@ -2401,11 +2436,25 @@ _webhookSettings:
|
||||
renote: "リノートされるとき~!"
|
||||
reaction: "ツッコまれたとき~!"
|
||||
mention: "メンションがあるとき~!"
|
||||
_systemEvents:
|
||||
abuseReport: "ユーザーから通報があったとき"
|
||||
abuseReportResolved: "ユーザーからの通報を処理したとき"
|
||||
userCreated: "ユーザーが作成されたとき"
|
||||
deleteConfirm: "ほんまにWebhookをほかしてもええんか?"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
createRecipient: "通報の通知先を追加"
|
||||
modifyRecipient: "通報の通知先を編集"
|
||||
recipientType: "通知先の種類"
|
||||
_recipientType:
|
||||
mail: "メール"
|
||||
webhook: "Webhook"
|
||||
_captions:
|
||||
mail: "モデレーター権限を持つユーザーのメアドに通知を送るで(通報を受けた時のみ)"
|
||||
webhook: "指定したSystemWebhookに通知を送るで(通報を受けた時と通報を解決した時にそれぞれ発信)"
|
||||
keywords: "キーワード"
|
||||
notifiedUser: "通知先ユーザー"
|
||||
notifiedWebhook: "使用するWebhook"
|
||||
deleteConfirm: "通知先を削除してもええか?"
|
||||
_moderationLogTypes:
|
||||
createRole: "ロールを追加すんで"
|
||||
@@ -2444,6 +2493,8 @@ _moderationLogTypes:
|
||||
deleteAvatarDecoration: "アイコンデコレーションを削除"
|
||||
unsetUserAvatar: "この子のアイコン元に戻す"
|
||||
unsetUserBanner: "この子のバナー元に戻す"
|
||||
createSystemWebhook: "SystemWebhookを作成"
|
||||
updateSystemWebhook: "SystemWebhookを更新"
|
||||
_fileViewer:
|
||||
title: "ファイルの詳しい情報"
|
||||
type: "ファイルの種類"
|
||||
|
@@ -2411,7 +2411,6 @@ _webhookSettings:
|
||||
modifyWebhook: "Webhook 수정"
|
||||
name: "이름"
|
||||
secret: "시크릿"
|
||||
events: "Webhook을 실행할 타이밍"
|
||||
active: "활성화"
|
||||
_events:
|
||||
follow: "누군가를 팔로우했을 때"
|
||||
|
@@ -1544,7 +1544,6 @@ _webhookSettings:
|
||||
createWebhook: "Stwórz Webhook"
|
||||
name: "Nazwa"
|
||||
secret: "Sekret"
|
||||
events: "Uruchomienie Webhooka"
|
||||
active: "Właczono"
|
||||
_events:
|
||||
follow: "Po zaobserwowaniu użytkownika"
|
||||
|
1252
locales/pt-PT.yml
1252
locales/pt-PT.yml
File diff suppressed because it is too large
Load Diff
@@ -60,6 +60,7 @@ copyFileId: "คัดลอกไฟล์ ID"
|
||||
copyFolderId: "คัดลอกโฟลเดอร์ ID"
|
||||
copyProfileUrl: "คัดลอกโปรไฟล์ URL"
|
||||
searchUser: "ค้นหาผู้ใช้"
|
||||
searchThisUsersNotes: "ค้นหาโน้ตของผู้ใช้"
|
||||
reply: "ตอบกลับ"
|
||||
loadMore: "แสดงเพิ่มเติม"
|
||||
showMore: "แสดงเพิ่มเติม"
|
||||
@@ -154,6 +155,7 @@ editList: "แก้ไขรายชื่อ"
|
||||
selectChannel: "เลือกช่อง"
|
||||
selectAntenna: "เลือกเสาอากาศ"
|
||||
editAntenna: "แก้ไขเสาอากาศ"
|
||||
createAntenna: "สร้างเสาอากาศ"
|
||||
selectWidget: "เลือกวิดเจ็ต"
|
||||
editWidgets: "แก้ไขวิดเจ็ต"
|
||||
editWidgetsExit: "เรียบร้อย"
|
||||
@@ -194,6 +196,7 @@ followConfirm: "ต้องการติดตาม {name} ใช่ไห
|
||||
proxyAccount: "บัญชีพร็อกซี่"
|
||||
proxyAccountDescription: "บัญชีพร็อกซี คือ บัญชีที่ทำหน้าที่ติดตาม(ผู้ใช้)ระยะไกลภายใต้เงื่อนไขบางประการ ตัวอย่างเช่น เมื่อผู้ใช้ท้องถิ่นเพิ่มผู้ใช้ระยะไกลลงรายชื่อ หากไม่มีใครติดตามผู้ใช้ระยะไกลในรายชื่อนั้น กิจกรรมก็จะไม่ถูกส่งมายังเซิร์ฟเวอร์ ดังนั้นจึงมีบัญชีพร็อกซีไว้ติดตามผู้ใช้ระยะไกลเหล่านั้น"
|
||||
host: "โฮสต์"
|
||||
selectSelf: "เลือกตัวเอง"
|
||||
selectUser: "เลือกผู้ใช้งาน"
|
||||
recipient: "ผู้รับ"
|
||||
annotation: "หมายเหตุประกอบ"
|
||||
@@ -209,6 +212,7 @@ perDay: "ต่อวัน"
|
||||
stopActivityDelivery: "หยุดส่งกิจกรรม"
|
||||
blockThisInstance: "บล็อกเซิร์ฟเวอร์นี้"
|
||||
silenceThisInstance: "ปิดปากเซิร์ฟเวอร์นี้"
|
||||
mediaSilenceThisInstance: "ปิดปากสื่อของเซิร์ฟเวอร์นี้"
|
||||
operations: "ดำเนินการ"
|
||||
software: "ซอฟต์แวร์"
|
||||
version: "เวอร์ชั่น"
|
||||
@@ -230,6 +234,8 @@ blockedInstances: "เซิร์ฟเวอร์ที่ถูกบล็
|
||||
blockedInstancesDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่ต้องการบล็อก คั่นด้วยการขึ้นบรรทัดใหม่ เซิร์ฟเวอร์ที่ถูกบล็อกจะไม่สามารถติดต่อกับอินสแตนซ์นี้ได้"
|
||||
silencedInstances: "ปิดปากเซิร์ฟเวอร์นี้แล้ว"
|
||||
silencedInstancesDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่ต้องการปิดปาก คั่นด้วยการขึ้นบรรทัดใหม่, บัญชีทั้งหมดของเซิร์ฟเวอร์ดังกล่าวจะถือว่าถูกปิดปากเช่นกัน ทำได้เฉพาะคำขอติดตามเท่านั้น และไม่สามารถกล่าวถึงบัญชีในเซิร์ฟเวอร์นี้ได้หากไม่ได้ถูกติดตามกลับ | สิ่งนี้ไม่มีผลต่ออินสแตนซ์ที่ถูกบล็อก"
|
||||
mediaSilencedInstances: "เซิร์ฟเวอร์ที่ถูกปิดปากสื่อ"
|
||||
mediaSilencedInstancesDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่ต้องการปิดปากสื่อ คั่นด้วยการขึ้นบรรทัดใหม่, ไฟล์ที่ถูกส่งจากบัญชีของเซิร์ฟเวอร์ดังกล่าวจะถือว่าถูกปิดปาก แล้วจะถูกติดเครื่องหมายว่ามีเนื้อหาละเอียดอ่อน และเอโมจิแบบกำหนดเองก็จะใช้ไม่ได้ด้วย | สิ่งนี้ไม่มีผลต่ออินสแตนซ์ที่ถูกบล็อก"
|
||||
muteAndBlock: "ปิดเสียงและบล็อก"
|
||||
mutedUsers: "ผู้ใช้ที่ถูกปิดเสียง"
|
||||
blockedUsers: "ผู้ใช้ที่ถูกบล็อก"
|
||||
@@ -881,7 +887,7 @@ accountDeletionInProgress: "กำลังดำเนินการลบบ
|
||||
usernameInfo: "ชื่อที่ระบุบัญชีของคุณจากผู้อื่นในเซิร์ฟเวอร์นี้ คุณสามารถใช้ตัวอักษร (a~z, A~Z), ตัวเลข (0~9) หรือขีดล่าง (_) ชื่อผู้ใช้ไม่สามารถเปลี่ยนแปลงได้ในภายหลัง"
|
||||
aiChanMode: "โหมด Ai "
|
||||
devMode: "โหมดนักพัฒนา"
|
||||
keepCw: "เก็บคำเตือนเนื้อหา"
|
||||
keepCw: "คงการเตือนเนื้อหาไว้"
|
||||
pubSub: "บัญชี Pub/Sub"
|
||||
lastCommunication: "การสื่อสารครั้งสุดท้ายล่าสุด"
|
||||
resolved: "คลี่คลายแล้ว"
|
||||
@@ -1028,15 +1034,15 @@ achievements: "ความสำเร็จ"
|
||||
gotInvalidResponseError: "การตอบสนองเซิร์ฟเวอร์ไม่ถูกต้อง"
|
||||
gotInvalidResponseErrorDescription: "เซิร์ฟเวอร์อาจไม่สามารถเข้าถึงได้หรืออาจจะกำลังอยู่ในระหว่างปรับปรุง กรุณาลองใหม่อีกครั้งในภายหลังนะคะ"
|
||||
thisPostMayBeAnnoying: "โน้ตนี้อาจจะเป็นการรบกวนผู้อื่นนะคะ"
|
||||
thisPostMayBeAnnoyingHome: "โพสต์ไปยังไทม์ไลน์หลัก"
|
||||
thisPostMayBeAnnoyingCancel: "เลิก"
|
||||
thisPostMayBeAnnoyingIgnore: "โพสต์ยังไงก็แล้วแต่"
|
||||
thisPostMayBeAnnoyingHome: "โพสต์ลงไทม์ไลน์หลักเท่านั้น"
|
||||
thisPostMayBeAnnoyingCancel: "ยกเลิก"
|
||||
thisPostMayBeAnnoyingIgnore: "โพสต์ไปเลย ไม่ต้องปรับการมองเห็น"
|
||||
collapseRenotes: "ยุบรีโน้ตที่คุณเคยเห็นแล้ว"
|
||||
collapseRenotesDescription: "พับย่อโน้ตที่เคยตอบสนองหรือรีโน้ตแล้ว"
|
||||
internalServerError: "เซิร์ฟเวอร์ภายในเกิดข้อผิดพลาด"
|
||||
internalServerErrorDescription: "เกิดข้อผิดพลาดที่ไม่คาดคิดภายในเซิร์ฟเวอร์"
|
||||
copyErrorInfo: "คัดลอกรายละเอียดข้อผิดพลาด"
|
||||
joinThisServer: "ลงทะเบียนบนเซิร์ฟเวอร์นี้"
|
||||
joinThisServer: "ลงทะเบียนในเซิร์ฟเวอร์นี้"
|
||||
exploreOtherServers: "มองหาเซิร์ฟเวอร์อื่น"
|
||||
letsLookAtTimeline: "มาดูไทม์ไลน์กัน"
|
||||
disableFederationConfirm: "ปิดใช้งานสหพันธ์เลยใช่ไหม?"
|
||||
@@ -1099,13 +1105,15 @@ vertical: "แนวตั้ง"
|
||||
horizontal: "แนวนอน"
|
||||
position: "ตำแหน่ง"
|
||||
serverRules: "กฎของเซิร์ฟเวอร์"
|
||||
pleaseConfirmBelowBeforeSignup: "หากต้องการลงทะเบียนบนเซิร์ฟเวอร์นี้ คุณต้องตรวจสอบและยอมรับสิ่งต่อไปนี้"
|
||||
pleaseConfirmBelowBeforeSignup: "หากต้องการลงทะเบียนในเซิร์ฟเวอร์นี้ คุณต้องตรวจสอบและยอมรับสิ่งต่อไปนี้"
|
||||
pleaseAgreeAllToContinue: "คุณต้องยอมรับทุกช่องตรงด้านบนเพื่อดำเนินการต่อค่ะ"
|
||||
continue: "ดำเนินการต่อ"
|
||||
preservedUsernames: "ชื่อผู้ใช้ที่สงวนไว้"
|
||||
preservedUsernamesDescription: "ระบุชื่อผู้ใช้ที่จะสงวนชื่อไว้ คั่นด้วยการขึ้นบรรทัดใหม่ ชื่อผู้ใช้ที่ระบุที่นี่จะไม่สามารถใช้งานได้อีกต่อไปเมื่อสร้างบัญชีใหม่ ยกเว้นเมื่อผู้ดูแลระบบสร้างบัญชี นอกจากนี้ บัญชีที่มีอยู่แล้วจะไม่ได้รับผลกระทบ"
|
||||
createNoteFromTheFile: "เรียบเรียงโน้ตจากไฟล์นี้"
|
||||
archive: "เก็บถาวร"
|
||||
archived: "เก็บถาวรแล้ว"
|
||||
unarchive: "เลิกการเก็บถาวร"
|
||||
channelArchiveConfirmTitle: "ต้องการเก็บถาวรเจ้า {name} ใช่ไหม?"
|
||||
channelArchiveConfirmDescription: "เมื่อเก็บถาวรแล้ว จะไม่ปรากฏในรายการช่องหรือผลการค้นหาอีกต่อไป และจะไม่สามารถโพสต์ใหม่ได้อีกต่อไป"
|
||||
thisChannelArchived: "ช่องนี้ถูกเก็บถาวรแล้วนะ"
|
||||
@@ -1116,6 +1124,9 @@ preventAiLearning: "ปฏิเสธการเรียนรู้ด้ว
|
||||
preventAiLearningDescription: "ส่งคำร้องขอไม่ให้ใช้ ข้อความในโน้ตที่โพสต์, หรือเนื้อหารูปภาพ ฯลฯ ในการเรียนรู้ของเครื่อง(machine learning) / Predictive AI / Generative AI โดยการเพิ่มแฟล็ก “noai” ลง HTML-Response ให้กับเนื้อหาที่เกี่ยวข้อง แต่ทั้งนี้ ไม่ได้ป้องกัน AI จากการเรียนรู้ได้อย่างสมบูรณ์ เนื่องจากมี AI บางตัวเท่านั้นที่จะเคารพคำขอดังกล่าว"
|
||||
options: "ตัวเลือกบทบาท"
|
||||
specifyUser: "ผู้ใช้เฉพาะ"
|
||||
lookupConfirm: "ต้องการเรียกดูข้อมูลใช่ไหม?"
|
||||
openTagPageConfirm: "ต้องการเปิดหน้าแฮชแท็กใช่ไหม?"
|
||||
specifyHost: "ระบุโฮสต์"
|
||||
failedToPreviewUrl: "ไม่สามารถดูตัวอย่างได้"
|
||||
update: "อัปเดต"
|
||||
rolesThatCanBeUsedThisEmojiAsReaction: "บทบาทที่สามารถใช้เอโมจินี้เป็นรีแอคชั่นได้"
|
||||
@@ -1250,6 +1261,8 @@ inquiry: "ติดต่อเรา"
|
||||
tryAgain: "โปรดลองอีกครั้ง"
|
||||
confirmWhenRevealingSensitiveMedia: "ตรวจสอบก่อนแสดงสื่อที่มีเนื้อหาละเอียดอ่อน"
|
||||
sensitiveMediaRevealConfirm: "สื่อนี้มีเนื้อหาละเอียดอ่อน, ต้องการแสดงใช่ไหม?"
|
||||
createdLists: "รายชื่อที่ถูกสร้าง"
|
||||
createdAntennas: "เสาอากาศที่ถูกสร้าง"
|
||||
_delivery:
|
||||
status: "สถานะการจัดส่ง"
|
||||
stop: "ระงับการส่ง"
|
||||
@@ -1348,9 +1361,9 @@ _initialTutorial:
|
||||
localOnly: "การโพสต์ด้วย flag นี้จะไม่รวมโน้ตไปยังเซิร์ฟเวอร์อื่น ผู้ใช้บนเซิร์ฟเวอร์อื่นจะไม่สามารถดูโน้ตเหล่านี้ได้โดยตรง โดยไม่คำนึงถึงการตั้งค่าการแสดงผลข้างต้น"
|
||||
_cw:
|
||||
title: "คำเตือนเกี่ยวกับเนื้อหา"
|
||||
description: "เนื้อหาที่เขียนด้วย “คำอธิบายประกอบ” จะแสดงแทนข้อความหลัก คลิก “ดูเพิ่มเติม” เพื่อแสดงข้อความเต็ม"
|
||||
description: "เนื้อหาที่เขียนใน “คำอธิบายประกอบ” จะแสดงแทนเนื้อหาหลัก ต้องคลิก “ดูเพิ่มเติม” เพื่อให้เนื้อหาหลักแสดง"
|
||||
_exampleNote:
|
||||
cw: "นี่อาจจะทำให้คุณหิวอย่างแน่นอน!"
|
||||
cw: " ห้ามดู ระวังหิว"
|
||||
note: "เพิ่งไปกินโดนัทเคลือบช็อคโกแลตมา 🍩😋"
|
||||
useCases: "ใช้สิ่งนี้เพื่อระบุโน้ตที่ต้องตามแนวทางปฏิบัติของเซิร์ฟเวอร์ หรือเพื่อควบคุมการสปอยล์และข้อความที่ละเอียดอ่อนด้วยตนเอง"
|
||||
_howToMakeAttachmentsSensitive:
|
||||
@@ -1466,15 +1479,15 @@ _achievements:
|
||||
title: "มือใหม่ III"
|
||||
description: "เข้าสู่ระบบเป็นเวลารวม 15 วัน"
|
||||
_login30:
|
||||
title: "มิสคิสท์ I"
|
||||
title: "มิสคิสต์ I"
|
||||
description: "เข้าสู่ระบบเป็นเวลารวม 30 วัน"
|
||||
_login60:
|
||||
title: "มิสคิสท์ II"
|
||||
title: "มิสคิสต์ II"
|
||||
description: "เข้าสู่ระบบเป็นเวลารวม 60 วัน"
|
||||
_login100:
|
||||
title: "มิสคิสท์ III"
|
||||
title: "มิสคิสต์ III"
|
||||
description: "เข้าสู่ระบบเป็นเวลารวม 100 วัน"
|
||||
flavor: "มิสคิสต์หัวรุนแรง"
|
||||
flavor: "Violent Misskist (ทำไมเหมือนชื่อหนังสักเรื่องจังเลยนะ)"
|
||||
_login200:
|
||||
title: "ลูกค้าประจำ I"
|
||||
description: "เข้าสู่ระบบเป็นเวลารวม 200 วัน"
|
||||
@@ -1954,6 +1967,7 @@ _soundSettings:
|
||||
driveFileTypeWarnDescription: "กรุณาเลือกไฟล์เสียง"
|
||||
driveFileDurationWarn: "เสียงยาวเกินไป"
|
||||
driveFileDurationWarnDescription: "การใช้เสียงที่ยาว อาจรบกวนการใช้งาน Misskey, ต้องการดำเนินการต่อใช่ไหม?"
|
||||
driveFileError: "ไม่สามารถโหลดไฟล์เสียงได้ กรุณาเปลี่ยนแปลงการตั้งค่า"
|
||||
_ago:
|
||||
future: "อนาคต"
|
||||
justNow: "เมื่อกี๊นี้"
|
||||
@@ -2141,7 +2155,7 @@ _widgets:
|
||||
serverMetric: "ตัวชี้วัดเซิร์ฟเวอร์"
|
||||
aiscript: " คอนโซล AiScript"
|
||||
aiscriptApp: "แอป AiScript"
|
||||
aichan: "ไอ"
|
||||
aichan: "藍 (ไอ)"
|
||||
userList: "รายชื่อผู้ใช้"
|
||||
_userList:
|
||||
chooseList: "เลือกรายชื่อ"
|
||||
@@ -2183,7 +2197,7 @@ _visibility:
|
||||
followersDescription: "เฉพาะผู้ติดตามเท่านั้นที่มองเห็นได้"
|
||||
specified: "ไดเร็ค"
|
||||
specifiedDescription: "ทำให้มองเห็นได้เฉพาะผู้ใช้ที่ระบุเท่านั้น"
|
||||
disableFederation: "ไม่มีสหพันธ์"
|
||||
disableFederation: "การปิดใช้งานสหพันธ์"
|
||||
disableFederationDescription: "อย่าส่งข้อมูลไปยังเซิร์ฟเวอร์อื่น"
|
||||
_postForm:
|
||||
replyPlaceholder: "ตอบกลับโน้ตนี้..."
|
||||
@@ -2412,7 +2426,7 @@ _webhookSettings:
|
||||
modifyWebhook: "แก้ไข Webhook"
|
||||
name: "ชื่อ"
|
||||
secret: "ความลับ"
|
||||
events: "อีเว้นท์ Webhook"
|
||||
trigger: "ทริกเกอร์"
|
||||
active: "เปิดใช้งาน"
|
||||
_events:
|
||||
follow: "เมื่อกำลังติดตามผู้ใช้"
|
||||
@@ -2536,7 +2550,7 @@ _externalResourceInstaller:
|
||||
description: "เกิดปัญหาระหว่างการติดตั้งธีม กรุณาลองอีกครั้ง. รายละเอียดข้อผิดพลาดสามารถดูได้ในคอนโซล Javascript"
|
||||
_dataSaver:
|
||||
_media:
|
||||
title: "โหลดมีเดีย"
|
||||
title: "โหลดสื่อ"
|
||||
description: "กันไม่ให้ภาพและวิดีโอโหลดโดยอัตโนมัติ แตะรูปภาพ/วิดีโอที่ซ่อนอยู่เพื่อโหลด"
|
||||
_avatar:
|
||||
title: "รูปไอคอน"
|
||||
@@ -2616,3 +2630,8 @@ _mediaControls:
|
||||
pip: "รูปภาพในรูปภาม"
|
||||
playbackRate: "ความเร็วในการเล่น"
|
||||
loop: "เล่นวนซ้ำ"
|
||||
_contextMenu:
|
||||
title: "เมนูเนื้อหา"
|
||||
app: "แอปพลิเคชัน"
|
||||
appWithShift: "แอปฟลิเคชันด้วยปุ่มยกแคร่ (Shift)"
|
||||
native: "UI ของเบราว์เซอร์"
|
||||
|
@@ -1918,7 +1918,6 @@ _webhookSettings:
|
||||
createWebhook: "Tạo Webhook"
|
||||
name: "Tên"
|
||||
secret: "Mã bí mật"
|
||||
events: "Sự kiện Webhook"
|
||||
active: "Đã bật"
|
||||
_events:
|
||||
reaction: "Khi nhận được sự kiện"
|
||||
|
@@ -212,6 +212,7 @@ perDay: "每天"
|
||||
stopActivityDelivery: "停止发送活动"
|
||||
blockThisInstance: "阻止此服务器向本服务器推流"
|
||||
silenceThisInstance: "使服务器静音"
|
||||
mediaSilenceThisInstance: "隐藏此服务器的媒体文件"
|
||||
operations: "操作"
|
||||
software: "软件"
|
||||
version: "版本"
|
||||
@@ -230,9 +231,11 @@ clearQueueConfirmText: "未送达的帖子将不会被投递。 通常无需执
|
||||
clearCachedFiles: "清除缓存"
|
||||
clearCachedFilesConfirm: "确定要清除所有缓存的远程文件?"
|
||||
blockedInstances: "被封锁的服务器"
|
||||
blockedInstancesDescription: "设定要封锁的服务器,以换行来进行分割。被封锁的服务器将无法与本服务器进行交换通讯。子域名也同样会被封锁。"
|
||||
blockedInstancesDescription: "设定要封锁的服务器,以换行分隔。被封锁的服务器将无法与本服务器进行交换通讯。子域名也同样会被封锁。"
|
||||
silencedInstances: "被静音的服务器"
|
||||
silencedInstancesDescription: "设置要静音的服务器,以换行符分隔。被静音的服务器内所有的账户将默认处于「静音」状态,仅能发送关注请求,并且在未关注状态下无法提及本地账户。被阻止的实例不受影响。"
|
||||
silencedInstancesDescription: "设置要静音的服务器,以换行分隔。被静音的服务器内所有的账户将默认处于「静音」状态,仅能发送关注请求,并且在未关注状态下无法提及本地账户。被阻止的实例不受影响。"
|
||||
mediaSilencedInstances: "已隐藏媒体文件的服务器"
|
||||
mediaSilencedInstancesDescription: "设置要隐藏媒体文件的服务器,以换行分隔。被设置为隐藏媒体文件服务器内所有账号的文件均按照「敏感内容」处理,且将无法使用自定义表情符号。被阻止的实例不受影响。"
|
||||
muteAndBlock: "静音/拉黑"
|
||||
mutedUsers: "已静音用户"
|
||||
blockedUsers: "已拉黑的用户"
|
||||
@@ -1121,6 +1124,8 @@ preventAiLearning: "拒绝接受生成式 AI 的学习"
|
||||
preventAiLearningDescription: "要求文章生成 AI 或图像生成 AI 不能够以发布的帖子和图像等内容作为学习对象。这是通过在 HTML 响应中包含 noai 标志来实现的,这不能完全阻止 AI 学习你的发布内容,并不是所有 AI 都会遵守这类请求。"
|
||||
options: "选项"
|
||||
specifyUser: "用户指定"
|
||||
lookupConfirm: "确定查询?"
|
||||
openTagPageConfirm: "确定打开话题标签页面?"
|
||||
specifyHost: "指定主机名"
|
||||
failedToPreviewUrl: "无法预览"
|
||||
update: "更新"
|
||||
@@ -1660,6 +1665,7 @@ _achievements:
|
||||
_bubbleGameDoubleExplodingHead:
|
||||
title: "两个🤯"
|
||||
description: "你合成出了2个游戏里最大的Emoji"
|
||||
flavor: ""
|
||||
_role:
|
||||
new: "创建角色"
|
||||
edit: "编辑角色"
|
||||
@@ -1961,6 +1967,7 @@ _soundSettings:
|
||||
driveFileTypeWarnDescription: "请选择音频文件"
|
||||
driveFileDurationWarn: "音频过长"
|
||||
driveFileDurationWarnDescription: "使用长音频可能会影响 Misskey 的使用。即使这样也要继续吗?"
|
||||
driveFileError: "无法读取声音。请更改设置。"
|
||||
_ago:
|
||||
future: "未来"
|
||||
justNow: "最近"
|
||||
@@ -2309,6 +2316,7 @@ _pages:
|
||||
eyeCatchingImageSet: "设置封面图片"
|
||||
eyeCatchingImageRemove: "删除封面图片"
|
||||
chooseBlock: "添加块"
|
||||
enterSectionTitle: "输入会话标题"
|
||||
selectType: "选择类型"
|
||||
contentBlocks: "内容"
|
||||
inputBlocks: "输入"
|
||||
@@ -2419,7 +2427,7 @@ _webhookSettings:
|
||||
modifyWebhook: "编辑 webhook"
|
||||
name: "名称"
|
||||
secret: "密钥"
|
||||
events: "何时运行 Webhook"
|
||||
trigger: "触发器"
|
||||
active: "已启用"
|
||||
_events:
|
||||
follow: "关注时"
|
||||
@@ -2492,6 +2500,10 @@ _moderationLogTypes:
|
||||
createAbuseReportNotificationRecipient: "新建了举报通知"
|
||||
updateAbuseReportNotificationRecipient: "更新了举报通知"
|
||||
deleteAbuseReportNotificationRecipient: "删除了举报通知"
|
||||
deleteAccount: "删除了账户"
|
||||
deletePage: "删除了页面"
|
||||
deleteFlash: "删除了 Play"
|
||||
deleteGalleryPost: "删除了图库稿件"
|
||||
_fileViewer:
|
||||
title: "文件信息"
|
||||
type: "文件类型"
|
||||
|
@@ -212,6 +212,7 @@ perDay: "每日"
|
||||
stopActivityDelivery: "停止發送活動"
|
||||
blockThisInstance: "封鎖此伺服器"
|
||||
silenceThisInstance: "禁言此伺服器"
|
||||
mediaSilenceThisInstance: "將這個伺服器的媒體設為禁言"
|
||||
operations: "操作"
|
||||
software: "軟體"
|
||||
version: "版本"
|
||||
@@ -233,6 +234,8 @@ blockedInstances: "已封鎖的伺服器"
|
||||
blockedInstancesDescription: "請逐行輸入需要封鎖的伺服器。已封鎖的伺服器將無法與本伺服器進行通訊。"
|
||||
silencedInstances: "被禁言的伺服器"
|
||||
silencedInstancesDescription: "設定要禁言的伺服器主機名稱,以換行分隔。隸屬於禁言伺服器的所有帳戶都將被視為「禁言帳戶」,只能發出「追隨請求」,而且無法提及未追隨的本地帳戶。這不會影響已封鎖的實例。"
|
||||
mediaSilencedInstances: "媒體被禁言的伺服器"
|
||||
mediaSilencedInstancesDescription: "設定您想要對媒體設定禁言的伺服器,以換行符號區隔。來自被媒體禁言的伺服器所屬帳戶的所有檔案都會被視為敏感檔案,且自訂表情符號不能使用。被封鎖的伺服器不受影響。"
|
||||
muteAndBlock: "靜音和封鎖"
|
||||
mutedUsers: "被靜音的使用者"
|
||||
blockedUsers: "被封鎖的使用者"
|
||||
@@ -1121,6 +1124,8 @@ preventAiLearning: "拒絕接受生成式AI的訓練"
|
||||
preventAiLearningDescription: "要求站外生成式 AI 不使用您發佈的內容訓練模型。此功能會使伺服器於 HTML 回應新增「noai」標籤,而因為要視乎 AI 會否遵守該標籤,所以此功能無法完全阻止所有 AI 使用您的內容。"
|
||||
options: "選項"
|
||||
specifyUser: "指定使用者"
|
||||
lookupConfirm: "要查詢嗎?"
|
||||
openTagPageConfirm: "要開啟標籤的頁面嗎?"
|
||||
specifyHost: "指定主機"
|
||||
failedToPreviewUrl: "無法預覽"
|
||||
update: "更新"
|
||||
@@ -1962,6 +1967,7 @@ _soundSettings:
|
||||
driveFileTypeWarnDescription: "請選擇音效檔案"
|
||||
driveFileDurationWarn: "音效太長了"
|
||||
driveFileDurationWarnDescription: "使用長音效檔可能會影響 Misskey 的使用體驗。仍要使用此檔案嗎?"
|
||||
driveFileError: "無法載入語音。請變更設定"
|
||||
_ago:
|
||||
future: "未來"
|
||||
justNow: "剛剛"
|
||||
@@ -2310,6 +2316,7 @@ _pages:
|
||||
eyeCatchingImageSet: "設定封面影像"
|
||||
eyeCatchingImageRemove: "刪除封面影像"
|
||||
chooseBlock: "新增方塊"
|
||||
enterSectionTitle: "輸入區段的標題"
|
||||
selectType: "選擇類型"
|
||||
contentBlocks: "內容"
|
||||
inputBlocks: "輸入"
|
||||
@@ -2420,7 +2427,7 @@ _webhookSettings:
|
||||
modifyWebhook: "編輯 Webhook"
|
||||
name: "名字"
|
||||
secret: "密鑰"
|
||||
events: "何時運行 Webhook"
|
||||
trigger: "觸發器"
|
||||
active: "已啟用"
|
||||
_events:
|
||||
follow: "當你追隨時"
|
||||
@@ -2433,6 +2440,7 @@ _webhookSettings:
|
||||
_systemEvents:
|
||||
abuseReport: "當使用者檢舉時"
|
||||
abuseReportResolved: "當處理了使用者的檢舉時"
|
||||
userCreated: "使用者被新增時"
|
||||
deleteConfirm: "請問是否要刪除 Webhook?"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
@@ -2492,6 +2500,10 @@ _moderationLogTypes:
|
||||
createAbuseReportNotificationRecipient: "建立接收檢舉的通知對象"
|
||||
updateAbuseReportNotificationRecipient: "更新接收檢舉的通知對象"
|
||||
deleteAbuseReportNotificationRecipient: "刪除接收檢舉的通知對象"
|
||||
deleteAccount: "刪除帳戶"
|
||||
deletePage: "刪除頁面"
|
||||
deleteFlash: "刪除 Play"
|
||||
deleteGalleryPost: "刪除相簿的貼文"
|
||||
_fileViewer:
|
||||
title: "檔案詳細資訊"
|
||||
type: "檔案類型 "
|
||||
@@ -2626,4 +2638,5 @@ _mediaControls:
|
||||
_contextMenu:
|
||||
title: "內容功能表"
|
||||
app: "應用程式"
|
||||
appWithShift: "Shift 鍵應用程式"
|
||||
native: "瀏覽器的使用者介面"
|
||||
|
21
package.json
21
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "2024.7.0-rc.6",
|
||||
"version": "2024.9.0-alpha.4",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -8,7 +8,9 @@
|
||||
},
|
||||
"packageManager": "pnpm@9.6.0",
|
||||
"workspaces": [
|
||||
"packages/frontend-shared",
|
||||
"packages/frontend",
|
||||
"packages/frontend-embed",
|
||||
"packages/backend",
|
||||
"packages/sw",
|
||||
"packages/misskey-js",
|
||||
@@ -35,6 +37,7 @@
|
||||
"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",
|
||||
@@ -53,24 +56,24 @@
|
||||
"fast-glob": "3.3.2",
|
||||
"ignore-walk": "6.0.5",
|
||||
"js-yaml": "4.1.0",
|
||||
"postcss": "8.4.40",
|
||||
"postcss": "8.4.47",
|
||||
"tar": "6.2.1",
|
||||
"terser": "5.31.3",
|
||||
"typescript": "5.5.4",
|
||||
"esbuild": "0.23.0",
|
||||
"terser": "5.33.0",
|
||||
"typescript": "5.6.2",
|
||||
"esbuild": "0.23.1",
|
||||
"glob": "11.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/eslint-plugin": "2.0.2",
|
||||
"@misskey-dev/eslint-plugin": "2.0.3",
|
||||
"@types/node": "20.14.12",
|
||||
"@typescript-eslint/eslint-plugin": "7.17.0",
|
||||
"@typescript-eslint/parser": "7.17.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "13.13.1",
|
||||
"cypress": "13.14.2",
|
||||
"eslint": "9.8.0",
|
||||
"globals": "15.8.0",
|
||||
"globals": "15.9.0",
|
||||
"ncp": "2.0.0",
|
||||
"start-server-and-test": "2.0.4"
|
||||
"start-server-and-test": "2.0.8"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tensorflow/tfjs-core": "4.4.0"
|
||||
|
31
packages/backend/assets/embed.js
Normal file
31
packages/backend/assets/embed.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class MediaSilenceForHosts1716197366117 {
|
||||
name = 'MediaSilenceForHosts1716197366117'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "mediaSilencedHosts" character varying(1024) array NOT NULL DEFAULT '{}'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mediaSilencedHosts"`);
|
||||
}
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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.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",
|
||||
"@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",
|
||||
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
||||
"@misskey-dev/summaly": "5.1.0",
|
||||
"@napi-rs/canvas": "^0.1.53",
|
||||
"@nestjs/common": "10.3.10",
|
||||
"@nestjs/core": "10.3.10",
|
||||
"@nestjs/testing": "10.3.10",
|
||||
"@napi-rs/canvas": "0.1.56",
|
||||
"@nestjs/common": "10.4.3",
|
||||
"@nestjs/core": "10.4.3",
|
||||
"@nestjs/testing": "10.4.3",
|
||||
"@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.2",
|
||||
"bullmq": "5.10.4",
|
||||
"body-parser": "1.20.3",
|
||||
"bullmq": "5.13.2",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"cbor": "9.0.2",
|
||||
"chalk": "5.3.0",
|
||||
@@ -112,27 +112,28 @@
|
||||
"content-disposition": "0.5.4",
|
||||
"date-fns": "2.30.0",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"fastify": "4.28.1",
|
||||
"fastify-raw-body": "4.3.0",
|
||||
"fastify": "5.0.0",
|
||||
"fastify-raw-body": "5.0.0",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "19.3.0",
|
||||
"file-type": "19.5.0",
|
||||
"fluent-ffmpeg": "2.1.3",
|
||||
"form-data": "4.0.0",
|
||||
"got": "14.4.2",
|
||||
"happy-dom": "10.0.3",
|
||||
"happy-dom": "15.7.4",
|
||||
"hpagent": "1.2.0",
|
||||
"htmlescape": "1.1.1",
|
||||
"http-link-header": "1.1.3",
|
||||
"ioredis": "5.4.1",
|
||||
"ip-cidr": "4.0.1",
|
||||
"ip-cidr": "4.0.2",
|
||||
"ipaddr.js": "2.2.0",
|
||||
"is-svg": "5.0.1",
|
||||
"is-svg": "5.1.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsdom": "24.1.1",
|
||||
"json5": "2.2.3",
|
||||
"jsonld": "8.3.2",
|
||||
"jsrsasign": "11.1.0",
|
||||
"meilisearch": "0.41.0",
|
||||
"meilisearch": "0.42.0",
|
||||
"juice": "11.0.0",
|
||||
"mfm-js": "0.24.0",
|
||||
"microformats-parser": "2.0.2",
|
||||
"mime-types": "2.1.35",
|
||||
@@ -142,24 +143,24 @@
|
||||
"nanoid": "5.0.7",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "6.9.14",
|
||||
"nodemailer": "6.9.15",
|
||||
"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.1",
|
||||
"otpauth": "9.3.2",
|
||||
"parse5": "7.1.2",
|
||||
"pg": "8.12.0",
|
||||
"pg": "8.13.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.3",
|
||||
"qrcode": "1.5.4",
|
||||
"random-seed": "0.3.0",
|
||||
"ratelimiter": "3.4.1",
|
||||
"re2": "1.21.3",
|
||||
"re2": "1.21.4",
|
||||
"redis-lock": "0.1.4",
|
||||
"reflect-metadata": "0.2.2",
|
||||
"rename": "1.0.4",
|
||||
@@ -167,17 +168,17 @@
|
||||
"rxjs": "7.8.1",
|
||||
"sanitize-html": "2.13.0",
|
||||
"secure-json-parse": "2.7.0",
|
||||
"sharp": "0.33.4",
|
||||
"sharp": "0.33.5",
|
||||
"slacc": "0.0.10",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"systeminformation": "5.22.11",
|
||||
"systeminformation": "5.23.5",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.3",
|
||||
"tsc-alias": "1.8.10",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typeorm": "0.3.20",
|
||||
"typescript": "5.5.4",
|
||||
"typescript": "5.6.2",
|
||||
"ulid": "2.3.0",
|
||||
"vary": "1.1.2",
|
||||
"web-push": "3.6.7",
|
||||
@@ -186,7 +187,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.7.0",
|
||||
"@nestjs/platform-express": "10.3.10",
|
||||
"@nestjs/platform-express": "10.4.3",
|
||||
"@simplewebauthn/types": "10.0.0",
|
||||
"@swc/jest": "0.2.36",
|
||||
"@types/accepts": "1.3.7",
|
||||
@@ -195,10 +196,10 @@
|
||||
"@types/body-parser": "1.19.5",
|
||||
"@types/color-convert": "2.0.3",
|
||||
"@types/content-disposition": "0.5.8",
|
||||
"@types/fluent-ffmpeg": "2.1.24",
|
||||
"@types/fluent-ffmpeg": "2.1.26",
|
||||
"@types/htmlescape": "1.1.3",
|
||||
"@types/http-link-header": "1.0.7",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/jest": "29.5.13",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/jsdom": "21.1.7",
|
||||
"@types/jsonld": "1.5.15",
|
||||
@@ -206,18 +207,18 @@
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/ms": "0.7.34",
|
||||
"@types/node": "20.14.12",
|
||||
"@types/nodemailer": "6.4.15",
|
||||
"@types/nodemailer": "6.4.16",
|
||||
"@types/oauth": "0.9.5",
|
||||
"@types/oauth2orize": "1.11.5",
|
||||
"@types/oauth2orize-pkce": "0.1.2",
|
||||
"@types/pg": "8.11.6",
|
||||
"@types/pg": "8.11.10",
|
||||
"@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.11.0",
|
||||
"@types/sanitize-html": "2.13.0",
|
||||
"@types/semver": "7.5.8",
|
||||
"@types/simple-oauth2": "5.0.7",
|
||||
"@types/sinonjs__fake-timers": "8.1.5",
|
||||
@@ -225,17 +226,17 @@
|
||||
"@types/tmp": "0.2.6",
|
||||
"@types/vary": "1.1.3",
|
||||
"@types/web-push": "3.6.3",
|
||||
"@types/ws": "8.5.11",
|
||||
"@types/ws": "8.5.12",
|
||||
"@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.29.1",
|
||||
"execa": "9.3.0",
|
||||
"eslint-plugin-import": "2.30.0",
|
||||
"execa": "9.4.0",
|
||||
"fkill": "9.0.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-mock": "29.7.0",
|
||||
"nodemon": "3.1.4",
|
||||
"nodemon": "3.1.7",
|
||||
"pid-port": "1.0.0",
|
||||
"simple-oauth2": "5.1.0"
|
||||
}
|
||||
|
@@ -13,6 +13,8 @@ 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,
|
||||
@@ -78,11 +80,76 @@ 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, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines],
|
||||
exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, RepositoryModule],
|
||||
providers: [$config, $db, $meta, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions],
|
||||
exports: [$config, $db, $meta, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions, RepositoryModule],
|
||||
})
|
||||
export class GlobalModule implements OnApplicationShutdown {
|
||||
constructor(
|
||||
@@ -91,6 +158,7 @@ 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> {
|
||||
@@ -103,6 +171,7 @@ export class GlobalModule implements OnApplicationShutdown {
|
||||
this.redisForPub.disconnect(),
|
||||
this.redisForSub.disconnect(),
|
||||
this.redisForTimelines.disconnect(),
|
||||
this.redisForReactions.disconnect(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@@ -49,6 +49,7 @@ type Source = {
|
||||
redisForPubsub?: RedisOptionsSource;
|
||||
redisForJobQueue?: RedisOptionsSource;
|
||||
redisForTimelines?: RedisOptionsSource;
|
||||
redisForReactions?: RedisOptionsSource;
|
||||
meilisearch?: {
|
||||
host: string;
|
||||
port: string;
|
||||
@@ -133,7 +134,7 @@ export type Config = {
|
||||
proxySmtp: string | undefined;
|
||||
proxyBypassHosts: string[] | undefined;
|
||||
allowedPrivateNetworks: string[] | undefined;
|
||||
maxFileSize: number | undefined;
|
||||
maxFileSize: number;
|
||||
clusterLimit: number | undefined;
|
||||
id: string;
|
||||
outgoingAddress: string | undefined;
|
||||
@@ -160,8 +161,10 @@ export type Config = {
|
||||
authUrl: string;
|
||||
driveUrl: string;
|
||||
userAgent: string;
|
||||
clientEntry: string;
|
||||
clientManifestExists: boolean;
|
||||
frontendEntry: string;
|
||||
frontendManifestExists: boolean;
|
||||
frontendEmbedEntry: string;
|
||||
frontendEmbedManifestExists: boolean;
|
||||
mediaProxy: string;
|
||||
externalMediaProxyEnabled: boolean;
|
||||
videoThumbnailGenerator: string | null;
|
||||
@@ -169,6 +172,7 @@ 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;
|
||||
@@ -196,10 +200,16 @@ const path = process.env.MISSKEY_CONFIG_YML
|
||||
|
||||
export function loadConfig(): Config {
|
||||
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.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'))
|
||||
|
||||
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'))
|
||||
: { '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 ?? '');
|
||||
@@ -243,6 +253,7 @@ 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,
|
||||
@@ -250,7 +261,7 @@ export function loadConfig(): Config {
|
||||
proxySmtp: config.proxySmtp,
|
||||
proxyBypassHosts: config.proxyBypassHosts,
|
||||
allowedPrivateNetworks: config.allowedPrivateNetworks,
|
||||
maxFileSize: config.maxFileSize,
|
||||
maxFileSize: config.maxFileSize ?? 262144000,
|
||||
clusterLimit: config.clusterLimit,
|
||||
outgoingAddress: config.outgoingAddress,
|
||||
outgoingAddressFamily: config.outgoingAddressFamily,
|
||||
@@ -270,8 +281,10 @@ export function loadConfig(): Config {
|
||||
config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator
|
||||
: null,
|
||||
userAgent: `Misskey/${version} (${config.url})`,
|
||||
clientEntry: clientManifest['src/_boot_.ts'],
|
||||
clientManifestExists: clientManifestExists,
|
||||
frontendEntry: frontendManifest['src/_boot_.ts'],
|
||||
frontendManifestExists: frontendManifestExists,
|
||||
frontendEmbedEntry: frontendEmbedManifest['src/boot.ts'],
|
||||
frontendEmbedManifestExists: frontendEmbedManifestExists,
|
||||
perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000,
|
||||
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500,
|
||||
deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
|
||||
|
@@ -8,6 +8,8 @@ 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,15 +27,19 @@ 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,
|
||||
) {
|
||||
@@ -93,10 +97,8 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
|
||||
.filter(x => x != null),
|
||||
);
|
||||
|
||||
// 送信先の鮮度を保つため、毎回取得する
|
||||
const meta = await this.metaService.fetch(true);
|
||||
recipientEMailAddresses.push(
|
||||
...(meta.email ? [meta.email] : []),
|
||||
...(this.meta.email ? [this.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, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MiMeta, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { RelationshipJobData, ThinUser } from '@/queue/types.js';
|
||||
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
@@ -22,13 +22,15 @@ 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,
|
||||
|
||||
@@ -57,7 +59,6 @@ export class AccountMoveService {
|
||||
private perUserFollowingChart: PerUserFollowingChart,
|
||||
private federatedInstanceService: FederatedInstanceService,
|
||||
private instanceChart: InstanceChart,
|
||||
private metaService: MetaService,
|
||||
private relayService: RelayService,
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
@@ -276,7 +277,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 ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowers(i.host, false);
|
||||
}
|
||||
});
|
||||
|
@@ -123,11 +123,14 @@ export class AntennaService implements OnApplicationShutdown {
|
||||
if (antenna.src === 'home') {
|
||||
// TODO
|
||||
} else if (antenna.src === 'list') {
|
||||
const listUsers = (await this.userListMembershipsRepository.findBy({
|
||||
userListId: antenna.userListId!,
|
||||
})).map(x => x.userId);
|
||||
|
||||
if (!listUsers.includes(note.userId)) return false;
|
||||
if (antenna.userListId == null) return false;
|
||||
const exists = await this.userListMembershipsRepository.exists({
|
||||
where: {
|
||||
userListId: antenna.userListId,
|
||||
userId: note.userId,
|
||||
},
|
||||
});
|
||||
if (!exists) 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);
|
||||
this.cache = new MemorySingleCache<MiAvatarDecoration[]>(1000 * 60 * 30); // 30s
|
||||
|
||||
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>(Infinity);
|
||||
this.localUserByNativeTokenCache = new MemoryKVCache<MiLocalUser | null>(Infinity);
|
||||
this.localUserByIdCache = new MemoryKVCache<MiLocalUser>(Infinity);
|
||||
this.uriPersonCache = new MemoryKVCache<MiUser | null>(Infinity);
|
||||
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.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.cache.entries()) {
|
||||
for (const [k, v] of this.uriPersonCache.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.cache.entries()) {
|
||||
for (const [k, v] of this.uriPersonCache.entries) {
|
||||
if (v.value?.id === user.id) {
|
||||
this.uriPersonCache.set(k, user);
|
||||
}
|
||||
|
@@ -13,6 +13,7 @@ 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';
|
||||
@@ -49,6 +50,7 @@ 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';
|
||||
@@ -192,6 +194,7 @@ 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 };
|
||||
@@ -211,6 +214,7 @@ 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 };
|
||||
@@ -340,6 +344,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
PushNotificationService,
|
||||
QueryService,
|
||||
ReactionService,
|
||||
ReactionsBufferingService,
|
||||
RelayService,
|
||||
RoleService,
|
||||
S3Service,
|
||||
@@ -359,6 +364,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
VideoProcessingService,
|
||||
UserWebhookService,
|
||||
SystemWebhookService,
|
||||
WebhookTestService,
|
||||
UtilityService,
|
||||
FileInfoService,
|
||||
SearchService,
|
||||
@@ -484,6 +490,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$PushNotificationService,
|
||||
$QueryService,
|
||||
$ReactionService,
|
||||
$ReactionsBufferingService,
|
||||
$RelayService,
|
||||
$RoleService,
|
||||
$S3Service,
|
||||
@@ -503,6 +510,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$VideoProcessingService,
|
||||
$UserWebhookService,
|
||||
$SystemWebhookService,
|
||||
$WebhookTestService,
|
||||
$UtilityService,
|
||||
$FileInfoService,
|
||||
$SearchService,
|
||||
@@ -629,6 +637,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
PushNotificationService,
|
||||
QueryService,
|
||||
ReactionService,
|
||||
ReactionsBufferingService,
|
||||
RelayService,
|
||||
RoleService,
|
||||
S3Service,
|
||||
@@ -648,6 +657,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
VideoProcessingService,
|
||||
UserWebhookService,
|
||||
SystemWebhookService,
|
||||
WebhookTestService,
|
||||
UtilityService,
|
||||
FileInfoService,
|
||||
SearchService,
|
||||
@@ -772,6 +782,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$PushNotificationService,
|
||||
$QueryService,
|
||||
$ReactionService,
|
||||
$ReactionsBufferingService,
|
||||
$RelayService,
|
||||
$RoleService,
|
||||
$S3Service,
|
||||
@@ -791,6 +802,7 @@ 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 cache: MemoryKVCache<MiEmoji | null>;
|
||||
private emojisCache: 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.cache = new MemoryKVCache<MiEmoji | null>(1000 * 60 * 60 * 12);
|
||||
this.emojisCache = new MemoryKVCache<MiEmoji | null>(1000 * 60 * 60 * 12); // 12h
|
||||
|
||||
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.cache.fetch(`${name} ${host}`, queryOrNull);
|
||||
const emoji = await this.emojisCache.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.cache.get(`${emoji.name} ${emoji.host}`) == null);
|
||||
const notCachedEmojis = emojis.filter(emoji => this.emojisCache.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.cache.set(`${emoji.name} ${emoji.host}`, emoji);
|
||||
this.emojisCache.set(`${emoji.name} ${emoji.host}`, emoji);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,7 +401,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.cache.dispose();
|
||||
this.emojisCache.dispose();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@@ -4,12 +4,15 @@
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UsersRepository } from '@/models/_.js';
|
||||
import { Not, IsNull } from 'typeorm';
|
||||
import type { FollowingsRepository, MiUser, UsersRepository } from '@/models/_.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { UserSuspendService } from '@/core/UserSuspendService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteAccountService {
|
||||
@@ -17,9 +20,14 @@ export class DeleteAccountService {
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
private userSuspendService: UserSuspendService,
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private apRendererService: ApRendererService,
|
||||
private queueService: QueueService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -27,16 +35,52 @@ export class DeleteAccountService {
|
||||
public async deleteAccount(user: {
|
||||
id: string;
|
||||
host: string | null;
|
||||
}): Promise<void> {
|
||||
}, moderator?: MiUser): Promise<void> {
|
||||
const _user = await this.usersRepository.findOneByOrFail({ id: user.id });
|
||||
if (_user.isRoot) throw new Error('cannot delete a root account');
|
||||
|
||||
// 物理削除する前にDelete activityを送信する
|
||||
await this.userSuspendService.doPostSuspend(user).catch(e => {});
|
||||
if (moderator != null) {
|
||||
this.moderationLogService.log(moderator, 'deleteAccount', {
|
||||
userId: user.id,
|
||||
userUsername: _user.username,
|
||||
userHost: user.host,
|
||||
});
|
||||
}
|
||||
|
||||
this.queueService.createDeleteAccountJob(user, {
|
||||
soft: false,
|
||||
});
|
||||
// 物理削除する前にDelete activityを送信する
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
// 知り得る全SharedInboxにDelete配信
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user));
|
||||
|
||||
const queue: string[] = [];
|
||||
|
||||
const followings = await this.followingsRepository.find({
|
||||
where: [
|
||||
{ followerSharedInbox: Not(IsNull()) },
|
||||
{ followeeSharedInbox: Not(IsNull()) },
|
||||
],
|
||||
select: ['followerSharedInbox', 'followeeSharedInbox'],
|
||||
});
|
||||
|
||||
const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox);
|
||||
|
||||
for (const inbox of inboxes) {
|
||||
if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
|
||||
}
|
||||
|
||||
for (const inbox of queue) {
|
||||
this.queueService.deliver(user, content, inbox, true);
|
||||
}
|
||||
|
||||
this.queueService.createDeleteAccountJob(user, {
|
||||
soft: false,
|
||||
});
|
||||
} else {
|
||||
// リモートユーザーの削除は、完全にDBから物理削除してしまうと再度連合してきてアカウントが復活する可能性があるため、soft指定する
|
||||
this.queueService.createDeleteAccountJob(user, {
|
||||
soft: true,
|
||||
});
|
||||
}
|
||||
|
||||
await this.usersRepository.update(user.id, {
|
||||
isDeleted: true,
|
||||
|
@@ -42,7 +42,7 @@ export class DownloadService {
|
||||
|
||||
const timeout = 30 * 1000;
|
||||
const operationTimeout = 60 * 1000;
|
||||
const maxSize = this.config.maxFileSize ?? 262144000;
|
||||
const maxSize = this.config.maxFileSize;
|
||||
|
||||
const urlObj = new URL(url);
|
||||
let filename = urlObj.pathname.split('/').pop() ?? 'untitled';
|
||||
|
@@ -11,11 +11,10 @@ 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 } from '@/models/_.js';
|
||||
import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository, MiMeta } 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';
|
||||
@@ -43,6 +42,7 @@ import { RoleService } from '@/core/RoleService.js';
|
||||
import { correctFilename } from '@/misc/correct-filename.js';
|
||||
import { isMimeImage } from '@/misc/is-mime-image.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
|
||||
type AddFileArgs = {
|
||||
/** User who wish to add file */
|
||||
@@ -98,6 +98,9 @@ export class DriveService {
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@@ -114,7 +117,6 @@ export class DriveService {
|
||||
private userEntityService: UserEntityService,
|
||||
private driveFileEntityService: DriveFileEntityService,
|
||||
private idService: IdService,
|
||||
private metaService: MetaService,
|
||||
private downloadService: DownloadService,
|
||||
private internalStorageService: InternalStorageService,
|
||||
private s3Service: S3Service,
|
||||
@@ -127,6 +129,7 @@ export class DriveService {
|
||||
private driveChart: DriveChart,
|
||||
private perUserDriveChart: PerUserDriveChart,
|
||||
private instanceChart: InstanceChart,
|
||||
private utilityService: UtilityService,
|
||||
) {
|
||||
const logger = new Logger('drive', 'blue');
|
||||
this.registerLogger = logger.createSubLogger('register', 'yellow');
|
||||
@@ -147,9 +150,7 @@ export class DriveService {
|
||||
// thunbnail, webpublic を必要なら生成
|
||||
const alts = await this.generateAlts(path, type, !file.uri);
|
||||
|
||||
const meta = await this.metaService.fetch();
|
||||
|
||||
if (meta.useObjectStorage) {
|
||||
if (this.meta.useObjectStorage) {
|
||||
//#region ObjectStorage params
|
||||
let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) ?? ['']);
|
||||
|
||||
@@ -168,11 +169,11 @@ export class DriveService {
|
||||
ext = '';
|
||||
}
|
||||
|
||||
const baseUrl = meta.objectStorageBaseUrl
|
||||
?? `${ meta.objectStorageUseSSL ? 'https' : 'http' }://${ meta.objectStorageEndpoint }${ meta.objectStoragePort ? `:${meta.objectStoragePort}` : '' }/${ meta.objectStorageBucket }`;
|
||||
const baseUrl = this.meta.objectStorageBaseUrl
|
||||
?? `${ this.meta.objectStorageUseSSL ? 'https' : 'http' }://${ this.meta.objectStorageEndpoint }${ this.meta.objectStoragePort ? `:${this.meta.objectStoragePort}` : '' }/${ this.meta.objectStorageBucket }`;
|
||||
|
||||
// for original
|
||||
const key = `${meta.objectStoragePrefix}/${randomUUID()}${ext}`;
|
||||
const key = `${this.meta.objectStoragePrefix}/${randomUUID()}${ext}`;
|
||||
const url = `${ baseUrl }/${ key }`;
|
||||
|
||||
// for alts
|
||||
@@ -189,7 +190,7 @@ export class DriveService {
|
||||
];
|
||||
|
||||
if (alts.webpublic) {
|
||||
webpublicKey = `${meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`;
|
||||
webpublicKey = `${this.meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`;
|
||||
webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
|
||||
|
||||
this.registerLogger.info(`uploading webpublic: ${webpublicKey}`);
|
||||
@@ -197,7 +198,7 @@ export class DriveService {
|
||||
}
|
||||
|
||||
if (alts.thumbnail) {
|
||||
thumbnailKey = `${meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`;
|
||||
thumbnailKey = `${this.meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`;
|
||||
thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
|
||||
|
||||
this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`);
|
||||
@@ -374,10 +375,8 @@ 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: meta.objectStorageBucket,
|
||||
Bucket: this.meta.objectStorageBucket,
|
||||
Key: key,
|
||||
Body: stream,
|
||||
ContentType: type,
|
||||
@@ -390,9 +389,9 @@ export class DriveService {
|
||||
// 許可されているファイル形式でしか拡張子をつけない
|
||||
ext ? correctFilename(filename, ext) : filename,
|
||||
);
|
||||
if (meta.objectStorageSetPublicRead) params.ACL = 'public-read';
|
||||
if (this.meta.objectStorageSetPublicRead) params.ACL = 'public-read';
|
||||
|
||||
await this.s3Service.upload(meta, params)
|
||||
await this.s3Service.upload(this.meta, params)
|
||||
.then(
|
||||
result => {
|
||||
if ('Bucket' in result) { // CompleteMultipartUploadCommandOutput
|
||||
@@ -458,32 +457,31 @@ 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 (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;
|
||||
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;
|
||||
|
||||
const info = await this.fileInfoService.getFileInfo(path, {
|
||||
skipSensitiveDetection: skipNsfwCheck,
|
||||
sensitiveThreshold: // 感度が高いほどしきい値は低くすることになる
|
||||
instance.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 :
|
||||
instance.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 :
|
||||
instance.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 :
|
||||
instance.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 :
|
||||
this.meta.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 :
|
||||
this.meta.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 :
|
||||
this.meta.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 :
|
||||
this.meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 :
|
||||
0.5,
|
||||
sensitiveThresholdForPorn: 0.75,
|
||||
enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos,
|
||||
enableSensitiveMediaDetectionForVideos: this.meta.enableSensitiveMediaDetectionForVideos,
|
||||
});
|
||||
this.registerLogger.info(`${JSON.stringify(info)}`);
|
||||
|
||||
// 現状 false positive が多すぎて実用に耐えない
|
||||
//if (info.porn && instance.disallowUploadWhenPredictedAsPorn) {
|
||||
//if (info.porn && this.meta.disallowUploadWhenPredictedAsPorn) {
|
||||
// throw new IdentifiableError('282f77bf-5816-4f72-9264-aa14d8261a21', 'Detected as porn.');
|
||||
//}
|
||||
|
||||
@@ -587,8 +585,9 @@ export class DriveService {
|
||||
sensitive ?? false
|
||||
: false;
|
||||
|
||||
if (user && this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) file.isSensitive = true;
|
||||
if (info.sensitive && profile!.autoSensitive) file.isSensitive = true;
|
||||
if (info.sensitive && instance.setSensitiveFlagAutomatically) file.isSensitive = true;
|
||||
if (info.sensitive && this.meta.setSensitiveFlagAutomatically) file.isSensitive = true;
|
||||
if (userRoleNSFW) file.isSensitive = true;
|
||||
|
||||
if (url !== null) {
|
||||
@@ -649,7 +648,7 @@ export class DriveService {
|
||||
// ローカルユーザーのみ
|
||||
this.perUserDriveChart.update(file, true);
|
||||
} else {
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateDrive(file, true);
|
||||
}
|
||||
}
|
||||
@@ -795,7 +794,7 @@ export class DriveService {
|
||||
// ローカルユーザーのみ
|
||||
this.perUserDriveChart.update(file, false);
|
||||
} else {
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateDrive(file, false);
|
||||
}
|
||||
}
|
||||
@@ -817,14 +816,13 @@ export class DriveService {
|
||||
|
||||
@bindThis
|
||||
public async deleteObjectStorageFile(key: string) {
|
||||
const meta = await this.metaService.fetch();
|
||||
try {
|
||||
const param = {
|
||||
Bucket: meta.objectStorageBucket,
|
||||
Bucket: this.meta.objectStorageBucket,
|
||||
Key: key,
|
||||
} as DeleteObjectCommandInput;
|
||||
|
||||
await this.s3Service.delete(meta, param);
|
||||
await this.s3Service.delete(this.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,18 +5,17 @@
|
||||
|
||||
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 { UserProfilesRepository } from '@/models/_.js';
|
||||
import type { MiMeta, 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 {
|
||||
@@ -26,49 +25,41 @@ 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) {
|
||||
const meta = await this.metaService.fetch(true);
|
||||
|
||||
if (!meta.enableEmail) return;
|
||||
if (!this.meta.enableEmail) return;
|
||||
|
||||
const iconUrl = `${this.config.url}/static-assets/mi-white.png`;
|
||||
const emailSettingUrl = `${this.config.url}/settings/email`;
|
||||
|
||||
const enableAuth = meta.smtpUser != null && meta.smtpUser !== '';
|
||||
const enableAuth = this.meta.smtpUser != null && this.meta.smtpUser !== '';
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: meta.smtpHost,
|
||||
port: meta.smtpPort,
|
||||
secure: meta.smtpSecure,
|
||||
host: this.meta.smtpHost,
|
||||
port: this.meta.smtpPort,
|
||||
secure: this.meta.smtpSecure,
|
||||
ignoreTLS: !enableAuth,
|
||||
proxy: this.config.proxySmtp,
|
||||
auth: enableAuth ? {
|
||||
user: meta.smtpUser,
|
||||
pass: meta.smtpPass,
|
||||
user: this.meta.smtpUser,
|
||||
pass: this.meta.smtpPass,
|
||||
} : undefined,
|
||||
} as any);
|
||||
|
||||
try {
|
||||
// TODO: htmlサニタイズ
|
||||
const info = await transporter.sendMail({
|
||||
from: meta.email!,
|
||||
to: to,
|
||||
subject: subject,
|
||||
text: text,
|
||||
html: `<!doctype html>
|
||||
const htmlContent = `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
@@ -133,7 +124,7 @@ export class EmailService {
|
||||
<body>
|
||||
<main>
|
||||
<header>
|
||||
<img src="${ meta.logoImageUrl ?? meta.iconUrl ?? iconUrl }"/>
|
||||
<img src="${ this.meta.logoImageUrl ?? this.meta.iconUrl ?? iconUrl }"/>
|
||||
</header>
|
||||
<article>
|
||||
<h1>${ subject }</h1>
|
||||
@@ -147,7 +138,18 @@ export class EmailService {
|
||||
<a href="${ this.config.url }">${ this.config.host }</a>
|
||||
</nav>
|
||||
</body>
|
||||
</html>`,
|
||||
</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,
|
||||
});
|
||||
|
||||
this.logger.info(`Message sent: ${info.messageId}`);
|
||||
@@ -162,8 +164,6 @@ 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 (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);
|
||||
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);
|
||||
} else {
|
||||
validated = await validateEmail({
|
||||
email: emailAddress,
|
||||
@@ -215,7 +215,7 @@ export class EmailService {
|
||||
}
|
||||
|
||||
const emailDomain: string = emailAddress.split('@')[1];
|
||||
const isBanned = this.utilityService.isBlockedHost(meta.bannedEmailDomains, emailDomain);
|
||||
const isBanned = this.utilityService.isBlockedHost(this.meta.bannedEmailDomains, emailDomain);
|
||||
|
||||
if (isBanned) {
|
||||
return {
|
||||
|
@@ -15,7 +15,7 @@ import isSvg from 'is-svg';
|
||||
import probeImageSize from 'probe-image-size';
|
||||
import { type predictionType } from 'nsfwjs';
|
||||
import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
|
||||
import { encode } from 'blurhash';
|
||||
import * as blurhash from 'blurhash';
|
||||
import { createTempDir } from '@/misc/create-temp.js';
|
||||
import { AiService } from '@/core/AiService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
@@ -452,7 +452,7 @@ export class FileInfoService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate average color of image
|
||||
* Calculate blurhash string of image
|
||||
*/
|
||||
@bindThis
|
||||
private getBlurhash(path: string, type: string): Promise<string> {
|
||||
@@ -467,7 +467,7 @@ export class FileInfoService {
|
||||
let hash;
|
||||
|
||||
try {
|
||||
hash = encode(new Uint8ClampedArray(buffer), info.width, info.height, 5, 5);
|
||||
hash = blurhash.encode(new Uint8ClampedArray(buffer), info.width, info.height, 5, 5);
|
||||
} catch (e) {
|
||||
return reject(e);
|
||||
}
|
||||
|
@@ -10,16 +10,18 @@ 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 } from '@/models/_.js';
|
||||
import type { HashtagsRepository, MiMeta } 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サーバーを設定できるようにする
|
||||
|
||||
@@ -29,7 +31,6 @@ export class HashtagService {
|
||||
private userEntityService: UserEntityService,
|
||||
private featuredService: FeaturedService,
|
||||
private idService: IdService,
|
||||
private metaService: MetaService,
|
||||
private utilityService: UtilityService,
|
||||
) {
|
||||
}
|
||||
@@ -160,10 +161,9 @@ export class HashtagService {
|
||||
|
||||
@bindThis
|
||||
public async updateHashtagsRanking(hashtag: string, userId: MiUser['id']): Promise<void> {
|
||||
const instance = await this.metaService.fetch();
|
||||
const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t));
|
||||
const hiddenTags = this.meta.hiddenTags.map(t => normalizeForSearch(t));
|
||||
if (hiddenTags.includes(hashtag)) return;
|
||||
if (this.utilityService.isKeyWordIncluded(hashtag, instance.sensitiveWords)) return;
|
||||
if (this.utilityService.isKeyWordIncluded(hashtag, this.meta.sensitiveWords)) return;
|
||||
|
||||
// YYYYMMDDHHmm (10分間隔)
|
||||
const now = new Date();
|
||||
|
@@ -9,7 +9,8 @@ import type { ModerationLogsRepository } from '@/models/_.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { ModerationLogPayloads, moderationLogTypes } from '@/types.js';
|
||||
import type { ModerationLogPayloads } from '@/types.js';
|
||||
import { moderationLogTypes } from '@/types.js';
|
||||
|
||||
@Injectable()
|
||||
export class ModerationLogService {
|
||||
|
@@ -8,13 +8,12 @@ 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, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import type { MiApp } from '@/models/App.js';
|
||||
import { concat } from '@/misc/prelude/array.js';
|
||||
@@ -23,11 +22,8 @@ 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';
|
||||
@@ -51,7 +47,6 @@ 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';
|
||||
@@ -156,6 +151,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@@ -210,7 +208,6 @@ 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,
|
||||
@@ -251,10 +248,8 @@ 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 = meta.sensitiveWords;
|
||||
const sensitiveWords = this.meta.sensitiveWords;
|
||||
if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) {
|
||||
data.visibility = 'home';
|
||||
} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
|
||||
@@ -262,17 +257,17 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
}
|
||||
}
|
||||
|
||||
const hasProhibitedWords = await this.checkProhibitedWordsContain({
|
||||
const hasProhibitedWords = this.checkProhibitedWordsContain({
|
||||
cw: data.cw,
|
||||
text: data.text,
|
||||
pollChoices: data.poll?.choices,
|
||||
}, meta.prohibitedWords);
|
||||
}, this.meta.prohibitedWords);
|
||||
|
||||
if (hasProhibitedWords) {
|
||||
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
|
||||
}
|
||||
|
||||
const inSilencedInstance = this.utilityService.isSilencedHost(meta.silencedHosts, user.host);
|
||||
const inSilencedInstance = this.utilityService.isSilencedHost(this.meta.silencedHosts, user.host);
|
||||
|
||||
if (data.visibility === 'public' && inSilencedInstance && user.host !== null) {
|
||||
data.visibility = 'home';
|
||||
@@ -364,6 +359,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
mentionedUsers = data.apMentions ?? await this.extractMentionedUsers(user, combinedTokens);
|
||||
}
|
||||
|
||||
// if the host is media-silenced, custom emojis are not allowed
|
||||
if (this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) emojis = [];
|
||||
|
||||
tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32);
|
||||
|
||||
if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) {
|
||||
@@ -503,10 +501,8 @@ 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 (meta.enableChartsForRemoteUser || (user.host == null)) {
|
||||
if (note.visibility !== 'specified' && (this.meta.enableChartsForRemoteUser || (user.host == null))) {
|
||||
this.perUserNotesChart.update(user, note, true);
|
||||
}
|
||||
|
||||
@@ -514,7 +510,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 ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateNote(i.host, note, true);
|
||||
}
|
||||
});
|
||||
@@ -850,15 +846,14 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
|
||||
@bindThis
|
||||
private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) {
|
||||
const meta = await this.metaService.fetch();
|
||||
if (!meta.enableFanoutTimeline) return;
|
||||
if (!this.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 ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
|
||||
this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r);
|
||||
|
||||
const channelFollowings = await this.channelFollowingsRepository.find({
|
||||
where: {
|
||||
@@ -868,9 +863,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
});
|
||||
|
||||
for (const channelFollowing of channelFollowings) {
|
||||
this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
||||
this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax, r);
|
||||
if (note.fileIds.length > 0) {
|
||||
this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
||||
this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -908,9 +903,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
if (!following.withReplies) continue;
|
||||
}
|
||||
|
||||
this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
||||
this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax, r);
|
||||
if (note.fileIds.length > 0) {
|
||||
this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
||||
this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -927,25 +922,25 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
if (!userListMembership.withReplies) continue;
|
||||
}
|
||||
|
||||
this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r);
|
||||
this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, this.meta.perUserListTimelineCacheMax, r);
|
||||
if (note.fileIds.length > 0) {
|
||||
this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r);
|
||||
this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, this.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, meta.perUserHomeTimelineCacheMax, r);
|
||||
this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, this.meta.perUserHomeTimelineCacheMax, r);
|
||||
if (note.fileIds.length > 0) {
|
||||
this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
||||
this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 自分自身以外への返信
|
||||
if (isReply(note)) {
|
||||
this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
|
||||
this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r);
|
||||
|
||||
if (note.visibility === 'public' && note.userHost == null) {
|
||||
this.fanoutTimelineService.push('localTimelineWithReplies', note.id, 300, r);
|
||||
@@ -954,9 +949,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
|
||||
this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r);
|
||||
if (note.fileIds.length > 0) {
|
||||
this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r);
|
||||
this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax / 2 : this.meta.perRemoteUserUserTimelineCacheMax / 2, r);
|
||||
}
|
||||
|
||||
if (note.visibility === 'public' && note.userHost == null) {
|
||||
@@ -1015,9 +1010,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
}
|
||||
}
|
||||
|
||||
public async checkProhibitedWordsContain(content: Parameters<UtilityService['concatNoteContentsForKeyWordCheck']>[0], prohibitedWords?: string[]) {
|
||||
public checkProhibitedWordsContain(content: Parameters<UtilityService['concatNoteContentsForKeyWordCheck']>[0], prohibitedWords?: string[]) {
|
||||
if (prohibitedWords == null) {
|
||||
prohibitedWords = (await this.metaService.fetch()).prohibitedWords;
|
||||
prohibitedWords = this.meta.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, NotesRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { InstancesRepository, MiMeta, NotesRepository, UsersRepository } from '@/models/_.js';
|
||||
import { RelayService } from '@/core/RelayService.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
@@ -19,9 +19,7 @@ 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';
|
||||
@@ -32,6 +30,9 @@ export class NoteDeleteService {
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@@ -42,13 +43,11 @@ 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,
|
||||
@@ -92,7 +91,7 @@ export class NoteDeleteService {
|
||||
this.deliverToConcerned(user, note, content);
|
||||
}
|
||||
|
||||
// also deliever delete activity to cascaded notes
|
||||
// also deliver delete activity to cascaded notes
|
||||
const federatedLocalCascadingNotes = (cascadingNotes).filter(note => !note.localOnly && note.userHost == null); // filter out local-only notes
|
||||
for (const cascadingNote of federatedLocalCascadingNotes) {
|
||||
if (!cascadingNote.user) continue;
|
||||
@@ -102,17 +101,15 @@ export class NoteDeleteService {
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const meta = await this.metaService.fetch();
|
||||
|
||||
this.notesChart.update(note, false);
|
||||
if (meta.enableChartsForRemoteUser || (user.host == null)) {
|
||||
if (this.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 ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateNote(i.host, note, false);
|
||||
}
|
||||
});
|
||||
|
@@ -4,26 +4,25 @@
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UsersRepository } from '@/models/_.js';
|
||||
import type { MiMeta, 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> {
|
||||
const meta = await this.metaService.fetch();
|
||||
if (meta.proxyAccountId == null) return null;
|
||||
return await this.usersRepository.findOneByOrFail({ id: meta.proxyAccountId }) as MiLocalUser;
|
||||
if (this.meta.proxyAccountId == null) return null;
|
||||
return await this.usersRepository.findOneByOrFail({ id: this.meta.proxyAccountId }) as MiLocalUser;
|
||||
}
|
||||
}
|
||||
|
@@ -10,8 +10,7 @@ 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 { MiSwSubscription, SwSubscriptionsRepository } from '@/models/_.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import type { MiMeta, MiSwSubscription, SwSubscriptionsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RedisKVCache } from '@/misc/cache.js';
|
||||
|
||||
@@ -54,13 +53,14 @@ 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,14 +73,12 @@ export class PushNotificationService implements OnApplicationShutdown {
|
||||
|
||||
@bindThis
|
||||
public async pushNotification<T extends keyof PushNotificationsTypes>(userId: string, type: T, body: PushNotificationsTypes[T]) {
|
||||
const meta = await this.metaService.fetch();
|
||||
|
||||
if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return;
|
||||
if (!this.meta.enableServiceWorker || this.meta.swPublicKey == null || this.meta.swPrivateKey == null) return;
|
||||
|
||||
// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録
|
||||
push.setVapidDetails(this.config.url,
|
||||
meta.swPublicKey,
|
||||
meta.swPrivateKey);
|
||||
this.meta.swPublicKey,
|
||||
this.meta.swPrivateKey);
|
||||
|
||||
const subscriptions = await this.subscriptionsCache.fetch(userId);
|
||||
|
||||
|
@@ -87,6 +87,12 @@ export class QueueService {
|
||||
repeat: { pattern: '*/5 * * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
this.systemQueue.add('bakeBufferedReactions', {
|
||||
}, {
|
||||
repeat: { pattern: '0 0 * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -452,10 +458,15 @@ export class QueueService {
|
||||
|
||||
/**
|
||||
* @see UserWebhookDeliverJobData
|
||||
* @see WebhookDeliverProcessorService
|
||||
* @see UserWebhookDeliverProcessorService
|
||||
*/
|
||||
@bindThis
|
||||
public userWebhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
|
||||
public userWebhookDeliver(
|
||||
webhook: MiWebhook,
|
||||
type: typeof webhookEventTypes[number],
|
||||
content: unknown,
|
||||
opts?: { attempts?: number },
|
||||
) {
|
||||
const data: UserWebhookDeliverJobData = {
|
||||
type,
|
||||
content,
|
||||
@@ -468,7 +479,7 @@ export class QueueService {
|
||||
};
|
||||
|
||||
return this.userWebhookDeliverQueue.add(webhook.id, data, {
|
||||
attempts: 4,
|
||||
attempts: opts?.attempts ?? 4,
|
||||
backoff: {
|
||||
type: 'custom',
|
||||
},
|
||||
@@ -479,10 +490,15 @@ export class QueueService {
|
||||
|
||||
/**
|
||||
* @see SystemWebhookDeliverJobData
|
||||
* @see WebhookDeliverProcessorService
|
||||
* @see SystemWebhookDeliverProcessorService
|
||||
*/
|
||||
@bindThis
|
||||
public systemWebhookDeliver(webhook: MiSystemWebhook, type: SystemWebhookEventType, content: unknown) {
|
||||
public systemWebhookDeliver(
|
||||
webhook: MiSystemWebhook,
|
||||
type: SystemWebhookEventType,
|
||||
content: unknown,
|
||||
opts?: { attempts?: number },
|
||||
) {
|
||||
const data: SystemWebhookDeliverJobData = {
|
||||
type,
|
||||
content,
|
||||
@@ -494,7 +510,7 @@ export class QueueService {
|
||||
};
|
||||
|
||||
return this.systemWebhookDeliverQueue.add(webhook.id, data, {
|
||||
attempts: 4,
|
||||
attempts: opts?.attempts ?? 4,
|
||||
backoff: {
|
||||
type: 'custom',
|
||||
},
|
||||
|
@@ -4,9 +4,8 @@
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/_.js';
|
||||
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository, MiMeta } 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';
|
||||
@@ -21,7 +20,6 @@ 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';
|
||||
@@ -30,9 +28,10 @@ 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': '👍',
|
||||
@@ -71,8 +70,8 @@ const decodeCustomEmojiRegexp = /^:([\w+-]+)(?:@([\w.-]+))?:$/;
|
||||
@Injectable()
|
||||
export class ReactionService {
|
||||
constructor(
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
@@ -87,12 +86,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,
|
||||
@@ -148,6 +147,11 @@ export class ReactionService {
|
||||
if ((note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && emoji.isSensitive) {
|
||||
reaction = FALLBACK;
|
||||
}
|
||||
|
||||
// for media silenced host, custom emoji reactions are not allowed
|
||||
if (reacterHost != null && this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, reacterHost)) {
|
||||
reaction = FALLBACK;
|
||||
}
|
||||
} else {
|
||||
// リアクションとして使う権限がない
|
||||
reaction = FALLBACK;
|
||||
@@ -167,7 +171,6 @@ export class ReactionService {
|
||||
reaction,
|
||||
};
|
||||
|
||||
// Create reaction
|
||||
try {
|
||||
await this.noteReactionsRepository.insert(record);
|
||||
} catch (e) {
|
||||
@@ -191,16 +194,20 @@ export class ReactionService {
|
||||
}
|
||||
|
||||
// Increment reactions count
|
||||
const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;
|
||||
await this.notesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
reactions: () => sql,
|
||||
...(note.reactionAndUserPairCache.length < PER_NOTE_REACTION_USER_PAIR_CACHE_MAX ? {
|
||||
reactionAndUserPairCache: () => `array_append("reactionAndUserPairCache", '${user.id}/${reaction}')`,
|
||||
} : {}),
|
||||
})
|
||||
.where('id = :id', { id: note.id })
|
||||
.execute();
|
||||
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({
|
||||
reactions: () => sql,
|
||||
...(note.reactionAndUserPairCache.length < PER_NOTE_REACTION_USER_PAIR_CACHE_MAX ? {
|
||||
reactionAndUserPairCache: () => `array_append("reactionAndUserPairCache", '${user.id}/${reaction}')`,
|
||||
} : {}),
|
||||
})
|
||||
.where('id = :id', { id: note.id })
|
||||
.execute();
|
||||
}
|
||||
|
||||
// 30%の確率、セルフではない、3日以内に投稿されたノートの場合ハイライト用ランキング更新
|
||||
if (
|
||||
@@ -220,9 +227,7 @@ export class ReactionService {
|
||||
}
|
||||
}
|
||||
|
||||
const meta = await this.metaService.fetch();
|
||||
|
||||
if (meta.enableChartsForRemoteUser || (user.host == null)) {
|
||||
if (this.meta.enableChartsForRemoteUser || (user.host == null)) {
|
||||
this.perUserReactionsChart.update(user, note);
|
||||
}
|
||||
|
||||
@@ -300,14 +305,18 @@ export class ReactionService {
|
||||
}
|
||||
|
||||
// Decrement reactions count
|
||||
const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`;
|
||||
await this.notesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
reactions: () => sql,
|
||||
reactionAndUserPairCache: () => `array_remove("reactionAndUserPairCache", '${user.id}/${exist.reaction}')`,
|
||||
})
|
||||
.where('id = :id', { id: note.id })
|
||||
.execute();
|
||||
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({
|
||||
reactions: () => sql,
|
||||
reactionAndUserPairCache: () => `array_remove("reactionAndUserPairCache", '${user.id}/${exist.reaction}')`,
|
||||
})
|
||||
.where('id = :id', { id: note.id })
|
||||
.execute();
|
||||
}
|
||||
|
||||
this.globalEventService.publishNoteStream(note.id, 'unreacted', {
|
||||
reaction: this.decodeReaction(exist.reaction).reaction,
|
||||
@@ -328,6 +337,7 @@ export class ReactionService {
|
||||
//#endregion
|
||||
}
|
||||
|
||||
// TODO: 廃止
|
||||
/**
|
||||
* 文字列タイプのレガシーな形式のリアクションを現在の形式に変換しつつ、
|
||||
* データベース上には存在する「0個のリアクションがついている」という情報を削除する。
|
||||
|
162
packages/backend/src/core/ReactionsBufferingService.ts
Normal file
162
packages/backend/src/core/ReactionsBufferingService.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* 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);
|
||||
this.relaysCache = new MemorySingleCache<MiRelay[]>(1000 * 60 * 10); // 10m
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@@ -6,6 +6,7 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { reversiUpdateKeys } from 'misskey-js';
|
||||
import * as Reversi from 'misskey-reversi';
|
||||
import { IsNull, LessThan, MoreThan } from 'typeorm';
|
||||
import type {
|
||||
@@ -399,7 +400,33 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async updateSettings(gameId: MiReversiGame['id'], user: MiUser, key: string, value: any) {
|
||||
public isValidReversiUpdateKey(key: unknown): key is typeof reversiUpdateKeys[number] {
|
||||
if (typeof key !== 'string') return false;
|
||||
return (reversiUpdateKeys as string[]).includes(key);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public isValidReversiUpdateValue<K extends typeof reversiUpdateKeys[number]>(key: K, value: unknown): value is MiReversiGame[K] {
|
||||
switch (key) {
|
||||
case 'map':
|
||||
return Array.isArray(value) && value.every(row => typeof row === 'string');
|
||||
case 'bw':
|
||||
return typeof value === 'string' && ['random', '1', '2'].includes(value);
|
||||
case 'isLlotheo':
|
||||
return typeof value === 'boolean';
|
||||
case 'canPutEverywhere':
|
||||
return typeof value === 'boolean';
|
||||
case 'loopedBoard':
|
||||
return typeof value === 'boolean';
|
||||
case 'timeLimitForEachTurn':
|
||||
return typeof value === 'number' && value >= 0;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async updateSettings<K extends typeof reversiUpdateKeys[number]>(gameId: MiReversiGame['id'], user: MiUser, key: K, value: MiReversiGame[K]) {
|
||||
const game = await this.get(gameId);
|
||||
if (game == null) throw new Error('game not found');
|
||||
if (game.isStarted) return;
|
||||
@@ -407,10 +434,6 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||
if ((game.user1Id === user.id) && game.user1Ready) return;
|
||||
if ((game.user2Id === user.id) && game.user2Ready) return;
|
||||
|
||||
if (!['map', 'bw', 'isLlotheo', 'canPutEverywhere', 'loopedBoard', 'timeLimitForEachTurn'].includes(key)) return;
|
||||
|
||||
// TODO: より厳格なバリデーション
|
||||
|
||||
const updatedGame = {
|
||||
...game,
|
||||
[key]: value,
|
||||
|
@@ -8,6 +8,7 @@ import * as Redis from 'ioredis';
|
||||
import { In } from 'typeorm';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import type {
|
||||
MiMeta,
|
||||
MiRole,
|
||||
MiRoleAssignment,
|
||||
RoleAssignmentsRepository,
|
||||
@@ -18,7 +19,6 @@ 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';
|
||||
@@ -58,6 +58,11 @@ export type RolePolicies = {
|
||||
userEachUserListsLimit: number;
|
||||
rateLimitFactor: number;
|
||||
avatarDecorationLimit: number;
|
||||
canImportAntennas: boolean;
|
||||
canImportBlocking: boolean;
|
||||
canImportFollowing: boolean;
|
||||
canImportMuting: boolean;
|
||||
canImportUserLists: boolean;
|
||||
};
|
||||
|
||||
export const DEFAULT_POLICIES: RolePolicies = {
|
||||
@@ -87,6 +92,11 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||
userEachUserListsLimit: 50,
|
||||
rateLimitFactor: 1,
|
||||
avatarDecorationLimit: 1,
|
||||
canImportAntennas: true,
|
||||
canImportBlocking: true,
|
||||
canImportFollowing: true,
|
||||
canImportMuting: true,
|
||||
canImportUserLists: true,
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -101,8 +111,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.redisForTimelines)
|
||||
private redisForTimelines: Redis.Redis,
|
||||
@@ -119,7 +129,6 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
@Inject(DI.roleAssignmentsRepository)
|
||||
private roleAssignmentsRepository: RoleAssignmentsRepository,
|
||||
|
||||
private metaService: MetaService,
|
||||
private cacheService: CacheService,
|
||||
private userEntityService: UserEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
@@ -127,10 +136,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
private moderationLogService: ModerationLogService,
|
||||
private fanoutTimelineService: FanoutTimelineService,
|
||||
) {
|
||||
//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.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60); // 1h
|
||||
this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 5); // 5m
|
||||
|
||||
this.redisForSub.on('message', this.onMessage);
|
||||
}
|
||||
@@ -341,8 +348,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
|
||||
@bindThis
|
||||
public async getUserPolicies(userId: MiUser['id'] | null): Promise<RolePolicies> {
|
||||
const meta = await this.metaService.fetch();
|
||||
const basePolicies = { ...DEFAULT_POLICIES, ...meta.policies };
|
||||
const basePolicies = { ...DEFAULT_POLICIES, ...this.meta.policies };
|
||||
|
||||
if (userId == null) return basePolicies;
|
||||
|
||||
@@ -389,6 +395,11 @@ 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 { UsedUsernamesRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { MiMeta, UsedUsernamesRepository, UsersRepository } from '@/models/_.js';
|
||||
import { MiUser } from '@/models/User.js';
|
||||
import { MiUserProfile } from '@/models/UserProfile.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
@@ -20,7 +20,6 @@ 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()
|
||||
@@ -29,6 +28,9 @@ export class SignupService {
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@@ -39,7 +41,6 @@ export class SignupService {
|
||||
private userService: UserService,
|
||||
private userEntityService: UserEntityService,
|
||||
private idService: IdService,
|
||||
private metaService: MetaService,
|
||||
private instanceActorService: InstanceActorService,
|
||||
private usersChart: UsersChart,
|
||||
) {
|
||||
@@ -88,8 +89,7 @@ export class SignupService {
|
||||
const isTheFirstUser = !await this.instanceActorService.realLocalUsersPresent();
|
||||
|
||||
if (!opts.ignorePreservedUsernames && !isTheFirstUser) {
|
||||
const instance = await this.metaService.fetch(true);
|
||||
const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
|
||||
const isPreserved = this.meta.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 async fetchSystemWebhooks(params?: {
|
||||
public fetchSystemWebhooks(params?: {
|
||||
ids?: MiSystemWebhook['id'][];
|
||||
isActive?: MiSystemWebhook['isActive'];
|
||||
on?: MiSystemWebhook['on'];
|
||||
@@ -165,19 +165,24 @@ export class SystemWebhookService implements OnApplicationShutdown {
|
||||
/**
|
||||
* SystemWebhook をWebhook配送キューに追加する
|
||||
* @see QueueService.systemWebhookDeliver
|
||||
* // TODO: contentの型を厳格化する
|
||||
*/
|
||||
@bindThis
|
||||
public async enqueueSystemWebhook(webhook: MiSystemWebhook | MiSystemWebhook['id'], type: SystemWebhookEventType, content: unknown) {
|
||||
public async enqueueSystemWebhook<T extends SystemWebhookEventType>(
|
||||
webhook: MiSystemWebhook | MiSystemWebhook['id'],
|
||||
type: T,
|
||||
content: unknown,
|
||||
) {
|
||||
const webhookEntity = typeof webhook === 'string'
|
||||
? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)
|
||||
: webhook;
|
||||
if (!webhookEntity || !webhookEntity.isActive) {
|
||||
this.logger.info(`Webhook is not active or not found : ${webhook}`);
|
||||
this.logger.info(`SystemWebhook is not active or not found : ${webhook}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!webhookEntity.on.includes(type)) {
|
||||
this.logger.info(`Webhook ${webhookEntity.id} is not listening to ${type}`);
|
||||
this.logger.info(`SystemWebhook ${webhookEntity.id} is not listening to ${type}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -13,23 +13,20 @@ 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, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, MiMeta, 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';
|
||||
|
||||
@@ -58,6 +55,9 @@ export class UserFollowingService implements OnModuleInit {
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@@ -79,13 +79,11 @@ 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,
|
||||
) {
|
||||
@@ -172,7 +170,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((await this.metaService.fetch()).silencedHosts, follower.host))
|
||||
(this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower) && this.utilityService.isSilencedHost(this.meta.silencedHosts, follower.host))
|
||||
) {
|
||||
let autoAccept = false;
|
||||
|
||||
@@ -307,14 +305,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 ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
if (this.meta.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 ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowers(i.host, true);
|
||||
}
|
||||
});
|
||||
@@ -439,14 +437,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 ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
if (this.meta.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 ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
if (this.meta.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: Infinity,
|
||||
memoryCacheLifetime: 1000 * 60 * 60, // 1h
|
||||
fetcher: (key) => this.userKeypairsRepository.findOneByOrFail({ userId: key }),
|
||||
toRedisConverter: (value) => JSON.stringify(value),
|
||||
fromRedisConverter: (value) => JSON.parse(value),
|
||||
|
@@ -5,7 +5,7 @@
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Not, IsNull } from 'typeorm';
|
||||
import type { FollowingsRepository } from '@/models/_.js';
|
||||
import type { FollowingsRepository, FollowRequestsRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
@@ -13,24 +13,75 @@ import { DI } from '@/di-symbols.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RelationshipJobData } from '@/queue/types.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserSuspendService {
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
@Inject(DI.followRequestsRepository)
|
||||
private followRequestsRepository: FollowRequestsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private queueService: QueueService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private apRendererService: ApRendererService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async doPostSuspend(user: { id: MiUser['id']; host: MiUser['host'] }): Promise<void> {
|
||||
public async suspend(user: MiUser, moderator: MiUser): Promise<void> {
|
||||
await this.usersRepository.update(user.id, {
|
||||
isSuspended: true,
|
||||
});
|
||||
|
||||
this.moderationLogService.log(moderator, 'suspend', {
|
||||
userId: user.id,
|
||||
userUsername: user.username,
|
||||
userHost: user.host,
|
||||
});
|
||||
|
||||
(async () => {
|
||||
await this.postSuspend(user).catch(e => {});
|
||||
await this.unFollowAll(user).catch(e => {});
|
||||
})();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async unsuspend(user: MiUser, moderator: MiUser): Promise<void> {
|
||||
await this.usersRepository.update(user.id, {
|
||||
isSuspended: false,
|
||||
});
|
||||
|
||||
this.moderationLogService.log(moderator, 'unsuspend', {
|
||||
userId: user.id,
|
||||
userUsername: user.username,
|
||||
userHost: user.host,
|
||||
});
|
||||
|
||||
(async () => {
|
||||
await this.postUnsuspend(user).catch(e => {});
|
||||
})();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async postSuspend(user: { id: MiUser['id']; host: MiUser['host'] }): Promise<void> {
|
||||
this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true });
|
||||
|
||||
this.followRequestsRepository.delete({
|
||||
followeeId: user.id,
|
||||
});
|
||||
this.followRequestsRepository.delete({
|
||||
followerId: user.id,
|
||||
});
|
||||
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
// 知り得る全SharedInboxにDelete配信
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user));
|
||||
@@ -58,7 +109,7 @@ export class UserSuspendService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async doPostUnsuspend(user: MiUser): Promise<void> {
|
||||
private async postUnsuspend(user: MiUser): Promise<void> {
|
||||
this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false });
|
||||
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
@@ -86,4 +137,26 @@ export class UserSuspendService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async unFollowAll(follower: MiUser) {
|
||||
const followings = await this.followingsRepository.find({
|
||||
where: {
|
||||
followerId: follower.id,
|
||||
followeeId: Not(IsNull()),
|
||||
},
|
||||
});
|
||||
|
||||
const jobs: RelationshipJobData[] = [];
|
||||
for (const following of followings) {
|
||||
if (following.followeeId && following.followerId) {
|
||||
jobs.push({
|
||||
from: { id: following.followerId },
|
||||
to: { id: following.followeeId },
|
||||
silent: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
this.queueService.createUnfollowJob(jobs);
|
||||
}
|
||||
}
|
||||
|
@@ -5,8 +5,8 @@
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { WebhooksRepository } from '@/models/_.js';
|
||||
import type { MiWebhook } from '@/models/Webhook.js';
|
||||
import { type WebhooksRepository } from '@/models/_.js';
|
||||
import { MiWebhook } from '@/models/Webhook.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
@@ -38,6 +38,31 @@ 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);
|
||||
|
@@ -42,6 +42,12 @@ export class UtilityService {
|
||||
return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public isMediaSilencedHost(silencedHosts: string[] | undefined, host: string | null): boolean {
|
||||
if (!silencedHosts || host == null) return false;
|
||||
return silencedHosts.some(x => host.toLowerCase() === x);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public concatNoteContentsForKeyWordCheck(content: {
|
||||
cw?: string | null;
|
||||
|
@@ -12,10 +12,9 @@ import {
|
||||
} from '@simplewebauthn/server';
|
||||
import { AttestationFormat, isoCBOR, isoUint8Array } from '@simplewebauthn/server/helpers';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { UserSecurityKeysRepository } from '@/models/_.js';
|
||||
import type { MiMeta, 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 {
|
||||
@@ -23,7 +22,6 @@ import type {
|
||||
AuthenticatorTransportFuture,
|
||||
CredentialDeviceType,
|
||||
PublicKeyCredentialCreationOptionsJSON,
|
||||
PublicKeyCredentialDescriptorFuture,
|
||||
PublicKeyCredentialRequestOptionsJSON,
|
||||
RegistrationResponseJSON,
|
||||
} from '@simplewebauthn/types';
|
||||
@@ -31,33 +29,33 @@ import type {
|
||||
@Injectable()
|
||||
export class WebAuthnService {
|
||||
constructor(
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.userSecurityKeysRepository)
|
||||
private userSecurityKeysRepository: UserSecurityKeysRepository,
|
||||
|
||||
private metaService: MetaService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getRelyingParty(): Promise<{ origin: string; rpId: string; rpName: string; rpIcon?: string; }> {
|
||||
const instance = await this.metaService.fetch();
|
||||
public getRelyingParty(): { origin: string; rpId: string; rpName: string; rpIcon?: string; } {
|
||||
return {
|
||||
origin: this.config.url,
|
||||
rpId: this.config.hostname,
|
||||
rpName: instance.name ?? this.config.host,
|
||||
rpIcon: instance.iconUrl ?? undefined,
|
||||
rpName: this.meta.name ?? this.config.host,
|
||||
rpIcon: this.meta.iconUrl ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async initiateRegistration(userId: MiUser['id'], userName: string, userDisplayName?: string): Promise<PublicKeyCredentialCreationOptionsJSON> {
|
||||
const relyingParty = await this.getRelyingParty();
|
||||
const relyingParty = this.getRelyingParty();
|
||||
const keys = await this.userSecurityKeysRepository.findBy({
|
||||
userId: userId,
|
||||
});
|
||||
@@ -104,7 +102,7 @@ export class WebAuthnService {
|
||||
|
||||
await this.redisClient.del(`webauthn:challenge:${userId}`);
|
||||
|
||||
const relyingParty = await this.getRelyingParty();
|
||||
const relyingParty = this.getRelyingParty();
|
||||
|
||||
let verification;
|
||||
try {
|
||||
@@ -143,7 +141,7 @@ export class WebAuthnService {
|
||||
|
||||
@bindThis
|
||||
public async initiateAuthentication(userId: MiUser['id']): Promise<PublicKeyCredentialRequestOptionsJSON> {
|
||||
const relyingParty = await this.getRelyingParty();
|
||||
const relyingParty = this.getRelyingParty();
|
||||
const keys = await this.userSecurityKeysRepository.findBy({
|
||||
userId: userId,
|
||||
});
|
||||
@@ -209,7 +207,7 @@ export class WebAuthnService {
|
||||
}
|
||||
}
|
||||
|
||||
const relyingParty = await this.getRelyingParty();
|
||||
const relyingParty = this.getRelyingParty();
|
||||
|
||||
let verification;
|
||||
try {
|
||||
|
434
packages/backend/src/core/WebhookTestService.ts
Normal file
434
packages/backend/src/core/WebhookTestService.ts
Normal file
@@ -0,0 +1,434 @@
|
||||
/*
|
||||
* 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>(Infinity);
|
||||
this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey | null>(Infinity);
|
||||
this.publicKeyCache = new MemoryKVCache<MiUserPublickey | null>(1000 * 60 * 60 * 12); // 12h
|
||||
this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey | null>(1000 * 60 * 60 * 12); // 12h
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@@ -17,14 +17,13 @@ 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 } from '@/models/_.js';
|
||||
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { MiRemoteUser } from '@/models/User.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
@@ -48,6 +47,9 @@ export class ApInboxService {
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@@ -64,7 +66,6 @@ export class ApInboxService {
|
||||
private noteEntityService: NoteEntityService,
|
||||
private utilityService: UtilityService,
|
||||
private idService: IdService,
|
||||
private metaService: MetaService,
|
||||
private abuseReportService: AbuseReportService,
|
||||
private userFollowingService: UserFollowingService,
|
||||
private apAudienceService: ApAudienceService,
|
||||
@@ -290,8 +291,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
// アナウンス先をブロックしてたら中断
|
||||
const meta = await this.metaService.fetch();
|
||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) return;
|
||||
if (this.utilityService.isBlockedHost(this.meta.blockedHosts, this.utilityService.extractDbHost(uri))) return;
|
||||
|
||||
const unlock = await this.appLockService.getApLock(uri);
|
||||
|
||||
|
@@ -6,6 +6,7 @@
|
||||
import * as crypto from 'node:crypto';
|
||||
import { URL } from 'node:url';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Window } from 'happy-dom';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
@@ -180,7 +181,8 @@ export class ApRequestService {
|
||||
* @param url URL to fetch
|
||||
*/
|
||||
@bindThis
|
||||
public async signedGet(url: string, user: { id: MiUser['id'] }): Promise<unknown> {
|
||||
public async signedGet(url: string, user: { id: MiUser['id'] }, followAlternate?: boolean): Promise<unknown> {
|
||||
const _followAlternate = followAlternate ?? true;
|
||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||
|
||||
const req = ApRequestCreator.createSignedGet({
|
||||
@@ -198,9 +200,54 @@ export class ApRequestService {
|
||||
headers: req.request.headers,
|
||||
}, {
|
||||
throwErrorWhenResponseNotOk: true,
|
||||
validators: [validateContentTypeSetAsActivityPub],
|
||||
});
|
||||
|
||||
//#region リクエスト先がhtmlかつactivity+jsonへのalternate linkタグがあるとき
|
||||
const contentType = res.headers.get('content-type');
|
||||
|
||||
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 document = window.document;
|
||||
try {
|
||||
document.documentElement.innerHTML = html;
|
||||
|
||||
const alternate = document.querySelector('head > link[rel="alternate"][type="application/activity+json"]');
|
||||
if (alternate) {
|
||||
const href = alternate.getAttribute('href');
|
||||
if (href) {
|
||||
return await this.signedGet(href, user, false);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// something went wrong parsing the HTML, ignore the whole thing
|
||||
} finally {
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
validateContentTypeSetAsActivityPub(res);
|
||||
|
||||
return await res.json();
|
||||
}
|
||||
}
|
||||
|
@@ -7,9 +7,8 @@ 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 } from '@/models/_.js';
|
||||
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta } 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';
|
||||
@@ -29,6 +28,7 @@ export class Resolver {
|
||||
|
||||
constructor(
|
||||
private config: Config,
|
||||
private meta: MiMeta,
|
||||
private usersRepository: UsersRepository,
|
||||
private notesRepository: NotesRepository,
|
||||
private pollsRepository: PollsRepository,
|
||||
@@ -36,7 +36,6 @@ export class Resolver {
|
||||
private followRequestsRepository: FollowRequestsRepository,
|
||||
private utilityService: UtilityService,
|
||||
private instanceActorService: InstanceActorService,
|
||||
private metaService: MetaService,
|
||||
private apRequestService: ApRequestService,
|
||||
private httpRequestService: HttpRequestService,
|
||||
private apRendererService: ApRendererService,
|
||||
@@ -94,8 +93,7 @@ export class Resolver {
|
||||
return await this.resolveLocal(value);
|
||||
}
|
||||
|
||||
const meta = await this.metaService.fetch();
|
||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, host)) {
|
||||
if (this.utilityService.isBlockedHost(this.meta.blockedHosts, host)) {
|
||||
throw new Error('Instance is blocked');
|
||||
}
|
||||
|
||||
@@ -178,6 +176,9 @@ export class ApResolverService {
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@@ -195,7 +196,6 @@ export class ApResolverService {
|
||||
|
||||
private utilityService: UtilityService,
|
||||
private instanceActorService: InstanceActorService,
|
||||
private metaService: MetaService,
|
||||
private apRequestService: ApRequestService,
|
||||
private httpRequestService: HttpRequestService,
|
||||
private apRendererService: ApRendererService,
|
||||
@@ -208,6 +208,7 @@ export class ApResolverService {
|
||||
public createResolver(): Resolver {
|
||||
return new Resolver(
|
||||
this.config,
|
||||
this.meta,
|
||||
this.usersRepository,
|
||||
this.notesRepository,
|
||||
this.pollsRepository,
|
||||
@@ -215,7 +216,6 @@ export class ApResolverService {
|
||||
this.followRequestsRepository,
|
||||
this.utilityService,
|
||||
this.instanceActorService,
|
||||
this.metaService,
|
||||
this.apRequestService,
|
||||
this.httpRequestService,
|
||||
this.apRendererService,
|
||||
|
@@ -5,10 +5,9 @@
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { DriveFilesRepository } from '@/models/_.js';
|
||||
import type { DriveFilesRepository, MiMeta } 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';
|
||||
@@ -24,10 +23,12 @@ 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,
|
||||
@@ -63,12 +64,10 @@ 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 = instance.cacheRemoteFiles && (instance.cacheRemoteSensitiveFiles || !image.sensitive);
|
||||
const shouldBeCached = this.meta.cacheRemoteFiles && (this.meta.cacheRemoteSensitiveFiles || !image.sensitive);
|
||||
|
||||
const file = await this.driveService.uploadFromUrl({
|
||||
url: image.url,
|
||||
|
@@ -6,13 +6,12 @@
|
||||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||
import { In } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { PollsRepository, EmojisRepository } from '@/models/_.js';
|
||||
import type { PollsRepository, EmojisRepository, MiMeta } from '@/models/_.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { MiRemoteUser } from '@/models/User.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import { 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';
|
||||
@@ -46,6 +45,9 @@ export class ApNoteService {
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.pollsRepository)
|
||||
private pollsRepository: PollsRepository,
|
||||
|
||||
@@ -65,7 +67,6 @@ export class ApNoteService {
|
||||
private apMentionService: ApMentionService,
|
||||
private apImageService: ApImageService,
|
||||
private apQuestionService: ApQuestionService,
|
||||
private metaService: MetaService,
|
||||
private appLockService: AppLockService,
|
||||
private pollService: PollService,
|
||||
private noteCreateService: NoteCreateService,
|
||||
@@ -78,9 +79,10 @@ export class ApNoteService {
|
||||
@bindThis
|
||||
public validateNote(object: IObject, uri: string): Error | null {
|
||||
const expectHost = this.utilityService.extractDbHost(uri);
|
||||
const apType = getApType(object);
|
||||
|
||||
if (!validPost.includes(getApType(object))) {
|
||||
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: invalid object type ${getApType(object)}`);
|
||||
if (apType == null || !validPost.includes(apType)) {
|
||||
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: invalid object type ${apType ?? 'undefined'}`);
|
||||
}
|
||||
|
||||
if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) {
|
||||
@@ -181,7 +183,7 @@ export class ApNoteService {
|
||||
/**
|
||||
* 禁止ワードチェック
|
||||
*/
|
||||
const hasProhibitedWords = await this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices });
|
||||
const hasProhibitedWords = this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices });
|
||||
if (hasProhibitedWords) {
|
||||
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
|
||||
}
|
||||
@@ -335,8 +337,7 @@ export class ApNoteService {
|
||||
const uri = getApId(value);
|
||||
|
||||
// ブロックしていたら中断
|
||||
const meta = await this.metaService.fetch();
|
||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) {
|
||||
if (this.utilityService.isBlockedHost(this.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, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { FollowingsRepository, InstancesRepository, MiMeta, 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,7 +35,6 @@ 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';
|
||||
@@ -48,7 +47,7 @@ import type { ApResolverService, Resolver } from '../ApResolverService.js';
|
||||
import type { ApLoggerService } from '../ApLoggerService.js';
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
import type { ApImageService } from './ApImageService.js';
|
||||
import type { IActor, IObject } from '../type.js';
|
||||
import type { IActor, ICollection, IObject, IOrderedCollection } from '../type.js';
|
||||
|
||||
const nameLength = 128;
|
||||
const summaryLength = 2048;
|
||||
@@ -62,7 +61,6 @@ 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;
|
||||
@@ -84,6 +82,9 @@ export class ApPersonService implements OnModuleInit {
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@@ -112,7 +113,6 @@ 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');
|
||||
@@ -296,6 +296,21 @@ export class ApPersonService implements OnModuleInit {
|
||||
|
||||
const isBot = getApType(object) === 'Service' || getApType(object) === 'Application';
|
||||
|
||||
const [followingVisibility, followersVisibility] = await Promise.all(
|
||||
[
|
||||
this.isPublicCollection(person.following, resolver),
|
||||
this.isPublicCollection(person.followers, resolver),
|
||||
].map((p): Promise<'public' | 'private'> => p
|
||||
.then(isPublic => isPublic ? 'public' : 'private')
|
||||
.catch(err => {
|
||||
if (!(err instanceof StatusError) || err.isRetryable) {
|
||||
this.logger.error('error occurred while fetching following/followers collection', { stack: err });
|
||||
}
|
||||
return 'private';
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
||||
|
||||
const url = getOneApHrefNullable(person.url);
|
||||
@@ -357,6 +372,8 @@ export class ApPersonService implements OnModuleInit {
|
||||
description: _description,
|
||||
url,
|
||||
fields,
|
||||
followingVisibility,
|
||||
followersVisibility,
|
||||
birthday: bday?.[0] ?? null,
|
||||
location: person['vcard:Address'] ?? null,
|
||||
userHost: host,
|
||||
@@ -390,10 +407,10 @@ export class ApPersonService implements OnModuleInit {
|
||||
this.cacheService.uriPersonCache.set(user.uri, user);
|
||||
|
||||
// Register host
|
||||
this.federatedInstanceService.fetch(host).then(async i => {
|
||||
this.federatedInstanceService.fetch(host).then(i => {
|
||||
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
|
||||
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.newUser(i.host);
|
||||
}
|
||||
});
|
||||
@@ -464,6 +481,23 @@ export class ApPersonService implements OnModuleInit {
|
||||
|
||||
const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32);
|
||||
|
||||
const [followingVisibility, followersVisibility] = await Promise.all(
|
||||
[
|
||||
this.isPublicCollection(person.following, resolver),
|
||||
this.isPublicCollection(person.followers, resolver),
|
||||
].map((p): Promise<'public' | 'private' | undefined> => p
|
||||
.then(isPublic => isPublic ? 'public' : 'private')
|
||||
.catch(err => {
|
||||
if (!(err instanceof StatusError) || err.isRetryable) {
|
||||
this.logger.error('error occurred while fetching following/followers collection', { stack: err });
|
||||
// Do not update the visibiility on transient errors.
|
||||
return undefined;
|
||||
}
|
||||
return 'private';
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
||||
|
||||
const url = getOneApHrefNullable(person.url);
|
||||
@@ -532,6 +566,8 @@ export class ApPersonService implements OnModuleInit {
|
||||
url,
|
||||
fields,
|
||||
description: _description,
|
||||
followingVisibility,
|
||||
followersVisibility,
|
||||
birthday: bday?.[0] ?? null,
|
||||
location: person['vcard:Address'] ?? null,
|
||||
});
|
||||
@@ -703,4 +739,16 @@ export class ApPersonService implements OnModuleInit {
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async isPublicCollection(collection: string | ICollection | IOrderedCollection | undefined, resolver: Resolver): Promise<boolean> {
|
||||
if (collection) {
|
||||
const resolved = await resolver.resolveCollection(collection);
|
||||
if (resolved.first || (resolved as ICollection).items || (resolved as IOrderedCollection).orderedItems) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@@ -60,11 +60,14 @@ export function getApId(value: string | IObject): string {
|
||||
|
||||
/**
|
||||
* Get ActivityStreams Object type
|
||||
*
|
||||
* タイプ判定ができなかった場合に、あえてエラーではなくnullを返すようにしている。
|
||||
* 詳細: https://github.com/misskey-dev/misskey/issues/14239
|
||||
*/
|
||||
export function getApType(value: IObject): string {
|
||||
export function getApType(value: IObject): string | null {
|
||||
if (typeof value.type === 'string') return value.type;
|
||||
if (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0];
|
||||
throw new Error('cannot detect type');
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getOneApHrefNullable(value: ApObject | undefined): string | undefined {
|
||||
@@ -97,19 +100,23 @@ export interface IActivity extends IObject {
|
||||
export interface ICollection extends IObject {
|
||||
type: 'Collection';
|
||||
totalItems: number;
|
||||
items: ApObject;
|
||||
first?: IObject | string;
|
||||
items?: ApObject;
|
||||
}
|
||||
|
||||
export interface IOrderedCollection extends IObject {
|
||||
type: 'OrderedCollection';
|
||||
totalItems: number;
|
||||
orderedItems: ApObject;
|
||||
first?: IObject | string;
|
||||
orderedItems?: ApObject;
|
||||
}
|
||||
|
||||
export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video', 'Event'];
|
||||
|
||||
export const isPost = (object: IObject): object is IPost =>
|
||||
validPost.includes(getApType(object));
|
||||
export const isPost = (object: IObject): object is IPost => {
|
||||
const type = getApType(object);
|
||||
return type != null && validPost.includes(type);
|
||||
};
|
||||
|
||||
export interface IPost extends IObject {
|
||||
type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event';
|
||||
@@ -156,8 +163,10 @@ export const isTombstone = (object: IObject): object is ITombstone =>
|
||||
|
||||
export const validActor = ['Person', 'Service', 'Group', 'Organization', 'Application'];
|
||||
|
||||
export const isActor = (object: IObject): object is IActor =>
|
||||
validActor.includes(getApType(object));
|
||||
export const isActor = (object: IObject): object is IActor => {
|
||||
const type = getApType(object);
|
||||
return type != null && validActor.includes(type);
|
||||
};
|
||||
|
||||
export interface IActor extends IObject {
|
||||
type: 'Person' | 'Service' | 'Organization' | 'Group' | 'Application';
|
||||
@@ -240,12 +249,16 @@ export interface IKey extends IObject {
|
||||
publicKeyPem: string | Buffer;
|
||||
}
|
||||
|
||||
export const validDocumentTypes = ['Audio', 'Document', 'Image', 'Page', 'Video'];
|
||||
|
||||
export interface IApDocument extends IObject {
|
||||
type: 'Audio' | 'Document' | 'Image' | 'Page' | 'Video';
|
||||
}
|
||||
|
||||
export const isDocument = (object: IObject): object is IApDocument =>
|
||||
['Audio', 'Document', 'Image', 'Page', 'Video'].includes(getApType(object));
|
||||
export const isDocument = (object: IObject): object is IApDocument => {
|
||||
const type = getApType(object);
|
||||
return type != null && validDocumentTypes.includes(type);
|
||||
};
|
||||
|
||||
export interface IApImage extends IApDocument {
|
||||
type: 'Image';
|
||||
@@ -323,7 +336,10 @@ export const isAccept = (object: IObject): object is IAccept => getApType(object
|
||||
export const isReject = (object: IObject): object is IReject => getApType(object) === 'Reject';
|
||||
export const isAdd = (object: IObject): object is IAdd => getApType(object) === 'Add';
|
||||
export const isRemove = (object: IObject): object is IRemove => getApType(object) === 'Remove';
|
||||
export const isLike = (object: IObject): object is ILike => getApType(object) === 'Like' || getApType(object) === 'EmojiReaction' || getApType(object) === 'EmojiReact';
|
||||
export const isLike = (object: IObject): object is ILike => {
|
||||
const type = getApType(object);
|
||||
return type != null && ['Like', 'EmojiReaction', 'EmojiReact'].includes(type);
|
||||
};
|
||||
export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce';
|
||||
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
|
||||
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
|
||||
|
@@ -5,10 +5,9 @@
|
||||
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import type { FollowingsRepository, InstancesRepository } from '@/models/_.js';
|
||||
import type { FollowingsRepository, InstancesRepository, MiMeta } 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';
|
||||
@@ -24,13 +23,15 @@ 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,
|
||||
) {
|
||||
@@ -43,8 +44,6 @@ 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\'');
|
||||
@@ -65,21 +64,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(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.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(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.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(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
|
||||
.andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`)
|
||||
.setParameters(pubsubSubQuery.getParameters())
|
||||
@@ -88,7 +87,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(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere('instance.suspensionState = \'none\'')
|
||||
.andWhere('instance.isNotResponding = false')
|
||||
.getRawOne()
|
||||
@@ -96,7 +95,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(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere('instance.suspensionState = \'none\'')
|
||||
.andWhere('instance.isNotResponding = false')
|
||||
.getRawOne()
|
||||
|
@@ -49,6 +49,7 @@ export class FlashEntityService {
|
||||
title: flash.title,
|
||||
summary: flash.summary,
|
||||
script: flash.script,
|
||||
visibility: flash.visibility,
|
||||
likedCount: flash.likedCount,
|
||||
isLiked: meId ? await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } }) : undefined,
|
||||
});
|
||||
|
@@ -3,19 +3,22 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Inject, 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(
|
||||
private metaService: MetaService,
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
private roleService: RoleService,
|
||||
|
||||
private utilityService: UtilityService,
|
||||
@@ -27,7 +30,6 @@ 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 {
|
||||
@@ -41,7 +43,7 @@ export class InstanceEntityService {
|
||||
isNotResponding: instance.isNotResponding,
|
||||
isSuspended: instance.suspensionState !== 'none',
|
||||
suspensionState: instance.suspensionState,
|
||||
isBlocked: this.utilityService.isBlockedHost(meta.blockedHosts, instance.host),
|
||||
isBlocked: this.utilityService.isBlockedHost(this.meta.blockedHosts, instance.host),
|
||||
softwareName: instance.softwareName,
|
||||
softwareVersion: instance.softwareVersion,
|
||||
openRegistrations: instance.openRegistrations,
|
||||
@@ -49,7 +51,8 @@ export class InstanceEntityService {
|
||||
description: instance.description,
|
||||
maintainerName: instance.maintainerName,
|
||||
maintainerEmail: instance.maintainerEmail,
|
||||
isSilenced: this.utilityService.isSilencedHost(meta.silencedHosts, instance.host),
|
||||
isSilenced: this.utilityService.isSilencedHost(this.meta.silencedHosts, instance.host),
|
||||
isMediaSilenced: this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, instance.host),
|
||||
iconUrl: instance.iconUrl,
|
||||
faviconUrl: instance.faviconUrl,
|
||||
themeColor: instance.themeColor,
|
||||
@@ -62,8 +65,9 @@ export class InstanceEntityService {
|
||||
@bindThis
|
||||
public packMany(
|
||||
instances: MiInstance[],
|
||||
me?: { id: MiUser['id']; } | null | undefined,
|
||||
) {
|
||||
return Promise.all(instances.map(x => this.pack(x)));
|
||||
return Promise.all(instances.map(x => this.pack(x, me)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -10,7 +10,6 @@ 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';
|
||||
@@ -24,11 +23,13 @@ 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,
|
||||
) { }
|
||||
|
||||
@@ -37,7 +38,7 @@ export class MetaEntityService {
|
||||
let instance = meta;
|
||||
|
||||
if (!instance) {
|
||||
instance = await this.metaService.fetch();
|
||||
instance = this.meta;
|
||||
}
|
||||
|
||||
const ads = await this.adsRepository.createQueryBuilder('ads')
|
||||
@@ -129,6 +130,7 @@ 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;
|
||||
@@ -139,7 +141,7 @@ export class MetaEntityService {
|
||||
let instance = meta;
|
||||
|
||||
if (!instance) {
|
||||
instance = await this.metaService.fetch();
|
||||
instance = this.meta;
|
||||
}
|
||||
|
||||
const packed = await this.pack(instance);
|
||||
|
@@ -11,29 +11,46 @@ 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 { MiNoteReaction } from '@/models/NoteReaction.js';
|
||||
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository } from '@/models/_.js';
|
||||
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, MiMeta } 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,
|
||||
|
||||
@@ -59,6 +76,8 @@ export class NoteEntityService implements OnModuleInit {
|
||||
//private driveFileEntityService: DriveFileEntityService,
|
||||
//private customEmojiService: CustomEmojiService,
|
||||
//private reactionService: ReactionService,
|
||||
//private reactionsBufferingService: ReactionsBufferingService,
|
||||
//private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -67,6 +86,7 @@ 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');
|
||||
}
|
||||
|
||||
@@ -287,6 +307,7 @@ 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'>>
|
||||
@@ -303,6 +324,20 @@ 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)) {
|
||||
@@ -315,7 +350,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||
: await this.channelsRepository.findOneBy({ id: note.channelId })
|
||||
: null;
|
||||
|
||||
const reactionEmojiNames = Object.keys(note.reactions)
|
||||
const reactionEmojiNames = Object.keys(reactions)
|
||||
.filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ
|
||||
.map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', ''));
|
||||
const packedFiles = options?._hint_?.packedFiles;
|
||||
@@ -334,10 +369,10 @@ export class NoteEntityService implements OnModuleInit {
|
||||
visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined,
|
||||
renoteCount: note.renoteCount,
|
||||
repliesCount: note.repliesCount,
|
||||
reactionCount: Object.values(note.reactions).reduce((a, b) => a + b, 0),
|
||||
reactions: this.reactionService.convertLegacyReactions(note.reactions),
|
||||
reactionCount: Object.values(reactions).reduce((a, b) => a + b, 0),
|
||||
reactions: reactions,
|
||||
reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host),
|
||||
reactionAndUserPairCache: opts.withReactionAndUserPairCache ? note.reactionAndUserPairCache : undefined,
|
||||
reactionAndUserPairCache: opts.withReactionAndUserPairCache ? reactionAndUserPairCache : undefined,
|
||||
emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined,
|
||||
tags: note.tags.length > 0 ? note.tags : undefined,
|
||||
fileIds: note.fileIds,
|
||||
@@ -376,8 +411,12 @@ export class NoteEntityService implements OnModuleInit {
|
||||
|
||||
poll: note.hasPoll ? this.populatePoll(note, meId) : undefined,
|
||||
|
||||
...(meId && Object.keys(note.reactions).length > 0 ? {
|
||||
myReaction: this.populateMyReaction(note, meId, options?._hint_),
|
||||
...(meId && Object.keys(reactions).length > 0 ? {
|
||||
myReaction: this.populateMyReaction({
|
||||
id: note.id,
|
||||
reactions: reactions,
|
||||
reactionAndUserPairCache: reactionAndUserPairCache,
|
||||
}, meId, options?._hint_),
|
||||
} : {}),
|
||||
} : {}),
|
||||
});
|
||||
@@ -400,6 +439,8 @@ 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) {
|
||||
@@ -410,23 +451,33 @@ 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(note.renote.reactions).reduce((a, b) => a + b, 0);
|
||||
const reactionsCount = Object.values(mergeReactions(note.renote.reactions, bufferdReactions?.get(note.renote.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
|
||||
if (reactionsCount === 0) {
|
||||
myReactionsMap.set(note.renote.id, null);
|
||||
} 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 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 {
|
||||
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(note.reactions).reduce((a, b) => a + b, 0);
|
||||
const reactionsCount = Object.values(mergeReactions(note.reactions, bufferdReactions?.get(note.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
|
||||
if (reactionsCount === 0) {
|
||||
myReactionsMap.set(note.id, null);
|
||||
} 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 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 {
|
||||
const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
|
||||
myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null);
|
||||
}
|
||||
} else {
|
||||
idsNeedFetchMyReaction.add(note.id);
|
||||
}
|
||||
@@ -461,6 +512,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||
return await Promise.all(notes.map(n => this.pack(n, me, {
|
||||
...options,
|
||||
_hint_: {
|
||||
bufferdReactions,
|
||||
myReactions: myReactionsMap,
|
||||
packedFiles,
|
||||
packedUsers,
|
||||
|
@@ -454,12 +454,12 @@ export class UserEntityService implements OnModuleInit {
|
||||
}
|
||||
|
||||
const followingCount = profile == null ? null :
|
||||
(profile.followingVisibility === 'public') || isMe ? user.followingCount :
|
||||
(profile.followingVisibility === 'public') || isMe || iAmModerator ? user.followingCount :
|
||||
(profile.followingVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount :
|
||||
null;
|
||||
|
||||
const followersCount = profile == null ? null :
|
||||
(profile.followersVisibility === 'public') || isMe ? user.followersCount :
|
||||
(profile.followersVisibility === 'public') || isMe || iAmModerator ? user.followersCount :
|
||||
(profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
|
||||
null;
|
||||
|
||||
|
@@ -3,13 +3,14 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import si from 'systeminformation';
|
||||
import Xev from 'xev';
|
||||
import * as osUtils from 'os-utils';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { 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();
|
||||
|
||||
@@ -23,7 +24,8 @@ export class ServerStatsService implements OnApplicationShutdown {
|
||||
private intervalId: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(
|
||||
private metaService: MetaService,
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -32,7 +34,7 @@ export class ServerStatsService implements OnApplicationShutdown {
|
||||
*/
|
||||
@bindThis
|
||||
public async start(): Promise<void> {
|
||||
if (!(await this.metaService.fetch(true)).enableServerMachineStats) return;
|
||||
if (!this.meta.enableServerMachineStats) return;
|
||||
|
||||
const log = [] as any[];
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user