mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-04-30 23:55:44 +02:00
Compare commits
97 Commits
copilot/bu
...
minify-bun
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1288076f31 | ||
|
|
4f67d671cc | ||
|
|
b53f3a5bd5 | ||
|
|
7bcfeba7e5 | ||
|
|
bbd3a176a2 | ||
|
|
5871037b31 | ||
|
|
4f65c1529b | ||
|
|
589ae8d4c6 | ||
|
|
6baa5463dc | ||
|
|
0be4405a79 | ||
|
|
2fba2e7049 | ||
|
|
96b03a7179 | ||
|
|
cdb958cdf0 | ||
|
|
245775ea87 | ||
|
|
40d55fc6a3 | ||
|
|
9c22538454 | ||
|
|
a1ba403f9a | ||
|
|
443e1ed29e | ||
|
|
b5454cb2c4 | ||
|
|
8577f10456 | ||
|
|
16ffd88ecc | ||
|
|
866e675134 | ||
|
|
01aa56c602 | ||
|
|
8bba88670b | ||
|
|
e44f993b6b | ||
|
|
ff7d2c1083 | ||
|
|
404fca6c2d | ||
|
|
3fe0477cac | ||
|
|
97d485bdd2 | ||
|
|
4285303c81 | ||
|
|
14f58255ee | ||
|
|
b69b0acf59 | ||
|
|
7a5430199f | ||
|
|
c32307dca4 | ||
|
|
bc78bb9b8e | ||
|
|
7ed582e3b7 | ||
|
|
a33b003282 | ||
|
|
74e847a04d | ||
|
|
06657c81d3 | ||
|
|
5c5e965151 | ||
|
|
b07a1e692f | ||
|
|
78348007ed | ||
|
|
92f1e599db | ||
|
|
26b5979c76 | ||
|
|
b1048525d2 | ||
|
|
4c31eb409c | ||
|
|
f739cb6270 | ||
|
|
81bacb6203 | ||
|
|
ee8dccea2f | ||
|
|
6d00645bc7 | ||
|
|
baeed4bc80 | ||
|
|
dba44daf9c | ||
|
|
46e6dd99d1 | ||
|
|
f48af7f73b | ||
|
|
834e8b4c24 | ||
|
|
7ef0c96758 | ||
|
|
b10074e939 | ||
|
|
260dbd150b | ||
|
|
79cbbcfe0f | ||
|
|
c893f85864 | ||
|
|
24d4ffa2ec | ||
|
|
0b931daefd | ||
|
|
cc05d93194 | ||
|
|
90345591bb | ||
|
|
730227f353 | ||
|
|
4acb37ee9d | ||
|
|
7025769c69 | ||
|
|
1a4ef8769f | ||
|
|
055cd0c250 | ||
|
|
d35ddc77d2 | ||
|
|
8d871a58e3 | ||
|
|
99b0b436e0 | ||
|
|
e3d5b95672 | ||
|
|
0d52145b2b | ||
|
|
467404d5bb | ||
|
|
99e25784ad | ||
|
|
9e1e40d35a | ||
|
|
8eb6e29d2c | ||
|
|
2d198a711b | ||
|
|
e0b872dc09 | ||
|
|
711b86ab7d | ||
|
|
e8b4dae553 | ||
|
|
36d404818d | ||
|
|
cb03f3f013 | ||
|
|
c109bec013 | ||
|
|
6fa4eb8c4f | ||
|
|
5fb4caa14b | ||
|
|
bc1f83664f | ||
|
|
78435dc8d4 | ||
|
|
d74aded35f | ||
|
|
d605680524 | ||
|
|
1096ce8e4a | ||
|
|
8e6fffee68 | ||
|
|
ab5c38875d | ||
|
|
01b0c764a5 | ||
|
|
c770a1746d | ||
|
|
83cdef298b |
@@ -107,13 +107,51 @@ port: 3000
|
|||||||
|
|
||||||
# Proxy trust settings
|
# Proxy trust settings
|
||||||
#
|
#
|
||||||
# Changes how the server interpret the origin IP of the request.
|
# Specifies the IP addresses that Misskey will use as trusted
|
||||||
|
# reverse proxies (e.g., nginx, Cloudflare). This affects how
|
||||||
|
# Misskey determines the source IP for each request and is used
|
||||||
|
# for important rate limiting and security features. If the value
|
||||||
|
# is not set correctly, Misskey may use the IP address of the
|
||||||
|
# reverse proxy instead of the actual source IP, which may lead to
|
||||||
|
# unintended rate limiting or security vulnerabilities.
|
||||||
|
# By default, the loopback network and private network address
|
||||||
|
# ranges shown below are trusted.
|
||||||
|
# If you are using a single reverse proxy and it is on the same
|
||||||
|
# machine or the same private network as Misskey, it is unlikely you
|
||||||
|
# need to change this setting, and the default setting is fine.
|
||||||
|
# Also, if you are using multiple reverse proxy servers and they are
|
||||||
|
# all on the same private network as Misskey, the default setting
|
||||||
|
# is fine.
|
||||||
|
# However, if you are using a reverse proxy server that accesses
|
||||||
|
# Misskey web servers and streaming servers via public IP addresses
|
||||||
|
# (for example, Cloudflare), you must set this variable.
|
||||||
|
# When changing this setting, you can use one of the following values:
|
||||||
#
|
#
|
||||||
# Any format supported by Fastify is accepted.
|
# - true: Trust all proxies
|
||||||
# Default: do not trust any proxies (i.e. trustProxy: false)
|
# - false: Do not trust any proxies
|
||||||
# See: https://fastify.dev/docs/latest/reference/server/#trustproxy
|
# - IP address, IP address range, or array of them: Trust hops that
|
||||||
|
# match the specified criteria.
|
||||||
|
# - Integer: Trust the nth hop from the front-facing proxy server as
|
||||||
|
# the client.
|
||||||
|
# For more information on how to configure this setting, please refer
|
||||||
|
# to the Fastify documentation:
|
||||||
|
# https://fastify.dev/docs/latest/Reference/Server/#trustproxy
|
||||||
#
|
#
|
||||||
# trustProxy: false
|
# Note that if this variable is set, it overrides the default range,
|
||||||
|
# so if you have both an external reverse proxy and a proxy on the
|
||||||
|
# local host, you must include both IPs (or IP ranges).
|
||||||
|
#
|
||||||
|
#trustProxy:
|
||||||
|
# - '10.0.0.0/8'
|
||||||
|
# - '172.16.0.0/12'
|
||||||
|
# - '192.168.0.0/16'
|
||||||
|
# - '127.0.0.1/32'
|
||||||
|
# - '::1/128'
|
||||||
|
# - 'fc00::/7'
|
||||||
|
# # Example: If you are using some external reverse proxies like CDNs,
|
||||||
|
# # you may need to add the CDN IP ranges here.
|
||||||
|
# # If you're using Cloudflare, you can find IP Ranges at:
|
||||||
|
# # https://www.cloudflare.com/ips/
|
||||||
|
|
||||||
# ┌──────────────────────────┐
|
# ┌──────────────────────────┐
|
||||||
#───┘ PostgreSQL configuration └────────────────────────────────
|
#───┘ PostgreSQL configuration └────────────────────────────────
|
||||||
@@ -283,6 +321,10 @@ id: 'aidx'
|
|||||||
# Whether disable HSTS
|
# Whether disable HSTS
|
||||||
#disableHsts: true
|
#disableHsts: true
|
||||||
|
|
||||||
|
# Enable internal IP-based rate limiting (default: true)
|
||||||
|
# To configure them in reverse proxy instead, set this to false.
|
||||||
|
#enableIpRateLimit: true
|
||||||
|
|
||||||
# Number of worker processes
|
# Number of worker processes
|
||||||
#clusterLimit: 1
|
#clusterLimit: 1
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
Dockerfile
|
Dockerfile
|
||||||
build/
|
build/
|
||||||
built/
|
built/
|
||||||
|
src-js/
|
||||||
db/
|
db/
|
||||||
.devcontainer/compose.yml
|
.devcontainer/compose.yml
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/01_bug-report.yml
vendored
4
.github/ISSUE_TEMPLATE/01_bug-report.yml
vendored
@@ -54,7 +54,7 @@ body:
|
|||||||
* Model and OS of the device(s): MacBook Pro (14inch, 2021), macOS Ventura 13.4
|
* Model and OS of the device(s): MacBook Pro (14inch, 2021), macOS Ventura 13.4
|
||||||
* Browser: Chrome 113.0.5672.126
|
* Browser: Chrome 113.0.5672.126
|
||||||
* Server URL: misskey.example.com
|
* Server URL: misskey.example.com
|
||||||
* Misskey: 2025.x.x
|
* Misskey: 2026.x.x
|
||||||
value: |
|
value: |
|
||||||
* Model and OS of the device(s):
|
* Model and OS of the device(s):
|
||||||
* Browser:
|
* Browser:
|
||||||
@@ -74,7 +74,7 @@ body:
|
|||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
* Installation Method or Hosting Service: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment
|
* Installation Method or Hosting Service: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment
|
||||||
* Misskey: 2025.x.x
|
* Misskey: 2026.x.x
|
||||||
* Node: 20.x.x
|
* Node: 20.x.x
|
||||||
* PostgreSQL: 18.x.x
|
* PostgreSQL: 18.x.x
|
||||||
* Redis: 7.x.x
|
* Redis: 7.x.x
|
||||||
|
|||||||
3
.github/dependabot.yml
vendored
3
.github/dependabot.yml
vendored
@@ -40,6 +40,3 @@ updates:
|
|||||||
typescript-eslint:
|
typescript-eslint:
|
||||||
patterns:
|
patterns:
|
||||||
- "@typescript-eslint/*"
|
- "@typescript-eslint/*"
|
||||||
tensorflow:
|
|
||||||
patterns:
|
|
||||||
- "@tensorflow/*"
|
|
||||||
|
|||||||
4
.github/workflows/api-misskey-js.yml
vendored
4
.github/workflows/api-misskey-js.yml
vendored
@@ -16,13 +16,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4.3.0
|
uses: actions/checkout@v6.0.1
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4.2.0
|
uses: pnpm/action-setup@v4.2.0
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|||||||
4
.github/workflows/changelog-check.yml
vendored
4
.github/workflows/changelog-check.yml
vendored
@@ -12,9 +12,9 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout head
|
- name: Checkout head
|
||||||
uses: actions/checkout@v4.3.0
|
uses: actions/checkout@v6.0.1
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
|
|
||||||
|
|||||||
22
.github/workflows/check-misskey-js-autogen.yml
vendored
22
.github/workflows/check-misskey-js-autogen.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }}
|
if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }}
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@v4.3.0
|
uses: actions/checkout@v6.0.1
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
|
|
||||||
- name: setup node
|
- name: setup node
|
||||||
id: setup-node
|
id: setup-node
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
@@ -53,7 +53,7 @@ jobs:
|
|||||||
|
|
||||||
# packages/misskey-js/generator/built/autogen
|
# packages/misskey-js/generator/built/autogen
|
||||||
- name: Upload Generated
|
- name: Upload Generated
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: generated-misskey-js
|
name: generated-misskey-js
|
||||||
path: packages/misskey-js/generator/built/autogen
|
path: packages/misskey-js/generator/built/autogen
|
||||||
@@ -66,14 +66,14 @@ jobs:
|
|||||||
if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }}
|
if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }}
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@v4.3.0
|
uses: actions/checkout@v6.0.1
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
ref: refs/pull/${{ github.event.pull_request.number }}/merge
|
ref: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||||
|
|
||||||
- name: Upload From Merged
|
- name: Upload From Merged
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: actual-misskey-js
|
name: actual-misskey-js
|
||||||
path: packages/misskey-js/src/autogen
|
path: packages/misskey-js/src/autogen
|
||||||
@@ -86,13 +86,13 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: download generated-misskey-js
|
- name: download generated-misskey-js
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: generated-misskey-js
|
name: generated-misskey-js
|
||||||
path: misskey-js-generated
|
path: misskey-js-generated
|
||||||
|
|
||||||
- name: download actual-misskey-js
|
- name: download actual-misskey-js
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: actual-misskey-js
|
name: actual-misskey-js
|
||||||
path: misskey-js-actual
|
path: misskey-js-actual
|
||||||
@@ -113,9 +113,9 @@ jobs:
|
|||||||
|
|
||||||
- name: send message
|
- name: send message
|
||||||
if: steps.check-changes.outputs.changes == 'true'
|
if: steps.check-changes.outputs.changes == 'true'
|
||||||
uses: thollander/actions-comment-pull-request@v2
|
uses: thollander/actions-comment-pull-request@v3
|
||||||
with:
|
with:
|
||||||
comment_tag: check-misskey-js-autogen
|
comment-tag: check-misskey-js-autogen
|
||||||
message: |-
|
message: |-
|
||||||
Thank you for sending us a great Pull Request! 👍
|
Thank you for sending us a great Pull Request! 👍
|
||||||
Please regenerate misskey-js type definitions! 🙏
|
Please regenerate misskey-js type definitions! 🙏
|
||||||
@@ -127,9 +127,9 @@ jobs:
|
|||||||
|
|
||||||
- name: send message
|
- name: send message
|
||||||
if: steps.check-changes.outputs.changes == 'false'
|
if: steps.check-changes.outputs.changes == 'false'
|
||||||
uses: thollander/actions-comment-pull-request@v2
|
uses: thollander/actions-comment-pull-request@v3
|
||||||
with:
|
with:
|
||||||
comment_tag: check-misskey-js-autogen
|
comment-tag: check-misskey-js-autogen
|
||||||
mode: delete
|
mode: delete
|
||||||
message: "Thank you!"
|
message: "Thank you!"
|
||||||
create_if_not_exists: false
|
create_if_not_exists: false
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4.3.0
|
uses: actions/checkout@v6.0.1
|
||||||
- name: Check version
|
- name: Check version
|
||||||
run: |
|
run: |
|
||||||
if [ "$(jq -r '.version' package.json)" != "$(jq -r '.version' packages/misskey-js/package.json)" ]; then
|
if [ "$(jq -r '.version' package.json)" != "$(jq -r '.version' packages/misskey-js/package.json)" ]; then
|
||||||
|
|||||||
2
.github/workflows/check-spdx-license-id.yml
vendored
2
.github/workflows/check-spdx-license-id.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4.3.0
|
uses: actions/checkout@v6.0.1
|
||||||
- name: Check
|
- name: Check
|
||||||
run: |
|
run: |
|
||||||
counter=0
|
counter=0
|
||||||
|
|||||||
2
.github/workflows/check_copyright_year.yml
vendored
2
.github/workflows/check_copyright_year.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
check_copyright_year:
|
check_copyright_year:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.3.0
|
- uses: actions/checkout@v6.0.1
|
||||||
- run: |
|
- run: |
|
||||||
if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then
|
if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then
|
||||||
echo "Please change copyright year!"
|
echo "Please change copyright year!"
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
wait_time: ${{ steps.get-wait-time.outputs.wait_time }}
|
wait_time: ${{ steps.get-wait-time.outputs.wait_time }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4.3.0
|
uses: actions/checkout@v6.0.1
|
||||||
|
|
||||||
- name: Check allowed users
|
- name: Check allowed users
|
||||||
id: check-allowed-users
|
id: check-allowed-users
|
||||||
|
|||||||
6
.github/workflows/docker-develop.yml
vendored
6
.github/workflows/docker-develop.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
platform=${{ matrix.platform }}
|
platform=${{ matrix.platform }}
|
||||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||||
- name: Check out the repo
|
- name: Check out the repo
|
||||||
uses: actions/checkout@v4.3.0
|
uses: actions/checkout@v6.0.1
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
@@ -53,7 +53,7 @@ jobs:
|
|||||||
digest="${{ steps.build.outputs.digest }}"
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
touch "/tmp/digests/${digest#sha256:}"
|
touch "/tmp/digests/${digest#sha256:}"
|
||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: digests-${{ env.PLATFORM_PAIR }}
|
name: digests-${{ env.PLATFORM_PAIR }}
|
||||||
path: /tmp/digests/*
|
path: /tmp/digests/*
|
||||||
@@ -66,7 +66,7 @@ jobs:
|
|||||||
- build
|
- build
|
||||||
steps:
|
steps:
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
path: /tmp/digests
|
path: /tmp/digests
|
||||||
pattern: digests-*
|
pattern: digests-*
|
||||||
|
|||||||
6
.github/workflows/docker.yml
vendored
6
.github/workflows/docker.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
|||||||
platform=${{ matrix.platform }}
|
platform=${{ matrix.platform }}
|
||||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||||
- name: Check out the repo
|
- name: Check out the repo
|
||||||
uses: actions/checkout@v4.3.0
|
uses: actions/checkout@v6.0.1
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
@@ -64,7 +64,7 @@ jobs:
|
|||||||
digest="${{ steps.build.outputs.digest }}"
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
touch "/tmp/digests/${digest#sha256:}"
|
touch "/tmp/digests/${digest#sha256:}"
|
||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: digests-${{ env.PLATFORM_PAIR }}
|
name: digests-${{ env.PLATFORM_PAIR }}
|
||||||
path: /tmp/digests/*
|
path: /tmp/digests/*
|
||||||
@@ -77,7 +77,7 @@ jobs:
|
|||||||
- build
|
- build
|
||||||
steps:
|
steps:
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
path: /tmp/digests
|
path: /tmp/digests
|
||||||
pattern: digests-*
|
pattern: digests-*
|
||||||
|
|||||||
41
.github/workflows/dockle.yml
vendored
41
.github/workflows/dockle.yml
vendored
@@ -11,38 +11,43 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
dockle:
|
dockle:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DOCKER_CONTENT_TRUST: 1
|
DOCKER_CONTENT_TRUST: 1
|
||||||
DOCKLE_VERSION: 0.4.15
|
DOCKLE_VERSION: 0.4.15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.3.0
|
- uses: actions/checkout@v6.0.1
|
||||||
|
|
||||||
- name: Download and install dockle v${{ env.DOCKLE_VERSION }}
|
- name: Download and install dockle v${{ env.DOCKLE_VERSION }}
|
||||||
run: |
|
run: |
|
||||||
|
set -eux
|
||||||
curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v${DOCKLE_VERSION}/dockle_${DOCKLE_VERSION}_Linux-64bit.deb"
|
curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v${DOCKLE_VERSION}/dockle_${DOCKLE_VERSION}_Linux-64bit.deb"
|
||||||
sudo dpkg -i dockle.deb
|
sudo dpkg -i dockle.deb
|
||||||
|
|
||||||
- run: |
|
- name: Build web image (docker build)
|
||||||
cp .config/docker_example.env .config/docker.env
|
|
||||||
cp ./compose_example.yml ./compose.yml
|
|
||||||
|
|
||||||
- run: |
|
|
||||||
docker compose up -d web
|
|
||||||
IMAGE_ID=$(docker compose images --format json web | jq -r '.[0].ID')
|
|
||||||
docker tag "${IMAGE_ID}" misskey-web:latest
|
|
||||||
|
|
||||||
- name: Prune docker junk (optional but recommended)
|
|
||||||
run: |
|
run: |
|
||||||
docker system prune -af
|
set -eux
|
||||||
docker volume prune -f
|
docker build -t "misskey-web:ci" .
|
||||||
|
docker image ls
|
||||||
|
|
||||||
- name: Save image for Dockle
|
- name: Mount tmpfs for Dockle tar
|
||||||
|
env:
|
||||||
|
TMPFS_SIZE: 8G
|
||||||
run: |
|
run: |
|
||||||
docker save misskey-web:latest -o ./misskey-web.tar
|
set -eux
|
||||||
ls -lh ./misskey-web.tar
|
sudo mkdir -p /mnt/dockle-tmp
|
||||||
|
sudo mount -t tmpfs -o size=${{ env.TMPFS_SIZE }} tmpfs /mnt/dockle-tmp
|
||||||
|
free -h
|
||||||
|
df -h
|
||||||
|
|
||||||
- name: Run Dockle with tar input
|
- name: Save image tar into tmpfs
|
||||||
run: |
|
run: |
|
||||||
dockle --exit-code 1 --input ./misskey-web.tar
|
set -eux
|
||||||
|
docker save misskey-web:ci -o /mnt/dockle-tmp/misskey-web.tar
|
||||||
|
ls -lh /mnt/dockle-tmp/misskey-web.tar
|
||||||
|
|
||||||
|
- name: Run Dockle Scan (tar input)
|
||||||
|
run: |
|
||||||
|
set -eux
|
||||||
|
dockle --exit-code 1 --input /mnt/dockle-tmp/misskey-web.tar
|
||||||
|
|||||||
8
.github/workflows/get-api-diff.yml
vendored
8
.github/workflows/get-api-diff.yml
vendored
@@ -25,14 +25,14 @@ jobs:
|
|||||||
ref: refs/pull/${{ github.event.number }}/merge
|
ref: refs/pull/${{ github.event.number }}/merge
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.3.0
|
- uses: actions/checkout@v6.0.1
|
||||||
with:
|
with:
|
||||||
ref: ${{ matrix.ref }}
|
ref: ${{ matrix.ref }}
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4.2.0
|
uses: pnpm/action-setup@v4.2.0
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
- name: Copy API.json
|
- name: Copy API.json
|
||||||
run: cp packages/backend/built/api.json ${{ matrix.api-json-name }}
|
run: cp packages/backend/built/api.json ${{ matrix.api-json-name }}
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: api-artifact-${{ matrix.api-json-name }}
|
name: api-artifact-${{ matrix.api-json-name }}
|
||||||
path: ${{ matrix.api-json-name }}
|
path: ${{ matrix.api-json-name }}
|
||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
PR_NUMBER: ${{ github.event.number }}
|
PR_NUMBER: ${{ github.event.number }}
|
||||||
run: |
|
run: |
|
||||||
echo "$PR_NUMBER" > ./pr_number
|
echo "$PR_NUMBER" > ./pr_number
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: api-artifact-pr-number
|
name: api-artifact-pr-number
|
||||||
path: pr_number
|
path: pr_number
|
||||||
|
|||||||
8
.github/workflows/get-backend-memory.yml
vendored
8
.github/workflows/get-backend-memory.yml
vendored
@@ -40,14 +40,14 @@ jobs:
|
|||||||
- 56312:6379
|
- 56312:6379
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.3.0
|
- uses: actions/checkout@v6.0.1
|
||||||
with:
|
with:
|
||||||
ref: ${{ matrix.ref }}
|
ref: ${{ matrix.ref }}
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4.2.0
|
uses: pnpm/action-setup@v4.2.0
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -67,7 +67,7 @@ jobs:
|
|||||||
# Start the server and measure memory usage
|
# Start the server and measure memory usage
|
||||||
node packages/backend/scripts/measure-memory.mjs > ${{ matrix.memory-json-name }}
|
node packages/backend/scripts/measure-memory.mjs > ${{ matrix.memory-json-name }}
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: memory-artifact-${{ matrix.memory-json-name }}
|
name: memory-artifact-${{ matrix.memory-json-name }}
|
||||||
path: ${{ matrix.memory-json-name }}
|
path: ${{ matrix.memory-json-name }}
|
||||||
@@ -81,7 +81,7 @@ jobs:
|
|||||||
PR_NUMBER: ${{ github.event.number }}
|
PR_NUMBER: ${{ github.event.number }}
|
||||||
run: |
|
run: |
|
||||||
echo "$PR_NUMBER" > ./pr_number
|
echo "$PR_NUMBER" > ./pr_number
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: memory-artifact-pr-number
|
name: memory-artifact-pr-number
|
||||||
path: pr_number
|
path: pr_number
|
||||||
|
|||||||
2
.github/workflows/labeler.yml
vendored
2
.github/workflows/labeler.yml
vendored
@@ -11,6 +11,6 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@v5
|
- uses: actions/labeler@v6
|
||||||
with:
|
with:
|
||||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
|||||||
12
.github/workflows/lint.yml
vendored
12
.github/workflows/lint.yml
vendored
@@ -36,13 +36,13 @@ jobs:
|
|||||||
pnpm_install:
|
pnpm_install:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.3.0
|
- uses: actions/checkout@v6.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4.2.0
|
uses: pnpm/action-setup@v4.2.0
|
||||||
- uses: actions/setup-node@v4.4.0
|
- uses: actions/setup-node@v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -69,13 +69,13 @@ jobs:
|
|||||||
eslint-cache-version: v1
|
eslint-cache-version: v1
|
||||||
eslint-cache-path: ${{ github.workspace }}/node_modules/.cache/eslint-${{ matrix.workspace }}
|
eslint-cache-path: ${{ github.workspace }}/node_modules/.cache/eslint-${{ matrix.workspace }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.3.0
|
- uses: actions/checkout@v6.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4.2.0
|
uses: pnpm/action-setup@v4.2.0
|
||||||
- uses: actions/setup-node@v4.4.0
|
- uses: actions/setup-node@v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -100,13 +100,13 @@ jobs:
|
|||||||
- sw
|
- sw
|
||||||
- misskey-js
|
- misskey-js
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.3.0
|
- uses: actions/checkout@v6.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4.2.0
|
uses: pnpm/action-setup@v4.2.0
|
||||||
- uses: actions/setup-node@v4.4.0
|
- uses: actions/setup-node@v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|||||||
4
.github/workflows/locale.yml
vendored
4
.github/workflows/locale.yml
vendored
@@ -16,13 +16,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.3.0
|
- uses: actions/checkout@v6.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4.2.0
|
uses: pnpm/action-setup@v4.2.0
|
||||||
- uses: actions/setup-node@v4.4.0
|
- uses: actions/setup-node@v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".node-version"
|
node-version-file: ".node-version"
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|||||||
4
.github/workflows/on-release-created.yml
vendored
4
.github/workflows/on-release-created.yml
vendored
@@ -16,13 +16,13 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.3.0
|
- uses: actions/checkout@v6.0.1
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4.2.0
|
uses: pnpm/action-setup@v4.2.0
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|||||||
2
.github/workflows/release-edit-with-push.yml
vendored
2
.github/workflows/release-edit-with-push.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
edit:
|
edit:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
# headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得
|
# headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得
|
||||||
- name: Get PR
|
- name: Get PR
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
2
.github/workflows/release-with-dispatch.yml
vendored
2
.github/workflows/release-with-dispatch.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
pr_number: ${{ steps.get_pr.outputs.pr_number }}
|
pr_number: ${{ steps.get_pr.outputs.pr_number }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
# headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得
|
# headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得
|
||||||
- name: Get PRs
|
- name: Get PRs
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
24
.github/workflows/report-api-diff.yml
vendored
24
.github/workflows/report-api-diff.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
# api-artifact
|
# api-artifact
|
||||||
steps:
|
steps:
|
||||||
- name: Download artifact
|
- name: Download artifact
|
||||||
uses: actions/github-script@v7.1.0
|
uses: actions/github-script@v8.0.0
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
- name: Echo full diff
|
- name: Echo full diff
|
||||||
run: cat ./api-full.json.diff
|
run: cat ./api-full.json.diff
|
||||||
- name: Upload full diff to Artifact
|
- name: Upload full diff to Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: api-artifact
|
name: api-artifact
|
||||||
path: |
|
path: |
|
||||||
@@ -73,9 +73,9 @@ jobs:
|
|||||||
HEADER="このPRによるapi.jsonの差分"
|
HEADER="このPRによるapi.jsonの差分"
|
||||||
FOOTER="[Get diff files from Workflow Page](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})"
|
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')"
|
DIFF_BYTES="$(stat ./api.json.diff -c '%s' | tr -d '\n')"
|
||||||
|
|
||||||
echo "$HEADER" > ./output.md
|
echo "$HEADER" > ./output.md
|
||||||
|
|
||||||
if (( "$DIFF_BYTES" <= 1 )); then
|
if (( "$DIFF_BYTES" <= 1 )); then
|
||||||
echo '差分はありません。' >> ./output.md
|
echo '差分はありません。' >> ./output.md
|
||||||
else
|
else
|
||||||
@@ -87,18 +87,18 @@ jobs:
|
|||||||
echo '```' >> ./output.md
|
echo '```' >> ./output.md
|
||||||
echo '</details>' >> .output.md
|
echo '</details>' >> .output.md
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$FOOTER" >> ./output.md
|
echo "$FOOTER" >> ./output.md
|
||||||
- uses: thollander/actions-comment-pull-request@v2
|
- uses: thollander/actions-comment-pull-request@v3
|
||||||
with:
|
with:
|
||||||
pr_number: ${{ steps.load-pr-num.outputs.pr-number }}
|
pr-number: ${{ steps.load-pr-num.outputs.pr-number }}
|
||||||
comment_tag: show_diff
|
comment-tag: show_diff
|
||||||
filePath: ./output.md
|
file-path: ./output.md
|
||||||
- name: Tell error to PR
|
- name: Tell error to PR
|
||||||
uses: thollander/actions-comment-pull-request@v2
|
uses: thollander/actions-comment-pull-request@v3
|
||||||
if: failure() && steps.load-pr-num.outputs.pr-number
|
if: failure() && steps.load-pr-num.outputs.pr-number
|
||||||
with:
|
with:
|
||||||
pr_number: ${{ steps.load-pr-num.outputs.pr-number }}
|
pr-number: ${{ steps.load-pr-num.outputs.pr-number }}
|
||||||
comment_tag: show_diff_error
|
comment-tag: show_diff_error
|
||||||
message: |
|
message: |
|
||||||
api.jsonの差分作成中にエラーが発生しました。詳細は[Workflowのログ](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})を確認してください。
|
api.jsonの差分作成中にエラーが発生しました。詳細は[Workflowのログ](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})を確認してください。
|
||||||
|
|||||||
16
.github/workflows/report-backend-memory.yml
vendored
16
.github/workflows/report-backend-memory.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download artifact
|
- name: Download artifact
|
||||||
uses: actions/github-script@v7.1.0
|
uses: actions/github-script@v8.0.0
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
@@ -107,16 +107,16 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$FOOTER" >> ./output.md
|
echo "$FOOTER" >> ./output.md
|
||||||
- uses: thollander/actions-comment-pull-request@v2
|
- uses: thollander/actions-comment-pull-request@v3
|
||||||
with:
|
with:
|
||||||
pr_number: ${{ steps.load-pr-num.outputs.pr-number }}
|
pr-number: ${{ steps.load-pr-num.outputs.pr-number }}
|
||||||
comment_tag: show_memory_diff
|
comment-tag: show_memory_diff
|
||||||
filePath: ./output.md
|
file-path: ./output.md
|
||||||
- name: Tell error to PR
|
- name: Tell error to PR
|
||||||
uses: thollander/actions-comment-pull-request@v2
|
uses: thollander/actions-comment-pull-request@v3
|
||||||
if: failure() && steps.load-pr-num.outputs.pr-number
|
if: failure() && steps.load-pr-num.outputs.pr-number
|
||||||
with:
|
with:
|
||||||
pr_number: ${{ steps.load-pr-num.outputs.pr-number }}
|
pr-number: ${{ steps.load-pr-num.outputs.pr-number }}
|
||||||
comment_tag: show_memory_diff_error
|
comment-tag: show_memory_diff_error
|
||||||
message: |
|
message: |
|
||||||
An error occurred while comparing backend memory usage. See [workflow logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.
|
An error occurred while comparing backend memory usage. See [workflow logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.
|
||||||
|
|||||||
2
.github/workflows/request-release-review.yml
vendored
2
.github/workflows/request-release-review.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Reply
|
- name: Reply
|
||||||
uses: actions/github-script@v6
|
uses: actions/github-script@v8
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const body = `To dev team (@misskey-dev/dev):
|
const body = `To dev team (@misskey-dev/dev):
|
||||||
|
|||||||
10
.github/workflows/storybook.yml
vendored
10
.github/workflows/storybook.yml
vendored
@@ -22,12 +22,12 @@ jobs:
|
|||||||
NODE_OPTIONS: "--max_old_space_size=7168"
|
NODE_OPTIONS: "--max_old_space_size=7168"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.3.0
|
- uses: actions/checkout@v6.0.1
|
||||||
if: github.event_name != 'pull_request_target'
|
if: github.event_name != 'pull_request_target'
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: true
|
submodules: true
|
||||||
- uses: actions/checkout@v4.3.0
|
- uses: actions/checkout@v6.0.1
|
||||||
if: github.event_name == 'pull_request_target'
|
if: github.event_name == 'pull_request_target'
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4.2.0
|
uses: pnpm/action-setup@v4.2.0
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -90,7 +90,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||||
- name: Notify that Chromatic detects changes
|
- name: Notify that Chromatic detects changes
|
||||||
uses: actions/github-script@v7.1.0
|
uses: actions/github-script@v8.0.0
|
||||||
if: github.event_name != 'pull_request_target' && steps.chromatic_push.outputs.success == 'false'
|
if: github.event_name != 'pull_request_target' && steps.chromatic_push.outputs.success == 'false'
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -102,7 +102,7 @@ jobs:
|
|||||||
body: 'Chromatic detects changes. Please [review the changes on Chromatic](https://www.chromatic.com/builds?appId=6428f7d7b962f0b79f97d6e4).'
|
body: 'Chromatic detects changes. Please [review the changes on Chromatic](https://www.chromatic.com/builds?appId=6428f7d7b962f0b79f97d6e4).'
|
||||||
})
|
})
|
||||||
- name: Upload Artifacts
|
- name: Upload Artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: storybook
|
name: storybook
|
||||||
path: packages/frontend/storybook-static
|
path: packages/frontend/storybook-static
|
||||||
|
|||||||
19
.github/workflows/test-backend.yml
vendored
19
.github/workflows/test-backend.yml
vendored
@@ -48,9 +48,16 @@ jobs:
|
|||||||
image: redis:7
|
image: redis:7
|
||||||
ports:
|
ports:
|
||||||
- 56312:6379
|
- 56312:6379
|
||||||
|
meilisearch:
|
||||||
|
image: getmeili/meilisearch:v1.3.4
|
||||||
|
ports:
|
||||||
|
- 57712:7700
|
||||||
|
env:
|
||||||
|
MEILI_NO_ANALYTICS: true
|
||||||
|
MEILI_ENV: development
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.3.0
|
- uses: actions/checkout@v6.0.1
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
@@ -86,7 +93,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ${{ matrix.node-version-file }}
|
node-version-file: ${{ matrix.node-version-file }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -129,13 +136,13 @@ jobs:
|
|||||||
- 56312:6379
|
- 56312:6379
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.3.0
|
- uses: actions/checkout@v6.0.1
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4.2.0
|
uses: pnpm/action-setup@v4.2.0
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ${{ matrix.node-version-file }}
|
node-version-file: ${{ matrix.node-version-file }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -173,7 +180,7 @@ jobs:
|
|||||||
POSTGRES_HOST_AUTH_METHOD: trust
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.3.0
|
- uses: actions/checkout@v6.0.1
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
@@ -182,7 +189,7 @@ jobs:
|
|||||||
id: current-date
|
id: current-date
|
||||||
run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ${{ matrix.node-version-file }}
|
node-version-file: ${{ matrix.node-version-file }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|||||||
4
.github/workflows/test-federation.yml
vendored
4
.github/workflows/test-federation.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
|||||||
- .node-version
|
- .node-version
|
||||||
- .github/min.node-version
|
- .github/min.node-version
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
@@ -68,7 +68,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ${{ matrix.node-version-file }}
|
node-version-file: ${{ matrix.node-version-file }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|||||||
12
.github/workflows/test-frontend.yml
vendored
12
.github/workflows/test-frontend.yml
vendored
@@ -28,13 +28,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.3.0
|
- uses: actions/checkout@v6.0.1
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4.2.0
|
uses: pnpm/action-setup@v4.2.0
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -76,7 +76,7 @@ jobs:
|
|||||||
- 56312:6379
|
- 56312:6379
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.3.0
|
- uses: actions/checkout@v6.0.1
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
# https://github.com/cypress-io/cypress-docker-images/issues/150
|
# https://github.com/cypress-io/cypress-docker-images/issues/150
|
||||||
@@ -88,7 +88,7 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4.2.0
|
uses: pnpm/action-setup@v4.2.0
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -113,12 +113,12 @@ jobs:
|
|||||||
wait-on: 'http://localhost:61812'
|
wait-on: 'http://localhost:61812'
|
||||||
headed: true
|
headed: true
|
||||||
browser: ${{ matrix.browser }}
|
browser: ${{ matrix.browser }}
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v6
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.browser }}-cypress-screenshots
|
name: ${{ matrix.browser }}-cypress-screenshots
|
||||||
path: cypress/screenshots
|
path: cypress/screenshots
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v6
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.browser }}-cypress-videos
|
name: ${{ matrix.browser }}-cypress-videos
|
||||||
|
|||||||
4
.github/workflows/test-misskey-js.yml
vendored
4
.github/workflows/test-misskey-js.yml
vendored
@@ -22,13 +22,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4.3.0
|
uses: actions/checkout@v6.0.1
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4.2.0
|
uses: pnpm/action-setup@v4.2.0
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|||||||
4
.github/workflows/test-production.yml
vendored
4
.github/workflows/test-production.yml
vendored
@@ -16,13 +16,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.3.0
|
- uses: actions/checkout@v6.0.1
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4.2.0
|
uses: pnpm/action-setup@v4.2.0
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|||||||
4
.github/workflows/validate-api-json.yml
vendored
4
.github/workflows/validate-api-json.yml
vendored
@@ -17,13 +17,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.3.0
|
- uses: actions/checkout@v6.0.1
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4.2.0
|
uses: pnpm/action-setup@v4.2.0
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -46,6 +46,7 @@ docker-compose.yml
|
|||||||
built
|
built
|
||||||
built-test
|
built-test
|
||||||
js-built
|
js-built
|
||||||
|
src-js
|
||||||
/data
|
/data
|
||||||
/.cache-loader
|
/.cache-loader
|
||||||
/db
|
/db
|
||||||
|
|||||||
60
CHANGELOG.md
60
CHANGELOG.md
@@ -1,11 +1,67 @@
|
|||||||
## Unreleased
|
## 2026.1.0
|
||||||
|
|
||||||
|
### Note
|
||||||
|
- `users/following` の `birthday` プロパティは非推奨になりました。代わりに `users/get-following-birthday-users` をご利用ください。
|
||||||
|
|
||||||
### General
|
### General
|
||||||
-
|
- Enhance: 「もうすぐ誕生日のユーザー」ウィジェットで、誕生日が至近のユーザーも表示できるように
|
||||||
|
(Cherry-picked from https://github.com/MisskeyIO/misskey)
|
||||||
|
- 「今日誕生日のユーザー」は「もうすぐ誕生日のユーザー」に名称変更されました
|
||||||
|
- 依存関係の更新
|
||||||
|
|
||||||
|
### Client
|
||||||
|
- Enhance: ドライブのファイル一覧で自動でもっと見るを利用可能に
|
||||||
|
- Enhance: ウィジェットの表示設定をプレビューを見ながら行えるように
|
||||||
|
- Enhance: ウィジェットの設定項目のラベルの多言語対応
|
||||||
|
- Fix: ドライブクリーナーでファイルを削除しても画面に反映されない問題を修正 #16061
|
||||||
|
- Fix: 非ログイン時にログインを求めるダイアログが表示された後にダイアログのぼかしが解除されず操作不能になることがある問題を修正
|
||||||
|
- Fix: ドライブのソートが「登録日(昇順)」の場合に正しく動作しない問題を修正
|
||||||
|
- Fix: 管理画面でアーカイブ済のお知らせを表示した際にアクティブなお知らせが多い旨の警告が出る問題を修正
|
||||||
|
- Fix: ファイルタブのセンシティブメディアを開く際に確認ダイアログを出す設定が適用されない問題を修正
|
||||||
|
|
||||||
|
### Server
|
||||||
|
- Enhance: OAuthのクライアント情報取得(Client Information Discovery)において、IndieWeb Living Standard 11 July 2024で定義されているJSONドキュメント形式に対応しました
|
||||||
|
- JSONによるClient Information Discoveryを行うには、レスポンスの`Content-Type`ヘッダーが`application/json`である必要があります
|
||||||
|
- 従来の実装(12 February 2022版・HTML Microformat形式)も引き続きサポートされます
|
||||||
|
- Enhance: メモリ使用量を削減
|
||||||
|
|
||||||
|
## 2025.12.2
|
||||||
|
|
||||||
|
### Note
|
||||||
|
v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`に変更」について、正しく環境に応じた設定を行わないとサインインが困難になるといった状態を緩和するために、以下の対応を行いました。
|
||||||
|
|
||||||
|
**正しく設定しないと、上記のような不具合の原因となったり、セキュリティリスクが高まったりする可能性があります。必ず現在のconfigをご確認の上、必要に応じて値を変更してください。**
|
||||||
|
|
||||||
|
- `trustProxy`について、デフォルト(configに値が設定されていない状態)ではループバックアドレスとローカルIPアドレス空間を信頼するようにしました。
|
||||||
|
- `trustProxy`の設定方法について、より詳細に記述しました。
|
||||||
|
- リバースプロキシやCDNなどのより上流のレイヤでレートリミットを設定したい場合や、緊急時の一時的な緩和策として、Misskey内部でのIPアドレスペースでのレートリミットを無効化できるようにしました。
|
||||||
|
|
||||||
|
### General
|
||||||
|
- 依存関係の更新
|
||||||
|
|
||||||
|
### Client
|
||||||
|
- Enhance: デッキのUI説明を追加
|
||||||
|
- Enhance: 設定がブラウザによって消去されないようにするオプションを追加
|
||||||
|
- Fix: バージョン表記のないPlayが正しく動作しない問題を修正
|
||||||
|
バージョン表記のないものは v0.x 系として実行されます。v1.x 系で動作させたい場合は必ずバージョン表記を含めてください。
|
||||||
|
- Fix: デッキUIでメニュー位置を下にしているとプロファイル削除ボタンが表示されないのを修正
|
||||||
|
- Fix: 一部のUnicode絵文字のリアクションがボタンにならない問題を修正
|
||||||
|
|
||||||
|
### Server
|
||||||
|
- Enhance: Misskey内部でのIPアドレスペースでのレートリミットを無効化できるように
|
||||||
|
- リバースプロキシやCDNなど別のレイヤで別途レートリミットを設定する場合や、ローカルでのテスト用途等として利用することを想定しています。
|
||||||
|
- デフォルトは `enableIpRateLimit: true`(Misskey内部でのIPアドレスペースでのレートリミットは有効)です。
|
||||||
|
- Fix: コントロールパネルのジョブキューページで使用される一部APIの応答速度を改善
|
||||||
|
|
||||||
|
## 2025.12.1
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
- Fix: 特定の条件下でMisskeyが起動せず空白のページが表示されることがある問題を軽減
|
- Fix: 特定の条件下でMisskeyが起動せず空白のページが表示されることがある問題を軽減
|
||||||
- Fix: 初回読み込み時などに、言語設定で不整合が発生することがある問題を修正
|
- Fix: 初回読み込み時などに、言語設定で不整合が発生することがある問題を修正
|
||||||
|
- Fix: 削除されたノートのリノートが正しく動作されない問題を修正
|
||||||
|
- Fix: チャンネルオーナーが削除済みの時にチャンネルのヘッダーメニューが表示されない不具合を修正
|
||||||
|
- Fix: ドライブで登録日以外でソートする場合は月でグループ化して表示しないように
|
||||||
|
- Fix: `null` を返す note_view_intrruptor プラグインが動作しない問題を修正
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
- Fix: ジョブキューでSentryが有効にならない問題を修正
|
- Fix: ジョブキューでSentryが有効にならない問題を修正
|
||||||
|
|||||||
2
COPYING
2
COPYING
@@ -1,5 +1,5 @@
|
|||||||
Unless otherwise stated this repository is
|
Unless otherwise stated this repository is
|
||||||
Copyright © 2014-2025 syuilo and contributors
|
Copyright © 2014-2026 syuilo and contributors
|
||||||
|
|
||||||
And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE.
|
And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE.
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-js/
|
|||||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-reversi/built ./packages/misskey-reversi/built
|
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-reversi/built ./packages/misskey-reversi/built
|
||||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-bubble-game/built ./packages/misskey-bubble-game/built
|
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-bubble-game/built ./packages/misskey-bubble-game/built
|
||||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/built ./packages/backend/built
|
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/built ./packages/backend/built
|
||||||
|
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/src-js ./packages/backend/src-js
|
||||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/i18n/built ./packages/i18n/built
|
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/i18n/built ./packages/i18n/built
|
||||||
COPY --chown=misskey:misskey --from=native-builder /misskey/fluent-emojis /misskey/fluent-emojis
|
COPY --chown=misskey:misskey --from=native-builder /misskey/fluent-emojis /misskey/fluent-emojis
|
||||||
COPY --chown=misskey:misskey . ./
|
COPY --chown=misskey:misskey . ./
|
||||||
|
|||||||
@@ -26,6 +26,8 @@
|
|||||||
|
|
||||||
[](https://deepwiki.com/misskey-dev/misskey)
|
[](https://deepwiki.com/misskey-dev/misskey)
|
||||||
|
|
||||||
|
<a href="https://flatt.tech/oss/gmo/trampoline" target="_blank"><img src="https://flatt.tech/assets/images/badges/gmo-oss.svg" height="24px"/></a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Thanks
|
## Thanks
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .config/docker.env
|
- .config/docker.env
|
||||||
volumes:
|
volumes:
|
||||||
- ./db:/var/lib/postgresql/data
|
- ./db:/var/lib/postgresql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"
|
test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"
|
||||||
interval: 5s
|
interval: 5s
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .config/docker.env
|
- .config/docker.env
|
||||||
volumes:
|
volumes:
|
||||||
- ./db:/var/lib/postgresql/data
|
- ./db:/var/lib/postgresql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"
|
test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"
|
||||||
interval: 5s
|
interval: 5s
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ files: "Dateien"
|
|||||||
download: "Herunterladen"
|
download: "Herunterladen"
|
||||||
driveFileDeleteConfirm: "Möchtest du die Datei „{name}“ wirklich löschen? Einige Inhalte, die diese Datei verwenden, werden auch verschwinden."
|
driveFileDeleteConfirm: "Möchtest du die Datei „{name}“ wirklich löschen? Einige Inhalte, die diese Datei verwenden, werden auch verschwinden."
|
||||||
unfollowConfirm: "Möchtest du {name} wirklich nicht mehr folgen?"
|
unfollowConfirm: "Möchtest du {name} wirklich nicht mehr folgen?"
|
||||||
|
rejectFollowRequestConfirm: "Möchtest du die Follow-Anfrage von {name} ablehnen?"
|
||||||
exportRequested: "Du hast einen Export angefragt. Dies kann etwas Zeit in Anspruch nehmen. Sobald der Export abgeschlossen ist, wird er deiner Drive hinzugefügt."
|
exportRequested: "Du hast einen Export angefragt. Dies kann etwas Zeit in Anspruch nehmen. Sobald der Export abgeschlossen ist, wird er deiner Drive hinzugefügt."
|
||||||
importRequested: "Du hast einen Import angefragt. Dies kann etwas Zeit in Anspruch nehmen."
|
importRequested: "Du hast einen Import angefragt. Dies kann etwas Zeit in Anspruch nehmen."
|
||||||
lists: "Listen"
|
lists: "Listen"
|
||||||
@@ -1018,6 +1019,7 @@ pushNotificationAlreadySubscribed: "Push-Benachrichtigungen sind bereits aktivie
|
|||||||
pushNotificationNotSupported: "Entweder dein Browser oder deine Instanz unterstützt Push-Benachrichtigungen nicht"
|
pushNotificationNotSupported: "Entweder dein Browser oder deine Instanz unterstützt Push-Benachrichtigungen nicht"
|
||||||
sendPushNotificationReadMessage: "Push-Benachrichtigungen löschen, sobald sie gelesen wurden"
|
sendPushNotificationReadMessage: "Push-Benachrichtigungen löschen, sobald sie gelesen wurden"
|
||||||
sendPushNotificationReadMessageCaption: "Dies kann gegebenenfalls den Batterieverbrauch deines Gerätes erhöhen."
|
sendPushNotificationReadMessageCaption: "Dies kann gegebenenfalls den Batterieverbrauch deines Gerätes erhöhen."
|
||||||
|
pleaseAllowPushNotification: "Bitte erlauben Sie Benachrichtigungen in Ihrem Browser."
|
||||||
windowMaximize: "Maximieren"
|
windowMaximize: "Maximieren"
|
||||||
windowMinimize: "Minimieren"
|
windowMinimize: "Minimieren"
|
||||||
windowRestore: "Wiederherstellen"
|
windowRestore: "Wiederherstellen"
|
||||||
@@ -1054,6 +1056,7 @@ permissionDeniedError: "Aktion verweigert"
|
|||||||
permissionDeniedErrorDescription: "Dieses Benutzerkonto besitzt nicht die Berechtigung, um diese Aktion auszuführen."
|
permissionDeniedErrorDescription: "Dieses Benutzerkonto besitzt nicht die Berechtigung, um diese Aktion auszuführen."
|
||||||
preset: "Vorlage"
|
preset: "Vorlage"
|
||||||
selectFromPresets: "Aus Vorlagen wählen"
|
selectFromPresets: "Aus Vorlagen wählen"
|
||||||
|
custom: "Benutzerdefiniert"
|
||||||
achievements: "Errungenschaften"
|
achievements: "Errungenschaften"
|
||||||
gotInvalidResponseError: "Ungültige Antwort des Servers"
|
gotInvalidResponseError: "Ungültige Antwort des Servers"
|
||||||
gotInvalidResponseErrorDescription: "Eventuell ist der Server momentan nicht erreichbar oder untergeht Wartungsarbeiten. Bitte versuche es später noch einmal."
|
gotInvalidResponseErrorDescription: "Eventuell ist der Server momentan nicht erreichbar oder untergeht Wartungsarbeiten. Bitte versuche es später noch einmal."
|
||||||
@@ -1243,6 +1246,7 @@ releaseToRefresh: "Zum Aktualisieren loslassen"
|
|||||||
refreshing: "Wird aktualisiert..."
|
refreshing: "Wird aktualisiert..."
|
||||||
pullDownToRefresh: "Zum Aktualisieren ziehen"
|
pullDownToRefresh: "Zum Aktualisieren ziehen"
|
||||||
useGroupedNotifications: "Benachrichtigungen gruppieren"
|
useGroupedNotifications: "Benachrichtigungen gruppieren"
|
||||||
|
emailVerificationFailedError: "Es gab ein Problem bei der Überprüfung Ihrer E-Mail-Adresse. Der Link ist möglicherweise abgelaufen."
|
||||||
cwNotationRequired: "Ist \"Inhaltswarnung verwenden\" aktiviert, muss eine Beschreibung gegeben werden."
|
cwNotationRequired: "Ist \"Inhaltswarnung verwenden\" aktiviert, muss eine Beschreibung gegeben werden."
|
||||||
doReaction: "Reagieren"
|
doReaction: "Reagieren"
|
||||||
code: "Code"
|
code: "Code"
|
||||||
@@ -1370,7 +1374,12 @@ defaultImageCompressionLevel: "Standard-Bildkomprimierungsstufe"
|
|||||||
defaultImageCompressionLevel_description: "Ein niedrigerer Wert erhält die Bildqualität, erhöht aber die Dateigröße. <br>Höhere Werte reduzieren die Dateigröße, verringern aber die Bildqualität."
|
defaultImageCompressionLevel_description: "Ein niedrigerer Wert erhält die Bildqualität, erhöht aber die Dateigröße. <br>Höhere Werte reduzieren die Dateigröße, verringern aber die Bildqualität."
|
||||||
inMinutes: "Minute(n)"
|
inMinutes: "Minute(n)"
|
||||||
inDays: "Tag(en)"
|
inDays: "Tag(en)"
|
||||||
|
safeModeEnabled: "Der abgesicherte Modus ist aktiviert."
|
||||||
|
schedule: "Planen"
|
||||||
|
scheduled: "Geplant"
|
||||||
widgets: "Widgets"
|
widgets: "Widgets"
|
||||||
|
deviceInfo: "Geräteinformation"
|
||||||
|
youAreAdmin: "Sie sind ein Administrator"
|
||||||
presets: "Vorlage"
|
presets: "Vorlage"
|
||||||
_imageEditing:
|
_imageEditing:
|
||||||
_vars:
|
_vars:
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ files: "Files"
|
|||||||
download: "Download"
|
download: "Download"
|
||||||
driveFileDeleteConfirm: "Are you sure you want to delete \"{name}\"? All notes with this file attached will also be deleted."
|
driveFileDeleteConfirm: "Are you sure you want to delete \"{name}\"? All notes with this file attached will also be deleted."
|
||||||
unfollowConfirm: "Are you sure you want to unfollow {name}?"
|
unfollowConfirm: "Are you sure you want to unfollow {name}?"
|
||||||
|
cancelFollowRequestConfirm: "Are you sure that you want to cancel your follow request to {name}?"
|
||||||
|
rejectFollowRequestConfirm: "Are you sure that you want to reject the follow request from {name}?"
|
||||||
exportRequested: "You've requested an export. This may take a while. It will be added to your Drive once completed."
|
exportRequested: "You've requested an export. This may take a while. It will be added to your Drive once completed."
|
||||||
importRequested: "You've requested an import. This may take a while."
|
importRequested: "You've requested an import. This may take a while."
|
||||||
lists: "Lists"
|
lists: "Lists"
|
||||||
|
|||||||
@@ -1252,7 +1252,7 @@ detachAll: "Quitar todo"
|
|||||||
angle: "Ángulo"
|
angle: "Ángulo"
|
||||||
flip: "Echar de un capirotazo"
|
flip: "Echar de un capirotazo"
|
||||||
showAvatarDecorations: "Mostrar decoraciones de avatar"
|
showAvatarDecorations: "Mostrar decoraciones de avatar"
|
||||||
releaseToRefresh: "Soltar para recargar"
|
releaseToRefresh: "Suelta para recargar"
|
||||||
refreshing: "Recargando..."
|
refreshing: "Recargando..."
|
||||||
pullDownToRefresh: "Tira hacia abajo para recargar"
|
pullDownToRefresh: "Tira hacia abajo para recargar"
|
||||||
useGroupedNotifications: "Mostrar notificaciones agrupadas"
|
useGroupedNotifications: "Mostrar notificaciones agrupadas"
|
||||||
|
|||||||
@@ -1406,6 +1406,7 @@ youAreAdmin: "あなたは管理者です"
|
|||||||
frame: "フレーム"
|
frame: "フレーム"
|
||||||
presets: "プリセット"
|
presets: "プリセット"
|
||||||
zeroPadding: "ゼロ埋め"
|
zeroPadding: "ゼロ埋め"
|
||||||
|
nothingToConfigure: "設定項目はありません"
|
||||||
|
|
||||||
_imageEditing:
|
_imageEditing:
|
||||||
_vars:
|
_vars:
|
||||||
@@ -1557,6 +1558,9 @@ _settings:
|
|||||||
showPageTabBarBottom: "ページのタブバーを下部に表示"
|
showPageTabBarBottom: "ページのタブバーを下部に表示"
|
||||||
emojiPaletteBanner: "絵文字ピッカーに固定表示するプリセットをパレットとして登録したり、ピッカーの表示方法をカスタマイズしたりできます。"
|
emojiPaletteBanner: "絵文字ピッカーに固定表示するプリセットをパレットとして登録したり、ピッカーの表示方法をカスタマイズしたりできます。"
|
||||||
enableAnimatedImages: "アニメーション画像を有効にする"
|
enableAnimatedImages: "アニメーション画像を有効にする"
|
||||||
|
settingsPersistence_title: "設定の永続化"
|
||||||
|
settingsPersistence_description1: "設定の永続化を有効にすると、設定情報が失われるのを防止できます。"
|
||||||
|
settingsPersistence_description2: "環境によっては有効化できない場合があります。"
|
||||||
|
|
||||||
_chat:
|
_chat:
|
||||||
showSenderName: "送信者の名前を表示"
|
showSenderName: "送信者の名前を表示"
|
||||||
@@ -2596,9 +2600,48 @@ _widgets:
|
|||||||
_userList:
|
_userList:
|
||||||
chooseList: "リストを選択"
|
chooseList: "リストを選択"
|
||||||
clicker: "クリッカー"
|
clicker: "クリッカー"
|
||||||
birthdayFollowings: "今日誕生日のユーザー"
|
birthdayFollowings: "もうすぐ誕生日のユーザー"
|
||||||
chat: "ダイレクトメッセージ"
|
chat: "ダイレクトメッセージ"
|
||||||
|
|
||||||
|
_widgetOptions:
|
||||||
|
showHeader: "ヘッダーを表示"
|
||||||
|
transparent: "背景を透明にする"
|
||||||
|
height: "高さ"
|
||||||
|
_button:
|
||||||
|
colored: "色付き"
|
||||||
|
_clock:
|
||||||
|
size: "サイズ"
|
||||||
|
thickness: "針の太さ"
|
||||||
|
thicknessThin: "細い"
|
||||||
|
thicknessMedium: "普通"
|
||||||
|
thicknessThick: "太い"
|
||||||
|
graduations: "文字盤の目盛り"
|
||||||
|
graduationDots: "ドット"
|
||||||
|
graduationArabic: "アラビア数字"
|
||||||
|
fadeGraduations: "目盛りをフェード"
|
||||||
|
sAnimation: "秒針のアニメーション"
|
||||||
|
sAnimationElastic: "リアル"
|
||||||
|
sAnimationEaseOut: "滑らか"
|
||||||
|
twentyFour: "24時間表示"
|
||||||
|
labelTime: "時刻"
|
||||||
|
labelTz: "タイムゾーン"
|
||||||
|
labelTimeAndTz: "時刻とタイムゾーン"
|
||||||
|
timezone: "タイムゾーン"
|
||||||
|
showMs: "ミリ秒を表示"
|
||||||
|
showLabel: "ラベルを表示"
|
||||||
|
_jobQueue:
|
||||||
|
sound: "音を鳴らす"
|
||||||
|
_rss:
|
||||||
|
url: "RSSフィードのURL"
|
||||||
|
refreshIntervalSec: "更新間隔(秒)"
|
||||||
|
maxEntries: "最大表示件数"
|
||||||
|
_rssTicker:
|
||||||
|
shuffle: "表示順をシャッフル"
|
||||||
|
duration: "ティッカーのスクロール速度(秒)"
|
||||||
|
reverse: "逆方向にスクロール"
|
||||||
|
_birthdayFollowings:
|
||||||
|
period: "期間"
|
||||||
|
|
||||||
_cw:
|
_cw:
|
||||||
hide: "隠す"
|
hide: "隠す"
|
||||||
show: "もっと見る"
|
show: "もっと見る"
|
||||||
@@ -2890,6 +2933,15 @@ _deck:
|
|||||||
usedAsMinWidthWhenFlexible: "「幅を自動調整」が有効の場合、これが幅の最小値となります"
|
usedAsMinWidthWhenFlexible: "「幅を自動調整」が有効の場合、これが幅の最小値となります"
|
||||||
flexible: "幅を自動調整"
|
flexible: "幅を自動調整"
|
||||||
enableSyncBetweenDevicesForProfiles: "プロファイル情報のデバイス間同期を有効にする"
|
enableSyncBetweenDevicesForProfiles: "プロファイル情報のデバイス間同期を有効にする"
|
||||||
|
showHowToUse: "UIの説明を見る"
|
||||||
|
|
||||||
|
_howToUse:
|
||||||
|
addColumn_title: "カラム追加"
|
||||||
|
addColumn_description: "カラムの種類を選んで追加できます。"
|
||||||
|
settings_title: "UI設定"
|
||||||
|
settings_description: "デッキUIの詳細設定を行えます。"
|
||||||
|
switchProfile_title: "プロファイル切り替え"
|
||||||
|
switchProfile_description: "UIのレイアウトをプロファイルとして保存し、いつでも切り替えられるようにできます。"
|
||||||
|
|
||||||
_columns:
|
_columns:
|
||||||
main: "メイン"
|
main: "メイン"
|
||||||
@@ -3406,7 +3458,6 @@ _imageEffector:
|
|||||||
title: "エフェクト"
|
title: "エフェクト"
|
||||||
addEffect: "エフェクトを追加"
|
addEffect: "エフェクトを追加"
|
||||||
discardChangesConfirm: "変更を破棄して終了しますか?"
|
discardChangesConfirm: "変更を破棄して終了しますか?"
|
||||||
nothingToConfigure: "設定項目はありません"
|
|
||||||
failedToLoadImage: "画像の読み込みに失敗しました"
|
failedToLoadImage: "画像の読み込みに失敗しました"
|
||||||
|
|
||||||
_fxs:
|
_fxs:
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ copyRemoteLink: "复制远程链接"
|
|||||||
copyLinkRenote: "复制转帖链接"
|
copyLinkRenote: "复制转帖链接"
|
||||||
delete: "删除"
|
delete: "删除"
|
||||||
deleteAndEdit: "删除并编辑"
|
deleteAndEdit: "删除并编辑"
|
||||||
deleteAndEditConfirm: "要删除此帖并再次编辑吗?对此帖的所有回应、转发和回复也将被删除。"
|
deleteAndEditConfirm: "要删除此帖并再次编辑吗?此帖下所有的回应、转发和回复也将被删除。"
|
||||||
addToList: "添加至列表"
|
addToList: "添加至列表"
|
||||||
addToAntenna: "添加到天线"
|
addToAntenna: "添加到天线"
|
||||||
sendMessage: "发送消息"
|
sendMessage: "发送消息"
|
||||||
@@ -136,7 +136,7 @@ emojiPicker: "表情符号选择器"
|
|||||||
pinnedEmojisForReactionSettingDescription: "可以设置发表回应时置顶显示的表情符号"
|
pinnedEmojisForReactionSettingDescription: "可以设置发表回应时置顶显示的表情符号"
|
||||||
pinnedEmojisSettingDescription: "可以设置输入表情符号时置顶显示的表情符号"
|
pinnedEmojisSettingDescription: "可以设置输入表情符号时置顶显示的表情符号"
|
||||||
emojiPickerDisplay: "选择器显示设置"
|
emojiPickerDisplay: "选择器显示设置"
|
||||||
overwriteFromPinnedEmojisForReaction: "从「置顶(回应)」设置覆盖"
|
overwriteFromPinnedEmojisForReaction: "使用「置顶(回应)」设置覆盖"
|
||||||
overwriteFromPinnedEmojis: "从全局设置覆盖"
|
overwriteFromPinnedEmojis: "从全局设置覆盖"
|
||||||
reactionSettingDescription2: "拖动重新排序,单击删除,点击 + 添加。"
|
reactionSettingDescription2: "拖动重新排序,单击删除,点击 + 添加。"
|
||||||
rememberNoteVisibility: "保存上次设置的可见性"
|
rememberNoteVisibility: "保存上次设置的可见性"
|
||||||
@@ -153,8 +153,8 @@ block: "拉黑"
|
|||||||
unblock: "取消拉黑"
|
unblock: "取消拉黑"
|
||||||
suspend: "冻结"
|
suspend: "冻结"
|
||||||
unsuspend: "解除冻结"
|
unsuspend: "解除冻结"
|
||||||
blockConfirm: "确定要拉黑吗?"
|
blockConfirm: "确定要屏蔽吗?"
|
||||||
unblockConfirm: "确定要取消拉黑吗?"
|
unblockConfirm: "确定要取消屏蔽吗?"
|
||||||
suspendConfirm: "要冻结吗?"
|
suspendConfirm: "要冻结吗?"
|
||||||
unsuspendConfirm: "要解除冻结吗?"
|
unsuspendConfirm: "要解除冻结吗?"
|
||||||
selectList: "选择列表"
|
selectList: "选择列表"
|
||||||
@@ -174,7 +174,7 @@ emojiUrl: "emoji 地址"
|
|||||||
addEmoji: "添加表情符号"
|
addEmoji: "添加表情符号"
|
||||||
settingGuide: "推荐配置"
|
settingGuide: "推荐配置"
|
||||||
cacheRemoteFiles: "缓存远程文件"
|
cacheRemoteFiles: "缓存远程文件"
|
||||||
cacheRemoteFilesDescription: "启用此设定时,将在此服务器上缓存远程文件。虽然可以加快图片显示的速度,但是相对的会消耗大量的服务器存储空间。用户角色内的网盘容量决定了这个远程用户能在服务器上保留多少缓存。当超出了这个限制时,旧的文件将从缓存中被删除,成为链接。当禁用此设定时,则是从一开始就将远程文件保留为链接。此时推荐将 default.yml 的 proxyRemoteFiles 设置为 true 以优化缩略图生成及保护用户隐私。"
|
cacheRemoteFilesDescription: "启用此设定时,将在此服务器上缓存远程文件。虽然可以加快图片显示的速度,但是相对的会消耗大量的服务器存储空间。用户角色内的网盘容量决定了这个远程用户能在服务器上保留多少缓存。当超出了这个限制时,旧的文件将从缓存中被删除,成为链接。当禁用此设定时,则是从一开始就将远程文件保留为链接。此时推荐将 的 proxyRemoteFiles 设置为 true 以优化缩略图生成及保护用户隐私。"
|
||||||
youCanCleanRemoteFilesCache: "可以使用文件管理的🗑️按钮来删除所有的缓存。"
|
youCanCleanRemoteFilesCache: "可以使用文件管理的🗑️按钮来删除所有的缓存。"
|
||||||
cacheRemoteSensitiveFiles: "缓存远程敏感媒体文件"
|
cacheRemoteSensitiveFiles: "缓存远程敏感媒体文件"
|
||||||
cacheRemoteSensitiveFilesDescription: "如果禁用这项设定,远程服务器的敏感媒体将不会被缓存,而是直接链接。"
|
cacheRemoteSensitiveFilesDescription: "如果禁用这项设定,远程服务器的敏感媒体将不会被缓存,而是直接链接。"
|
||||||
@@ -184,7 +184,7 @@ flagAsCat: "喵!!!!!!!!!!!!"
|
|||||||
flagAsCatDescription: "喵喵喵??"
|
flagAsCatDescription: "喵喵喵??"
|
||||||
flagShowTimelineReplies: "在时间线上显示帖子的回复"
|
flagShowTimelineReplies: "在时间线上显示帖子的回复"
|
||||||
flagShowTimelineRepliesDescription: "启用时,时间线除了显示用户的帖子外,还会显示其他用户对帖子的回复。"
|
flagShowTimelineRepliesDescription: "启用时,时间线除了显示用户的帖子外,还会显示其他用户对帖子的回复。"
|
||||||
autoAcceptFollowed: "自动允许来自我关注的用户对我的关注请求"
|
autoAcceptFollowed: "自动允许回关请求"
|
||||||
addAccount: "添加账户"
|
addAccount: "添加账户"
|
||||||
reloadAccountsList: "更新账户列表"
|
reloadAccountsList: "更新账户列表"
|
||||||
loginFailed: "登录失败"
|
loginFailed: "登录失败"
|
||||||
@@ -247,8 +247,8 @@ mediaSilencedInstancesDescription: "设置要隐藏媒体文件的服务器,
|
|||||||
federationAllowedHosts: "允许联合的服务器"
|
federationAllowedHosts: "允许联合的服务器"
|
||||||
federationAllowedHostsDescription: "设定允许联合的服务器,以换行分隔。"
|
federationAllowedHostsDescription: "设定允许联合的服务器,以换行分隔。"
|
||||||
muteAndBlock: "屏蔽/拉黑"
|
muteAndBlock: "屏蔽/拉黑"
|
||||||
mutedUsers: "已屏蔽用户"
|
mutedUsers: "已静音的用户"
|
||||||
blockedUsers: "已拉黑的用户"
|
blockedUsers: "已屏蔽的用户"
|
||||||
noUsers: "无用户"
|
noUsers: "无用户"
|
||||||
editProfile: "编辑资料"
|
editProfile: "编辑资料"
|
||||||
noteDeleteConfirm: "确定要删除该帖子吗?"
|
noteDeleteConfirm: "确定要删除该帖子吗?"
|
||||||
@@ -1344,7 +1344,7 @@ skip: "跳过"
|
|||||||
restore: "恢复"
|
restore: "恢复"
|
||||||
syncBetweenDevices: "设备间同步"
|
syncBetweenDevices: "设备间同步"
|
||||||
preferenceSyncConflictTitle: "服务器上已存在设定值"
|
preferenceSyncConflictTitle: "服务器上已存在设定值"
|
||||||
preferenceSyncConflictText: "服务器上已有此设置的设定值。要覆盖哪个设定值?"
|
preferenceSyncConflictText: "即将保存设定值到服务器,但检测到服务器上已有此设置的设定值。要使用哪个设定值?"
|
||||||
preferenceSyncConflictChoiceMerge: "合并"
|
preferenceSyncConflictChoiceMerge: "合并"
|
||||||
preferenceSyncConflictChoiceServer: "服务器上的设定值"
|
preferenceSyncConflictChoiceServer: "服务器上的设定值"
|
||||||
preferenceSyncConflictChoiceDevice: "设备上的设定值"
|
preferenceSyncConflictChoiceDevice: "设备上的设定值"
|
||||||
@@ -3270,7 +3270,7 @@ _watermarkEditor:
|
|||||||
driveFileTypeWarn: "不支持此文件"
|
driveFileTypeWarn: "不支持此文件"
|
||||||
driveFileTypeWarnDescription: "请选择图像文件"
|
driveFileTypeWarnDescription: "请选择图像文件"
|
||||||
title: "编辑水印"
|
title: "编辑水印"
|
||||||
cover: "覆盖全体"
|
cover: "覆盖所有"
|
||||||
repeat: "平铺"
|
repeat: "平铺"
|
||||||
preserveBoundingRect: "调整为旋转时不超出范围"
|
preserveBoundingRect: "调整为旋转时不超出范围"
|
||||||
opacity: "不透明度"
|
opacity: "不透明度"
|
||||||
|
|||||||
36
package.json
36
package.json
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "2025.12.0",
|
"version": "2026.1.0-alpha.0",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/misskey-dev/misskey.git"
|
"url": "https://github.com/misskey-dev/misskey.git"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.24.0",
|
"packageManager": "pnpm@10.26.2",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/misskey-js",
|
"packages/misskey-js",
|
||||||
"packages/i18n",
|
"packages/i18n",
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
"build": "pnpm build-pre && pnpm -r build && pnpm build-assets",
|
"build": "pnpm build-pre && pnpm -r build && pnpm build-assets",
|
||||||
"build-storybook": "pnpm --filter frontend build-storybook",
|
"build-storybook": "pnpm --filter frontend build-storybook",
|
||||||
"build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api",
|
"build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api",
|
||||||
"start": "pnpm check:connect && cd packages/backend && pnpm compile-config && node ./built/boot/entry.js",
|
"start": "cd packages/backend && pnpm compile-config && node ./built/boot/entry.js",
|
||||||
"start:inspect": "cd packages/backend && pnpm compile-config && node --inspect ./built/boot/entry.js",
|
"start:inspect": "cd packages/backend && pnpm compile-config && node --inspect ./built/boot/entry.js",
|
||||||
"start:test": "ncp ./.github/misskey/test.yml ./.config/test.yml && cd packages/backend && cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./built/boot/entry.js",
|
"start:test": "ncp ./.github/misskey/test.yml ./.config/test.yml && cd packages/backend && cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./built/boot/entry.js",
|
||||||
"cli": "cd packages/backend && pnpm cli",
|
"cli": "cd packages/backend && pnpm cli",
|
||||||
@@ -53,40 +53,36 @@
|
|||||||
"cleanall": "pnpm clean-all"
|
"cleanall": "pnpm clean-all"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"chokidar": "4.0.3",
|
"chokidar": "5.0.0",
|
||||||
"lodash": "4.17.21"
|
"lodash": "4.17.21"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssnano": "7.1.2",
|
"cssnano": "7.1.2",
|
||||||
"esbuild": "0.27.0",
|
"esbuild": "0.27.2",
|
||||||
"execa": "9.6.0",
|
"execa": "9.6.1",
|
||||||
"fast-glob": "3.3.3",
|
|
||||||
"glob": "13.0.0",
|
|
||||||
"ignore-walk": "8.0.0",
|
"ignore-walk": "8.0.0",
|
||||||
"js-yaml": "4.1.1",
|
"js-yaml": "4.1.1",
|
||||||
"postcss": "8.5.6",
|
"postcss": "8.5.6",
|
||||||
"tar": "7.5.2",
|
"tar": "7.5.2",
|
||||||
"terser": "5.44.1",
|
"terser": "5.44.1"
|
||||||
"typescript": "5.9.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "9.39.1",
|
"@eslint/js": "9.39.2",
|
||||||
"@misskey-dev/eslint-plugin": "2.2.0",
|
"@misskey-dev/eslint-plugin": "2.2.0",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/node": "24.10.1",
|
"@types/node": "24.10.4",
|
||||||
"@typescript-eslint/eslint-plugin": "8.47.0",
|
"@typescript-eslint/eslint-plugin": "8.50.1",
|
||||||
"@typescript-eslint/parser": "8.47.0",
|
"@typescript-eslint/parser": "8.50.1",
|
||||||
|
"@typescript/native-preview": "7.0.0-dev.20251226.1",
|
||||||
"cross-env": "10.1.0",
|
"cross-env": "10.1.0",
|
||||||
"cypress": "15.7.0",
|
"cypress": "15.8.1",
|
||||||
"eslint": "9.39.1",
|
"eslint": "9.39.2",
|
||||||
"globals": "16.5.0",
|
"globals": "16.5.0",
|
||||||
"ncp": "2.0.0",
|
"ncp": "2.0.0",
|
||||||
"pnpm": "10.24.0",
|
"pnpm": "10.26.2",
|
||||||
|
"typescript": "5.9.3",
|
||||||
"start-server-and-test": "2.1.3"
|
"start-server-and-test": "2.1.3"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
|
||||||
"@tensorflow/tfjs-core": "4.22.0"
|
|
||||||
},
|
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@aiscript-dev/aiscript-languageserver": "-"
|
"@aiscript-dev/aiscript-languageserver": "-"
|
||||||
|
|||||||
122
packages/backend/build.js
Normal file
122
packages/backend/build.js
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { build } from 'esbuild';
|
||||||
|
import { swcPlugin } from 'esbuild-plugin-swc';
|
||||||
|
|
||||||
|
const _filename = fileURLToPath(import.meta.url);
|
||||||
|
const _dirname = dirname(_filename);
|
||||||
|
const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8'));
|
||||||
|
|
||||||
|
const resolveTsPathsPlugin = {
|
||||||
|
name: 'resolve-ts-paths',
|
||||||
|
setup(build) {
|
||||||
|
build.onResolve({ filter: /^\.{1,2}\/.*\.js$/ }, (args) => {
|
||||||
|
if (args.importer) {
|
||||||
|
const absPath = join(args.resolveDir, args.path);
|
||||||
|
const tsPath = absPath.slice(0, -3) + '.ts';
|
||||||
|
if (fs.existsSync(tsPath)) return { path: tsPath };
|
||||||
|
const tsxPath = absPath.slice(0, -3) + '.tsx';
|
||||||
|
if (fs.existsSync(tsxPath)) return { path: tsxPath };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const externalIpaddrPlugin = {
|
||||||
|
name: 'external-ipaddr',
|
||||||
|
setup(build) {
|
||||||
|
build.onResolve({ filter: /^ipaddr\.js$/ }, (args) => {
|
||||||
|
return { path: args.path, external: true };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {import('esbuild').BuildOptions} */
|
||||||
|
const options = {
|
||||||
|
entryPoints: ['./src/boot/entry.ts'],
|
||||||
|
minify: true,
|
||||||
|
keepNames: true,
|
||||||
|
bundle: true,
|
||||||
|
outdir: './built/boot',
|
||||||
|
target: 'node22',
|
||||||
|
platform: 'node',
|
||||||
|
format: 'esm',
|
||||||
|
sourcemap: false,
|
||||||
|
packages: 'bundle',
|
||||||
|
banner: {
|
||||||
|
js: 'import { createRequire as topLevelCreateRequire } from "module";' +
|
||||||
|
'import ___url___ from "url";' +
|
||||||
|
'const require = topLevelCreateRequire(import.meta.url);' +
|
||||||
|
'const __filename = ___url___.fileURLToPath(import.meta.url);' +
|
||||||
|
'const __dirname = ___url___.fileURLToPath(new URL(".", import.meta.url));',
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
externalIpaddrPlugin,
|
||||||
|
resolveTsPathsPlugin,
|
||||||
|
swcPlugin({
|
||||||
|
jsc: {
|
||||||
|
parser: {
|
||||||
|
syntax: 'typescript',
|
||||||
|
decorators: true,
|
||||||
|
dynamicImport: true,
|
||||||
|
},
|
||||||
|
transform: {
|
||||||
|
legacyDecorator: true,
|
||||||
|
decoratorMetadata: true,
|
||||||
|
},
|
||||||
|
experimental: {
|
||||||
|
keepImportAssertions: true,
|
||||||
|
},
|
||||||
|
baseUrl: join(_dirname, 'src'),
|
||||||
|
paths: {
|
||||||
|
'@/*': ['*'],
|
||||||
|
},
|
||||||
|
target: 'esnext',
|
||||||
|
keepClassNames: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
//externalIpaddrPlugin,
|
||||||
|
],
|
||||||
|
external: [
|
||||||
|
'slacc-*',
|
||||||
|
'class-transformer',
|
||||||
|
'class-validator',
|
||||||
|
'@sentry/*',
|
||||||
|
'@nestjs/websockets/socket-module',
|
||||||
|
'@nestjs/microservices/microservices-module',
|
||||||
|
'@nestjs/microservices',
|
||||||
|
'@napi-rs/canvas-win32-x64-msvc',
|
||||||
|
'mock-aws-s3',
|
||||||
|
'aws-sdk',
|
||||||
|
'nock',
|
||||||
|
'sharp',
|
||||||
|
're2',
|
||||||
|
'@napi-rs/canvas',
|
||||||
|
'oauth2orize',
|
||||||
|
'oauth2orize-pkce',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const args = process.argv.slice(2).map(arg => arg.toLowerCase());
|
||||||
|
|
||||||
|
if (!args.includes('--no-clean')) {
|
||||||
|
fs.rmSync('./built', { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
await buildSrc();
|
||||||
|
|
||||||
|
async function buildSrc() {
|
||||||
|
console.log(`[${_package.name}] start building...`);
|
||||||
|
|
||||||
|
await build(options)
|
||||||
|
.then(() => {
|
||||||
|
console.log(`[${_package.name}] build succeeded.`);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
process.stderr.write(err.stderr || err.message || err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[${_package.name}] finish building.`);
|
||||||
|
}
|
||||||
20
packages/backend/migration/1767169026317-birthday-index.js
Normal file
20
packages/backend/migration/1767169026317-birthday-index.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class BirthdayIndex1767169026317 {
|
||||||
|
name = 'BirthdayIndex1767169026317'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_de22cd2b445eee31ae51cdbe99"`);
|
||||||
|
await queryRunner.query(`CREATE OR REPLACE FUNCTION get_birthday_date(birthday TEXT) RETURNS SMALLINT AS $$ BEGIN RETURN CAST((SUBSTR(birthday, 6, 2) || SUBSTR(birthday, 9, 2)) AS SMALLINT); END; $$ LANGUAGE plpgsql IMMUTABLE;`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_USERPROFILE_BIRTHDAY_DATE" ON "user_profile" (get_birthday_date("birthday"))`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_de22cd2b445eee31ae51cdbe99" ON "user_profile" (substr("birthday", 6, 5))`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_USERPROFILE_BIRTHDAY_DATE"`);
|
||||||
|
await queryRunner.query(`DROP FUNCTION IF EXISTS get_birthday_date(birthday TEXT)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
|||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { loadConfig } from './built/config.js';
|
import { loadConfig } from './src-js/config.js';
|
||||||
import { entities } from './built/postgres.js';
|
import { entities } from './src-js/postgres.js';
|
||||||
|
|
||||||
const isConcurrentIndexMigrationEnabled = process.env.MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY === '1';
|
const isConcurrentIndexMigrationEnabled = process.env.MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY === '1';
|
||||||
|
|
||||||
|
|||||||
@@ -12,17 +12,17 @@
|
|||||||
"start:test": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./built/boot/entry.js",
|
"start:test": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./built/boot/entry.js",
|
||||||
"migrate": "pnpm compile-config && pnpm typeorm migration:run -d ormconfig.js",
|
"migrate": "pnpm compile-config && pnpm typeorm migration:run -d ormconfig.js",
|
||||||
"revert": "pnpm compile-config && pnpm typeorm migration:revert -d ormconfig.js",
|
"revert": "pnpm compile-config && pnpm typeorm migration:revert -d ormconfig.js",
|
||||||
"cli": "pnpm compile-config && node ./built/boot/cli.js",
|
"cli": "pnpm compile-config && node ./src-js/boot/cli.js",
|
||||||
"check:connect": "pnpm compile-config && node ./scripts/check_connect.js",
|
"check:connect": "pnpm compile-config && node ./scripts/check_connect.js",
|
||||||
"compile-config": "node ./scripts/compile_config.js",
|
"compile-config": "node ./scripts/compile_config.js",
|
||||||
"build": "swc src -d built -D --strip-leading-paths",
|
"build": "swc src -d src-js -D --strip-leading-paths && node ./build.js",
|
||||||
"build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc --strip-leading-paths",
|
"build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc --strip-leading-paths",
|
||||||
"watch:swc": "swc src -d built -D -w --strip-leading-paths",
|
"watch:swc": "swc src -d built -D -w --strip-leading-paths",
|
||||||
"build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
|
"build:tsc": "tsgo -p tsconfig.json && tsc-alias -p tsconfig.json",
|
||||||
"watch": "pnpm compile-config && node ./scripts/watch.mjs",
|
"watch": "pnpm compile-config && node ./scripts/watch.mjs",
|
||||||
"restart": "pnpm build && pnpm start",
|
"restart": "pnpm build && pnpm start",
|
||||||
"dev": "pnpm compile-config && node ./scripts/dev.mjs",
|
"dev": "pnpm compile-config && node ./scripts/dev.mjs",
|
||||||
"typecheck": "tsc --noEmit && tsc -p test --noEmit && tsc -p test-federation --noEmit",
|
"typecheck": "tsgo --noEmit && tsgo -p test --noEmit && tsgo -p test-federation --noEmit",
|
||||||
"eslint": "eslint --quiet \"{src,test-federation}/**/*.ts\"",
|
"eslint": "eslint --quiet \"{src,test-federation}/**/*.ts\"",
|
||||||
"lint": "pnpm typecheck && pnpm eslint",
|
"lint": "pnpm typecheck && pnpm eslint",
|
||||||
"jest": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./jest.js --forceExit --config jest.config.unit.cjs",
|
"jest": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./jest.js --forceExit --config jest.config.unit.cjs",
|
||||||
@@ -41,20 +41,18 @@
|
|||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@swc/core-android-arm64": "1.3.11",
|
"@swc/core-android-arm64": "1.3.11",
|
||||||
"@swc/core-darwin-arm64": "1.15.3",
|
"@swc/core-darwin-arm64": "1.15.7",
|
||||||
"@swc/core-darwin-x64": "1.15.3",
|
"@swc/core-darwin-x64": "1.15.7",
|
||||||
"@swc/core-freebsd-x64": "1.3.11",
|
"@swc/core-freebsd-x64": "1.3.11",
|
||||||
"@swc/core-linux-arm-gnueabihf": "1.15.3",
|
"@swc/core-linux-arm-gnueabihf": "1.15.7",
|
||||||
"@swc/core-linux-arm64-gnu": "1.15.3",
|
"@swc/core-linux-arm64-gnu": "1.15.7",
|
||||||
"@swc/core-linux-arm64-musl": "1.15.3",
|
"@swc/core-linux-arm64-musl": "1.15.7",
|
||||||
"@swc/core-linux-x64-gnu": "1.15.3",
|
"@swc/core-linux-x64-gnu": "1.15.7",
|
||||||
"@swc/core-linux-x64-musl": "1.15.3",
|
"@swc/core-linux-x64-musl": "1.15.7",
|
||||||
"@swc/core-win32-arm64-msvc": "1.15.3",
|
"@swc/core-win32-arm64-msvc": "1.15.7",
|
||||||
"@swc/core-win32-ia32-msvc": "1.15.3",
|
"@swc/core-win32-ia32-msvc": "1.15.7",
|
||||||
"@swc/core-win32-x64-msvc": "1.15.3",
|
"@swc/core-win32-x64-msvc": "1.15.7",
|
||||||
"@tensorflow/tfjs": "4.22.0",
|
"bufferutil": "4.1.0",
|
||||||
"@tensorflow/tfjs-node": "4.22.0",
|
|
||||||
"bufferutil": "4.0.9",
|
|
||||||
"slacc-android-arm-eabi": "0.0.10",
|
"slacc-android-arm-eabi": "0.0.10",
|
||||||
"slacc-android-arm64": "0.0.10",
|
"slacc-android-arm64": "0.0.10",
|
||||||
"slacc-darwin-arm64": "0.0.10",
|
"slacc-darwin-arm64": "0.0.10",
|
||||||
@@ -68,34 +66,33 @@
|
|||||||
"slacc-linux-x64-musl": "0.0.10",
|
"slacc-linux-x64-musl": "0.0.10",
|
||||||
"slacc-win32-arm64-msvc": "0.0.10",
|
"slacc-win32-arm64-msvc": "0.0.10",
|
||||||
"slacc-win32-x64-msvc": "0.0.10",
|
"slacc-win32-x64-msvc": "0.0.10",
|
||||||
"utf-8-validate": "6.0.5"
|
"utf-8-validate": "6.0.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "3.940.0",
|
"@aws-sdk/client-s3": "3.958.0",
|
||||||
"@aws-sdk/lib-storage": "3.940.0",
|
"@aws-sdk/lib-storage": "3.958.0",
|
||||||
"@discordapp/twemoji": "16.0.1",
|
"@discordapp/twemoji": "16.0.1",
|
||||||
"@fastify/accepts": "5.0.3",
|
"@fastify/accepts": "5.0.4",
|
||||||
"@fastify/cookie": "11.0.2",
|
"@fastify/cors": "11.2.0",
|
||||||
"@fastify/cors": "11.1.0",
|
|
||||||
"@fastify/express": "4.0.2",
|
"@fastify/express": "4.0.2",
|
||||||
"@fastify/http-proxy": "11.3.0",
|
"@fastify/http-proxy": "11.4.1",
|
||||||
"@fastify/multipart": "9.3.0",
|
"@fastify/multipart": "9.3.0",
|
||||||
"@fastify/static": "8.3.0",
|
"@fastify/static": "8.3.0",
|
||||||
"@kitajs/html": "4.2.11",
|
"@kitajs/html": "4.2.11",
|
||||||
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
||||||
"@misskey-dev/summaly": "5.2.5",
|
"@misskey-dev/summaly": "5.2.5",
|
||||||
"@napi-rs/canvas": "0.1.83",
|
"@napi-rs/canvas": "0.1.87",
|
||||||
"@nestjs/common": "11.1.9",
|
"@nestjs/common": "11.1.10",
|
||||||
"@nestjs/core": "11.1.9",
|
"@nestjs/core": "11.1.10",
|
||||||
"@nestjs/testing": "11.1.9",
|
"@nestjs/testing": "11.1.10",
|
||||||
"@peertube/http-signature": "1.7.0",
|
"@peertube/http-signature": "1.7.0",
|
||||||
"@sentry/node": "10.27.0",
|
"@sentry/node": "10.32.1",
|
||||||
"@sentry/profiling-node": "10.27.0",
|
"@sentry/profiling-node": "10.32.1",
|
||||||
"@simplewebauthn/server": "13.2.2",
|
"@simplewebauthn/server": "13.2.2",
|
||||||
"@sinonjs/fake-timers": "15.0.0",
|
"@sinonjs/fake-timers": "15.1.0",
|
||||||
"@smithy/node-http-handler": "4.4.5",
|
"@smithy/node-http-handler": "4.4.7",
|
||||||
"@swc/cli": "0.7.9",
|
"@swc/cli": "0.7.9",
|
||||||
"@swc/core": "1.15.3",
|
"@swc/core": "1.15.7",
|
||||||
"@twemoji/parser": "16.0.0",
|
"@twemoji/parser": "16.0.0",
|
||||||
"@types/redis-info": "3.0.3",
|
"@types/redis-info": "3.0.3",
|
||||||
"accepts": "1.3.8",
|
"accepts": "1.3.8",
|
||||||
@@ -105,12 +102,11 @@
|
|||||||
"bcryptjs": "3.0.3",
|
"bcryptjs": "3.0.3",
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
"body-parser": "2.2.1",
|
"body-parser": "2.2.1",
|
||||||
"bullmq": "5.65.0",
|
"bullmq": "5.66.3",
|
||||||
"cacheable-lookup": "7.0.0",
|
"cacheable-lookup": "7.0.0",
|
||||||
"cbor": "10.0.11",
|
|
||||||
"chalk": "5.6.2",
|
"chalk": "5.6.2",
|
||||||
"chalk-template": "1.1.2",
|
"chalk-template": "1.1.2",
|
||||||
"chokidar": "4.0.3",
|
"chokidar": "5.0.0",
|
||||||
"color-convert": "3.1.3",
|
"color-convert": "3.1.3",
|
||||||
"content-disposition": "1.0.1",
|
"content-disposition": "1.0.1",
|
||||||
"date-fns": "4.1.0",
|
"date-fns": "4.1.0",
|
||||||
@@ -118,7 +114,7 @@
|
|||||||
"fastify": "5.6.2",
|
"fastify": "5.6.2",
|
||||||
"fastify-raw-body": "5.0.0",
|
"fastify-raw-body": "5.0.0",
|
||||||
"feed": "5.1.0",
|
"feed": "5.1.0",
|
||||||
"file-type": "21.1.1",
|
"file-type": "21.2.0",
|
||||||
"fluent-ffmpeg": "2.1.3",
|
"fluent-ffmpeg": "2.1.3",
|
||||||
"form-data": "4.0.5",
|
"form-data": "4.0.5",
|
||||||
"got": "14.6.5",
|
"got": "14.6.5",
|
||||||
@@ -131,7 +127,6 @@
|
|||||||
"is-svg": "6.1.0",
|
"is-svg": "6.1.0",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"jsonld": "9.0.0",
|
"jsonld": "9.0.0",
|
||||||
"jsrsasign": "11.1.0",
|
|
||||||
"juice": "11.0.3",
|
"juice": "11.0.3",
|
||||||
"meilisearch": "0.54.0",
|
"meilisearch": "0.54.0",
|
||||||
"mfm-js": "0.25.0",
|
"mfm-js": "0.25.0",
|
||||||
@@ -143,9 +138,7 @@
|
|||||||
"nested-property": "4.0.0",
|
"nested-property": "4.0.0",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
"node-html-parser": "7.0.1",
|
"node-html-parser": "7.0.1",
|
||||||
"nodemailer": "7.0.11",
|
"nodemailer": "7.0.12",
|
||||||
"nsfwjs": "4.2.0",
|
|
||||||
"oauth": "0.10.2",
|
|
||||||
"oauth2orize": "1.12.0",
|
"oauth2orize": "1.12.0",
|
||||||
"oauth2orize-pkce": "0.1.2",
|
"oauth2orize-pkce": "0.1.2",
|
||||||
"os-utils": "0.0.14",
|
"os-utils": "0.0.14",
|
||||||
@@ -157,7 +150,7 @@
|
|||||||
"qrcode": "1.5.4",
|
"qrcode": "1.5.4",
|
||||||
"random-seed": "0.3.0",
|
"random-seed": "0.3.0",
|
||||||
"ratelimiter": "3.4.1",
|
"ratelimiter": "3.4.1",
|
||||||
"re2": "1.22.3",
|
"re2": "1.23.0",
|
||||||
"redis-info": "3.1.0",
|
"redis-info": "3.1.0",
|
||||||
"reflect-metadata": "0.2.2",
|
"reflect-metadata": "0.2.2",
|
||||||
"rename": "1.0.4",
|
"rename": "1.0.4",
|
||||||
@@ -170,14 +163,12 @@
|
|||||||
"slacc": "0.0.10",
|
"slacc": "0.0.10",
|
||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"stringz": "2.1.0",
|
"stringz": "2.1.0",
|
||||||
"systeminformation": "5.27.11",
|
"systeminformation": "5.28.1",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"tmp": "0.2.5",
|
"tmp": "0.2.5",
|
||||||
"tsc-alias": "1.8.16",
|
"tsc-alias": "1.8.16",
|
||||||
"tsconfig-paths": "4.2.0",
|
"typeorm": "0.3.28",
|
||||||
"typeorm": "0.3.27",
|
"ulid": "3.0.2",
|
||||||
"typescript": "5.9.3",
|
|
||||||
"ulid": "3.0.1",
|
|
||||||
"vary": "1.1.2",
|
"vary": "1.1.2",
|
||||||
"web-push": "3.6.7",
|
"web-push": "3.6.7",
|
||||||
"ws": "8.18.3",
|
"ws": "8.18.3",
|
||||||
@@ -186,8 +177,8 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/globals": "29.7.0",
|
"@jest/globals": "29.7.0",
|
||||||
"@kitajs/ts-html-plugin": "4.1.3",
|
"@kitajs/ts-html-plugin": "4.1.3",
|
||||||
"@nestjs/platform-express": "11.1.9",
|
"@nestjs/platform-express": "11.1.10",
|
||||||
"@sentry/vue": "10.27.0",
|
"@sentry/vue": "10.32.1",
|
||||||
"@simplewebauthn/types": "12.0.0",
|
"@simplewebauthn/types": "12.0.0",
|
||||||
"@swc/jest": "0.2.39",
|
"@swc/jest": "0.2.39",
|
||||||
"@types/accepts": "1.3.7",
|
"@types/accepts": "1.3.7",
|
||||||
@@ -199,22 +190,20 @@
|
|||||||
"@types/http-link-header": "1.0.7",
|
"@types/http-link-header": "1.0.7",
|
||||||
"@types/jest": "29.5.14",
|
"@types/jest": "29.5.14",
|
||||||
"@types/jsonld": "1.5.15",
|
"@types/jsonld": "1.5.15",
|
||||||
"@types/jsrsasign": "10.5.15",
|
|
||||||
"@types/mime-types": "3.0.1",
|
"@types/mime-types": "3.0.1",
|
||||||
"@types/ms": "2.1.0",
|
"@types/ms": "2.1.0",
|
||||||
"@types/node": "24.10.1",
|
"@types/node": "24.10.4",
|
||||||
"@types/nodemailer": "7.0.4",
|
"@types/nodemailer": "7.0.4",
|
||||||
"@types/oauth": "0.9.6",
|
|
||||||
"@types/oauth2orize": "1.11.5",
|
"@types/oauth2orize": "1.11.5",
|
||||||
"@types/oauth2orize-pkce": "0.1.2",
|
"@types/oauth2orize-pkce": "0.1.2",
|
||||||
"@types/pg": "8.15.6",
|
"@types/pg": "8.16.0",
|
||||||
"@types/qrcode": "1.5.6",
|
"@types/qrcode": "1.5.6",
|
||||||
"@types/random-seed": "0.3.5",
|
"@types/random-seed": "0.3.5",
|
||||||
"@types/ratelimiter": "3.4.6",
|
"@types/ratelimiter": "3.4.6",
|
||||||
"@types/rename": "1.0.7",
|
"@types/rename": "1.0.7",
|
||||||
"@types/sanitize-html": "2.16.0",
|
"@types/sanitize-html": "2.16.0",
|
||||||
"@types/semver": "7.7.1",
|
"@types/semver": "7.7.1",
|
||||||
"@types/simple-oauth2": "5.0.7",
|
"@types/simple-oauth2": "5.0.8",
|
||||||
"@types/sinonjs__fake-timers": "15.0.1",
|
"@types/sinonjs__fake-timers": "15.0.1",
|
||||||
"@types/supertest": "6.0.3",
|
"@types/supertest": "6.0.3",
|
||||||
"@types/tinycolor2": "1.4.6",
|
"@types/tinycolor2": "1.4.6",
|
||||||
@@ -222,21 +211,22 @@
|
|||||||
"@types/vary": "1.1.3",
|
"@types/vary": "1.1.3",
|
||||||
"@types/web-push": "3.6.4",
|
"@types/web-push": "3.6.4",
|
||||||
"@types/ws": "8.18.1",
|
"@types/ws": "8.18.1",
|
||||||
"@typescript-eslint/eslint-plugin": "8.48.0",
|
"@typescript-eslint/eslint-plugin": "8.50.1",
|
||||||
"@typescript-eslint/parser": "8.48.0",
|
"@typescript-eslint/parser": "8.50.1",
|
||||||
"aws-sdk-client-mock": "4.1.0",
|
"aws-sdk-client-mock": "4.1.0",
|
||||||
|
"cbor": "10.0.11",
|
||||||
"cross-env": "10.1.0",
|
"cross-env": "10.1.0",
|
||||||
|
"esbuild-plugin-swc": "1.0.1",
|
||||||
"eslint-plugin-import": "2.32.0",
|
"eslint-plugin-import": "2.32.0",
|
||||||
"execa": "9.6.0",
|
"execa": "9.6.1",
|
||||||
"fkill": "10.0.1",
|
"fkill": "10.0.1",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-mock": "29.7.0",
|
"jest-mock": "29.7.0",
|
||||||
"jest-util": "29.7.0",
|
|
||||||
"js-yaml": "4.1.1",
|
"js-yaml": "4.1.1",
|
||||||
"nodemon": "3.1.11",
|
"nodemon": "3.1.11",
|
||||||
"pid-port": "2.0.0",
|
"pid-port": "2.0.0",
|
||||||
"simple-oauth2": "5.1.0",
|
"simple-oauth2": "5.1.0",
|
||||||
"supertest": "7.1.4",
|
"supertest": "7.1.4",
|
||||||
"vite": "7.2.4"
|
"vite": "7.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Redis from 'ioredis';
|
import Redis from 'ioredis';
|
||||||
import { loadConfig } from '../built/config.js';
|
import { loadConfig } from '../src-js/config.js';
|
||||||
import { createPostgresDataSource } from '../built/postgres.js';
|
import { createPostgresDataSource } from '../src-js/postgres.js';
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
@@ -28,10 +28,8 @@ async function connectToRedis(redisOptions) {
|
|||||||
try {
|
try {
|
||||||
await redis.connect();
|
await redis.connect();
|
||||||
resolve();
|
resolve();
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(e);
|
reject(e);
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
redis.disconnect(false);
|
redis.disconnect(false);
|
||||||
}
|
}
|
||||||
@@ -50,7 +48,7 @@ const promises = Array
|
|||||||
]))
|
]))
|
||||||
.map(connectToRedis)
|
.map(connectToRedis)
|
||||||
.concat([
|
.concat([
|
||||||
connectToPostgres()
|
connectToPostgres(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { writeFileSync, existsSync } from 'node:fs';
|
||||||
import { execa } from 'execa';
|
import { execa } from 'execa';
|
||||||
import { writeFileSync, existsSync } from "node:fs";
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
if (!process.argv.includes('--no-build')) {
|
if (!process.argv.includes('--no-build')) {
|
||||||
@@ -19,10 +19,10 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @type {import('../src/config.js')} */
|
/** @type {import('../src/config.js')} */
|
||||||
const { loadConfig } = await import('../built/config.js');
|
const { loadConfig } = await import('../src-js/config.js');
|
||||||
|
|
||||||
/** @type {import('../src/server/api/openapi/gen-spec.js')} */
|
/** @type {import('../src/server/api/openapi/gen-spec.js')} */
|
||||||
const { genOpenapiSpec } = await import('../built/server/api/openapi/gen-spec.js');
|
const { genOpenapiSpec } = await import('../src-js/server/api/openapi/gen-spec.js');
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
const spec = genOpenapiSpec(config, true);
|
const spec = genOpenapiSpec(config, true);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { execa } from 'execa';
|
|||||||
});
|
});
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|
||||||
execa('tsc', ['-w', '-p', 'tsconfig.json'], {
|
execa('tsgo', ['-w', '-p', 'tsconfig.json'], {
|
||||||
stdout: process.stdout,
|
stdout: process.stdout,
|
||||||
stderr: process.stderr,
|
stderr: process.stderr,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,8 +4,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
import { dirname } from 'node:path';
|
|
||||||
import * as os from 'node:os';
|
import * as os from 'node:os';
|
||||||
import cluster from 'node:cluster';
|
import cluster from 'node:cluster';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
@@ -17,20 +15,15 @@ import { showMachineInfo } from '@/misc/show-machine-info.js';
|
|||||||
import { envOption } from '@/env.js';
|
import { envOption } from '@/env.js';
|
||||||
import { jobQueue, server } from './common.js';
|
import { jobQueue, server } from './common.js';
|
||||||
|
|
||||||
const _filename = fileURLToPath(import.meta.url);
|
|
||||||
const _dirname = dirname(_filename);
|
|
||||||
|
|
||||||
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
|
|
||||||
|
|
||||||
const logger = new Logger('core', 'cyan');
|
const logger = new Logger('core', 'cyan');
|
||||||
const bootLogger = logger.createSubLogger('boot', 'magenta');
|
const bootLogger = logger.createSubLogger('boot', 'magenta');
|
||||||
|
|
||||||
const themeColor = chalk.hex('#86b300');
|
const themeColor = chalk.hex('#86b300');
|
||||||
|
|
||||||
function greet() {
|
function greet(props: { version: string }) {
|
||||||
if (!envOption.quiet) {
|
if (!envOption.quiet) {
|
||||||
//#region Misskey logo
|
//#region Misskey logo
|
||||||
const v = `v${meta.version}`;
|
const v = `v${props.version}`;
|
||||||
console.log(themeColor(' _____ _ _ '));
|
console.log(themeColor(' _____ _ _ '));
|
||||||
console.log(themeColor(' | |_|___ ___| |_ ___ _ _ '));
|
console.log(themeColor(' | |_|___ ___| |_ ___ _ _ '));
|
||||||
console.log(themeColor(' | | | | |_ -|_ -| \'_| -_| | |'));
|
console.log(themeColor(' | | | | |_ -|_ -| \'_| -_| | |'));
|
||||||
@@ -46,7 +39,7 @@ function greet() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bootLogger.info('Welcome to Misskey!');
|
bootLogger.info('Welcome to Misskey!');
|
||||||
bootLogger.info(`Misskey v${meta.version}`, null, true);
|
bootLogger.info(`Misskey v${props.version}`, null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,11 +50,11 @@ export async function masterMain() {
|
|||||||
|
|
||||||
// initialize app
|
// initialize app
|
||||||
try {
|
try {
|
||||||
greet();
|
config = loadConfigBoot();
|
||||||
|
greet({ version: config.version });
|
||||||
showEnvironment();
|
showEnvironment();
|
||||||
await showMachineInfo(bootLogger);
|
await showMachineInfo(bootLogger);
|
||||||
showNodejsVersion();
|
showNodejsVersion();
|
||||||
config = loadConfigBoot();
|
|
||||||
//await connectDb();
|
//await connectDb();
|
||||||
if (config.pidFile) fs.writeFileSync(config.pidFile, process.pid.toString());
|
if (config.pidFile) fs.writeFileSync(config.pidFile, process.pid.toString());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ type Source = {
|
|||||||
socket?: string;
|
socket?: string;
|
||||||
trustProxy?: FastifyServerOptions['trustProxy'];
|
trustProxy?: FastifyServerOptions['trustProxy'];
|
||||||
chmodSocket?: string;
|
chmodSocket?: string;
|
||||||
|
enableIpRateLimit?: boolean;
|
||||||
disableHsts?: boolean;
|
disableHsts?: boolean;
|
||||||
db: {
|
db: {
|
||||||
host: string;
|
host: string;
|
||||||
@@ -120,8 +121,9 @@ export type Config = {
|
|||||||
url: string;
|
url: string;
|
||||||
port: number;
|
port: number;
|
||||||
socket: string | undefined;
|
socket: string | undefined;
|
||||||
trustProxy: FastifyServerOptions['trustProxy'];
|
trustProxy: NonNullable<FastifyServerOptions['trustProxy']>;
|
||||||
chmodSocket: string | undefined;
|
chmodSocket: string | undefined;
|
||||||
|
enableIpRateLimit: boolean;
|
||||||
disableHsts: boolean | undefined;
|
disableHsts: boolean | undefined;
|
||||||
db: {
|
db: {
|
||||||
host: string;
|
host: string;
|
||||||
@@ -217,24 +219,42 @@ export type FulltextSearchProvider = 'sqlLike' | 'sqlPgroonga' | 'meilisearch';
|
|||||||
const _filename = fileURLToPath(import.meta.url);
|
const _filename = fileURLToPath(import.meta.url);
|
||||||
const _dirname = dirname(_filename);
|
const _dirname = dirname(_filename);
|
||||||
|
|
||||||
const compiledConfigFilePathForTest = resolve(_dirname, '../../../built/._config_.json');
|
/** Path of repository root directory */
|
||||||
|
let rootDir = _dirname;
|
||||||
|
// 見つかるまで上に遡る
|
||||||
|
while (!fs.existsSync(resolve(rootDir, 'packages'))) {
|
||||||
|
const parentDir = dirname(rootDir);
|
||||||
|
if (parentDir === rootDir) {
|
||||||
|
throw new Error('Cannot find root directory');
|
||||||
|
}
|
||||||
|
rootDir = parentDir;
|
||||||
|
}
|
||||||
|
|
||||||
export const compiledConfigFilePath = fs.existsSync(compiledConfigFilePathForTest) ? compiledConfigFilePathForTest : resolve(_dirname, '../../../built/.config.json');
|
/** Path of configuration directory */
|
||||||
|
const configDir = resolve(rootDir, '.config');
|
||||||
|
/** Path of built directory */
|
||||||
|
const projectBuiltDir = resolve(rootDir, 'built');
|
||||||
|
|
||||||
|
const compiledConfigFilePathForTest = resolve(projectBuiltDir, '._config_.json');
|
||||||
|
|
||||||
|
export const compiledConfigFilePath = fs.existsSync(compiledConfigFilePathForTest)
|
||||||
|
? compiledConfigFilePathForTest
|
||||||
|
: resolve(projectBuiltDir, '.config.json');
|
||||||
|
|
||||||
export function loadConfig(): Config {
|
export function loadConfig(): Config {
|
||||||
if (!fs.existsSync(compiledConfigFilePath)) {
|
if (!fs.existsSync(compiledConfigFilePath)) {
|
||||||
throw new Error('Compiled configuration file not found. Try running \'pnpm compile-config\'.');
|
throw new Error('Compiled configuration file not found. Try running \'pnpm compile-config\'.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8'));
|
const meta = JSON.parse(fs.readFileSync(resolve(projectBuiltDir, 'meta.json'), 'utf-8'));
|
||||||
|
|
||||||
const frontendManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_vite_/manifest.json');
|
const frontendManifestExists = fs.existsSync(resolve(projectBuiltDir, '_frontend_vite_/manifest.json'));
|
||||||
const frontendEmbedManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_embed_vite_/manifest.json');
|
const frontendEmbedManifestExists = fs.existsSync(resolve(projectBuiltDir, '_frontend_embed_vite_/manifest.json'));
|
||||||
const frontendManifest = frontendManifestExists ?
|
const frontendManifest = frontendManifestExists ?
|
||||||
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_vite_/manifest.json`, 'utf-8'))
|
JSON.parse(fs.readFileSync(resolve(projectBuiltDir, '_frontend_vite_/manifest.json'), 'utf-8'))
|
||||||
: { 'src/_boot_.ts': { file: null } };
|
: { 'src/_boot_.ts': { file: null } };
|
||||||
const frontendEmbedManifest = frontendEmbedManifestExists ?
|
const frontendEmbedManifest = frontendEmbedManifestExists ?
|
||||||
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_embed_vite_/manifest.json`, 'utf-8'))
|
JSON.parse(fs.readFileSync(resolve(projectBuiltDir, '_frontend_embed_vite_/manifest.json'), 'utf-8'))
|
||||||
: { 'src/boot.ts': { file: null } };
|
: { 'src/boot.ts': { file: null } };
|
||||||
|
|
||||||
const config = JSON.parse(fs.readFileSync(compiledConfigFilePath, 'utf-8')) as Source;
|
const config = JSON.parse(fs.readFileSync(compiledConfigFilePath, 'utf-8')) as Source;
|
||||||
@@ -263,9 +283,17 @@ export function loadConfig(): Config {
|
|||||||
url: url.origin,
|
url: url.origin,
|
||||||
port: config.port ?? parseInt(process.env.PORT ?? '', 10),
|
port: config.port ?? parseInt(process.env.PORT ?? '', 10),
|
||||||
socket: config.socket,
|
socket: config.socket,
|
||||||
trustProxy: config.trustProxy,
|
trustProxy: config.trustProxy ?? [
|
||||||
|
'10.0.0.0/8',
|
||||||
|
'172.16.0.0/12',
|
||||||
|
'192.168.0.0/16',
|
||||||
|
'127.0.0.1/32',
|
||||||
|
'::1/128',
|
||||||
|
'fc00::/7',
|
||||||
|
],
|
||||||
chmodSocket: config.chmodSocket,
|
chmodSocket: config.chmodSocket,
|
||||||
disableHsts: config.disableHsts,
|
disableHsts: config.disableHsts,
|
||||||
|
enableIpRateLimit: config.enableIpRateLimit ?? true,
|
||||||
host,
|
host,
|
||||||
hostname,
|
hostname,
|
||||||
scheme,
|
scheme,
|
||||||
|
|||||||
@@ -3,88 +3,17 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as fs from 'node:fs';
|
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
import { dirname } from 'node:path';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import si from 'systeminformation';
|
|
||||||
import { Mutex } from 'async-mutex';
|
|
||||||
import fetch from 'node-fetch';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { NSFWJS, PredictionType } from 'nsfwjs';
|
|
||||||
|
|
||||||
const _filename = fileURLToPath(import.meta.url);
|
|
||||||
const _dirname = dirname(_filename);
|
|
||||||
|
|
||||||
const REQUIRED_CPU_FLAGS_X64 = ['avx2', 'fma'];
|
|
||||||
let isSupportedCpu: undefined | boolean = undefined;
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AiService {
|
export class AiService {
|
||||||
private model: NSFWJS;
|
|
||||||
private modelLoadMutex: Mutex = new Mutex();
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async detectSensitive(source: string | Buffer): Promise<PredictionType[] | null> {
|
public async detectSensitive(source: string | Buffer): Promise<null> {
|
||||||
try {
|
return null;
|
||||||
if (isSupportedCpu === undefined) {
|
|
||||||
isSupportedCpu = await this.computeIsSupportedCpu();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isSupportedCpu) {
|
|
||||||
console.error('This CPU cannot use TensorFlow.');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tf = await import('@tensorflow/tfjs-node');
|
|
||||||
tf.env().global.fetch = fetch;
|
|
||||||
|
|
||||||
if (this.model == null) {
|
|
||||||
const nsfw = await import('nsfwjs');
|
|
||||||
await this.modelLoadMutex.runExclusive(async () => {
|
|
||||||
if (this.model == null) {
|
|
||||||
this.model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { size: 299 });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const buffer = source instanceof Buffer ? source : await fs.promises.readFile(source);
|
|
||||||
const image = await tf.node.decodeImage(buffer, 3) as any;
|
|
||||||
try {
|
|
||||||
const predictions = await this.model.classify(image);
|
|
||||||
return predictions;
|
|
||||||
} finally {
|
|
||||||
image.dispose();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async computeIsSupportedCpu(): Promise<boolean> {
|
|
||||||
switch (process.arch) {
|
|
||||||
case 'x64': {
|
|
||||||
const cpuFlags = await this.getCpuFlags();
|
|
||||||
return REQUIRED_CPU_FLAGS_X64.every(required => cpuFlags.includes(required));
|
|
||||||
}
|
|
||||||
case 'arm64': {
|
|
||||||
// As far as I know, no required CPU flags for ARM64.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private async getCpuFlags(): Promise<string[]> {
|
|
||||||
const str = await si.cpuFlags();
|
|
||||||
return str.split(/\s+/);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ import { ApLoggerService } from './activitypub/ApLoggerService.js';
|
|||||||
import { ApMfmService } from './activitypub/ApMfmService.js';
|
import { ApMfmService } from './activitypub/ApMfmService.js';
|
||||||
import { ApRendererService } from './activitypub/ApRendererService.js';
|
import { ApRendererService } from './activitypub/ApRendererService.js';
|
||||||
import { ApRequestService } from './activitypub/ApRequestService.js';
|
import { ApRequestService } from './activitypub/ApRequestService.js';
|
||||||
import { ApResolverService } from './activitypub/ApResolverService.js';
|
import { ApResolverService, Resolver } from './activitypub/ApResolverService.js';
|
||||||
import { JsonLdService } from './activitypub/JsonLdService.js';
|
import { JsonLdService } from './activitypub/JsonLdService.js';
|
||||||
import { RemoteLoggerService } from './RemoteLoggerService.js';
|
import { RemoteLoggerService } from './RemoteLoggerService.js';
|
||||||
import { RemoteUserResolveService } from './RemoteUserResolveService.js';
|
import { RemoteUserResolveService } from './RemoteUserResolveService.js';
|
||||||
@@ -447,6 +447,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
ApRendererService,
|
ApRendererService,
|
||||||
ApRequestService,
|
ApRequestService,
|
||||||
ApResolverService,
|
ApResolverService,
|
||||||
|
Resolver,
|
||||||
JsonLdService,
|
JsonLdService,
|
||||||
RemoteLoggerService,
|
RemoteLoggerService,
|
||||||
RemoteUserResolveService,
|
RemoteUserResolveService,
|
||||||
@@ -745,6 +746,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
ApRendererService,
|
ApRendererService,
|
||||||
ApRequestService,
|
ApRequestService,
|
||||||
ApResolverService,
|
ApResolverService,
|
||||||
|
Resolver,
|
||||||
JsonLdService,
|
JsonLdService,
|
||||||
RemoteLoggerService,
|
RemoteLoggerService,
|
||||||
RemoteUserResolveService,
|
RemoteUserResolveService,
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export class ApInboxService {
|
|||||||
if (isCollectionOrOrderedCollection(activity)) {
|
if (isCollectionOrOrderedCollection(activity)) {
|
||||||
const results = [] as [string, string | void][];
|
const results = [] as [string, string | void][];
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
resolver ??= this.apResolverService.createResolver();
|
resolver ??= await this.apResolverService.createResolver();
|
||||||
|
|
||||||
const items = toArray(isCollection(activity) ? activity.items : activity.orderedItems);
|
const items = toArray(isCollection(activity) ? activity.items : activity.orderedItems);
|
||||||
if (items.length >= resolver.getRecursionLimit()) {
|
if (items.length >= resolver.getRecursionLimit()) {
|
||||||
@@ -221,7 +221,7 @@ export class ApInboxService {
|
|||||||
this.logger.info(`Accept: ${uri}`);
|
this.logger.info(`Accept: ${uri}`);
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
resolver ??= this.apResolverService.createResolver();
|
resolver ??= await this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(activity.object).catch(err => {
|
const object = await resolver.resolve(activity.object).catch(err => {
|
||||||
this.logger.error(`Resolution failed: ${err}`);
|
this.logger.error(`Resolution failed: ${err}`);
|
||||||
@@ -284,7 +284,7 @@ export class ApInboxService {
|
|||||||
this.logger.info(`Announce: ${uri}`);
|
this.logger.info(`Announce: ${uri}`);
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
resolver ??= this.apResolverService.createResolver();
|
resolver ??= await this.apResolverService.createResolver();
|
||||||
|
|
||||||
if (!activity.object) return 'skip: activity has no object property';
|
if (!activity.object) return 'skip: activity has no object property';
|
||||||
const targetUri = getApId(activity.object);
|
const targetUri = getApId(activity.object);
|
||||||
@@ -406,7 +406,7 @@ export class ApInboxService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
resolver ??= this.apResolverService.createResolver();
|
resolver ??= await this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(activity.object).catch(e => {
|
const object = await resolver.resolve(activity.object).catch(e => {
|
||||||
this.logger.error(`Resolution failed: ${e}`);
|
this.logger.error(`Resolution failed: ${e}`);
|
||||||
@@ -575,7 +575,7 @@ export class ApInboxService {
|
|||||||
this.logger.info(`Reject: ${uri}`);
|
this.logger.info(`Reject: ${uri}`);
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
resolver ??= this.apResolverService.createResolver();
|
resolver ??= await this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(activity.object).catch(e => {
|
const object = await resolver.resolve(activity.object).catch(e => {
|
||||||
this.logger.error(`Resolution failed: ${e}`);
|
this.logger.error(`Resolution failed: ${e}`);
|
||||||
@@ -642,7 +642,7 @@ export class ApInboxService {
|
|||||||
this.logger.info(`Undo: ${uri}`);
|
this.logger.info(`Undo: ${uri}`);
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
resolver ??= this.apResolverService.createResolver();
|
resolver ??= await this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(activity.object).catch(e => {
|
const object = await resolver.resolve(activity.object).catch(e => {
|
||||||
this.logger.error(`Resolution failed: ${e}`);
|
this.logger.error(`Resolution failed: ${e}`);
|
||||||
@@ -774,7 +774,7 @@ export class ApInboxService {
|
|||||||
this.logger.debug('Update');
|
this.logger.debug('Update');
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
resolver ??= this.apResolverService.createResolver();
|
resolver ??= await this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(activity.object).catch(e => {
|
const object = await resolver.resolve(activity.object).catch(e => {
|
||||||
this.logger.error(`Resolution failed: ${e}`);
|
this.logger.error(`Resolution failed: ${e}`);
|
||||||
|
|||||||
@@ -3,10 +3,17 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||||
import { IsNull, Not } from 'typeorm';
|
import { IsNull, Not } from 'typeorm';
|
||||||
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
||||||
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js';
|
import type {
|
||||||
|
FollowRequestsRepository,
|
||||||
|
MiMeta,
|
||||||
|
NoteReactionsRepository,
|
||||||
|
NotesRepository,
|
||||||
|
PollsRepository,
|
||||||
|
UsersRepository
|
||||||
|
} from '@/models/_.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
@@ -16,26 +23,43 @@ import { LoggerService } from '@/core/LoggerService.js';
|
|||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { SystemAccountService } from '@/core/SystemAccountService.js';
|
import { SystemAccountService } from '@/core/SystemAccountService.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
|
import type { ICollection, IObject, IOrderedCollection } from './type.js';
|
||||||
import { isCollectionOrOrderedCollection } from './type.js';
|
import { isCollectionOrOrderedCollection } from './type.js';
|
||||||
import { ApDbResolverService } from './ApDbResolverService.js';
|
import { ApDbResolverService } from './ApDbResolverService.js';
|
||||||
import { ApRendererService } from './ApRendererService.js';
|
import { ApRendererService } from './ApRendererService.js';
|
||||||
import { ApRequestService } from './ApRequestService.js';
|
import { ApRequestService } from './ApRequestService.js';
|
||||||
import { FetchAllowSoftFailMask } from './misc/check-against-url.js';
|
import { FetchAllowSoftFailMask } from './misc/check-against-url.js';
|
||||||
import type { IObject, ICollection, IOrderedCollection } from './type.js';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
|
|
||||||
|
@Injectable({ scope: Scope.TRANSIENT })
|
||||||
export class Resolver {
|
export class Resolver {
|
||||||
private history: Set<string>;
|
private history: Set<string>;
|
||||||
private user?: MiLocalUser;
|
private user?: MiLocalUser;
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
private recursionLimit = 256;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
|
@Inject(DI.meta)
|
||||||
private meta: MiMeta,
|
private meta: MiMeta,
|
||||||
|
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
|
@Inject(DI.pollsRepository)
|
||||||
private pollsRepository: PollsRepository,
|
private pollsRepository: PollsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.noteReactionsRepository)
|
||||||
private noteReactionsRepository: NoteReactionsRepository,
|
private noteReactionsRepository: NoteReactionsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.followRequestsRepository)
|
||||||
private followRequestsRepository: FollowRequestsRepository,
|
private followRequestsRepository: FollowRequestsRepository,
|
||||||
|
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private systemAccountService: SystemAccountService,
|
private systemAccountService: SystemAccountService,
|
||||||
private apRequestService: ApRequestService,
|
private apRequestService: ApRequestService,
|
||||||
@@ -43,7 +67,6 @@ export class Resolver {
|
|||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private apDbResolverService: ApDbResolverService,
|
private apDbResolverService: ApDbResolverService,
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
private recursionLimit = 256,
|
|
||||||
) {
|
) {
|
||||||
this.history = new Set();
|
this.history = new Set();
|
||||||
this.logger = this.loggerService.getLogger('ap-resolve');
|
this.logger = this.loggerService.getLogger('ap-resolve');
|
||||||
@@ -180,54 +203,12 @@ export class Resolver {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApResolverService {
|
export class ApResolverService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
private moduleRef: ModuleRef,
|
||||||
private config: Config,
|
|
||||||
|
|
||||||
@Inject(DI.meta)
|
|
||||||
private meta: MiMeta,
|
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
|
||||||
private usersRepository: UsersRepository,
|
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
|
||||||
private notesRepository: NotesRepository,
|
|
||||||
|
|
||||||
@Inject(DI.pollsRepository)
|
|
||||||
private pollsRepository: PollsRepository,
|
|
||||||
|
|
||||||
@Inject(DI.noteReactionsRepository)
|
|
||||||
private noteReactionsRepository: NoteReactionsRepository,
|
|
||||||
|
|
||||||
@Inject(DI.followRequestsRepository)
|
|
||||||
private followRequestsRepository: FollowRequestsRepository,
|
|
||||||
|
|
||||||
private utilityService: UtilityService,
|
|
||||||
private systemAccountService: SystemAccountService,
|
|
||||||
private apRequestService: ApRequestService,
|
|
||||||
private httpRequestService: HttpRequestService,
|
|
||||||
private apRendererService: ApRendererService,
|
|
||||||
private apDbResolverService: ApDbResolverService,
|
|
||||||
private loggerService: LoggerService,
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public createResolver(): Resolver {
|
public async createResolver(): Promise<Resolver> {
|
||||||
return new Resolver(
|
return await this.moduleRef.create(Resolver);
|
||||||
this.config,
|
|
||||||
this.meta,
|
|
||||||
this.usersRepository,
|
|
||||||
this.notesRepository,
|
|
||||||
this.pollsRepository,
|
|
||||||
this.noteReactionsRepository,
|
|
||||||
this.followRequestsRepository,
|
|
||||||
this.utilityService,
|
|
||||||
this.systemAccountService,
|
|
||||||
this.apRequestService,
|
|
||||||
this.httpRequestService,
|
|
||||||
this.apRendererService,
|
|
||||||
this.apDbResolverService,
|
|
||||||
this.loggerService,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export class ApImageService {
|
|||||||
throw new Error('actor has been suspended');
|
throw new Error('actor has been suspended');
|
||||||
}
|
}
|
||||||
|
|
||||||
const image = await this.apResolverService.createResolver().resolve(value);
|
const image = await (await this.apResolverService.createResolver()).resolve(value);
|
||||||
|
|
||||||
if (!isDocument(image)) return null;
|
if (!isDocument(image)) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export class ApNoteService {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public async createNote(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver, silent = false): Promise<MiNote | null> {
|
public async createNote(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver, silent = false): Promise<MiNote | null> {
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
if (resolver == null) resolver = await this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(value);
|
const object = await resolver.resolve(value);
|
||||||
|
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
if (resolver == null) resolver = await this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(uri);
|
const object = await resolver.resolve(uri);
|
||||||
if (object.id == null) throw new Error('invalid object.id: ' + object.id);
|
if (object.id == null) throw new Error('invalid object.id: ' + object.id);
|
||||||
@@ -500,7 +500,7 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
if (resolver == null) resolver = await this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = hint ?? await resolver.resolve(uri);
|
const object = hint ?? await resolver.resolve(uri);
|
||||||
|
|
||||||
@@ -678,7 +678,7 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
|
|
||||||
// リモートサーバーからフェッチしてきて登録
|
// リモートサーバーからフェッチしてきて登録
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
if (resolver == null) resolver = await this.apResolverService.createResolver();
|
||||||
return await this.createPerson(uri, resolver);
|
return await this.createPerson(uri, resolver);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -707,7 +707,7 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
|
|
||||||
this.logger.info(`Updating the featured: ${user.uri}`);
|
this.logger.info(`Updating the featured: ${user.uri}`);
|
||||||
|
|
||||||
const _resolver = resolver ?? this.apResolverService.createResolver();
|
const _resolver = resolver ?? await this.apResolverService.createResolver();
|
||||||
|
|
||||||
// Resolve to (Ordered)Collection Object
|
// Resolve to (Ordered)Collection Object
|
||||||
const collection = await _resolver.resolveCollection(user.featured);
|
const collection = await _resolver.resolveCollection(user.featured);
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export class ApQuestionService {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public async extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise<IPoll> {
|
public async extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise<IPoll> {
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
if (resolver == null) resolver = await this.apResolverService.createResolver();
|
||||||
|
|
||||||
const question = await resolver.resolve(source);
|
const question = await resolver.resolve(source);
|
||||||
if (!isQuestion(question)) throw new Error('invalid type');
|
if (!isQuestion(question)) throw new Error('invalid type');
|
||||||
@@ -91,7 +91,7 @@ export class ApQuestionService {
|
|||||||
|
|
||||||
// resolve new Question object
|
// resolve new Question object
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
if (resolver == null) resolver = await this.apResolverService.createResolver();
|
||||||
const question = await resolver.resolve(value);
|
const question = await resolver.resolve(value);
|
||||||
this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
|
this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
|
||||||
|
|
||||||
|
|||||||
@@ -720,7 +720,7 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
me,
|
me,
|
||||||
{
|
{
|
||||||
...options,
|
...options,
|
||||||
userProfile: profilesMap.get(u.id),
|
userProfile: profilesMap?.get(u.id),
|
||||||
userRelations: userRelations,
|
userRelations: userRelations,
|
||||||
userMemos: userMemos,
|
userMemos: userMemos,
|
||||||
pinNotes: pinNotes,
|
pinNotes: pinNotes,
|
||||||
|
|||||||
@@ -4,13 +4,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import si from 'systeminformation';
|
|
||||||
import Xev from 'xev';
|
import Xev from 'xev';
|
||||||
import * as osUtils from 'os-utils';
|
import * as osUtils from 'os-utils';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
|
||||||
import { MiMeta } from '@/models/_.js';
|
import { MiMeta } from '@/models/_.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
const ev = new Xev();
|
const ev = new Xev();
|
||||||
|
|
||||||
@@ -97,12 +96,14 @@ function cpuUsage(): Promise<number> {
|
|||||||
|
|
||||||
// MEMORY STAT
|
// MEMORY STAT
|
||||||
async function mem() {
|
async function mem() {
|
||||||
|
const si = await import('systeminformation');
|
||||||
const data = await si.mem();
|
const data = await si.mem();
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NETWORK STAT
|
// NETWORK STAT
|
||||||
async function net() {
|
async function net() {
|
||||||
|
const si = await import('systeminformation');
|
||||||
const iface = await si.networkInterfaceDefault();
|
const iface = await si.networkInterfaceDefault();
|
||||||
const data = await si.networkStats(iface);
|
const data = await si.networkStats(iface);
|
||||||
return data[0];
|
return data[0];
|
||||||
@@ -110,5 +111,6 @@ async function net() {
|
|||||||
|
|
||||||
// FS STAT
|
// FS STAT
|
||||||
async function fs() {
|
async function fs() {
|
||||||
|
const si = await import('systeminformation');
|
||||||
return await si.disksIO().catch(() => ({ rIO_sec: 0, wIO_sec: 0 }));
|
return await si.disksIO().catch(() => ({ rIO_sec: 0, wIO_sec: 0 }));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as os from 'node:os';
|
import * as os from 'node:os';
|
||||||
import sysUtils from 'systeminformation';
|
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
|
|
||||||
export async function showMachineInfo(parentLogger: Logger) {
|
export async function showMachineInfo(parentLogger: Logger) {
|
||||||
const logger = parentLogger.createSubLogger('machine');
|
const logger = parentLogger.createSubLogger('machine');
|
||||||
logger.debug(`Hostname: ${os.hostname()}`);
|
logger.debug(`Hostname: ${os.hostname()}`);
|
||||||
logger.debug(`Platform: ${process.platform} Arch: ${process.arch}`);
|
logger.debug(`Platform: ${process.platform} Arch: ${process.arch}`);
|
||||||
const mem = await sysUtils.mem();
|
logger.debug(`CPU: ${os.cpus().length} core MEM: ${(os.totalmem() / 1024 / 1024 / 1024).toFixed(1)}GB (available: ${(os.freemem() / 1024 / 1024 / 1024).toFixed(1)}GB)`);
|
||||||
const totalmem = (mem.total / 1024 / 1024 / 1024).toFixed(1);
|
|
||||||
const availmem = (mem.available / 1024 / 1024 / 1024).toFixed(1);
|
|
||||||
logger.debug(`CPU: ${os.cpus().length} core MEM: ${totalmem}GB (available: ${availmem}GB)`);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { NodeinfoServerService } from './NodeinfoServerService.js';
|
|||||||
import { ServerService } from './ServerService.js';
|
import { ServerService } from './ServerService.js';
|
||||||
import { WellKnownServerService } from './WellKnownServerService.js';
|
import { WellKnownServerService } from './WellKnownServerService.js';
|
||||||
import { GetterService } from './api/GetterService.js';
|
import { GetterService } from './api/GetterService.js';
|
||||||
import { ChannelsService } from './api/stream/ChannelsService.js';
|
|
||||||
import { ActivityPubServerService } from './ActivityPubServerService.js';
|
import { ActivityPubServerService } from './ActivityPubServerService.js';
|
||||||
import { ApiLoggerService } from './api/ApiLoggerService.js';
|
import { ApiLoggerService } from './api/ApiLoggerService.js';
|
||||||
import { ApiServerService } from './api/ApiServerService.js';
|
import { ApiServerService } from './api/ApiServerService.js';
|
||||||
@@ -31,24 +30,25 @@ import { UrlPreviewService } from './web/UrlPreviewService.js';
|
|||||||
import { ClientLoggerService } from './web/ClientLoggerService.js';
|
import { ClientLoggerService } from './web/ClientLoggerService.js';
|
||||||
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
||||||
|
|
||||||
import { MainChannelService } from './api/stream/channels/main.js';
|
import MainStreamConnection from '@/server/api/stream/Connection.js';
|
||||||
import { AdminChannelService } from './api/stream/channels/admin.js';
|
import { MainChannel } from './api/stream/channels/main.js';
|
||||||
import { AntennaChannelService } from './api/stream/channels/antenna.js';
|
import { AdminChannel } from './api/stream/channels/admin.js';
|
||||||
import { ChannelChannelService } from './api/stream/channels/channel.js';
|
import { AntennaChannel } from './api/stream/channels/antenna.js';
|
||||||
import { DriveChannelService } from './api/stream/channels/drive.js';
|
import { ChannelChannel } from './api/stream/channels/channel.js';
|
||||||
import { GlobalTimelineChannelService } from './api/stream/channels/global-timeline.js';
|
import { DriveChannel } from './api/stream/channels/drive.js';
|
||||||
import { HashtagChannelService } from './api/stream/channels/hashtag.js';
|
import { GlobalTimelineChannel } from './api/stream/channels/global-timeline.js';
|
||||||
import { HomeTimelineChannelService } from './api/stream/channels/home-timeline.js';
|
import { HashtagChannel } from './api/stream/channels/hashtag.js';
|
||||||
import { HybridTimelineChannelService } from './api/stream/channels/hybrid-timeline.js';
|
import { HomeTimelineChannel } from './api/stream/channels/home-timeline.js';
|
||||||
import { LocalTimelineChannelService } from './api/stream/channels/local-timeline.js';
|
import { HybridTimelineChannel } from './api/stream/channels/hybrid-timeline.js';
|
||||||
import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js';
|
import { LocalTimelineChannel } from './api/stream/channels/local-timeline.js';
|
||||||
import { ServerStatsChannelService } from './api/stream/channels/server-stats.js';
|
import { QueueStatsChannel } from './api/stream/channels/queue-stats.js';
|
||||||
import { UserListChannelService } from './api/stream/channels/user-list.js';
|
import { ServerStatsChannel } from './api/stream/channels/server-stats.js';
|
||||||
import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js';
|
import { UserListChannel } from './api/stream/channels/user-list.js';
|
||||||
import { ChatUserChannelService } from './api/stream/channels/chat-user.js';
|
import { RoleTimelineChannel } from './api/stream/channels/role-timeline.js';
|
||||||
import { ChatRoomChannelService } from './api/stream/channels/chat-room.js';
|
import { ChatUserChannel } from './api/stream/channels/chat-user.js';
|
||||||
import { ReversiChannelService } from './api/stream/channels/reversi.js';
|
import { ChatRoomChannel } from './api/stream/channels/chat-room.js';
|
||||||
import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js';
|
import { ReversiChannel } from './api/stream/channels/reversi.js';
|
||||||
|
import { ReversiGameChannel } from './api/stream/channels/reversi-game.js';
|
||||||
import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
|
import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -69,7 +69,7 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
|
|||||||
ServerService,
|
ServerService,
|
||||||
WellKnownServerService,
|
WellKnownServerService,
|
||||||
GetterService,
|
GetterService,
|
||||||
ChannelsService,
|
MainStreamConnection,
|
||||||
ApiCallService,
|
ApiCallService,
|
||||||
ApiLoggerService,
|
ApiLoggerService,
|
||||||
ApiServerService,
|
ApiServerService,
|
||||||
@@ -80,24 +80,24 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
|
|||||||
SigninService,
|
SigninService,
|
||||||
SignupApiService,
|
SignupApiService,
|
||||||
StreamingApiServerService,
|
StreamingApiServerService,
|
||||||
MainChannelService,
|
MainChannel,
|
||||||
AdminChannelService,
|
AdminChannel,
|
||||||
AntennaChannelService,
|
AntennaChannel,
|
||||||
ChannelChannelService,
|
ChannelChannel,
|
||||||
DriveChannelService,
|
DriveChannel,
|
||||||
GlobalTimelineChannelService,
|
GlobalTimelineChannel,
|
||||||
HashtagChannelService,
|
HashtagChannel,
|
||||||
RoleTimelineChannelService,
|
RoleTimelineChannel,
|
||||||
ChatUserChannelService,
|
ChatUserChannel,
|
||||||
ChatRoomChannelService,
|
ChatRoomChannel,
|
||||||
ReversiChannelService,
|
ReversiChannel,
|
||||||
ReversiGameChannelService,
|
ReversiGameChannel,
|
||||||
HomeTimelineChannelService,
|
HomeTimelineChannel,
|
||||||
HybridTimelineChannelService,
|
HybridTimelineChannel,
|
||||||
LocalTimelineChannelService,
|
LocalTimelineChannel,
|
||||||
QueueStatsChannelService,
|
QueueStatsChannel,
|
||||||
ServerStatsChannelService,
|
ServerStatsChannel,
|
||||||
UserListChannelService,
|
UserListChannel,
|
||||||
OpenApiServerService,
|
OpenApiServerService,
|
||||||
OAuth2ProviderService,
|
OAuth2ProviderService,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export class ServerService implements OnApplicationShutdown {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public async launch(): Promise<void> {
|
public async launch(): Promise<void> {
|
||||||
const fastify = Fastify({
|
const fastify = Fastify({
|
||||||
trustProxy: this.config.trustProxy ?? false,
|
trustProxy: this.config.trustProxy,
|
||||||
logger: false,
|
logger: false,
|
||||||
});
|
});
|
||||||
this.#fastify = fastify;
|
this.#fastify = fastify;
|
||||||
|
|||||||
@@ -313,11 +313,14 @@ export class ApiCallService implements OnApplicationShutdown {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ep.meta.limit) {
|
if (ep.meta.limit) {
|
||||||
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
|
let limitActor: string | null = null;
|
||||||
let limitActor: string;
|
|
||||||
if (user) {
|
if (user) {
|
||||||
limitActor = user.id;
|
limitActor = user.id;
|
||||||
} else {
|
} else if (this.config.enableIpRateLimit) {
|
||||||
|
if (process.env.NODE_ENV === 'production' && (request.ip === '::1' || request.ip === '127.0.0.1')) {
|
||||||
|
this.logger.warn('Recieved API request from localhost IP address for rate limiting in production environment. This is likely due to an improper trustProxy setting in the config file.');
|
||||||
|
}
|
||||||
|
|
||||||
limitActor = getIpHash(request.ip);
|
limitActor = getIpHash(request.ip);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,7 +333,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
|||||||
// TODO: 毎リクエスト計算するのもあれだしキャッシュしたい
|
// TODO: 毎リクエスト計算するのもあれだしキャッシュしたい
|
||||||
const factor = user ? (await this.roleService.getUserPolicies(user.id)).rateLimitFactor : 1;
|
const factor = user ? (await this.roleService.getUserPolicies(user.id)).rateLimitFactor : 1;
|
||||||
|
|
||||||
if (factor > 0) {
|
if (limitActor != null && factor > 0) {
|
||||||
// Rate limit
|
// Rate limit
|
||||||
const rateLimit = await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor);
|
const rateLimit = await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor);
|
||||||
if (rateLimit != null) {
|
if (rateLimit != null) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import type {
|
|||||||
UserSecurityKeysRepository,
|
UserSecurityKeysRepository,
|
||||||
UsersRepository,
|
UsersRepository,
|
||||||
} from '@/models/_.js';
|
} from '@/models/_.js';
|
||||||
|
import type Logger from '@/logger.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { getIpHash } from '@/misc/get-ip-hash.js';
|
import { getIpHash } from '@/misc/get-ip-hash.js';
|
||||||
import type { MiLocalUser } from '@/models/User.js';
|
import type { MiLocalUser } from '@/models/User.js';
|
||||||
@@ -23,6 +24,7 @@ import { bindThis } from '@/decorators.js';
|
|||||||
import { WebAuthnService } from '@/core/WebAuthnService.js';
|
import { WebAuthnService } from '@/core/WebAuthnService.js';
|
||||||
import { UserAuthService } from '@/core/UserAuthService.js';
|
import { UserAuthService } from '@/core/UserAuthService.js';
|
||||||
import { CaptchaService } from '@/core/CaptchaService.js';
|
import { CaptchaService } from '@/core/CaptchaService.js';
|
||||||
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
|
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
|
||||||
import { RateLimiterService } from './RateLimiterService.js';
|
import { RateLimiterService } from './RateLimiterService.js';
|
||||||
import { SigninService } from './SigninService.js';
|
import { SigninService } from './SigninService.js';
|
||||||
@@ -31,6 +33,8 @@ import type { FastifyReply, FastifyRequest } from 'fastify';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SigninApiService {
|
export class SigninApiService {
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
@@ -50,6 +54,7 @@ export class SigninApiService {
|
|||||||
@Inject(DI.signinsRepository)
|
@Inject(DI.signinsRepository)
|
||||||
private signinsRepository: SigninsRepository,
|
private signinsRepository: SigninsRepository,
|
||||||
|
|
||||||
|
private loggerService: LoggerService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private rateLimiterService: RateLimiterService,
|
private rateLimiterService: RateLimiterService,
|
||||||
private signinService: SigninService,
|
private signinService: SigninService,
|
||||||
@@ -57,6 +62,7 @@ export class SigninApiService {
|
|||||||
private webAuthnService: WebAuthnService,
|
private webAuthnService: WebAuthnService,
|
||||||
private captchaService: CaptchaService,
|
private captchaService: CaptchaService,
|
||||||
) {
|
) {
|
||||||
|
this.logger = this.loggerService.getLogger('Signin');
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -90,16 +96,21 @@ export class SigninApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// not more than 1 attempt per second and not more than 10 attempts per hour
|
// not more than 1 attempt per second and not more than 10 attempts per hour
|
||||||
const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
|
if (this.config.enableIpRateLimit) {
|
||||||
if (rateLimit != null) {
|
if (process.env.NODE_ENV === 'production' && (request.ip === '::1' || request.ip === '127.0.0.1')) {
|
||||||
reply.code(429);
|
this.logger.warn('Recieved signin request from localhost IP address for rate limiting in production environment. This is likely due to an improper trustProxy setting in the config file.');
|
||||||
return {
|
}
|
||||||
error: {
|
const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
|
||||||
message: 'Too many failed attempts to sign in. Try again later.',
|
if (rateLimit != null) {
|
||||||
code: 'TOO_MANY_AUTHENTICATION_FAILURES',
|
reply.code(429);
|
||||||
id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
|
return {
|
||||||
},
|
error: {
|
||||||
};
|
message: 'Too many failed attempts to sign in. Try again later.',
|
||||||
|
code: 'TOO_MANY_AUTHENTICATION_FAILURES',
|
||||||
|
id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof username !== 'string') {
|
if (typeof username !== 'string') {
|
||||||
|
|||||||
@@ -84,19 +84,25 @@ export class SigninWithPasskeyApiService {
|
|||||||
return error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' });
|
return error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' });
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
if (this.config.enableIpRateLimit) {
|
||||||
|
if (process.env.NODE_ENV === 'production' && (request.ip === '::1' || request.ip === '127.0.0.1')) {
|
||||||
|
this.logger.warn('Recieved signin with passkey request from localhost IP address for rate limiting in production environment. This is likely due to an improper trustProxy setting in the config file.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
// Not more than 1 API call per 250ms and not more than 100 attempts per 30min
|
// Not more than 1 API call per 250ms and not more than 100 attempts per 30min
|
||||||
// NOTE: 1 Sign-in require 2 API calls
|
// NOTE: 1 Sign-in require 2 API calls
|
||||||
await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 30 * 1000, max: 200, minInterval: 250 }, getIpHash(request.ip));
|
await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 30 * 1000, max: 200, minInterval: 250 }, getIpHash(request.ip));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reply.code(429);
|
reply.code(429);
|
||||||
return {
|
return {
|
||||||
error: {
|
error: {
|
||||||
message: 'Too many failed attempts to sign in. Try again later.',
|
message: 'Too many failed attempts to sign in. Try again later.',
|
||||||
code: 'TOO_MANY_AUTHENTICATION_FAILURES',
|
code: 'TOO_MANY_AUTHENTICATION_FAILURES',
|
||||||
id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
|
id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initiate Passkey Auth challenge with context
|
// Initiate Passkey Auth challenge with context
|
||||||
|
|||||||
@@ -8,18 +8,14 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import * as WebSocket from 'ws';
|
import * as WebSocket from 'ws';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { UsersRepository, MiAccessToken } from '@/models/_.js';
|
import type { MiAccessToken } from '@/models/_.js';
|
||||||
import { NotificationService } from '@/core/NotificationService.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
|
||||||
import { MiLocalUser } from '@/models/User.js';
|
import { MiLocalUser } from '@/models/User.js';
|
||||||
import { UserService } from '@/core/UserService.js';
|
import { UserService } from '@/core/UserService.js';
|
||||||
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
|
|
||||||
import { ChannelMutingService } from '@/core/ChannelMutingService.js';
|
|
||||||
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
|
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
|
||||||
import MainStreamConnection from './stream/Connection.js';
|
import MainStreamConnection, { ConnectionRequest } from './stream/Connection.js';
|
||||||
import { ChannelsService } from './stream/ChannelsService.js';
|
|
||||||
import type * as http from 'node:http';
|
import type * as http from 'node:http';
|
||||||
|
import { ContextIdFactory, ModuleRef } from '@nestjs/core';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class StreamingApiServerService {
|
export class StreamingApiServerService {
|
||||||
@@ -31,16 +27,9 @@ export class StreamingApiServerService {
|
|||||||
@Inject(DI.redisForSub)
|
@Inject(DI.redisForSub)
|
||||||
private redisForSub: Redis.Redis,
|
private redisForSub: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
private moduleRef: ModuleRef,
|
||||||
private usersRepository: UsersRepository,
|
|
||||||
|
|
||||||
private cacheService: CacheService,
|
|
||||||
private authenticateService: AuthenticateService,
|
private authenticateService: AuthenticateService,
|
||||||
private channelsService: ChannelsService,
|
|
||||||
private notificationService: NotificationService,
|
|
||||||
private usersService: UserService,
|
private usersService: UserService,
|
||||||
private channelFollowingService: ChannelFollowingService,
|
|
||||||
private channelMutingService: ChannelMutingService,
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,14 +83,12 @@ export class StreamingApiServerService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stream = new MainStreamConnection(
|
const contextId = ContextIdFactory.create();
|
||||||
this.channelsService,
|
this.moduleRef.registerRequestByContextId<ConnectionRequest>({
|
||||||
this.notificationService,
|
user,
|
||||||
this.cacheService,
|
token: app,
|
||||||
this.channelFollowingService,
|
}, contextId);
|
||||||
this.channelMutingService,
|
const stream = await this.moduleRef.create(MainStreamConnection, contextId);
|
||||||
user, app,
|
|
||||||
);
|
|
||||||
|
|
||||||
await stream.init();
|
await stream.init();
|
||||||
|
|
||||||
|
|||||||
@@ -391,6 +391,7 @@ export * as 'users/featured-notes' from './endpoints/users/featured-notes.js';
|
|||||||
export * as 'users/flashs' from './endpoints/users/flashs.js';
|
export * as 'users/flashs' from './endpoints/users/flashs.js';
|
||||||
export * as 'users/followers' from './endpoints/users/followers.js';
|
export * as 'users/followers' from './endpoints/users/followers.js';
|
||||||
export * as 'users/following' from './endpoints/users/following.js';
|
export * as 'users/following' from './endpoints/users/following.js';
|
||||||
|
export * as 'users/get-following-birthday-users' from './endpoints/users/get-following-birthday-users.js';
|
||||||
export * as 'users/gallery/posts' from './endpoints/users/gallery/posts.js';
|
export * as 'users/gallery/posts' from './endpoints/users/gallery/posts.js';
|
||||||
export * as 'users/get-frequently-replied-users' from './endpoints/users/get-frequently-replied-users.js';
|
export * as 'users/get-frequently-replied-users' from './endpoints/users/get-frequently-replied-users.js';
|
||||||
export * as 'users/lists/create' from './endpoints/users/lists/create.js';
|
export * as 'users/lists/create' from './endpoints/users/lists/create.js';
|
||||||
|
|||||||
@@ -52,18 +52,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const jobs = await this.deliverQueue.getJobs(['delayed']);
|
const jobs = await this.deliverQueue.getJobs(['delayed']);
|
||||||
|
|
||||||
const res = [] as [string, number][];
|
const counts = new Map<string, number>();
|
||||||
|
|
||||||
for (const job of jobs) {
|
for (const job of jobs) {
|
||||||
const host = new URL(job.data.to).host;
|
const host = new URL(job.data.to).host;
|
||||||
if (res.find(x => x[0] === host)) {
|
counts.set(host, (counts.get(host) ?? 0) + 1);
|
||||||
res.find(x => x[0] === host)![1]++;
|
|
||||||
} else {
|
|
||||||
res.push([host, 1]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.sort((a, b) => b[1] - a[1]);
|
const res = [...counts.entries()].sort((a, b) => b[1] - a[1]);
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,18 +52,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const jobs = await this.inboxQueue.getJobs(['delayed']);
|
const jobs = await this.inboxQueue.getJobs(['delayed']);
|
||||||
|
|
||||||
const res = [] as [string, number][];
|
const counts = new Map<string, number>();
|
||||||
|
|
||||||
for (const job of jobs) {
|
for (const job of jobs) {
|
||||||
const host = new URL(job.data.signature.keyId).host;
|
const host = new URL(job.data.signature.keyId).host;
|
||||||
if (res.find(x => x[0] === host)) {
|
counts.set(host, (counts.get(host) ?? 0) + 1);
|
||||||
res.find(x => x[0] === host)![1]++;
|
|
||||||
} else {
|
|
||||||
res.push([host, 1]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.sort((a, b) => b[1] - a[1]);
|
const res = [...counts.entries()].sort((a, b) => b[1] - a[1]);
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as os from 'node:os';
|
import * as os from 'node:os';
|
||||||
import si from 'systeminformation';
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
@@ -112,6 +111,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
|
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async () => {
|
super(meta, paramDef, async () => {
|
||||||
|
const si = await import('systeminformation');
|
||||||
|
|
||||||
const memStats = await si.mem();
|
const memStats = await si.mem();
|
||||||
const fsStats = await si.fsSize();
|
const fsStats = await si.fsSize();
|
||||||
const netInterface = await si.networkInterfaceDefault();
|
const netInterface = await si.networkInterfaceDefault();
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
private apResolverService: ApResolverService,
|
private apResolverService: ApResolverService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const resolver = this.apResolverService.createResolver();
|
const resolver = await this.apResolverService.createResolver();
|
||||||
const object = await resolver.resolve(ps.uri);
|
const object = await resolver.resolve(ps.uri);
|
||||||
return object;
|
return object;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
if (this.utilityService.isSelfHost(host)) return null;
|
if (this.utilityService.isSelfHost(host)) return null;
|
||||||
|
|
||||||
// リモートから一旦オブジェクトフェッチ
|
// リモートから一旦オブジェクトフェッチ
|
||||||
const resolver = this.apResolverService.createResolver();
|
const resolver = await this.apResolverService.createResolver();
|
||||||
// allow ap/show exclusively to lookup URLs that are cross-origin or non-canonical (like https://alice.example.com/@bob@bob.example.com -> https://bob.example.com/@bob)
|
// allow ap/show exclusively to lookup URLs that are cross-origin or non-canonical (like https://alice.example.com/@bob@bob.example.com -> https://bob.example.com/@bob)
|
||||||
const object = await resolver.resolve(uri, FetchAllowSoftFailMask.CrossOrigin | FetchAllowSoftFailMask.NonCanonicalId).catch((err) => {
|
const object = await resolver.resolve(uri, FetchAllowSoftFailMask.CrossOrigin | FetchAllowSoftFailMask.NonCanonicalId).catch((err) => {
|
||||||
if (err instanceof IdentifiableError) {
|
if (err instanceof IdentifiableError) {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as os from 'node:os';
|
import * as os from 'node:os';
|
||||||
import si from 'systeminformation';
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { MiMeta } from '@/models/_.js';
|
import { MiMeta } from '@/models/_.js';
|
||||||
@@ -93,6 +92,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const si = await import('systeminformation');
|
||||||
|
|
||||||
const memStats = await si.mem();
|
const memStats = await si.mem();
|
||||||
const fsStats = await si.fsSize();
|
const fsStats = await si.fsSize();
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export const paramDef = {
|
|||||||
sinceDate: { type: 'integer' },
|
sinceDate: { type: 'integer' },
|
||||||
untilDate: { type: 'integer' },
|
untilDate: { type: 'integer' },
|
||||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||||
birthday: { ...birthdaySchema, nullable: true },
|
birthday: { ...birthdaySchema, nullable: true, description: '@deprecated use get-following-birthday-users instead.' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -146,14 +146,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
.andWhere('following.followerId = :userId', { userId: user.id })
|
.andWhere('following.followerId = :userId', { userId: user.id })
|
||||||
.innerJoinAndSelect('following.followee', 'followee');
|
.innerJoinAndSelect('following.followee', 'followee');
|
||||||
|
|
||||||
|
// @deprecated use get-following-birthday-users instead.
|
||||||
if (ps.birthday) {
|
if (ps.birthday) {
|
||||||
try {
|
query.innerJoin(this.userProfilesRepository.metadata.targetName, 'followeeProfile', 'followeeProfile.userId = following.followeeId');
|
||||||
const birthday = ps.birthday.substring(5, 10);
|
|
||||||
const birthdayUserQuery = this.userProfilesRepository.createQueryBuilder('user_profile');
|
|
||||||
birthdayUserQuery.select('user_profile.userId')
|
|
||||||
.where(`SUBSTR(user_profile.birthday, 6, 5) = '${birthday}'`);
|
|
||||||
|
|
||||||
query.andWhere(`following.followeeId IN (${ birthdayUserQuery.getQuery() })`);
|
try {
|
||||||
|
const birthday = ps.birthday.split('-');
|
||||||
|
birthday.shift(); // 年の部分を削除
|
||||||
|
// なぜか get_birthday_date() = :birthday だとインデックスが効かないので、BETWEEN で対応
|
||||||
|
query.andWhere('get_birthday_date(followeeProfile.birthday) BETWEEN :birthday AND :birthday', { birthday: parseInt(birthday.join('')) });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new ApiError(meta.errors.birthdayInvalid);
|
throw new ApiError(meta.errors.birthdayInvalid);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Brackets } from 'typeorm';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import type {
|
||||||
|
FollowingsRepository,
|
||||||
|
UserProfilesRepository,
|
||||||
|
} from '@/models/_.js';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['users'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
kind: 'read:account',
|
||||||
|
|
||||||
|
description: 'Find users who have a birthday on the specified range.',
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'misskey:id',
|
||||||
|
},
|
||||||
|
birthday: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
ref: 'UserLite',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||||
|
offset: { type: 'integer', default: 0 },
|
||||||
|
birthday: {
|
||||||
|
oneOf: [{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
month: { type: 'integer', minimum: 1, maximum: 12 },
|
||||||
|
day: { type: 'integer', minimum: 1, maximum: 31 },
|
||||||
|
},
|
||||||
|
required: ['month', 'day'],
|
||||||
|
}, {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
begin: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
month: { type: 'integer', minimum: 1, maximum: 12 },
|
||||||
|
day: { type: 'integer', minimum: 1, maximum: 31 },
|
||||||
|
},
|
||||||
|
required: ['month', 'day'],
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
month: { type: 'integer', minimum: 1, maximum: 12 },
|
||||||
|
day: { type: 'integer', minimum: 1, maximum: 31 },
|
||||||
|
},
|
||||||
|
required: ['month', 'day'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['begin', 'end'],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['birthday'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.userProfilesRepository)
|
||||||
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
@Inject(DI.followingsRepository)
|
||||||
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
|
private userEntityService: UserEntityService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const query = this.followingsRepository
|
||||||
|
.createQueryBuilder('following')
|
||||||
|
.andWhere('following.followerId = :userId', { userId: me.id })
|
||||||
|
.innerJoin(this.userProfilesRepository.metadata.targetName, 'followeeProfile', 'followeeProfile.userId = following.followeeId');
|
||||||
|
|
||||||
|
if (Object.hasOwn(ps.birthday, 'begin') && Object.hasOwn(ps.birthday, 'end')) {
|
||||||
|
const range = ps.birthday as { begin: { month: number; day: number }; end: { month: number; day: number }; };
|
||||||
|
|
||||||
|
// 誕生日は mmdd の形式の最大4桁の数字(例: 8月30日 → 830)でインデックスが効くようになっているので、その形式に変換
|
||||||
|
const begin = range.begin.month * 100 + range.begin.day;
|
||||||
|
const end = range.end.month * 100 + range.end.day;
|
||||||
|
|
||||||
|
if (begin <= end) {
|
||||||
|
query.andWhere('get_birthday_date(followeeProfile.birthday) BETWEEN :begin AND :end', { begin, end });
|
||||||
|
} else {
|
||||||
|
// 12/31 から 1/1 の範囲を取得するために OR で対応
|
||||||
|
query.andWhere(new Brackets(qb => {
|
||||||
|
qb.where('get_birthday_date(followeeProfile.birthday) BETWEEN :begin AND 1231', { begin });
|
||||||
|
qb.orWhere('get_birthday_date(followeeProfile.birthday) BETWEEN 101 AND :end', { end });
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const { month, day } = ps.birthday as { month: number; day: number };
|
||||||
|
// なぜか get_birthday_date() = :birthday だとインデックスが効かないので、BETWEEN で対応
|
||||||
|
query.andWhere('get_birthday_date(followeeProfile.birthday) BETWEEN :birthday AND :birthday', { birthday: month * 100 + day });
|
||||||
|
}
|
||||||
|
|
||||||
|
query.select('following.followeeId', 'user_id');
|
||||||
|
query.addSelect('get_birthday_date(followeeProfile.birthday)', 'birthday_date');
|
||||||
|
query.orderBy('birthday_date', 'ASC');
|
||||||
|
|
||||||
|
const birthdayUsers = await query
|
||||||
|
.offset(ps.offset).limit(ps.limit)
|
||||||
|
.getRawMany<{ birthday_date: number; user_id: string }>();
|
||||||
|
|
||||||
|
const users = new Map<string, Packed<'UserLite'>>((
|
||||||
|
await this.userEntityService.packMany(
|
||||||
|
birthdayUsers.map(u => u.user_id),
|
||||||
|
me,
|
||||||
|
{ schema: 'UserLite' },
|
||||||
|
)
|
||||||
|
).map(u => [u.id, u]));
|
||||||
|
|
||||||
|
return birthdayUsers
|
||||||
|
.map(item => {
|
||||||
|
const birthday = new Date();
|
||||||
|
birthday.setHours(0, 0, 0, 0);
|
||||||
|
// item.birthday_date は mmdd の形式の最大4桁の数字(例: 8月30日 → 830)で出力されるので、日付に戻してDateオブジェクトに設定
|
||||||
|
birthday.setMonth(Math.floor(item.birthday_date / 100) - 1, item.birthday_date % 100);
|
||||||
|
|
||||||
|
if (birthday.getTime() < new Date().setHours(0, 0, 0, 0)) {
|
||||||
|
birthday.setFullYear(new Date().getFullYear() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const birthdayStr = `${birthday.getFullYear()}-${(birthday.getMonth() + 1).toString().padStart(2, '0')}-${(birthday.getDate()).toString().padStart(2, '0')}`;
|
||||||
|
return {
|
||||||
|
id: item.user_id,
|
||||||
|
birthday: birthdayStr,
|
||||||
|
user: users.get(item.user_id),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(item => item.user != null)
|
||||||
|
.map(item => item as { id: string; birthday: string; user: Packed<'UserLite'> });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,72 +4,54 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { HybridTimelineChannel } from './channels/hybrid-timeline.js';
|
||||||
|
import { LocalTimelineChannel } from './channels/local-timeline.js';
|
||||||
|
import { HomeTimelineChannel } from './channels/home-timeline.js';
|
||||||
|
import { GlobalTimelineChannel } from './channels/global-timeline.js';
|
||||||
|
import { MainChannel } from './channels/main.js';
|
||||||
|
import { ChannelChannel } from './channels/channel.js';
|
||||||
|
import { AdminChannel } from './channels/admin.js';
|
||||||
|
import { ServerStatsChannel } from './channels/server-stats.js';
|
||||||
|
import { QueueStatsChannel } from './channels/queue-stats.js';
|
||||||
|
import { UserListChannel } from './channels/user-list.js';
|
||||||
|
import { AntennaChannel } from './channels/antenna.js';
|
||||||
|
import { DriveChannel } from './channels/drive.js';
|
||||||
|
import { HashtagChannel } from './channels/hashtag.js';
|
||||||
|
import { RoleTimelineChannel } from './channels/role-timeline.js';
|
||||||
|
import { ChatUserChannel } from './channels/chat-user.js';
|
||||||
|
import { ChatRoomChannel } from './channels/chat-room.js';
|
||||||
|
import { ReversiChannel } from './channels/reversi.js';
|
||||||
|
import { ReversiGameChannel } from './channels/reversi-game.js';
|
||||||
|
import type { ChannelConstructor } from './channel.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { HybridTimelineChannelService } from './channels/hybrid-timeline.js';
|
|
||||||
import { LocalTimelineChannelService } from './channels/local-timeline.js';
|
|
||||||
import { HomeTimelineChannelService } from './channels/home-timeline.js';
|
|
||||||
import { GlobalTimelineChannelService } from './channels/global-timeline.js';
|
|
||||||
import { MainChannelService } from './channels/main.js';
|
|
||||||
import { ChannelChannelService } from './channels/channel.js';
|
|
||||||
import { AdminChannelService } from './channels/admin.js';
|
|
||||||
import { ServerStatsChannelService } from './channels/server-stats.js';
|
|
||||||
import { QueueStatsChannelService } from './channels/queue-stats.js';
|
|
||||||
import { UserListChannelService } from './channels/user-list.js';
|
|
||||||
import { AntennaChannelService } from './channels/antenna.js';
|
|
||||||
import { DriveChannelService } from './channels/drive.js';
|
|
||||||
import { HashtagChannelService } from './channels/hashtag.js';
|
|
||||||
import { RoleTimelineChannelService } from './channels/role-timeline.js';
|
|
||||||
import { ChatUserChannelService } from './channels/chat-user.js';
|
|
||||||
import { ChatRoomChannelService } from './channels/chat-room.js';
|
|
||||||
import { ReversiChannelService } from './channels/reversi.js';
|
|
||||||
import { ReversiGameChannelService } from './channels/reversi-game.js';
|
|
||||||
import { type MiChannelService } from './channel.js';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ChannelsService {
|
export class ChannelsService {
|
||||||
constructor(
|
constructor(
|
||||||
private mainChannelService: MainChannelService,
|
|
||||||
private homeTimelineChannelService: HomeTimelineChannelService,
|
|
||||||
private localTimelineChannelService: LocalTimelineChannelService,
|
|
||||||
private hybridTimelineChannelService: HybridTimelineChannelService,
|
|
||||||
private globalTimelineChannelService: GlobalTimelineChannelService,
|
|
||||||
private userListChannelService: UserListChannelService,
|
|
||||||
private hashtagChannelService: HashtagChannelService,
|
|
||||||
private roleTimelineChannelService: RoleTimelineChannelService,
|
|
||||||
private antennaChannelService: AntennaChannelService,
|
|
||||||
private channelChannelService: ChannelChannelService,
|
|
||||||
private driveChannelService: DriveChannelService,
|
|
||||||
private serverStatsChannelService: ServerStatsChannelService,
|
|
||||||
private queueStatsChannelService: QueueStatsChannelService,
|
|
||||||
private adminChannelService: AdminChannelService,
|
|
||||||
private chatUserChannelService: ChatUserChannelService,
|
|
||||||
private chatRoomChannelService: ChatRoomChannelService,
|
|
||||||
private reversiChannelService: ReversiChannelService,
|
|
||||||
private reversiGameChannelService: ReversiGameChannelService,
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public getChannelService(name: string): MiChannelService<boolean> {
|
public getChannelConstructor(name: string): ChannelConstructor<boolean> {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case 'main': return this.mainChannelService;
|
case 'main': return MainChannel;
|
||||||
case 'homeTimeline': return this.homeTimelineChannelService;
|
case 'homeTimeline': return HomeTimelineChannel;
|
||||||
case 'localTimeline': return this.localTimelineChannelService;
|
case 'localTimeline': return LocalTimelineChannel;
|
||||||
case 'hybridTimeline': return this.hybridTimelineChannelService;
|
case 'hybridTimeline': return HybridTimelineChannel;
|
||||||
case 'globalTimeline': return this.globalTimelineChannelService;
|
case 'globalTimeline': return GlobalTimelineChannel;
|
||||||
case 'userList': return this.userListChannelService;
|
case 'userList': return UserListChannel;
|
||||||
case 'hashtag': return this.hashtagChannelService;
|
case 'hashtag': return HashtagChannel;
|
||||||
case 'roleTimeline': return this.roleTimelineChannelService;
|
case 'roleTimeline': return RoleTimelineChannel;
|
||||||
case 'antenna': return this.antennaChannelService;
|
case 'antenna': return AntennaChannel;
|
||||||
case 'channel': return this.channelChannelService;
|
case 'channel': return ChannelChannel;
|
||||||
case 'drive': return this.driveChannelService;
|
case 'drive': return DriveChannel;
|
||||||
case 'serverStats': return this.serverStatsChannelService;
|
case 'serverStats': return ServerStatsChannel;
|
||||||
case 'queueStats': return this.queueStatsChannelService;
|
case 'queueStats': return QueueStatsChannel;
|
||||||
case 'admin': return this.adminChannelService;
|
case 'admin': return AdminChannel;
|
||||||
case 'chatUser': return this.chatUserChannelService;
|
case 'chatUser': return ChatUserChannel;
|
||||||
case 'chatRoom': return this.chatRoomChannelService;
|
case 'chatRoom': return ChatRoomChannel;
|
||||||
case 'reversi': return this.reversiChannelService;
|
case 'reversi': return ReversiChannel;
|
||||||
case 'reversiGame': return this.reversiGameChannelService;
|
case 'reversiGame': return ReversiGameChannel;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`no such channel: ${name}`);
|
throw new Error(`no such channel: ${name}`);
|
||||||
|
|||||||
@@ -6,19 +6,39 @@
|
|||||||
import * as WebSocket from 'ws';
|
import * as WebSocket from 'ws';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import type { MiAccessToken } from '@/models/AccessToken.js';
|
import type { MiAccessToken } from '@/models/AccessToken.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import type { NotificationService } from '@/core/NotificationService.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { MiFollowing, MiUserProfile } from '@/models/_.js';
|
import { MiFollowing, MiUserProfile } from '@/models/_.js';
|
||||||
import type { GlobalEvents, StreamEventEmitter } from '@/core/GlobalEventService.js';
|
import type { GlobalEvents, StreamEventEmitter } from '@/core/GlobalEventService.js';
|
||||||
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
|
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
|
||||||
import { ChannelMutingService } from '@/core/ChannelMutingService.js';
|
import { ChannelMutingService } from '@/core/ChannelMutingService.js';
|
||||||
import { isJsonObject } from '@/misc/json-value.js';
|
|
||||||
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
|
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
|
||||||
import type { ChannelsService } from './ChannelsService.js';
|
import { isJsonObject } from '@/misc/json-value.js';
|
||||||
import type { EventEmitter } from 'events';
|
import type { EventEmitter } from 'events';
|
||||||
import type Channel from './channel.js';
|
import type Channel from './channel.js';
|
||||||
|
import type { ChannelConstructor } from './channel.js';
|
||||||
|
import type { ChannelRequest } from './channel.js';
|
||||||
|
import { ContextIdFactory, ModuleRef, REQUEST } from '@nestjs/core';
|
||||||
|
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||||
|
import { MainChannel } from '@/server/api/stream/channels/main.js';
|
||||||
|
import { HomeTimelineChannel } from '@/server/api/stream/channels/home-timeline.js';
|
||||||
|
import { LocalTimelineChannel } from '@/server/api/stream/channels/local-timeline.js';
|
||||||
|
import { HybridTimelineChannel } from '@/server/api/stream/channels/hybrid-timeline.js';
|
||||||
|
import { GlobalTimelineChannel } from '@/server/api/stream/channels/global-timeline.js';
|
||||||
|
import { UserListChannel } from '@/server/api/stream/channels/user-list.js';
|
||||||
|
import { HashtagChannel } from '@/server/api/stream/channels/hashtag.js';
|
||||||
|
import { RoleTimelineChannel } from '@/server/api/stream/channels/role-timeline.js';
|
||||||
|
import { AntennaChannel } from '@/server/api/stream/channels/antenna.js';
|
||||||
|
import { ChannelChannel } from '@/server/api/stream/channels/channel.js';
|
||||||
|
import { DriveChannel } from '@/server/api/stream/channels/drive.js';
|
||||||
|
import { ServerStatsChannel } from '@/server/api/stream/channels/server-stats.js';
|
||||||
|
import { QueueStatsChannel } from '@/server/api/stream/channels/queue-stats.js';
|
||||||
|
import { AdminChannel } from '@/server/api/stream/channels/admin.js';
|
||||||
|
import { ChatUserChannel } from '@/server/api/stream/channels/chat-user.js';
|
||||||
|
import { ChatRoomChannel } from '@/server/api/stream/channels/chat-room.js';
|
||||||
|
import { ReversiChannel } from '@/server/api/stream/channels/reversi.js';
|
||||||
|
import { ReversiGameChannel } from '@/server/api/stream/channels/reversi-game.js';
|
||||||
|
|
||||||
const MAX_CHANNELS_PER_CONNECTION = 32;
|
const MAX_CHANNELS_PER_CONNECTION = 32;
|
||||||
|
|
||||||
@@ -26,6 +46,7 @@ const MAX_CHANNELS_PER_CONNECTION = 32;
|
|||||||
* Main stream connection
|
* Main stream connection
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
@Injectable({ scope: Scope.TRANSIENT })
|
||||||
export default class Connection {
|
export default class Connection {
|
||||||
public user?: MiUser;
|
public user?: MiUser;
|
||||||
public token?: MiAccessToken;
|
public token?: MiAccessToken;
|
||||||
@@ -44,16 +65,16 @@ export default class Connection {
|
|||||||
private fetchIntervalId: NodeJS.Timeout | null = null;
|
private fetchIntervalId: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private channelsService: ChannelsService,
|
private moduleRef: ModuleRef,
|
||||||
private notificationService: NotificationService,
|
private notificationService: NotificationService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
private channelFollowingService: ChannelFollowingService,
|
private channelFollowingService: ChannelFollowingService,
|
||||||
private channelMutingService: ChannelMutingService,
|
private channelMutingService: ChannelMutingService,
|
||||||
user: MiUser | null | undefined,
|
@Inject(REQUEST)
|
||||||
token: MiAccessToken | null | undefined,
|
request: ConnectionRequest,
|
||||||
) {
|
) {
|
||||||
if (user) this.user = user;
|
if (request.user) this.user = request.user;
|
||||||
if (token) this.token = token;
|
if (request.token) this.token = request.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -232,28 +253,34 @@ export default class Connection {
|
|||||||
* チャンネルに接続
|
* チャンネルに接続
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public connectChannel(id: string, params: JsonObject | undefined, channel: string, pong = false) {
|
public async connectChannel(id: string, params: JsonObject | undefined, channel: string, pong = false) {
|
||||||
if (this.channels.length >= MAX_CHANNELS_PER_CONNECTION) {
|
if (this.channels.length >= MAX_CHANNELS_PER_CONNECTION) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const channelService = this.channelsService.getChannelService(channel);
|
const channelConstructor = this.getChannelConstructor(channel);
|
||||||
|
|
||||||
if (channelService.requireCredential && this.user == null) {
|
if (channelConstructor.requireCredential && this.user == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.token && ((channelService.kind && !this.token.permission.some(p => p === channelService.kind))
|
if (this.token && ((channelConstructor.kind && !this.token.permission.some(p => p === channelConstructor.kind))
|
||||||
|| (!channelService.kind && channelService.requireCredential))) {
|
|| (!channelConstructor.kind && channelConstructor.requireCredential))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 共有可能チャンネルに接続しようとしていて、かつそのチャンネルに既に接続していたら無意味なので無視
|
// 共有可能チャンネルに接続しようとしていて、かつそのチャンネルに既に接続していたら無意味なので無視
|
||||||
if (channelService.shouldShare && this.channels.some(c => c.chName === channel)) {
|
if (channelConstructor.shouldShare && this.channels.some(c => c.chName === channel)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ch: Channel = channelService.create(id, this);
|
const contextId = ContextIdFactory.create();
|
||||||
|
this.moduleRef.registerRequestByContextId<ChannelRequest>({
|
||||||
|
id: id,
|
||||||
|
connection: this,
|
||||||
|
}, contextId);
|
||||||
|
const ch: Channel = await this.moduleRef.create<Channel>(channelConstructor, contextId);
|
||||||
|
|
||||||
this.channels.push(ch);
|
this.channels.push(ch);
|
||||||
ch.init(params ?? {});
|
ch.init(params ?? {});
|
||||||
|
|
||||||
@@ -264,6 +291,33 @@ export default class Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public getChannelConstructor(name: string): ChannelConstructor<boolean> {
|
||||||
|
switch (name) {
|
||||||
|
case 'main': return MainChannel;
|
||||||
|
case 'homeTimeline': return HomeTimelineChannel;
|
||||||
|
case 'localTimeline': return LocalTimelineChannel;
|
||||||
|
case 'hybridTimeline': return HybridTimelineChannel;
|
||||||
|
case 'globalTimeline': return GlobalTimelineChannel;
|
||||||
|
case 'userList': return UserListChannel;
|
||||||
|
case 'hashtag': return HashtagChannel;
|
||||||
|
case 'roleTimeline': return RoleTimelineChannel;
|
||||||
|
case 'antenna': return AntennaChannel;
|
||||||
|
case 'channel': return ChannelChannel;
|
||||||
|
case 'drive': return DriveChannel;
|
||||||
|
case 'serverStats': return ServerStatsChannel;
|
||||||
|
case 'queueStats': return QueueStatsChannel;
|
||||||
|
case 'admin': return AdminChannel;
|
||||||
|
case 'chatUser': return ChatUserChannel;
|
||||||
|
case 'chatRoom': return ChatRoomChannel;
|
||||||
|
case 'reversi': return ReversiChannel;
|
||||||
|
case 'reversiGame': return ReversiGameChannel;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`no such channel: ${name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* チャンネルから切断
|
* チャンネルから切断
|
||||||
* @param id チャンネルコネクションID
|
* @param id チャンネルコネクションID
|
||||||
@@ -306,3 +360,8 @@ export default class Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConnectionRequest {
|
||||||
|
user: MiUser | null | undefined,
|
||||||
|
token: MiAccessToken | null | undefined,
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export default abstract class Channel {
|
|||||||
public abstract readonly chName: string;
|
public abstract readonly chName: string;
|
||||||
public static readonly shouldShare: boolean;
|
public static readonly shouldShare: boolean;
|
||||||
public static readonly requireCredential: boolean;
|
public static readonly requireCredential: boolean;
|
||||||
public static readonly kind?: string | null;
|
public static readonly kind: string | null;
|
||||||
|
|
||||||
protected get user() {
|
protected get user() {
|
||||||
return this.connection.user;
|
return this.connection.user;
|
||||||
@@ -85,9 +85,9 @@ export default abstract class Channel {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(id: string, connection: Connection) {
|
constructor(request: ChannelRequest) {
|
||||||
this.id = id;
|
this.id = request.id;
|
||||||
this.connection = connection;
|
this.connection = request.connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
public send(payload: { type: string, body: JsonValue }): void;
|
public send(payload: { type: string, body: JsonValue }): void;
|
||||||
@@ -111,9 +111,14 @@ export default abstract class Channel {
|
|||||||
public onMessage?(type: string, body: JsonValue): void;
|
public onMessage?(type: string, body: JsonValue): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MiChannelService<T extends boolean> = {
|
export interface ChannelRequest {
|
||||||
|
id: string,
|
||||||
|
connection: Connection,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChannelConstructor<T extends boolean> {
|
||||||
|
new(...args: any[]): Channel;
|
||||||
shouldShare: boolean;
|
shouldShare: boolean;
|
||||||
requireCredential: T;
|
requireCredential: T;
|
||||||
kind: T extends true ? string : string | null | undefined;
|
kind: T extends true ? string : string | null | undefined;
|
||||||
create: (id: string, connection: Connection) => Channel;
|
}
|
||||||
};
|
|
||||||
|
|||||||
@@ -3,17 +3,26 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { JsonObject } from '@/misc/json-value.js';
|
import type { JsonObject } from '@/misc/json-value.js';
|
||||||
import Channel, { type MiChannelService } from '../channel.js';
|
import Channel, { type ChannelRequest } from '../channel.js';
|
||||||
|
import { REQUEST } from '@nestjs/core';
|
||||||
|
|
||||||
class AdminChannel extends Channel {
|
@Injectable({ scope: Scope.TRANSIENT })
|
||||||
|
export class AdminChannel extends Channel {
|
||||||
public readonly chName = 'admin';
|
public readonly chName = 'admin';
|
||||||
public static shouldShare = true;
|
public static shouldShare = true;
|
||||||
public static requireCredential = true as const;
|
public static requireCredential = true as const;
|
||||||
public static kind = 'read:admin:stream';
|
public static kind = 'read:admin:stream';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(REQUEST)
|
||||||
|
request: ChannelRequest,
|
||||||
|
) {
|
||||||
|
super(request);
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async init(params: JsonObject) {
|
public async init(params: JsonObject) {
|
||||||
// Subscribe admin stream
|
// Subscribe admin stream
|
||||||
@@ -22,22 +31,3 @@ class AdminChannel extends Channel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AdminChannelService implements MiChannelService<true> {
|
|
||||||
public readonly shouldShare = AdminChannel.shouldShare;
|
|
||||||
public readonly requireCredential = AdminChannel.requireCredential;
|
|
||||||
public readonly kind = AdminChannel.kind;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public create(id: string, connection: Channel['connection']): AdminChannel {
|
|
||||||
return new AdminChannel(
|
|
||||||
id,
|
|
||||||
connection,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,14 +3,16 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
import type { JsonObject } from '@/misc/json-value.js';
|
import type { JsonObject } from '@/misc/json-value.js';
|
||||||
import Channel, { type MiChannelService } from '../channel.js';
|
import Channel, { type ChannelRequest } from '../channel.js';
|
||||||
|
import { REQUEST } from '@nestjs/core';
|
||||||
|
|
||||||
class AntennaChannel extends Channel {
|
@Injectable({ scope: Scope.TRANSIENT })
|
||||||
|
export class AntennaChannel extends Channel {
|
||||||
public readonly chName = 'antenna';
|
public readonly chName = 'antenna';
|
||||||
public static shouldShare = false;
|
public static shouldShare = false;
|
||||||
public static requireCredential = true as const;
|
public static requireCredential = true as const;
|
||||||
@@ -18,12 +20,12 @@ class AntennaChannel extends Channel {
|
|||||||
private antennaId: string;
|
private antennaId: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private noteEntityService: NoteEntityService,
|
@Inject(REQUEST)
|
||||||
|
request: ChannelRequest,
|
||||||
|
|
||||||
id: string,
|
private noteEntityService: NoteEntityService,
|
||||||
connection: Channel['connection'],
|
|
||||||
) {
|
) {
|
||||||
super(id, connection);
|
super(request);
|
||||||
//this.onEvent = this.onEvent.bind(this);
|
//this.onEvent = this.onEvent.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,24 +57,3 @@ class AntennaChannel extends Channel {
|
|||||||
this.subscriber.off(`antennaStream:${this.antennaId}`, this.onEvent);
|
this.subscriber.off(`antennaStream:${this.antennaId}`, this.onEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AntennaChannelService implements MiChannelService<true> {
|
|
||||||
public readonly shouldShare = AntennaChannel.shouldShare;
|
|
||||||
public readonly requireCredential = AntennaChannel.requireCredential;
|
|
||||||
public readonly kind = AntennaChannel.kind;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private noteEntityService: NoteEntityService,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public create(id: string, connection: Channel['connection']): AntennaChannel {
|
|
||||||
return new AntennaChannel(
|
|
||||||
this.noteEntityService,
|
|
||||||
id,
|
|
||||||
connection,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
@@ -11,20 +11,23 @@ import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
|||||||
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
||||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import type { JsonObject } from '@/misc/json-value.js';
|
import type { JsonObject } from '@/misc/json-value.js';
|
||||||
import Channel, { type MiChannelService } from '../channel.js';
|
import Channel, { type ChannelRequest } from '../channel.js';
|
||||||
|
import { REQUEST } from '@nestjs/core';
|
||||||
|
|
||||||
class ChannelChannel extends Channel {
|
@Injectable({ scope: Scope.TRANSIENT })
|
||||||
|
export class ChannelChannel extends Channel {
|
||||||
public readonly chName = 'channel';
|
public readonly chName = 'channel';
|
||||||
public static shouldShare = false;
|
public static shouldShare = false;
|
||||||
public static requireCredential = false as const;
|
public static requireCredential = false as const;
|
||||||
private channelId: string;
|
private channelId: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(REQUEST)
|
||||||
|
request: ChannelRequest,
|
||||||
|
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
id: string,
|
|
||||||
connection: Channel['connection'],
|
|
||||||
) {
|
) {
|
||||||
super(id, connection);
|
super(request);
|
||||||
//this.onNote = this.onNote.bind(this);
|
//this.onNote = this.onNote.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,24 +95,3 @@ class ChannelChannel extends Channel {
|
|||||||
this.subscriber.off('notesStream', this.onNote);
|
this.subscriber.off('notesStream', this.onNote);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ChannelChannelService implements MiChannelService<false> {
|
|
||||||
public readonly shouldShare = ChannelChannel.shouldShare;
|
|
||||||
public readonly requireCredential = ChannelChannel.requireCredential;
|
|
||||||
public readonly kind = ChannelChannel.kind;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private noteEntityService: NoteEntityService,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public create(id: string, connection: Channel['connection']): ChannelChannel {
|
|
||||||
return new ChannelChannel(
|
|
||||||
this.noteEntityService,
|
|
||||||
id,
|
|
||||||
connection,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,14 +3,16 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
import type { JsonObject } from '@/misc/json-value.js';
|
import type { JsonObject } from '@/misc/json-value.js';
|
||||||
import { ChatService } from '@/core/ChatService.js';
|
import { ChatService } from '@/core/ChatService.js';
|
||||||
import Channel, { type MiChannelService } from '../channel.js';
|
import Channel, { type ChannelRequest } from '../channel.js';
|
||||||
|
import { REQUEST } from '@nestjs/core';
|
||||||
|
|
||||||
class ChatRoomChannel extends Channel {
|
@Injectable({ scope: Scope.TRANSIENT })
|
||||||
|
export class ChatRoomChannel extends Channel {
|
||||||
public readonly chName = 'chatRoom';
|
public readonly chName = 'chatRoom';
|
||||||
public static shouldShare = false;
|
public static shouldShare = false;
|
||||||
public static requireCredential = true as const;
|
public static requireCredential = true as const;
|
||||||
@@ -18,12 +20,12 @@ class ChatRoomChannel extends Channel {
|
|||||||
private roomId: string;
|
private roomId: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private chatService: ChatService,
|
@Inject(REQUEST)
|
||||||
|
request: ChannelRequest,
|
||||||
|
|
||||||
id: string,
|
private chatService: ChatService,
|
||||||
connection: Channel['connection'],
|
|
||||||
) {
|
) {
|
||||||
super(id, connection);
|
super(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -55,24 +57,3 @@ class ChatRoomChannel extends Channel {
|
|||||||
this.subscriber.off(`chatRoomStream:${this.roomId}`, this.onEvent);
|
this.subscriber.off(`chatRoomStream:${this.roomId}`, this.onEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ChatRoomChannelService implements MiChannelService<true> {
|
|
||||||
public readonly shouldShare = ChatRoomChannel.shouldShare;
|
|
||||||
public readonly requireCredential = ChatRoomChannel.requireCredential;
|
|
||||||
public readonly kind = ChatRoomChannel.kind;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private chatService: ChatService,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public create(id: string, connection: Channel['connection']): ChatRoomChannel {
|
|
||||||
return new ChatRoomChannel(
|
|
||||||
this.chatService,
|
|
||||||
id,
|
|
||||||
connection,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,14 +3,16 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
import type { JsonObject } from '@/misc/json-value.js';
|
import type { JsonObject } from '@/misc/json-value.js';
|
||||||
import { ChatService } from '@/core/ChatService.js';
|
import { ChatService } from '@/core/ChatService.js';
|
||||||
import Channel, { type MiChannelService } from '../channel.js';
|
import Channel, { type ChannelRequest } from '../channel.js';
|
||||||
|
import { REQUEST } from '@nestjs/core';
|
||||||
|
|
||||||
class ChatUserChannel extends Channel {
|
@Injectable({ scope: Scope.TRANSIENT })
|
||||||
|
export class ChatUserChannel extends Channel {
|
||||||
public readonly chName = 'chatUser';
|
public readonly chName = 'chatUser';
|
||||||
public static shouldShare = false;
|
public static shouldShare = false;
|
||||||
public static requireCredential = true as const;
|
public static requireCredential = true as const;
|
||||||
@@ -18,12 +20,12 @@ class ChatUserChannel extends Channel {
|
|||||||
private otherId: string;
|
private otherId: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private chatService: ChatService,
|
@Inject(REQUEST)
|
||||||
|
request: ChannelRequest,
|
||||||
|
|
||||||
id: string,
|
private chatService: ChatService,
|
||||||
connection: Channel['connection'],
|
|
||||||
) {
|
) {
|
||||||
super(id, connection);
|
super(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -55,24 +57,3 @@ class ChatUserChannel extends Channel {
|
|||||||
this.subscriber.off(`chatUserStream:${this.user!.id}-${this.otherId}`, this.onEvent);
|
this.subscriber.off(`chatUserStream:${this.user!.id}-${this.otherId}`, this.onEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ChatUserChannelService implements MiChannelService<true> {
|
|
||||||
public readonly shouldShare = ChatUserChannel.shouldShare;
|
|
||||||
public readonly requireCredential = ChatUserChannel.requireCredential;
|
|
||||||
public readonly kind = ChatUserChannel.kind;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private chatService: ChatService,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public create(id: string, connection: Channel['connection']): ChatUserChannel {
|
|
||||||
return new ChatUserChannel(
|
|
||||||
this.chatService,
|
|
||||||
id,
|
|
||||||
connection,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,17 +3,26 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { JsonObject } from '@/misc/json-value.js';
|
import type { JsonObject } from '@/misc/json-value.js';
|
||||||
import Channel, { type MiChannelService } from '../channel.js';
|
import Channel, { type ChannelRequest } from '../channel.js';
|
||||||
|
import { REQUEST } from '@nestjs/core';
|
||||||
|
|
||||||
class DriveChannel extends Channel {
|
@Injectable({ scope: Scope.TRANSIENT })
|
||||||
|
export class DriveChannel extends Channel {
|
||||||
public readonly chName = 'drive';
|
public readonly chName = 'drive';
|
||||||
public static shouldShare = true;
|
public static shouldShare = true;
|
||||||
public static requireCredential = true as const;
|
public static requireCredential = true as const;
|
||||||
public static kind = 'read:account';
|
public static kind = 'read:account';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(REQUEST)
|
||||||
|
request: ChannelRequest,
|
||||||
|
) {
|
||||||
|
super(request);
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async init(params: JsonObject) {
|
public async init(params: JsonObject) {
|
||||||
// Subscribe drive stream
|
// Subscribe drive stream
|
||||||
@@ -22,22 +31,3 @@ class DriveChannel extends Channel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class DriveChannelService implements MiChannelService<true> {
|
|
||||||
public readonly shouldShare = DriveChannel.shouldShare;
|
|
||||||
public readonly requireCredential = DriveChannel.requireCredential;
|
|
||||||
public readonly kind = DriveChannel.kind;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public create(id: string, connection: Channel['connection']): DriveChannel {
|
|
||||||
return new DriveChannel(
|
|
||||||
id,
|
|
||||||
connection,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
@@ -11,9 +11,11 @@ import { bindThis } from '@/decorators.js';
|
|||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||||
import type { JsonObject } from '@/misc/json-value.js';
|
import type { JsonObject } from '@/misc/json-value.js';
|
||||||
import Channel, { type MiChannelService } from '../channel.js';
|
import Channel, { type ChannelRequest } from '../channel.js';
|
||||||
|
import { REQUEST } from '@nestjs/core';
|
||||||
|
|
||||||
class GlobalTimelineChannel extends Channel {
|
@Injectable({ scope: Scope.TRANSIENT })
|
||||||
|
export class GlobalTimelineChannel extends Channel {
|
||||||
public readonly chName = 'globalTimeline';
|
public readonly chName = 'globalTimeline';
|
||||||
public static shouldShare = false;
|
public static shouldShare = false;
|
||||||
public static requireCredential = false as const;
|
public static requireCredential = false as const;
|
||||||
@@ -21,14 +23,14 @@ class GlobalTimelineChannel extends Channel {
|
|||||||
private withFiles: boolean;
|
private withFiles: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(REQUEST)
|
||||||
|
request: ChannelRequest,
|
||||||
|
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
|
|
||||||
id: string,
|
|
||||||
connection: Channel['connection'],
|
|
||||||
) {
|
) {
|
||||||
super(id, connection);
|
super(request);
|
||||||
//this.onNote = this.onNote.bind(this);
|
//this.onNote = this.onNote.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,28 +76,3 @@ class GlobalTimelineChannel extends Channel {
|
|||||||
this.subscriber.off('notesStream', this.onNote);
|
this.subscriber.off('notesStream', this.onNote);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class GlobalTimelineChannelService implements MiChannelService<false> {
|
|
||||||
public readonly shouldShare = GlobalTimelineChannel.shouldShare;
|
|
||||||
public readonly requireCredential = GlobalTimelineChannel.requireCredential;
|
|
||||||
public readonly kind = GlobalTimelineChannel.kind;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private metaService: MetaService,
|
|
||||||
private roleService: RoleService,
|
|
||||||
private noteEntityService: NoteEntityService,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public create(id: string, connection: Channel['connection']): GlobalTimelineChannel {
|
|
||||||
return new GlobalTimelineChannel(
|
|
||||||
this.metaService,
|
|
||||||
this.roleService,
|
|
||||||
this.noteEntityService,
|
|
||||||
id,
|
|
||||||
connection,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,28 +3,30 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||||
import type { JsonObject } from '@/misc/json-value.js';
|
import type { JsonObject } from '@/misc/json-value.js';
|
||||||
import Channel, { type MiChannelService } from '../channel.js';
|
import Channel, { type ChannelRequest } from '../channel.js';
|
||||||
|
import { REQUEST } from '@nestjs/core';
|
||||||
|
|
||||||
class HashtagChannel extends Channel {
|
@Injectable({ scope: Scope.TRANSIENT })
|
||||||
|
export class HashtagChannel extends Channel {
|
||||||
public readonly chName = 'hashtag';
|
public readonly chName = 'hashtag';
|
||||||
public static shouldShare = false;
|
public static shouldShare = false;
|
||||||
public static requireCredential = false as const;
|
public static requireCredential = false as const;
|
||||||
private q: string[][];
|
private q: string[][];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private noteEntityService: NoteEntityService,
|
@Inject(REQUEST)
|
||||||
|
request: ChannelRequest,
|
||||||
|
|
||||||
id: string,
|
private noteEntityService: NoteEntityService,
|
||||||
connection: Channel['connection'],
|
|
||||||
) {
|
) {
|
||||||
super(id, connection);
|
super(request);
|
||||||
//this.onNote = this.onNote.bind(this);
|
//this.onNote = this.onNote.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,24 +64,3 @@ class HashtagChannel extends Channel {
|
|||||||
this.subscriber.off('notesStream', this.onNote);
|
this.subscriber.off('notesStream', this.onNote);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class HashtagChannelService implements MiChannelService<false> {
|
|
||||||
public readonly shouldShare = HashtagChannel.shouldShare;
|
|
||||||
public readonly requireCredential = HashtagChannel.requireCredential;
|
|
||||||
public readonly kind = HashtagChannel.kind;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private noteEntityService: NoteEntityService,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public create(id: string, connection: Channel['connection']): HashtagChannel {
|
|
||||||
return new HashtagChannel(
|
|
||||||
this.noteEntityService,
|
|
||||||
id,
|
|
||||||
connection,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,15 +3,17 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||||
import type { JsonObject } from '@/misc/json-value.js';
|
import type { JsonObject } from '@/misc/json-value.js';
|
||||||
import Channel, { type MiChannelService } from '../channel.js';
|
import Channel, { type ChannelRequest } from '../channel.js';
|
||||||
|
import { REQUEST } from '@nestjs/core';
|
||||||
|
|
||||||
class HomeTimelineChannel extends Channel {
|
@Injectable({ scope: Scope.TRANSIENT })
|
||||||
|
export class HomeTimelineChannel extends Channel {
|
||||||
public readonly chName = 'homeTimeline';
|
public readonly chName = 'homeTimeline';
|
||||||
public static shouldShare = false;
|
public static shouldShare = false;
|
||||||
public static requireCredential = true as const;
|
public static requireCredential = true as const;
|
||||||
@@ -20,12 +22,12 @@ class HomeTimelineChannel extends Channel {
|
|||||||
private withFiles: boolean;
|
private withFiles: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private noteEntityService: NoteEntityService,
|
@Inject(REQUEST)
|
||||||
|
request: ChannelRequest,
|
||||||
|
|
||||||
id: string,
|
private noteEntityService: NoteEntityService,
|
||||||
connection: Channel['connection'],
|
|
||||||
) {
|
) {
|
||||||
super(id, connection);
|
super(request);
|
||||||
//this.onNote = this.onNote.bind(this);
|
//this.onNote = this.onNote.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,24 +100,3 @@ class HomeTimelineChannel extends Channel {
|
|||||||
this.subscriber.off('notesStream', this.onNote);
|
this.subscriber.off('notesStream', this.onNote);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class HomeTimelineChannelService implements MiChannelService<true> {
|
|
||||||
public readonly shouldShare = HomeTimelineChannel.shouldShare;
|
|
||||||
public readonly requireCredential = HomeTimelineChannel.requireCredential;
|
|
||||||
public readonly kind = HomeTimelineChannel.kind;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private noteEntityService: NoteEntityService,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public create(id: string, connection: Channel['connection']): HomeTimelineChannel {
|
|
||||||
return new HomeTimelineChannel(
|
|
||||||
this.noteEntityService,
|
|
||||||
id,
|
|
||||||
connection,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
@@ -11,9 +11,11 @@ import { bindThis } from '@/decorators.js';
|
|||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||||
import type { JsonObject } from '@/misc/json-value.js';
|
import type { JsonObject } from '@/misc/json-value.js';
|
||||||
import Channel, { type MiChannelService } from '../channel.js';
|
import Channel, { type ChannelRequest } from '../channel.js';
|
||||||
|
import { REQUEST } from '@nestjs/core';
|
||||||
|
|
||||||
class HybridTimelineChannel extends Channel {
|
@Injectable({ scope: Scope.TRANSIENT })
|
||||||
|
export class HybridTimelineChannel extends Channel {
|
||||||
public readonly chName = 'hybridTimeline';
|
public readonly chName = 'hybridTimeline';
|
||||||
public static shouldShare = false;
|
public static shouldShare = false;
|
||||||
public static requireCredential = true as const;
|
public static requireCredential = true as const;
|
||||||
@@ -23,14 +25,14 @@ class HybridTimelineChannel extends Channel {
|
|||||||
private withFiles: boolean;
|
private withFiles: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(REQUEST)
|
||||||
|
request: ChannelRequest,
|
||||||
|
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
|
|
||||||
id: string,
|
|
||||||
connection: Channel['connection'],
|
|
||||||
) {
|
) {
|
||||||
super(id, connection);
|
super(request);
|
||||||
//this.onNote = this.onNote.bind(this);
|
//this.onNote = this.onNote.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,28 +120,3 @@ class HybridTimelineChannel extends Channel {
|
|||||||
this.subscriber.off('notesStream', this.onNote);
|
this.subscriber.off('notesStream', this.onNote);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class HybridTimelineChannelService implements MiChannelService<true> {
|
|
||||||
public readonly shouldShare = HybridTimelineChannel.shouldShare;
|
|
||||||
public readonly requireCredential = HybridTimelineChannel.requireCredential;
|
|
||||||
public readonly kind = HybridTimelineChannel.kind;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private metaService: MetaService,
|
|
||||||
private roleService: RoleService,
|
|
||||||
private noteEntityService: NoteEntityService,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public create(id: string, connection: Channel['connection']): HybridTimelineChannel {
|
|
||||||
return new HybridTimelineChannel(
|
|
||||||
this.metaService,
|
|
||||||
this.roleService,
|
|
||||||
this.noteEntityService,
|
|
||||||
id,
|
|
||||||
connection,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user