Merge branch 'develop' into folding2

This commit is contained in:
FineArchs 2024-07-16 14:34:05 +09:00 committed by GitHub
commit b9d8ccc115
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
350 changed files with 10583 additions and 6927 deletions

View File

@ -1,5 +1,11 @@
# misskey settings
# MISSKEY_URL=https://example.tld/
# db settings # db settings
POSTGRES_PASSWORD=example-misskey-pass POSTGRES_PASSWORD=example-misskey-pass
# DATABASE_PASSWORD=${POSTGRES_PASSWORD}
POSTGRES_USER=example-misskey-user POSTGRES_USER=example-misskey-user
# DATABASE_USER=${POSTGRES_USER}
POSTGRES_DB=misskey POSTGRES_DB=misskey
# DATABASE_DB=${POSTGRES_DB}
DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}" DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}"

View File

@ -6,6 +6,7 @@
#───┘ URL └───────────────────────────────────────────────────── #───┘ URL └─────────────────────────────────────────────────────
# Final accessible URL seen by a user. # Final accessible URL seen by a user.
# You can set url from an environment variable instead.
url: https://example.tld/ url: https://example.tld/
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE # ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
@ -38,9 +39,11 @@ db:
port: 5432 port: 5432
# Database name # Database name
# You can set db from an environment variable instead.
db: misskey db: misskey
# Auth # Auth
# You can set user and pass from environment variables instead.
user: example-misskey-user user: example-misskey-user
pass: example-misskey-pass pass: example-misskey-pass

View File

@ -1,5 +1,3 @@
version: '3.8'
services: services:
app: app:
build: build:
@ -8,6 +6,7 @@ services:
volumes: volumes:
- ../:/workspace:cached - ../:/workspace:cached
- node_modules:/workspace/node_modules
command: sleep infinity command: sleep infinity
@ -46,6 +45,7 @@ services:
volumes: volumes:
postgres-data: postgres-data:
redis-data: redis-data:
node_modules:
networks: networks:
internal_network: internal_network:

View File

@ -1,6 +1,6 @@
{ {
"name": "Misskey", "name": "Misskey",
"dockerComposeFile": "docker-compose.yml", "dockerComposeFile": "compose.yml",
"service": "app", "service": "app",
"workspaceFolder": "/workspace", "workspaceFolder": "/workspace",
"features": { "features": {
@ -10,7 +10,7 @@
"ghcr.io/devcontainers-contrib/features/corepack:1": {} "ghcr.io/devcontainers-contrib/features/corepack:1": {}
}, },
"forwardPorts": [3000], "forwardPorts": [3000],
"postCreateCommand": "sudo chmod 755 .devcontainer/init.sh && .devcontainer/init.sh", "postCreateCommand": "/bin/bash .devcontainer/init.sh",
"customizations": { "customizations": {
"vscode": { "vscode": {
"extensions": [ "extensions": [

View File

@ -2,7 +2,8 @@
set -xe set -xe
sudo chown -R node /workspace sudo chown node node_modules
git config --global --add safe.directory /workspace
git submodule update --init git submodule update --init
corepack install corepack install
corepack enable corepack enable

View File

@ -7,7 +7,7 @@ Dockerfile
build/ build/
built/ built/
db/ db/
docker-compose.yml .devcontainer/compose.yml
node_modules/ node_modules/
packages/*/node_modules packages/*/node_modules
redis/ redis/
@ -28,4 +28,4 @@ fluent-emojis/
.idea/ .idea/
packages/*/.vscode/ packages/*/.vscode/
packages/backend/test/docker-compose.yml packages/backend/test/compose.yml

View File

@ -2,3 +2,7 @@ contact_links:
- name: 💬 Misskey official Discord - name: 💬 Misskey official Discord
url: https://discord.gg/Wp8gVStHW3 url: https://discord.gg/Wp8gVStHW3
about: Chat freely about Misskey about: Chat freely about Misskey
# 仮
- name: 💬 Start discussion
url: https://github.com/misskey-dev/misskey/discussions
about: The official forum to join conversation and ask question

View File

@ -4,10 +4,11 @@ on:
push: push:
paths: paths:
- packages/misskey-js/** - packages/misskey-js/**
- .github/workflows/api-misskey-js.yml
pull_request: pull_request:
paths: paths:
- packages/misskey-js/** - packages/misskey-js/**
- .github/workflows/api-misskey-js.yml
jobs: jobs:
report: report:
@ -20,7 +21,7 @@ jobs:
- run: corepack enable - run: corepack enable
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

View File

@ -14,7 +14,7 @@ jobs:
- name: Checkout head - name: Checkout head
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.1
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'

View File

@ -28,7 +28,7 @@ jobs:
- name: setup node - name: setup node
id: setup-node id: setup-node
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: pnpm cache: pnpm

View File

@ -6,12 +6,13 @@ on:
paths: paths:
- packages/misskey-js/package.json - packages/misskey-js/package.json
- package.json - package.json
- .github/workflows/check-misskey-js-version.yml
pull_request: pull_request:
branches: [ develop ] branches: [ develop ]
paths: paths:
- packages/misskey-js/package.json - packages/misskey-js/package.json
- package.json - package.json
- .github/workflows/check-misskey-js-version.yml
jobs: jobs:
check-version: check-version:
# ルートの package.json と packages/misskey-js/package.json のバージョンが一致しているかを確認する # ルートの package.json と packages/misskey-js/package.json のバージョンが一致しているかを確認する

View File

@ -37,7 +37,7 @@ jobs:
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push by digest - name: Build and push by digest
id: build id: build
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
with: with:
context: . context: .
push: true push: true

View File

@ -48,7 +48,7 @@ jobs:
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and Push to Docker Hub - name: Build and Push to Docker Hub
id: build id: build
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
with: with:
context: . context: .
push: true push: true

View File

@ -13,14 +13,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
DOCKER_CONTENT_TRUST: 1 DOCKER_CONTENT_TRUST: 1
DOCKLE_VERSION: 0.4.14
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.1
- run: | - name: Download and install dockle v${{ env.DOCKLE_VERSION }}
curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb" run: |
curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v${DOCKLE_VERSION}/dockle_${DOCKLE_VERSION}_Linux-64bit.deb"
sudo dpkg -i dockle.deb sudo dpkg -i dockle.deb
- run: | - run: |
cp .config/docker_example.env .config/docker.env cp .config/docker_example.env .config/docker.env
cp ./docker-compose_example.yml ./docker-compose.yml cp ./compose_example.yml ./compose.yml
- run: | - run: |
docker compose up -d web docker compose up -d web
docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" misskey-web:latest docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" misskey-web:latest

View File

@ -9,7 +9,7 @@ on:
paths: paths:
- packages/backend/** - packages/backend/**
- .github/workflows/get-api-diff.yml - .github/workflows/get-api-diff.yml
- .github/workflows/get-api-diff.yml
jobs: jobs:
get-from-misskey: get-from-misskey:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -34,7 +34,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View File

@ -10,15 +10,16 @@ on:
- packages/frontend/** - packages/frontend/**
- packages/sw/** - packages/sw/**
- packages/misskey-js/** - packages/misskey-js/**
- packages/shared/.eslintrc.js - packages/shared/eslint.config.js
- .github/workflows/lint.yml
pull_request: pull_request:
paths: paths:
- packages/backend/** - packages/backend/**
- packages/frontend/** - packages/frontend/**
- packages/sw/** - packages/sw/**
- packages/misskey-js/** - packages/misskey-js/**
- packages/shared/.eslintrc.js - packages/shared/eslint.config.js
- .github/workflows/lint.yml
jobs: jobs:
pnpm_install: pnpm_install:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -28,7 +29,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.0.2 - uses: actions/setup-node@v4.0.3
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'
@ -39,6 +40,8 @@ jobs:
needs: [pnpm_install] needs: [pnpm_install]
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: true continue-on-error: true
env:
eslint-cache-version: v1
strategy: strategy:
matrix: matrix:
workspace: workspace:
@ -52,13 +55,20 @@ jobs:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.0.2 - uses: actions/setup-node@v4.0.3
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'
- run: corepack enable - run: corepack enable
- run: pnpm i --frozen-lockfile - run: pnpm i --frozen-lockfile
- run: pnpm --filter ${{ matrix.workspace }} run eslint - name: Restore eslint cache
uses: actions/cache@v4.0.2
with:
path: node_modules/.cache/eslint
key: eslint-${{ env.eslint-cache-version }}-${{ hashFiles('/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}
restore-keys: |
eslint-${{ env.eslint-cache-version }}-${{ hashFiles('/pnpm-lock.yaml') }}-
- run: pnpm --filter ${{ matrix.workspace }} run eslint --cache --cache-location node_modules/.cache/eslint --cache-strategy content
typecheck: typecheck:
needs: [pnpm_install] needs: [pnpm_install]
@ -75,7 +85,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.0.2 - uses: actions/setup-node@v4.0.3
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

View File

@ -4,10 +4,11 @@ on:
push: push:
paths: paths:
- locales/** - locales/**
- .github/workflows/locale.yml
pull_request: pull_request:
paths: paths:
- locales/** - locales/**
- .github/workflows/locale.yml
jobs: jobs:
locale_verify: locale_verify:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -18,7 +19,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.0.2 - uses: actions/setup-node@v4.0.3
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

View File

@ -26,7 +26,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View File

@ -3,10 +3,10 @@ name: "Release Manager: sync changelog with PR"
on: on:
push: push:
branches: branches:
- release/** - develop
paths: paths:
- 'CHANGELOG.md' - 'CHANGELOG.md'
# - .github/workflows/release-edit-with-push.yml
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -20,24 +20,29 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# headがrelease/かつopenのPRを1つ取得 # headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得
- name: Get PR - name: Get PR
run: | run: |
echo "pr_number=$(gh pr list --limit 1 --head "$GITHUB_REF_NAME" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT echo "pr_number=$(gh pr list --limit 1 --search "head:$GITHUB_REF_NAME base:$STABLE_BRANCH is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
id: get_pr id: get_pr
env:
STABLE_BRANCH: ${{ vars.STABLE_BRANCH }}
- name: Get target version - name: Get target version
uses: misskey-dev/release-manager-actions/.github/actions/get-target-version@v1 if: steps.get_pr.outputs.pr_number != ''
uses: misskey-dev/release-manager-actions/.github/actions/get-target-version@v2
id: v id: v
# CHANGELOG.mdの内容を取得 # CHANGELOG.mdの内容を取得
- name: Get changelog - name: Get changelog
uses: misskey-dev/release-manager-actions/.github/actions/get-changelog@v1 if: steps.get_pr.outputs.pr_number != ''
uses: misskey-dev/release-manager-actions/.github/actions/get-changelog@v2
with: with:
version: ${{ steps.v.outputs.target_version }} version: ${{ steps.v.outputs.target_version }}
id: changelog id: changelog
# PRのnotesを更新 # PRのnotesを更新
- name: Update PR - name: Update PR
if: steps.get_pr.outputs.pr_number != ''
run: | run: |
gh pr edit "$PR_NUMBER" --body "$CHANGELOG" gh pr edit "$PR_NUMBER" --body "$CHANGELOG"
env: env:
CHANGELOG: ${{ steps.changelog.outputs.changelog }}
PR_NUMBER: ${{ steps.get_pr.outputs.pr_number }} PR_NUMBER: ${{ steps.get_pr.outputs.pr_number }}
CHANGELOG: ${{ steps.changelog.outputs.changelog }}

View File

@ -33,18 +33,21 @@ jobs:
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@v4
# headがrelease/かつopenのPRを1つ取得 # headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得
- name: Get PRs - name: Get PRs
run: | run: |
echo "pr_number=$(gh pr list --limit 1 --search "head:release/ is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT echo "pr_number=$(gh pr list --limit 1 --search "head:$GITHUB_REF_NAME base:$STABLE_BRANCH is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
id: get_pr id: get_pr
env:
STABLE_BRANCH: ${{ vars.STABLE_BRANCH }}
merge: merge:
uses: misskey-dev/release-manager-actions/.github/workflows/merge.yml@v1 uses: misskey-dev/release-manager-actions/.github/workflows/merge.yml@v2
needs: get-pr needs: get-pr
if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge == true }} if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge == true }}
with: with:
pr_number: ${{ needs.get-pr.outputs.pr_number }} pr_number: ${{ needs.get-pr.outputs.pr_number }}
user: 'github-actions[bot]'
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }} package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
# Text to prepend to the changelog # Text to prepend to the changelog
# The first line must be `## Unreleased` # The first line must be `## Unreleased`
@ -65,15 +68,14 @@ jobs:
secrets: secrets:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }} RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
RULESET_EDIT_APP_ID: ${{ secrets.RULESET_EDIT_APP_ID }}
RULESET_EDIT_APP_PRIVATE_KEY: ${{ secrets.RULESET_EDIT_APP_PRIVATE_KEY }}
create-prerelease: create-prerelease:
uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v1 uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v2
needs: get-pr needs: get-pr
if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge != true }} if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge != true }}
with: with:
pr_number: ${{ needs.get-pr.outputs.pr_number }} pr_number: ${{ needs.get-pr.outputs.pr_number }}
user: 'github-actions[bot]'
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }} package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }} use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
indent: ${{ vars.INDENT }} indent: ${{ vars.INDENT }}
@ -82,10 +84,11 @@ jobs:
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
create-target: create-target:
uses: misskey-dev/release-manager-actions/.github/workflows/create-target.yml@v1 uses: misskey-dev/release-manager-actions/.github/workflows/create-target.yml@v2
needs: get-pr needs: get-pr
if: ${{ needs.get-pr.outputs.pr_number == '' }} if: ${{ needs.get-pr.outputs.pr_number == '' }}
with: with:
user: 'github-actions[bot]'
# The script for version increment. # The script for version increment.
# process.env.CURRENT_VERSION: The current version. # process.env.CURRENT_VERSION: The current version.
# #
@ -118,8 +121,7 @@ jobs:
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }} package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }} use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
indent: ${{ vars.INDENT }} indent: ${{ vars.INDENT }}
stable_branch: ${{ vars.STABLE_BRANCH }}
secrets: secrets:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }} RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
RULESET_EDIT_APP_ID: ${{ secrets.RULESET_EDIT_APP_ID }}
RULESET_EDIT_APP_PRIVATE_KEY: ${{ secrets.RULESET_EDIT_APP_PRIVATE_KEY }}

View File

@ -16,23 +16,26 @@ jobs:
check: check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
ref: ${{ steps.get_pr.outputs.ref }} head: ${{ steps.get_pr.outputs.head }}
base: ${{ steps.get_pr.outputs.base }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# PR情報を取得 # PR情報を取得
- name: Get PR - name: Get PR
run: | run: |
pr_json=$(gh pr view "$PR_NUMBER" --json isDraft,headRefName) pr_json=$(gh pr view "$PR_NUMBER" --json isDraft,headRefName,baseRefName)
echo "ref=$(echo $pr_json | jq -r '.headRefName')" >> $GITHUB_OUTPUT echo "head=$(echo $pr_json | jq -r '.headRefName')" >> $GITHUB_OUTPUT
echo "base=$(echo $pr_json | jq -r '.baseRefName')" >> $GITHUB_OUTPUT
id: get_pr id: get_pr
env: env:
PR_NUMBER: ${{ github.event.pull_request.number }} PR_NUMBER: ${{ github.event.pull_request.number }}
release: release:
uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v1 uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v2
needs: check needs: check
if: startsWith(needs.check.outputs.ref, 'release/') if: needs.check.outputs.head == github.event.repository.default_branch && needs.check.outputs.base == vars.STABLE_BRANCH
with: with:
pr_number: ${{ github.event.pull_request.number }} pr_number: ${{ github.event.pull_request.number }}
user: 'github-actions[bot]'
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }} package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }} use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
indent: ${{ vars.INDENT }} indent: ${{ vars.INDENT }}

View File

@ -36,7 +36,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js 20.x - name: Use Node.js 20.x
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'
@ -88,7 +88,7 @@ jobs:
if [ "$BRANCH" = "misskey-dev:$HEAD_REF" ]; then if [ "$BRANCH" = "misskey-dev:$HEAD_REF" ]; then
BRANCH="$HEAD_REF" BRANCH="$HEAD_REF"
fi fi
pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static --branch-name $BRANCH $(echo "$CHROMATIC_PARAMETER") pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static --branch-name "$BRANCH" $(echo "$CHROMATIC_PARAMETER")
env: env:
HEAD_REF: ${{ github.event.pull_request.head.ref }} HEAD_REF: ${{ github.event.pull_request.head.ref }}
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}

View File

@ -9,12 +9,13 @@ on:
- packages/backend/** - packages/backend/**
# for permissions # for permissions
- packages/misskey-js/** - packages/misskey-js/**
- .github/workflows/test-backend.yml
pull_request: pull_request:
paths: paths:
- packages/backend/** - packages/backend/**
# for permissions # for permissions
- packages/misskey-js/** - packages/misskey-js/**
- .github/workflows/test-backend.yml
jobs: jobs:
unit: unit:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -45,7 +46,7 @@ jobs:
- name: Install FFmpeg - name: Install FFmpeg
uses: FedericoCarboni/setup-ffmpeg@v3 uses: FedericoCarboni/setup-ffmpeg@v3
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'
@ -92,7 +93,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View File

@ -11,7 +11,7 @@ on:
- packages/misskey-js/** - packages/misskey-js/**
# for e2e # for e2e
- packages/backend/** - packages/backend/**
- .github/workflows/test-frontend.yml
pull_request: pull_request:
paths: paths:
- packages/frontend/** - packages/frontend/**
@ -19,7 +19,7 @@ on:
- packages/misskey-js/** - packages/misskey-js/**
# for e2e # for e2e
- packages/backend/** - packages/backend/**
- .github/workflows/test-frontend.yml
jobs: jobs:
vitest: vitest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -35,7 +35,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'
@ -90,7 +90,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View File

@ -8,11 +8,12 @@ on:
branches: [ develop ] branches: [ develop ]
paths: paths:
- packages/misskey-js/** - packages/misskey-js/**
- .github/workflows/test-misskey-js.yml
pull_request: pull_request:
branches: [ develop ] branches: [ develop ]
paths: paths:
- packages/misskey-js/** - packages/misskey-js/**
- .github/workflows/test-misskey-js.yml
jobs: jobs:
test: test:
@ -30,7 +31,7 @@ jobs:
- run: corepack enable - run: corepack enable
- name: Setup Node.js ${{ matrix.node-version }} - name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View File

@ -25,7 +25,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View File

@ -7,10 +7,11 @@ on:
- develop - develop
paths: paths:
- packages/backend/** - packages/backend/**
- .github/workflows/validate-api-json.yml
pull_request: pull_request:
paths: paths:
- packages/backend/** - packages/backend/**
- .github/workflows/validate-api-json.yml
jobs: jobs:
validate-api-json: validate-api-json:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -26,7 +27,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

4
.gitignore vendored
View File

@ -35,8 +35,8 @@ coverage
!/.config/example.yml !/.config/example.yml
!/.config/docker_example.yml !/.config/docker_example.yml
!/.config/docker_example.env !/.config/docker_example.env
docker-compose.yml .devcontainer/compose.yml
!/.devcontainer/docker-compose.yml !/.devcontainer/compose.yml
# misskey # misskey
/build /build

View File

@ -1,15 +1,70 @@
## Unreleased ## Unreleased
### Note
- デッキUIの新着ートをサウンドで通知する機能の追加v2024.5.0)に伴い、以前から動作しなくなっていたクライアント設定内の「アンテナ受信」「チャンネル通知」サウンドを削除しました。
### General ### General
- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705 - Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
- Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に
- 変更不可となっていても、設定済みのものを解除してデフォルト画像に戻すことは出来ます
- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正 - Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
- Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正
- Enhance: Allow negative delay for MFM animation elements (`tada`, `jelly`, `twitch`, `shake`, `spin`, `jump`, `bounce`, `rainbow`)
### Client ### Client
- Enhance: 内蔵APIドキュメントのデザイン・パフォーマンスを改善
- Enhance: 非ログイン時に他サーバーに遷移するアクションを追加
- Enhance: 非ログイン時のハイライトTLのデザインを改善
- Enhance: フロントエンドのアクセシビリティ改善
(Based on https://github.com/taiyme/misskey/pull/226)
- Enhance: サーバー情報ページ・お問い合わせページを改善
(Cherry-picked from https://github.com/taiyme/misskey/pull/238)
- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
- Fix: リバーシの対局を正しく共有できないことがある問題を修正
- Fix: コントロールパネルでベースロールのポリシーを編集してもUI上では変更が反映されない問題を修正
- Fix: アンテナの編集画面のボタンに隙間を追加
- Fix: テーマプレビューが見れない問題を修正
- Fix: ショートカットキーが連打できる問題を修正
(Cherry-picked from https://github.com/taiyme/misskey/pull/234)
- Fix: MkSignin.vueのcredentialRequestからReactivityを削除ProxyがPasskey認証処理に渡ることを避けるため
- ートを畳む条件を「高精度の算出方法MFMの引数なども考慮する」、「旧式の算出方法従来通り」、「実際の表示の大きさ」から選べるように - ートを畳む条件を「高精度の算出方法MFMの引数なども考慮する」、「旧式の算出方法従来通り」、「実際の表示の大きさ」から選べるように
### Server ### Server
- チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正 - Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)
- Enhance: エンドポイント`clips/update`の必須項目を`clipId`のみに
- Enhance: エンドポイント`admin/roles/update`の必須項目を`roleId`のみに
- Enhance: エンドポイント`pages/update`の必須項目を`pageId`のみに
- Enhance: エンドポイント`gallery/posts/update`の必須項目を`postId`のみに
- Enhance: エンドポイント`i/webhook/update`の必須項目を`webhookId`のみに
- Enhance: エンドポイント`admin/ad/update`の必須項目を`id`のみに
- Enhance: `default.yml`内の`url`, `db.db`, `db.user`, `db.pass`を環境変数から読み込めるように
- Fix: チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正
- Fix: ユーザーのフィードページのMFMをHTMLに展開するように (#14006)
- Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036)
- Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059)
- Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正
- Fix: 自分以外のクリップ内のノート個数が見えることがあるのを修正
- Fix: 空文字列のリアクションはフォールバックされるように
- Fix: リノートにリアクションできないように
- Fix: ユーザー名の前後に空白文字列がある場合は省略するように
- Fix: プロフィール編集時に名前を空白文字列のみにできる問題を修正
- Fix: ユーザ名のサジェスト時に表示される内容と順番を調整(以下の順番になります) #14149
1. フォロー中かつアクティブなユーザ
2. フォロー中かつ非アクティブなユーザ
3. フォローしていないアクティブなユーザ
4. フォローしていない非アクティブなユーザ
また、自分自身のアカウントもサジェストされるようになりました。
- Fix: 一般ユーザーから見たユーザーのバッジの一覧に公開されていないものが含まれることがある問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/652)
- Fix: ユーザーのリアクション一覧でミュート/ブロックが機能していなかった問題を修正
- Fix: エラーメッセージの誤字を修正 (#14213)
### Misskey.js
- Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応)
- Feat: `/admin/role/create` のロールポリシーの型を修正
## 2024.5.0 ## 2024.5.0

View File

@ -165,7 +165,7 @@ cp .github/misskey/test.yml .config/
``` ```
Prepare DB/Redis for testing. Prepare DB/Redis for testing.
``` ```
docker compose -f packages/backend/test/docker-compose.yml up docker compose -f packages/backend/test/compose.yml up
``` ```
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`. Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.

View File

@ -82,6 +82,10 @@ RUN apt-get update \
USER misskey USER misskey
WORKDIR /misskey WORKDIR /misskey
# add package.json to add pnpm
COPY --chown=misskey:misskey ./package.json ./package.json
RUN corepack install
COPY --chown=misskey:misskey --from=target-builder /misskey/node_modules ./node_modules COPY --chown=misskey:misskey --from=target-builder /misskey/node_modules ./node_modules
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/backend/node_modules ./packages/backend/node_modules COPY --chown=misskey:misskey --from=target-builder /misskey/packages/backend/node_modules ./packages/backend/node_modules
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules

View File

@ -1,5 +1,3 @@
version: "3"
# このconfigは、 dockerでMisskey本体を起動せず、 redisとpostgresql などだけを起動します # このconfigは、 dockerでMisskey本体を起動せず、 redisとpostgresql などだけを起動します
services: services:

View File

@ -1,5 +1,3 @@
version: "3"
services: services:
web: web:
build: . build: .
@ -19,6 +17,8 @@ services:
networks: networks:
- internal_network - internal_network
- external_network - external_network
# env_file:
# - .config/docker.env
volumes: volumes:
- ./files:/misskey/files - ./files:/misskey/files
- ./.config:/misskey/.config:ro - ./.config:/misskey/.config:ro

46
locales/index.d.ts vendored
View File

@ -736,6 +736,22 @@ export interface Locale extends ILocale {
* *
*/ */
"showOnRemote": string; "showOnRemote": string;
/**
*
*/
"continueOnRemote": string;
/**
* Misskey Hubからサーバーを選択
*/
"chooseServerOnMisskeyHub": string;
/**
*
*/
"specifyServerHost": string;
/**
*
*/
"inputHostName": string;
/** /**
* *
*/ */
@ -1921,9 +1937,13 @@ export interface Locale extends ILocale {
*/ */
"onlyOneFileCanBeAttached": string; "onlyOneFileCanBeAttached": string;
/** /**
* *
*/ */
"signinRequired": string; "signinRequired": string;
/**
* 使
*/
"signinOrContinueOnRemote": string;
/** /**
* *
*/ */
@ -4984,6 +5004,10 @@ export interface Locale extends ILocale {
* *
*/ */
"inquiry": string; "inquiry": string;
/**
*
*/
"tryAgain": string;
"_delivery": { "_delivery": {
/** /**
* *
@ -6594,6 +6618,10 @@ export interface Locale extends ILocale {
* NSFWを常に付与 * NSFWを常に付与
*/ */
"alwaysMarkNsfw": string; "alwaysMarkNsfw": string;
/**
*
*/
"canUpdateBioMedia": string;
/** /**
* *
*/ */
@ -7515,14 +7543,6 @@ export interface Locale extends ILocale {
* *
*/ */
"notification": string; "notification": string;
/**
*
*/
"antenna": string;
/**
*
*/
"channel": string;
/** /**
* *
*/ */
@ -9761,7 +9781,7 @@ export interface Locale extends ILocale {
"_dataSaver": { "_dataSaver": {
"_media": { "_media": {
/** /**
* *
*/ */
"title": string; "title": string;
/** /**
@ -9771,7 +9791,7 @@ export interface Locale extends ILocale {
}; };
"_avatar": { "_avatar": {
/** /**
* *
*/ */
"title": string; "title": string;
/** /**
@ -9781,7 +9801,7 @@ export interface Locale extends ILocale {
}; };
"_urlPreview": { "_urlPreview": {
/** /**
* URLプレビューのサムネイル * URLプレビューのサムネイルを非表示
*/ */
"title": string; "title": string;
/** /**
@ -9791,7 +9811,7 @@ export interface Locale extends ILocale {
}; };
"_code": { "_code": {
/** /**
* *
*/ */
"title": string; "title": string;
/** /**

View File

@ -52,7 +52,11 @@ const primaries = {
const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), ''); const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
export function build() { export function build() {
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {}); // vitestの挙動を調整するため、一度ローカル変数化する必要がある
// https://github.com/vitest-dev/vitest/issues/3988#issuecomment-1686599577
// https://github.com/misskey-dev/misskey/pull/14057#issuecomment-2192833785
const metaUrl = import.meta.url;
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, metaUrl), 'utf-8'))) || {}, a), {});
// 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す // 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す
const removeEmpty = (obj) => { const removeEmpty = (obj) => {

View File

@ -180,6 +180,10 @@ addAccount: "アカウントを追加"
reloadAccountsList: "アカウントリストの情報を更新" reloadAccountsList: "アカウントリストの情報を更新"
loginFailed: "ログインに失敗しました" loginFailed: "ログインに失敗しました"
showOnRemote: "リモートで表示" showOnRemote: "リモートで表示"
continueOnRemote: "リモートで続行"
chooseServerOnMisskeyHub: "Misskey Hubからサーバーを選択"
specifyServerHost: "サーバーのドメインを直接指定"
inputHostName: "ドメインを入力してください"
general: "全般" general: "全般"
wallpaper: "壁紙" wallpaper: "壁紙"
setWallpaper: "壁紙を設定" setWallpaper: "壁紙を設定"
@ -476,7 +480,8 @@ attachAsFileQuestion: "クリップボードのテキストが長いです。テ
noMessagesYet: "まだチャットはありません" noMessagesYet: "まだチャットはありません"
newMessageExists: "新しいメッセージがあります" newMessageExists: "新しいメッセージがあります"
onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです" onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです"
signinRequired: "続行する前に、サインアップまたはサインインが必要です" signinRequired: "続行する前に、登録またはログインが必要です"
signinOrContinueOnRemote: "続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります"
invitations: "招待" invitations: "招待"
invitationCode: "招待コード" invitationCode: "招待コード"
checking: "確認しています" checking: "確認しています"
@ -1247,6 +1252,7 @@ keepOriginalFilenameDescription: "この設定をオフにすると、アップ
noDescription: "説明文はありません" noDescription: "説明文はありません"
alwaysConfirmFollow: "フォローの際常に確認する" alwaysConfirmFollow: "フォローの際常に確認する"
inquiry: "お問い合わせ" inquiry: "お問い合わせ"
tryAgain: "もう一度お試しください。"
_delivery: _delivery:
status: "配信状態" status: "配信状態"
@ -1710,6 +1716,7 @@ _role:
canManageAvatarDecorations: "アバターデコレーションの管理" canManageAvatarDecorations: "アバターデコレーションの管理"
driveCapacity: "ドライブ容量" driveCapacity: "ドライブ容量"
alwaysMarkNsfw: "ファイルにNSFWを常に付与" alwaysMarkNsfw: "ファイルにNSFWを常に付与"
canUpdateBioMedia: "アイコンとバナーの更新を許可"
pinMax: "ノートのピン留めの最大数" pinMax: "ノートのピン留めの最大数"
antennaMax: "アンテナの作成可能数" antennaMax: "アンテナの作成可能数"
wordMuteMax: "ワードミュートの最大文字数" wordMuteMax: "ワードミュートの最大文字数"
@ -1976,8 +1983,6 @@ _sfx:
note: "ノート" note: "ノート"
noteMy: "ノート(自分)" noteMy: "ノート(自分)"
notification: "通知" notification: "通知"
antenna: "アンテナ受信"
channel: "チャンネル通知"
reaction: "リアクション選択時" reaction: "リアクション選択時"
_soundSettings: _soundSettings:
@ -2604,16 +2609,16 @@ _externalResourceInstaller:
_dataSaver: _dataSaver:
_media: _media:
title: "メディアの読み込み" title: "メディアの読み込みを無効化"
description: "画像・動画が自動で読み込まれるのを防止します。隠れている画像・動画はタップすると読み込まれます。" description: "画像・動画が自動で読み込まれるのを防止します。隠れている画像・動画はタップすると読み込まれます。"
_avatar: _avatar:
title: "アイコン画像" title: "アイコン画像のアニメーションを無効化"
description: "アイコン画像のアニメーションが停止します。アニメーション画像は通常の画像よりファイルサイズが大きいことがあるので、データ通信量をさらに削減できます。" description: "アイコン画像のアニメーションが停止します。アニメーション画像は通常の画像よりファイルサイズが大きいことがあるので、データ通信量をさらに削減できます。"
_urlPreview: _urlPreview:
title: "URLプレビューのサムネイル" title: "URLプレビューのサムネイルを非表示"
description: "URLプレビューのサムネイル画像が読み込まれなくなります。" description: "URLプレビューのサムネイル画像が読み込まれなくなります。"
_code: _code:
title: "コードハイライト" title: "コードハイライトを非表示"
description: "MFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。" description: "MFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。"
_hemisphere: _hemisphere:

View File

@ -55,20 +55,22 @@
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"postcss": "8.4.38", "postcss": "8.4.38",
"tar": "6.2.1", "tar": "6.2.1",
"terser": "5.30.3", "terser": "5.31.1",
"typescript": "5.4.5", "typescript": "5.5.3",
"esbuild": "0.20.2", "esbuild": "0.22.0",
"glob": "10.3.12" "glob": "10.3.12"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "20.12.7", "@misskey-dev/eslint-plugin": "2.0.2",
"@typescript-eslint/eslint-plugin": "7.7.1", "@types/node": "20.14.9",
"@typescript-eslint/parser": "7.7.1", "@typescript-eslint/eslint-plugin": "7.15.0",
"@typescript-eslint/parser": "7.15.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "13.7.3", "cypress": "13.13.0",
"eslint": "8.57.0", "eslint": "9.6.0",
"globals": "15.7.0",
"ncp": "2.0.0", "ncp": "2.0.0",
"start-server-and-test": "2.0.3" "start-server-and-test": "2.0.4"
}, },
"optionalDependencies": { "optionalDependencies": {
"@tensorflow/tfjs-core": "4.4.0" "@tensorflow/tfjs-core": "4.4.0"

View File

@ -1,4 +0,0 @@
node_modules
/built
/.eslintrc.js
/@types/**/*

View File

@ -1,32 +0,0 @@
module.exports = {
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json', './test/tsconfig.json'],
},
extends: [
'../shared/.eslintrc.js',
],
rules: {
'import/order': ['warn', {
'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
'pathGroups': [
{
'pattern': '@/**',
'group': 'external',
'position': 'after'
}
],
}],
'no-restricted-globals': [
'error',
{
'name': '__dirname',
'message': 'Not in ESModule. Use `import.meta.url` instead.'
},
{
'name': '__filename',
'message': 'Not in ESModule. Use `import.meta.url` instead.'
}
]
},
};

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<title>Misskey API</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<script
id="api-reference"
data-url="/api.json"></script>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
</body>
</html>

View File

@ -1,24 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Misskey API</title>
<!-- needed for adaptive design -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
<!--
ReDoc doesn't change outer page styles
-->
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<redoc spec-url="/api.json" expand-responses="200" expand-single-schema-field="true"></redoc>
<script src="https://cdn.redoc.ly/redoc/v2.1.3/bundles/redoc.standalone.js" integrity="sha256-u4DgqzYXoArvNF/Ymw3puKexfOC6lYfw0sfmeliBJ1I=" crossorigin="anonymous"></script>
</body>
</html>

View File

@ -0,0 +1,46 @@
import tsParser from '@typescript-eslint/parser';
import sharedConfig from '../shared/eslint.config.js';
export default [
...sharedConfig,
{
ignores: ['**/node_modules', 'built', '@types/**/*'],
},
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parserOptions: {
parser: tsParser,
project: ['./tsconfig.json', './test/tsconfig.json'],
sourceType: 'module',
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
'import/order': ['warn', {
groups: [
'builtin',
'external',
'internal',
'parent',
'sibling',
'index',
'object',
'type',
],
pathGroups: [{
pattern: '@/**',
group: 'external',
position: 'after',
}],
}],
'no-restricted-globals': ['error', {
name: '__dirname',
message: 'Not in ESModule. Use `import.meta.url` instead.',
}, {
name: '__filename',
message: 'Not in ESModule. Use `import.meta.url` instead.',
}],
},
},
];

View File

@ -4,7 +4,7 @@
"private": true, "private": true,
"type": "module", "type": "module",
"engines": { "engines": {
"node": "^20.10.0" "node": "^20.10.0 || ^22.0.0"
}, },
"scripts": { "scripts": {
"start": "node ./built/boot/entry.js", "start": "node ./built/boot/entry.js",
@ -65,43 +65,43 @@
"utf-8-validate": "6.0.3" "utf-8-validate": "6.0.3"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "3.412.0", "@aws-sdk/client-s3": "3.600.0",
"@aws-sdk/lib-storage": "3.412.0", "@aws-sdk/lib-storage": "3.600.0",
"@bull-board/api": "5.17.0", "@bull-board/api": "5.20.5",
"@bull-board/fastify": "5.17.0", "@bull-board/fastify": "5.20.5",
"@bull-board/ui": "5.17.0", "@bull-board/ui": "5.20.5",
"@discordapp/twemoji": "15.0.3", "@discordapp/twemoji": "15.0.3",
"@fastify/accepts": "4.3.0", "@fastify/accepts": "4.3.0",
"@fastify/cookie": "9.3.1", "@fastify/cookie": "9.3.1",
"@fastify/cors": "9.0.1", "@fastify/cors": "9.0.1",
"@fastify/express": "3.0.0", "@fastify/express": "3.0.0",
"@fastify/http-proxy": "9.5.0", "@fastify/http-proxy": "9.5.0",
"@fastify/multipart": "8.2.0", "@fastify/multipart": "8.3.0",
"@fastify/static": "7.0.3", "@fastify/static": "7.0.4",
"@fastify/view": "9.1.0", "@fastify/view": "9.1.0",
"@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.1.0", "@misskey-dev/summaly": "5.1.0",
"@napi-rs/canvas": "^0.1.52", "@napi-rs/canvas": "^0.1.53",
"@nestjs/common": "10.3.8", "@nestjs/common": "10.3.10",
"@nestjs/core": "10.3.8", "@nestjs/core": "10.3.10",
"@nestjs/testing": "10.3.8", "@nestjs/testing": "10.3.10",
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@sentry/node": "^8.5.0", "@sentry/node": "8.13.0",
"@sentry/profiling-node": "^8.5.0", "@sentry/profiling-node": "8.13.0",
"@simplewebauthn/server": "10.0.0", "@simplewebauthn/server": "10.0.0",
"@sinonjs/fake-timers": "11.2.2", "@sinonjs/fake-timers": "11.2.2",
"@smithy/node-http-handler": "2.5.0", "@smithy/node-http-handler": "2.5.0",
"@swc/cli": "0.3.12", "@swc/cli": "0.3.12",
"@swc/core": "1.4.17", "@swc/core": "1.6.6",
"@twemoji/parser": "15.1.1", "@twemoji/parser": "15.1.1",
"accepts": "1.3.8", "accepts": "1.3.8",
"ajv": "8.13.0", "ajv": "8.16.0",
"archiver": "7.0.1", "archiver": "7.0.1",
"async-mutex": "0.5.0", "async-mutex": "0.5.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"body-parser": "1.20.2", "body-parser": "1.20.2",
"bullmq": "5.7.8", "bullmq": "5.8.3",
"cacheable-lookup": "7.0.0", "cacheable-lookup": "7.0.0",
"cbor": "9.0.2", "cbor": "9.0.2",
"chalk": "5.3.0", "chalk": "5.3.0",
@ -112,27 +112,27 @@
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"date-fns": "2.30.0", "date-fns": "2.30.0",
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"fastify": "4.26.2", "fastify": "4.28.1",
"fastify-raw-body": "4.3.0", "fastify-raw-body": "4.3.0",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "19.0.0", "file-type": "19.0.0",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.3",
"form-data": "4.0.0", "form-data": "4.0.0",
"got": "14.2.1", "got": "14.4.1",
"happy-dom": "10.0.3", "happy-dom": "10.0.3",
"hpagent": "1.2.0", "hpagent": "1.2.0",
"htmlescape": "1.1.1", "htmlescape": "1.1.1",
"http-link-header": "1.1.3", "http-link-header": "1.1.3",
"ioredis": "5.4.1", "ioredis": "5.4.1",
"ip-cidr": "3.1.0", "ip-cidr": "4.0.1",
"ipaddr.js": "2.2.0", "ipaddr.js": "2.2.0",
"is-svg": "5.0.0", "is-svg": "5.0.1",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsdom": "24.0.0", "jsdom": "24.1.0",
"json5": "2.2.3", "json5": "2.2.3",
"jsonld": "8.3.2", "jsonld": "8.3.2",
"jsrsasign": "11.1.0", "jsrsasign": "11.1.0",
"meilisearch": "0.38.0", "meilisearch": "0.41.0",
"mfm-js": "0.24.0", "mfm-js": "0.24.0",
"microformats-parser": "2.0.2", "microformats-parser": "2.0.2",
"mime-types": "2.1.35", "mime-types": "2.1.35",
@ -142,24 +142,24 @@
"nanoid": "5.0.7", "nanoid": "5.0.7",
"nested-property": "4.0.0", "nested-property": "4.0.0",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"nodemailer": "6.9.13", "nodemailer": "6.9.14",
"nsfwjs": "2.4.2", "nsfwjs": "2.4.2",
"oauth": "0.10.0", "oauth": "0.10.0",
"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",
"otpauth": "9.2.3", "otpauth": "9.3.1",
"parse5": "7.1.2", "parse5": "7.1.2",
"pg": "8.11.5", "pg": "8.12.0",
"pkce-challenge": "4.1.0", "pkce-challenge": "4.1.0",
"probe-image-size": "7.2.3", "probe-image-size": "7.2.3",
"promise-limit": "2.7.0", "promise-limit": "2.7.0",
"pug": "3.0.2", "pug": "3.0.3",
"punycode": "2.3.1", "punycode": "2.3.1",
"qrcode": "1.5.3", "qrcode": "1.5.3",
"random-seed": "0.3.0", "random-seed": "0.3.0",
"ratelimiter": "3.4.1", "ratelimiter": "3.4.1",
"re2": "1.20.10", "re2": "1.21.3",
"redis-lock": "0.1.4", "redis-lock": "0.1.4",
"reflect-metadata": "0.2.2", "reflect-metadata": "0.2.2",
"rename": "1.0.4", "rename": "1.0.4",
@ -167,27 +167,26 @@
"rxjs": "7.8.1", "rxjs": "7.8.1",
"sanitize-html": "2.13.0", "sanitize-html": "2.13.0",
"secure-json-parse": "2.7.0", "secure-json-parse": "2.7.0",
"sharp": "0.33.3", "sharp": "0.33.4",
"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.22.7", "systeminformation": "5.22.11",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tmp": "0.2.3", "tmp": "0.2.3",
"tsc-alias": "1.8.8", "tsc-alias": "1.8.10",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"typeorm": "0.3.20", "typeorm": "0.3.20",
"typescript": "5.4.5", "typescript": "5.5.3",
"ulid": "2.3.0", "ulid": "2.3.0",
"vary": "1.1.2", "vary": "1.1.2",
"web-push": "3.6.7", "web-push": "3.6.7",
"ws": "8.17.0", "ws": "8.17.1",
"xev": "3.0.2" "xev": "3.0.2"
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "29.7.0", "@jest/globals": "29.7.0",
"@misskey-dev/eslint-plugin": "1.0.0", "@nestjs/platform-express": "10.3.10",
"@nestjs/platform-express": "10.3.8",
"@simplewebauthn/types": "10.0.0", "@simplewebauthn/types": "10.0.0",
"@swc/jest": "0.2.36", "@swc/jest": "0.2.36",
"@types/accepts": "1.3.7", "@types/accepts": "1.3.7",
@ -197,22 +196,21 @@
"@types/color-convert": "2.0.3", "@types/color-convert": "2.0.3",
"@types/content-disposition": "0.5.8", "@types/content-disposition": "0.5.8",
"@types/fluent-ffmpeg": "2.1.24", "@types/fluent-ffmpeg": "2.1.24",
"@types/htmlescape": "^1.1.3", "@types/htmlescape": "1.1.3",
"@types/http-link-header": "1.0.5", "@types/http-link-header": "1.0.7",
"@types/jest": "29.5.12", "@types/jest": "29.5.12",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/jsdom": "21.1.6", "@types/jsdom": "21.1.7",
"@types/jsonld": "1.5.13", "@types/jsonld": "1.5.14",
"@types/jsrsasign": "10.5.14", "@types/jsrsasign": "10.5.14",
"@types/mime-types": "2.1.4", "@types/mime-types": "2.1.4",
"@types/ms": "0.7.34", "@types/ms": "0.7.34",
"@types/node": "20.12.7", "@types/node": "20.14.9",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.15", "@types/nodemailer": "6.4.15",
"@types/oauth": "0.9.4", "@types/oauth": "0.9.5",
"@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.11.5", "@types/pg": "8.11.6",
"@types/pug": "2.0.10", "@types/pug": "2.0.10",
"@types/punycode": "2.1.4", "@types/punycode": "2.1.4",
"@types/qrcode": "1.5.5", "@types/qrcode": "1.5.5",
@ -228,18 +226,17 @@
"@types/vary": "1.1.3", "@types/vary": "1.1.3",
"@types/web-push": "3.6.3", "@types/web-push": "3.6.3",
"@types/ws": "8.5.10", "@types/ws": "8.5.10",
"@typescript-eslint/eslint-plugin": "7.7.1", "@typescript-eslint/eslint-plugin": "7.15.0",
"@typescript-eslint/parser": "7.7.1", "@typescript-eslint/parser": "7.15.0",
"aws-sdk-client-mock": "3.0.1", "aws-sdk-client-mock": "4.0.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.57.0",
"eslint-plugin-import": "2.29.1", "eslint-plugin-import": "2.29.1",
"execa": "8.0.1", "execa": "9.2.0",
"fkill": "^9.0.0", "fkill": "9.0.0",
"jest": "29.7.0", "jest": "29.7.0",
"jest-mock": "29.7.0", "jest-mock": "29.7.0",
"nodemon": "3.1.0", "nodemon": "3.1.4",
"pid-port": "1.0.0", "pid-port": "1.0.0",
"simple-oauth2": "5.0.0" "simple-oauth2": "5.0.1"
} }
} }

View File

@ -30,6 +30,7 @@ function execStart() {
async function killProc() { async function killProc() {
if (backendProcess) { if (backendProcess) {
backendProcess.catch(() => {}); // backendProcess.kill()によって発生する例外を無視するためにcatch()を呼び出す
backendProcess.kill(); backendProcess.kill();
await new Promise(resolve => backendProcess.on('exit', resolve)); await new Promise(resolve => backendProcess.on('exit', resolve));
backendProcess = undefined; backendProcess = undefined;
@ -46,6 +47,7 @@ async function killProc() {
], ],
{ {
stdio: [process.stdin, process.stdout, process.stderr, 'ipc'], stdio: [process.stdin, process.stdout, process.stderr, 'ipc'],
serialization: "json",
}) })
.on('message', async (message) => { .on('message', async (message) => {
if (message.type === 'exit') { if (message.type === 'exit') {

View File

@ -23,7 +23,7 @@ type RedisOptionsSource = Partial<RedisOptions> & {
* *
*/ */
type Source = { type Source = {
url: string; url?: string;
port?: number; port?: number;
socket?: string; socket?: string;
chmodSocket?: string; chmodSocket?: string;
@ -31,9 +31,9 @@ type Source = {
db: { db: {
host: string; host: string;
port: number; port: number;
db: string; db?: string;
user: string; user?: string;
pass: string; pass?: string;
disableCache?: boolean; disableCache?: boolean;
extra?: { [x: string]: string }; extra?: { [x: string]: string };
}; };
@ -202,13 +202,17 @@ export function loadConfig(): Config {
: { 'src/_boot_.ts': { file: 'src/_boot_.ts' } }; : { 'src/_boot_.ts': { file: 'src/_boot_.ts' } };
const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source; const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
const url = tryCreateUrl(config.url); const url = tryCreateUrl(config.url ?? process.env.MISSKEY_URL ?? '');
const version = meta.version; const version = meta.version;
const host = url.host; const host = url.host;
const hostname = url.hostname; const hostname = url.hostname;
const scheme = url.protocol.replace(/:$/, ''); const scheme = url.protocol.replace(/:$/, '');
const wsScheme = scheme.replace('http', 'ws'); const wsScheme = scheme.replace('http', 'ws');
const dbDb = config.db.db ?? process.env.DATABASE_DB ?? '';
const dbUser = config.db.user ?? process.env.DATABASE_USER ?? '';
const dbPass = config.db.pass ?? process.env.DATABASE_PASSWORD ?? '';
const externalMediaProxy = config.mediaProxy ? const externalMediaProxy = config.mediaProxy ?
config.mediaProxy.endsWith('/') ? config.mediaProxy.substring(0, config.mediaProxy.length - 1) : config.mediaProxy config.mediaProxy.endsWith('/') ? config.mediaProxy.substring(0, config.mediaProxy.length - 1) : config.mediaProxy
: null; : null;
@ -231,7 +235,7 @@ export function loadConfig(): Config {
apiUrl: `${scheme}://${host}/api`, apiUrl: `${scheme}://${host}/api`,
authUrl: `${scheme}://${host}/auth`, authUrl: `${scheme}://${host}/auth`,
driveUrl: `${scheme}://${host}/files`, driveUrl: `${scheme}://${host}/files`,
db: config.db, db: { ...config.db, db: dbDb, user: dbUser, pass: dbPass },
dbReplications: config.dbReplications, dbReplications: config.dbReplications,
dbSlaves: config.dbSlaves, dbSlaves: config.dbSlaves,
meilisearch: config.meilisearch, meilisearch: config.meilisearch,
@ -259,7 +263,7 @@ export function loadConfig(): Config {
deliverJobMaxAttempts: config.deliverJobMaxAttempts, deliverJobMaxAttempts: config.deliverJobMaxAttempts,
inboxJobMaxAttempts: config.inboxJobMaxAttempts, inboxJobMaxAttempts: config.inboxJobMaxAttempts,
proxyRemoteFiles: config.proxyRemoteFiles, proxyRemoteFiles: config.proxyRemoteFiles,
signToActivityPubGet: config.signToActivityPubGet, signToActivityPubGet: config.signToActivityPubGet ?? true,
mediaProxy: externalMediaProxy ?? internalMediaProxy, mediaProxy: externalMediaProxy ?? internalMediaProxy,
externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy, externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy,
videoThumbnailGenerator: config.videoThumbnailGenerator ? videoThumbnailGenerator: config.videoThumbnailGenerator ?

View File

@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
// dummy
export const MAX_NOTE_TEXT_LENGTH = 3000; export const MAX_NOTE_TEXT_LENGTH = 3000;
export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min

View File

@ -10,7 +10,6 @@ import sanitizeHtml from 'sanitize-html';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import type { import type {
AbuseReportNotificationRecipientRepository, AbuseReportNotificationRecipientRepository,
MiAbuseReportNotificationRecipient, MiAbuseReportNotificationRecipient,
@ -91,7 +90,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
const recipientEMailAddresses = await this.fetchEMailRecipients().then(it => it const recipientEMailAddresses = await this.fetchEMailRecipients().then(it => it
.filter(it => it.isActive && it.userProfile?.emailVerified) .filter(it => it.isActive && it.userProfile?.emailVerified)
.map(it => it.userProfile?.email) .map(it => it.userProfile?.email)
.filter(isNotNull), .filter(x => x != null),
); );
// 送信先の鮮度を保つため、毎回取得する // 送信先の鮮度を保つため、毎回取得する
@ -138,7 +137,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
.then(it => it .then(it => it
.filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook') .filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook')
.map(it => it.systemWebhookId) .map(it => it.systemWebhookId)
.filter(isNotNull)); .filter(x => x != null));
for (const webhookId of recipientWebhookIds) { for (const webhookId of recipientWebhookIds) {
await Promise.all( await Promise.all(
abuseReports.map(it => { abuseReports.map(it => {
@ -340,7 +339,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
@bindThis @bindThis
private async removeUnauthorizedRecipientUsers(recipients: MiAbuseReportNotificationRecipient[]): Promise<MiAbuseReportNotificationRecipient[]> { private async removeUnauthorizedRecipientUsers(recipients: MiAbuseReportNotificationRecipient[]): Promise<MiAbuseReportNotificationRecipient[]> {
const userRecipients = recipients.filter(it => it.userId !== null); const userRecipients = recipients.filter(it => it.userId !== null);
const recipientUserIds = new Set(userRecipients.map(it => it.userId).filter(isNotNull)); const recipientUserIds = new Set(userRecipients.map(it => it.userId).filter(x => x != null));
if (recipientUserIds.size <= 0) { if (recipientUserIds.size <= 0) {
// ユーザが通知先として設定されていない場合、この関数での処理を行うべきレコードが無い // ユーザが通知先として設定されていない場合、この関数での処理を行うべきレコードが無い
return recipients; return recipients;

View File

@ -41,7 +41,7 @@ export class ClipService {
const currentCount = await this.clipsRepository.countBy({ const currentCount = await this.clipsRepository.countBy({
userId: me.id, userId: me.id,
}); });
if (currentCount > (await this.roleService.getUserPolicies(me.id)).clipLimit) { if (currentCount >= (await this.roleService.getUserPolicies(me.id)).clipLimit) {
throw new ClipService.TooManyClipsError(); throw new ClipService.TooManyClipsError();
} }
@ -102,7 +102,7 @@ export class ClipService {
const currentCount = await this.clipNotesRepository.countBy({ const currentCount = await this.clipNotesRepository.countBy({
clipId: clip.id, clipId: clip.id,
}); });
if (currentCount > (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) { if (currentCount >= (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) {
throw new ClipService.TooManyClipNotesError(); throw new ClipService.TooManyClipNotesError();
} }

View File

@ -12,6 +12,7 @@ import {
} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js'; } from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js'; import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { UserSearchService } from '@/core/UserSearchService.js';
import { AccountMoveService } from './AccountMoveService.js'; import { AccountMoveService } from './AccountMoveService.js';
import { AccountUpdateService } from './AccountUpdateService.js'; import { AccountUpdateService } from './AccountUpdateService.js';
import { AiService } from './AiService.js'; import { AiService } from './AiService.js';
@ -202,6 +203,7 @@ const $UserFollowingService: Provider = { provide: 'UserFollowingService', useEx
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService }; const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService }; const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService }; const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
const $UserSearchService: Provider = { provide: 'UserSearchService', useExisting: UserSearchService };
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService }; const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService }; const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService }; const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
@ -348,6 +350,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
UserKeypairService, UserKeypairService,
UserListService, UserListService,
UserMutingService, UserMutingService,
UserSearchService,
UserSuspendService, UserSuspendService,
UserAuthService, UserAuthService,
VideoProcessingService, VideoProcessingService,
@ -490,6 +493,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$UserKeypairService, $UserKeypairService,
$UserListService, $UserListService,
$UserMutingService, $UserMutingService,
$UserSearchService,
$UserSuspendService, $UserSuspendService,
$UserAuthService, $UserAuthService,
$VideoProcessingService, $VideoProcessingService,
@ -633,6 +637,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
UserKeypairService, UserKeypairService,
UserListService, UserListService,
UserMutingService, UserMutingService,
UserSearchService,
UserSuspendService, UserSuspendService,
UserAuthService, UserAuthService,
VideoProcessingService, VideoProcessingService,
@ -774,6 +779,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$UserKeypairService, $UserKeypairService,
$UserListService, $UserListService,
$UserMutingService, $UserMutingService,
$UserSearchService,
$UserSuspendService, $UserSuspendService,
$UserAuthService, $UserAuthService,
$VideoProcessingService, $VideoProcessingService,

View File

@ -55,9 +55,6 @@ export class FanoutTimelineEndpointService {
@bindThis @bindThis
private async getMiNotes(ps: TimelineOptions): Promise<MiNote[]> { private async getMiNotes(ps: TimelineOptions): Promise<MiNote[]> {
let noteIds: string[];
let shouldFallbackToDb = false;
// 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える // 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える
if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]); if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]);
@ -67,12 +64,11 @@ export class FanoutTimelineEndpointService {
const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId); const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId);
// TODO: いい感じにgetMulti内でソート済だからuniqするときにredisResultが全てソート済なのを利用して再ソートを避けたい // TODO: いい感じにgetMulti内でソート済だからuniqするときにredisResultが全てソート済なのを利用して再ソートを避けたい
const redisResultIds = Array.from(new Set(redisResult.flat(1))); const redisResultIds = Array.from(new Set(redisResult.flat(1))).sort(idCompare);
redisResultIds.sort(idCompare); let noteIds = redisResultIds.slice(0, ps.limit);
noteIds = redisResultIds.slice(0, ps.limit); const oldestNoteId = ascending ? redisResultIds[0] : redisResultIds[redisResultIds.length - 1];
const shouldFallbackToDb = noteIds.length === 0 || ps.sinceId != null && ps.sinceId < oldestNoteId;
shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
if (!shouldFallbackToDb) { if (!shouldFallbackToDb) {
let filter = ps.noteFilter ?? (_note => true); let filter = ps.noteFilter ?? (_note => true);

View File

@ -40,6 +40,7 @@ export class FederatedInstanceService implements OnApplicationShutdown {
firstRetrievedAt: new Date(parsed.firstRetrievedAt), firstRetrievedAt: new Date(parsed.firstRetrievedAt),
latestRequestReceivedAt: parsed.latestRequestReceivedAt ? new Date(parsed.latestRequestReceivedAt) : null, latestRequestReceivedAt: parsed.latestRequestReceivedAt ? new Date(parsed.latestRequestReceivedAt) : null,
infoUpdatedAt: parsed.infoUpdatedAt ? new Date(parsed.infoUpdatedAt) : null, infoUpdatedAt: parsed.infoUpdatedAt ? new Date(parsed.infoUpdatedAt) : null,
notRespondingSince: parsed.notRespondingSince ? new Date(parsed.notRespondingSince) : null,
}; };
}, },
}); });

View File

@ -13,10 +13,12 @@ import { intersperse } from '@/misc/prelude/array.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import type { IMentionedRemoteUsers } from '@/models/Note.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js'; import type { DefaultTreeAdapterMap } from 'parse5';
import type * as mfm from 'mfm-js'; import type * as mfm from 'mfm-js';
const treeAdapter = TreeAdapter.defaultTreeAdapter; const treeAdapter = parse5.defaultTreeAdapter;
type Node = DefaultTreeAdapterMap['node'];
type ChildNode = DefaultTreeAdapterMap['childNode'];
const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/; const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/; const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
@ -46,7 +48,7 @@ export class MfmService {
return text.trim(); return text.trim();
function getText(node: TreeAdapter.Node): string { function getText(node: Node): string {
if (treeAdapter.isTextNode(node)) return node.value; if (treeAdapter.isTextNode(node)) return node.value;
if (!treeAdapter.isElementNode(node)) return ''; if (!treeAdapter.isElementNode(node)) return '';
if (node.nodeName === 'br') return '\n'; if (node.nodeName === 'br') return '\n';
@ -58,7 +60,7 @@ export class MfmService {
return ''; return '';
} }
function appendChildren(childNodes: TreeAdapter.ChildNode[]): void { function appendChildren(childNodes: ChildNode[]): void {
if (childNodes) { if (childNodes) {
for (const n of childNodes) { for (const n of childNodes) {
analyze(n); analyze(n);
@ -66,14 +68,16 @@ export class MfmService {
} }
} }
function analyze(node: TreeAdapter.Node) { function analyze(node: Node) {
if (treeAdapter.isTextNode(node)) { if (treeAdapter.isTextNode(node)) {
text += node.value; text += node.value;
return; return;
} }
// Skip comment or document type node // Skip comment or document type node
if (!treeAdapter.isElementNode(node)) return; if (!treeAdapter.isElementNode(node)) {
return;
}
switch (node.nodeName) { switch (node.nodeName) {
case 'br': { case 'br': {
@ -81,8 +85,7 @@ export class MfmService {
break; break;
} }
case 'a': case 'a': {
{
const txt = getText(node); const txt = getText(node);
const rel = node.attrs.find(x => x.name === 'rel'); const rel = node.attrs.find(x => x.name === 'rel');
const href = node.attrs.find(x => x.name === 'href'); const href = node.attrs.find(x => x.name === 'href');
@ -90,7 +93,7 @@ export class MfmService {
// ハッシュタグ // ハッシュタグ
if (normalizedHashtagNames && href && normalizedHashtagNames.has(normalizeForSearch(txt))) { if (normalizedHashtagNames && href && normalizedHashtagNames.has(normalizeForSearch(txt))) {
text += txt; text += txt;
// メンション // メンション
} else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) { } else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) {
const part = txt.split('@'); const part = txt.split('@');
@ -102,7 +105,7 @@ export class MfmService {
} else if (part.length === 3) { } else if (part.length === 3) {
text += txt; text += txt;
} }
// その他 // その他
} else { } else {
const generateLink = () => { const generateLink = () => {
if (!href && !txt) { if (!href && !txt) {
@ -130,8 +133,7 @@ export class MfmService {
break; break;
} }
case 'h1': case 'h1': {
{
text += '【'; text += '【';
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += '】\n'; text += '】\n';
@ -139,16 +141,14 @@ export class MfmService {
} }
case 'b': case 'b':
case 'strong': case 'strong': {
{
text += '**'; text += '**';
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += '**'; text += '**';
break; break;
} }
case 'small': case 'small': {
{
text += '<small>'; text += '<small>';
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += '</small>'; text += '</small>';
@ -156,8 +156,7 @@ export class MfmService {
} }
case 's': case 's':
case 'del': case 'del': {
{
text += '~~'; text += '~~';
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += '~~'; text += '~~';
@ -165,8 +164,7 @@ export class MfmService {
} }
case 'i': case 'i':
case 'em': case 'em': {
{
text += '<i>'; text += '<i>';
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += '</i>'; text += '</i>';
@ -207,8 +205,7 @@ export class MfmService {
case 'h3': case 'h3':
case 'h4': case 'h4':
case 'h5': case 'h5':
case 'h6': case 'h6': {
{
text += '\n\n'; text += '\n\n';
appendChildren(node.childNodes); appendChildren(node.childNodes);
break; break;
@ -221,8 +218,7 @@ export class MfmService {
case 'article': case 'article':
case 'li': case 'li':
case 'dt': case 'dt':
case 'dd': case 'dd': {
{
text += '\n'; text += '\n';
appendChildren(node.childNodes); appendChildren(node.childNodes);
break; break;

View File

@ -59,7 +59,6 @@ import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js';
import { isReply } from '@/misc/is-reply.js'; import { isReply } from '@/misc/is-reply.js';
import { trackPromise } from '@/misc/promise-tracker.js'; import { trackPromise } from '@/misc/promise-tracker.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@ -839,7 +838,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const mentions = extractMentions(tokens); const mentions = extractMentions(tokens);
let mentionedUsers = (await Promise.all(mentions.map(m => let mentionedUsers = (await Promise.all(mentions.map(m =>
this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null), this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null),
))).filter(isNotNull); ))).filter(x => x != null);
// Drop duplicate users // Drop duplicate users
mentionedUsers = mentionedUsers.filter((u, i, self) => mentionedUsers = mentionedUsers.filter((u, i, self) =>

View File

@ -29,6 +29,7 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { FeaturedService } from '@/core/FeaturedService.js'; import { FeaturedService } from '@/core/FeaturedService.js';
import { trackPromise } from '@/misc/promise-tracker.js'; import { trackPromise } from '@/misc/promise-tracker.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
const FALLBACK = '\u2764'; const FALLBACK = '\u2764';
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16; const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
@ -117,11 +118,16 @@ export class ReactionService {
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.'); throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
} }
// Check if note is Renote
if (isRenote(note) && !isQuote(note)) {
throw new IdentifiableError('12c35529-3c79-4327-b1cc-e2cf63a71925', 'You cannot react to Renote.');
}
let reaction = _reaction ?? FALLBACK; let reaction = _reaction ?? FALLBACK;
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) { if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) {
reaction = '\u2764'; reaction = '\u2764';
} else if (_reaction) { } else if (_reaction != null) {
const custom = reaction.match(isCustomEmojiRegexp); const custom = reaction.match(isCustomEmojiRegexp);
if (custom) { if (custom) {
const reacterHost = this.utilityService.toPunyNullable(user.host); const reacterHost = this.utilityService.toPunyNullable(user.host);

View File

@ -47,6 +47,7 @@ export type RolePolicies = {
canHideAds: boolean; canHideAds: boolean;
driveCapacityMb: number; driveCapacityMb: number;
alwaysMarkNsfw: boolean; alwaysMarkNsfw: boolean;
canUpdateBioMedia: boolean;
pinLimit: number; pinLimit: number;
antennaLimit: number; antennaLimit: number;
wordMuteLimit: number; wordMuteLimit: number;
@ -75,6 +76,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
canHideAds: false, canHideAds: false,
driveCapacityMb: 100, driveCapacityMb: 100,
alwaysMarkNsfw: false, alwaysMarkNsfw: false,
canUpdateBioMedia: true,
pinLimit: 5, pinLimit: 5,
antennaLimit: 5, antennaLimit: 5,
wordMuteLimit: 200, wordMuteLimit: 200,
@ -376,6 +378,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)), canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)), driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)), alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
canUpdateBioMedia: calc('canUpdateBioMedia', vs => vs.some(v => v === true)),
pinLimit: calc('pinLimit', vs => Math.max(...vs)), pinLimit: calc('pinLimit', vs => Math.max(...vs)),
antennaLimit: calc('antennaLimit', vs => Math.max(...vs)), antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)), wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)),

View File

@ -95,7 +95,7 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit {
const currentCount = await this.userListMembershipsRepository.countBy({ const currentCount = await this.userListMembershipsRepository.countBy({
userListId: list.id, userListId: list.id,
}); });
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) { if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
throw new UserListService.TooManyUsersError(); throw new UserListService.TooManyUsersError();
} }

View File

@ -0,0 +1,205 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Brackets, SelectQueryBuilder } from 'typeorm';
import { DI } from '@/di-symbols.js';
import { type FollowingsRepository, MiUser, type UsersRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
import type { Config } from '@/config.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { Packed } from '@/misc/json-schema.js';
function defaultActiveThreshold() {
return new Date(Date.now() - 1000 * 60 * 60 * 24 * 30);
}
@Injectable()
export class UserSearchService {
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
private userEntityService: UserEntityService,
) {
}
/**
* .
*
* - .
* 1.
* 2.
* 3.
* 4.
* - .
* 1.
* 2.
* - .
* - (IDが重複することはないが).
* 12, 3, 4
* - .
* - .
* - .
* - {@link opts.limit} .
*
* {@link params.activeThreshold} .
*
* @param params .
* @param opts .
* @param me . .
* @see {@link UserSearchService#buildSearchUserQueries}
* @see {@link UserSearchService#buildSearchUserNoLoginQueries}
*/
@bindThis
public async search(
params: {
username?: string | null,
host?: string | null,
activeThreshold?: Date,
},
opts?: {
limit?: number,
detail?: boolean,
},
me?: MiUser | null,
): Promise<Packed<'User'>[]> {
const queries = me ? this.buildSearchUserQueries(me, params) : this.buildSearchUserNoLoginQueries(params);
let resultSet = new Set<MiUser['id']>();
const limit = opts?.limit ?? 10;
for (const query of queries) {
const ids = await query
.select('user.id')
.limit(limit - resultSet.size)
.orderBy('user.usernameLower', 'ASC')
.getRawMany<{ user_id: MiUser['id'] }>()
.then(res => res.map(x => x.user_id));
resultSet = new Set([...resultSet, ...ids]);
if (resultSet.size >= limit) {
break;
}
}
return this.userEntityService.packMany<'UserLite' | 'UserDetailed'>(
[...resultSet].slice(0, limit),
me,
{ schema: opts?.detail ? 'UserDetailed' : 'UserLite' },
);
}
/**
* .
* @param me
* @param params
* @private
*/
@bindThis
private buildSearchUserQueries(
me: MiUser,
params: {
username?: string | null,
host?: string | null,
activeThreshold?: Date,
},
) {
// デフォルト30日以内に更新されたユーザーをアクティブユーザーとする
const activeThreshold = params.activeThreshold ?? defaultActiveThreshold();
const followingUserQuery = this.followingsRepository.createQueryBuilder('following')
.select('following.followeeId')
.where('following.followerId = :followerId', { followerId: me.id });
const activeFollowingUsersQuery = this.generateUserQueryBuilder(params)
.andWhere(`user.id IN (${followingUserQuery.getQuery()})`)
.andWhere('user.updatedAt > :activeThreshold', { activeThreshold });
activeFollowingUsersQuery.setParameters(followingUserQuery.getParameters());
const inactiveFollowingUsersQuery = this.generateUserQueryBuilder(params)
.andWhere(`user.id IN (${followingUserQuery.getQuery()})`)
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt <= :activeThreshold', { activeThreshold });
}));
inactiveFollowingUsersQuery.setParameters(followingUserQuery.getParameters());
// 自分自身がヒットするとしたらここ
const activeUserQuery = this.generateUserQueryBuilder(params)
.andWhere(`user.id NOT IN (${followingUserQuery.getQuery()})`)
.andWhere('user.updatedAt > :activeThreshold', { activeThreshold });
activeUserQuery.setParameters(followingUserQuery.getParameters());
const inactiveUserQuery = this.generateUserQueryBuilder(params)
.andWhere(`user.id NOT IN (${followingUserQuery.getQuery()})`)
.andWhere('user.updatedAt <= :activeThreshold', { activeThreshold });
inactiveUserQuery.setParameters(followingUserQuery.getParameters());
return [activeFollowingUsersQuery, inactiveFollowingUsersQuery, activeUserQuery, inactiveUserQuery];
}
/**
* .
* @param params
* @private
*/
@bindThis
private buildSearchUserNoLoginQueries(params: {
username?: string | null,
host?: string | null,
activeThreshold?: Date,
}) {
// デフォルト30日以内に更新されたユーザーをアクティブユーザーとする
const activeThreshold = params.activeThreshold ?? defaultActiveThreshold();
const activeUserQuery = this.generateUserQueryBuilder(params)
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold });
}));
const inactiveUserQuery = this.generateUserQueryBuilder(params)
.andWhere('user.updatedAt <= :activeThreshold', { activeThreshold });
return [activeUserQuery, inactiveUserQuery];
}
/**
* .
* @param params
* @private
*/
@bindThis
private generateUserQueryBuilder(params: {
username?: string | null,
host?: string | null,
}): SelectQueryBuilder<MiUser> {
const userQuery = this.usersRepository.createQueryBuilder('user');
if (params.username) {
userQuery.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(params.username.toLowerCase()) + '%' });
}
if (params.host) {
if (params.host === this.config.hostname || params.host === '.') {
userQuery.andWhere('user.host IS NULL');
} else {
userQuery.andWhere('user.host LIKE :host', {
host: sqlLikeEscape(params.host.toLowerCase()) + '%',
});
}
}
userQuery.andWhere('user.isSuspended = FALSE');
return userQuery;
}
}

View File

@ -8,7 +8,6 @@ import promiseLimit from 'promise-limit';
import type { MiRemoteUser, MiUser } from '@/models/User.js'; import type { MiRemoteUser, MiUser } from '@/models/User.js';
import { concat, unique } from '@/misc/prelude/array.js'; import { concat, unique } from '@/misc/prelude/array.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { getApIds } from './type.js'; import { getApIds } from './type.js';
import { ApPersonService } from './models/ApPersonService.js'; import { ApPersonService } from './models/ApPersonService.js';
import type { ApObject } from './type.js'; import type { ApObject } from './type.js';
@ -41,7 +40,7 @@ export class ApAudienceService {
const limit = promiseLimit<MiUser | null>(2); const limit = promiseLimit<MiUser | null>(2);
const mentionedUsers = (await Promise.all( const mentionedUsers = (await Promise.all(
others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))), others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))),
)).filter(isNotNull); )).filter(x => x != null);
if (toGroups.public.length > 0) { if (toGroups.public.length > 0) {
return { return {

View File

@ -27,7 +27,6 @@ import { QueueService } from '@/core/QueueService.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js'; import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { MiRemoteUser } from '@/models/User.js'; import type { MiRemoteUser } from '@/models/User.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { AbuseReportService } from '@/core/AbuseReportService.js'; import { AbuseReportService } from '@/core/AbuseReportService.js';
import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
@ -538,7 +537,7 @@ export class ApInboxService {
const userIds = uris const userIds = uris
.filter(uri => uri.startsWith(this.config.url + '/users/')) .filter(uri => uri.startsWith(this.config.url + '/users/'))
.map(uri => uri.split('/').at(-1)) .map(uri => uri.split('/').at(-1))
.filter(isNotNull); .filter(x => x != null);
const users = await this.usersRepository.findBy({ const users = await this.usersRepository.findBy({
id: In(userIds), id: In(userIds),
}); });

View File

@ -25,7 +25,7 @@ export class ApMfmService {
} }
@bindThis @bindThis
public getNoteHtml(note: MiNote, apAppend?: string) { public getNoteHtml(note: Pick<MiNote, 'text' | 'mentionedRemoteUsers'>, apAppend?: string) {
let noMisskeyContent = false; let noMisskeyContent = false;
const srcMfm = (note.text ?? '') + (apAppend ?? ''); const srcMfm = (note.text ?? '') + (apAppend ?? '');

View File

@ -26,7 +26,6 @@ import type { MiUserKeypair } from '@/models/UserKeypair.js';
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository } from '@/models/_.js'; import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { JsonLdService } from './JsonLdService.js'; import { JsonLdService } from './JsonLdService.js';
import { ApMfmService } from './ApMfmService.js'; import { ApMfmService } from './ApMfmService.js';
@ -317,7 +316,7 @@ export class ApRendererService {
const getPromisedFiles = async (ids: string[]): Promise<MiDriveFile[]> => { const getPromisedFiles = async (ids: string[]): Promise<MiDriveFile[]> => {
if (ids.length === 0) return []; if (ids.length === 0) return [];
const items = await this.driveFilesRepository.findBy({ id: In(ids) }); const items = await this.driveFilesRepository.findBy({ id: In(ids) });
return ids.map(id => items.find(item => item.id === id)).filter(isNotNull); return ids.map(id => items.find(item => item.id === id)).filter(x => x != null);
}; };
let inReplyTo; let inReplyTo;
@ -686,7 +685,7 @@ export class ApRendererService {
if (names.length === 0) return []; if (names.length === 0) return [];
const allEmojis = await this.customEmojiService.localEmojisCache.fetch(); const allEmojis = await this.customEmojiService.localEmojisCache.fetch();
const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull); const emojis = names.map(name => allEmojis.get(name)).filter(x => x != null);
return emojis; return emojis;
} }

View File

@ -8,7 +8,6 @@ import promiseLimit from 'promise-limit';
import type { MiUser } from '@/models/_.js'; import type { MiUser } from '@/models/_.js';
import { toArray, unique } from '@/misc/prelude/array.js'; import { toArray, unique } from '@/misc/prelude/array.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { isMention } from '../type.js'; import { isMention } from '../type.js';
import { Resolver } from '../ApResolverService.js'; import { Resolver } from '../ApResolverService.js';
import { ApPersonService } from './ApPersonService.js'; import { ApPersonService } from './ApPersonService.js';
@ -28,7 +27,7 @@ export class ApMentionService {
const limit = promiseLimit<MiUser | null>(2); const limit = promiseLimit<MiUser | null>(2);
const mentionedUsers = (await Promise.all( const mentionedUsers = (await Promise.all(
hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))), hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))),
)).filter(isNotNull); )).filter(x => x != null);
return mentionedUsers; return mentionedUsers;
} }

View File

@ -24,7 +24,6 @@ import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { checkHttps } from '@/misc/check-https.js'; import { checkHttps } from '@/misc/check-https.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js'; import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
import { ApLoggerService } from '../ApLoggerService.js'; import { ApLoggerService } from '../ApLoggerService.js';
import { ApMfmService } from '../ApMfmService.js'; import { ApMfmService } from '../ApMfmService.js';
@ -253,7 +252,7 @@ export class ApNoteService {
} }
}; };
const uris = unique([note._misskey_quote, note.quoteUrl].filter(isNotNull)); const uris = unique([note._misskey_quote, note.quoteUrl].filter(x => x != null));
const results = await Promise.all(uris.map(tryResolveNote)); const results = await Promise.all(uris.map(tryResolveNote));
quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0); quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);

View File

@ -34,11 +34,11 @@ import { StatusError } from '@/misc/status-error.js';
import type { UtilityService } from '@/core/UtilityService.js'; import type { UtilityService } from '@/core/UtilityService.js';
import type { UserEntityService } from '@/core/entities/UserEntityService.js'; import type { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import type { AccountMoveService } from '@/core/AccountMoveService.js'; import type { AccountMoveService } from '@/core/AccountMoveService.js';
import { checkHttps } from '@/misc/check-https.js'; import { checkHttps } from '@/misc/check-https.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js'; import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
import { extractApHashtags } from './tag.js'; import { extractApHashtags } from './tag.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
@ -101,6 +101,8 @@ export class ApPersonService implements OnModuleInit {
@Inject(DI.followingsRepository) @Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository, private followingsRepository: FollowingsRepository,
private roleService: RoleService,
) { ) {
} }
@ -239,6 +241,11 @@ export class ApPersonService implements OnModuleInit {
return this.apImageService.resolveImage(user, img).catch(() => null); return this.apImageService.resolveImage(user, img).catch(() => null);
})); }));
if (((avatar != null && avatar.id != null) || (banner != null && banner.id != null))
&& !(await this.roleService.getUserPolicies(user.id)).canUpdateBioMedia) {
return {};
}
/* /*
we don't want to return nulls on errors! if the database fields we don't want to return nulls on errors! if the database fields
are already null, nothing changes; if the database has old are already null, nothing changes; if the database has old
@ -637,7 +644,7 @@ export class ApPersonService implements OnModuleInit {
// とりあえずidを別の時間で生成して順番を維持 // とりあえずidを別の時間で生成して順番を維持
let td = 0; let td = 0;
for (const note of featuredNotes.filter(isNotNull)) { for (const note of featuredNotes.filter(x => x != null)) {
td -= 1000; td -= 1000;
transactionalEntityManager.insert(MiUserNotePining, { transactionalEntityManager.insert(MiUserNotePining, {
id: this.idService.gen(Date.now() + td), id: this.idService.gen(Date.now() + td),

View File

@ -10,7 +10,6 @@ import type { Config } from '@/config.js';
import type { IPoll } from '@/models/Poll.js'; import type { IPoll } from '@/models/Poll.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { isQuestion } from '../type.js'; import { isQuestion } from '../type.js';
import { ApLoggerService } from '../ApLoggerService.js'; import { ApLoggerService } from '../ApLoggerService.js';
import { ApResolverService } from '../ApResolverService.js'; import { ApResolverService } from '../ApResolverService.js';
@ -52,7 +51,7 @@ export class ApQuestionService {
const choices = question[multiple ? 'anyOf' : 'oneOf'] const choices = question[multiple ? 'anyOf' : 'oneOf']
?.map((x) => x.name) ?.map((x) => x.name)
.filter(isNotNull) .filter(x => x != null)
?? []; ?? [];
const votes = question[multiple ? 'anyOf' : 'oneOf']?.map((x) => x.replies?.totalItems ?? x._misskey_votes ?? 0); const votes = question[multiple ? 'anyOf' : 'oneOf']?.map((x) => x.replies?.totalItems ?? x._misskey_votes ?? 0);
@ -75,10 +74,10 @@ export class ApQuestionService {
//#region このサーバーに既に登録されているか //#region このサーバーに既に登録されているか
const note = await this.notesRepository.findOneBy({ uri }); const note = await this.notesRepository.findOneBy({ uri });
if (note == null) throw new Error('Question is not registed'); if (note == null) throw new Error('Question is not registered');
const poll = await this.pollsRepository.findOneBy({ noteId: note.id }); const poll = await this.pollsRepository.findOneBy({ noteId: note.id });
if (poll == null) throw new Error('Question is not registed'); if (poll == null) throw new Error('Question is not registered');
//#endregion //#endregion
// resolve new Question object // resolve new Question object

View File

@ -4,7 +4,6 @@
*/ */
import { toArray } from '@/misc/prelude/array.js'; import { toArray } from '@/misc/prelude/array.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { isHashtag } from '../type.js'; import { isHashtag } from '../type.js';
import type { IObject, IApHashtag } from '../type.js'; import type { IObject, IApHashtag } from '../type.js';
@ -16,7 +15,7 @@ export function extractApHashtags(tags: IObject | IObject[] | null | undefined):
return hashtags.map(tag => { return hashtags.map(tag => {
const m = tag.name.match(/^#(.+)/); const m = tag.name.match(/^#(.+)/);
return m ? m[1] : null; return m ? m[1] : null;
}).filter(isNotNull); }).filter(x => x != null);
} }
export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] { export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] {

View File

@ -11,7 +11,6 @@ import { bindThis } from '@/decorators.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { Packed } from '@/misc/json-schema.js'; import { Packed } from '@/misc/json-schema.js';
import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js'; import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
import { isNotNull } from '@/misc/is-not-null.js';
@Injectable() @Injectable()
export class AbuseReportNotificationRecipientEntityService { export class AbuseReportNotificationRecipientEntityService {
@ -66,13 +65,13 @@ export class AbuseReportNotificationRecipientEntityService {
); );
} }
const userIds = objs.map(it => it.userId).filter(isNotNull); const userIds = objs.map(it => it.userId).filter(x => x != null);
const users: Map<string, Packed<'UserLite'>> = (userIds.length > 0) const users: Map<string, Packed<'UserLite'>> = (userIds.length > 0)
? await this.userEntityService.packMany(userIds) ? await this.userEntityService.packMany(userIds)
.then(it => new Map(it.map(it => [it.id, it]))) .then(it => new Map(it.map(it => [it.id, it])))
: new Map(); : new Map();
const systemWebhookIds = objs.map(it => it.systemWebhookId).filter(isNotNull); const systemWebhookIds = objs.map(it => it.systemWebhookId).filter(x => x != null);
const systemWebhooks: Map<string, Packed<'SystemWebhook'>> = (systemWebhookIds.length > 0) const systemWebhooks: Map<string, Packed<'SystemWebhook'>> = (systemWebhookIds.length > 0)
? await this.systemWebhookEntityService.packMany(systemWebhookIds) ? await this.systemWebhookEntityService.packMany(systemWebhookIds)
.then(it => new Map(it.map(it => [it.id, it]))) .then(it => new Map(it.map(it => [it.id, it])))

View File

@ -10,7 +10,6 @@ import { awaitAll } from '@/misc/prelude/await-all.js';
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import { UserEntityService } from './UserEntityService.js'; import { UserEntityService } from './UserEntityService.js';
@ -63,7 +62,7 @@ export class AbuseUserReportEntityService {
) { ) {
const _reporters = reports.map(({ reporter, reporterId }) => reporter ?? reporterId); const _reporters = reports.map(({ reporter, reporterId }) => reporter ?? reporterId);
const _targetUsers = reports.map(({ targetUser, targetUserId }) => targetUser ?? targetUserId); const _targetUsers = reports.map(({ targetUser, targetUserId }) => targetUser ?? targetUserId);
const _assignees = reports.map(({ assignee, assigneeId }) => assignee ?? assigneeId).filter(isNotNull); const _assignees = reports.map(({ assignee, assigneeId }) => assignee ?? assigneeId).filter(x => x != null);
const _userMap = await this.userEntityService.packMany( const _userMap = await this.userEntityService.packMany(
[..._reporters, ..._targetUsers, ..._assignees], [..._reporters, ..._targetUsers, ..._assignees],
null, null,

View File

@ -53,7 +53,7 @@ export class ClipEntityService {
isPublic: clip.isPublic, isPublic: clip.isPublic,
favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }), favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
isFavorited: meId ? await this.clipFavoritesRepository.exists({ where: { clipId: clip.id, userId: meId } }) : undefined, isFavorited: meId ? await this.clipFavoritesRepository.exists({ where: { clipId: clip.id, userId: meId } }) : undefined,
notesCount: meId ? await this.clipNotesRepository.countBy({ clipId: clip.id }) : undefined, notesCount: (meId === clip.userId) ? await this.clipNotesRepository.countBy({ clipId: clip.id }) : undefined,
}); });
} }

View File

@ -16,7 +16,6 @@ import { appendQuery, query } from '@/misc/prelude/url.js';
import { deepClone } from '@/misc/clone.js'; import { deepClone } from '@/misc/clone.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isMimeImage } from '@/misc/is-mime-image.js'; import { isMimeImage } from '@/misc/is-mime-image.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { UtilityService } from '../UtilityService.js'; import { UtilityService } from '../UtilityService.js';
import { VideoProcessingService } from '../VideoProcessingService.js'; import { VideoProcessingService } from '../VideoProcessingService.js';
@ -261,11 +260,11 @@ export class DriveFileEntityService {
files: MiDriveFile[], files: MiDriveFile[],
options?: PackOptions, options?: PackOptions,
): Promise<Packed<'DriveFile'>[]> { ): Promise<Packed<'DriveFile'>[]> {
const _user = files.map(({ user, userId }) => user ?? userId).filter(isNotNull); const _user = files.map(({ user, userId }) => user ?? userId).filter(x => x != null);
const _userMap = await this.userEntityService.packMany(_user) const _userMap = await this.userEntityService.packMany(_user)
.then(users => new Map(users.map(user => [user.id, user]))); .then(users => new Map(users.map(user => [user.id, user])));
const items = await Promise.all(files.map(f => this.packNullable(f, options, f.userId ? { packedUser: _userMap.get(f.userId) } : {}))); const items = await Promise.all(files.map(f => this.packNullable(f, options, f.userId ? { packedUser: _userMap.get(f.userId) } : {})));
return items.filter(isNotNull); return items.filter(x => x != null);
} }
@bindThis @bindThis
@ -290,6 +289,6 @@ export class DriveFileEntityService {
): Promise<Packed<'DriveFile'>[]> { ): Promise<Packed<'DriveFile'>[]> {
if (fileIds.length === 0) return []; if (fileIds.length === 0) return [];
const filesMap = await this.packManyByIdsMap(fileIds, options); const filesMap = await this.packManyByIdsMap(fileIds, options);
return fileIds.map(id => filesMap.get(id)).filter(isNotNull); return fileIds.map(id => filesMap.get(id)).filter(x => x != null);
} }
} }

View File

@ -12,7 +12,6 @@ import type { MiUser } from '@/models/User.js';
import type { MiRegistrationTicket } from '@/models/RegistrationTicket.js'; import type { MiRegistrationTicket } from '@/models/RegistrationTicket.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { UserEntityService } from './UserEntityService.js'; import { UserEntityService } from './UserEntityService.js';
@Injectable() @Injectable()
@ -59,8 +58,8 @@ export class InviteCodeEntityService {
tickets: MiRegistrationTicket[], tickets: MiRegistrationTicket[],
me: { id: MiUser['id'] }, me: { id: MiUser['id'] },
) { ) {
const _createdBys = tickets.map(({ createdBy, createdById }) => createdBy ?? createdById).filter(isNotNull); const _createdBys = tickets.map(({ createdBy, createdById }) => createdBy ?? createdById).filter(x => x != null);
const _usedBys = tickets.map(({ usedBy, usedById }) => usedBy ?? usedById).filter(isNotNull); const _usedBys = tickets.map(({ usedBy, usedById }) => usedBy ?? usedById).filter(x => x != null);
const _userMap = await this.userEntityService.packMany([..._createdBys, ..._usedBys], me) const _userMap = await this.userEntityService.packMany([..._createdBys, ..._usedBys], me)
.then(users => new Map(users.map(u => [u.id, u]))); .then(users => new Map(users.map(u => [u.id, u])));
return Promise.all( return Promise.all(

View File

@ -50,6 +50,22 @@ export class MetaEntityService {
})) }))
.getMany(); .getMany();
// クライアントの手間を減らすためあらかじめJSONに変換しておく
let defaultLightTheme = null;
let defaultDarkTheme = null;
if (instance.defaultLightTheme) {
try {
defaultLightTheme = JSON.stringify(JSON5.parse(instance.defaultLightTheme));
} catch (e) {
}
}
if (instance.defaultDarkTheme) {
try {
defaultDarkTheme = JSON.stringify(JSON5.parse(instance.defaultDarkTheme));
} catch (e) {
}
}
const packed: Packed<'MetaLite'> = { const packed: Packed<'MetaLite'> = {
maintainerName: instance.maintainerName, maintainerName: instance.maintainerName,
maintainerEmail: instance.maintainerEmail, maintainerEmail: instance.maintainerEmail,
@ -90,9 +106,8 @@ export class MetaEntityService {
backgroundImageUrl: instance.backgroundImageUrl, backgroundImageUrl: instance.backgroundImageUrl,
logoImageUrl: instance.logoImageUrl, logoImageUrl: instance.logoImageUrl,
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
// クライアントの手間を減らすためあらかじめJSONに変換しておく defaultLightTheme,
defaultLightTheme: instance.defaultLightTheme ? JSON.stringify(JSON5.parse(instance.defaultLightTheme)) : null, defaultDarkTheme,
defaultDarkTheme: instance.defaultDarkTheme ? JSON.stringify(JSON5.parse(instance.defaultDarkTheme)) : null,
ads: ads.map(ad => ({ ads: ads.map(ad => ({
id: ad.id, id: ad.id,
url: ad.url, url: ad.url,

View File

@ -14,7 +14,6 @@ import type { MiNote } from '@/models/Note.js';
import type { MiNoteReaction } from '@/models/NoteReaction.js'; import type { MiNoteReaction } from '@/models/NoteReaction.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository } from '@/models/_.js'; import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { DebounceLoader } from '@/misc/loader.js'; import { DebounceLoader } from '@/misc/loader.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
@ -276,7 +275,7 @@ export class NoteEntityService implements OnModuleInit {
packedFiles.set(k, v); packedFiles.set(k, v);
} }
} }
return fileIds.map(id => packedFiles.get(id)).filter(isNotNull); return fileIds.map(id => packedFiles.get(id)).filter(x => x != null);
} }
@bindThis @bindThis
@ -449,12 +448,12 @@ export class NoteEntityService implements OnModuleInit {
await this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis(notes)); await this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis(notes));
// TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく // TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく
const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(isNotNull); const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(x => x != null);
const packedFiles = fileIds.length > 0 ? await this.driveFileEntityService.packManyByIdsMap(fileIds) : new Map(); const packedFiles = fileIds.length > 0 ? await this.driveFileEntityService.packManyByIdsMap(fileIds) : new Map();
const users = [ const users = [
...notes.map(({ user, userId }) => user ?? userId), ...notes.map(({ user, userId }) => user ?? userId),
...notes.map(({ replyUserId }) => replyUserId).filter(isNotNull), ...notes.map(({ replyUserId }) => replyUserId).filter(x => x != null),
...notes.map(({ renoteUserId }) => renoteUserId).filter(isNotNull), ...notes.map(({ renoteUserId }) => renoteUserId).filter(x => x != null),
]; ];
const packedUsers = await this.userEntityService.packMany(users, me) const packedUsers = await this.userEntityService.packMany(users, me)
.then(users => new Map(users.map(u => [u.id, u]))); .then(users => new Map(users.map(u => [u.id, u])));

View File

@ -13,7 +13,6 @@ import type { MiGroupedNotification, MiNotification } from '@/models/Notificatio
import type { MiNote } from '@/models/Note.js'; import type { MiNote } from '@/models/Note.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { FilterUnionByProperty, groupedNotificationTypes } from '@/types.js'; import { FilterUnionByProperty, groupedNotificationTypes } from '@/types.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import { RoleEntityService } from './RoleEntityService.js'; import { RoleEntityService } from './RoleEntityService.js';
@ -103,7 +102,7 @@ export class NotificationEntityService implements OnModuleInit {
user, user,
reaction: reaction.reaction, reaction: reaction.reaction,
}; };
}))).filter(r => isNotNull(r.user)); }))).filter(r => r.user != null);
// if all users have been deleted, don't show this notification // if all users have been deleted, don't show this notification
if (reactions.length === 0) { if (reactions.length === 0) {
return null; return null;
@ -124,7 +123,7 @@ export class NotificationEntityService implements OnModuleInit {
} }
return this.userEntityService.pack(userId, { id: meId }); return this.userEntityService.pack(userId, { id: meId });
}))).filter(isNotNull); }))).filter(x => x != null);
// if all users have been deleted, don't show this notification // if all users have been deleted, don't show this notification
if (users.length === 0) { if (users.length === 0) {
return null; return null;
@ -181,7 +180,7 @@ export class NotificationEntityService implements OnModuleInit {
validNotifications = await this.#filterValidNotifier(validNotifications, meId); validNotifications = await this.#filterValidNotifier(validNotifications, meId);
const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(isNotNull); const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(x => x != null);
const notes = noteIds.length > 0 ? await this.notesRepository.find({ const notes = noteIds.length > 0 ? await this.notesRepository.find({
where: { id: In(noteIds) }, where: { id: In(noteIds) },
relations: ['user', 'reply', 'reply.user', 'renote', 'renote.user'], relations: ['user', 'reply', 'reply.user', 'renote', 'renote.user'],
@ -223,7 +222,7 @@ export class NotificationEntityService implements OnModuleInit {
); );
}); });
return (await Promise.all(packPromises)).filter(isNotNull); return (await Promise.all(packPromises)).filter(x => x != null);
} }
@bindThis @bindThis
@ -305,7 +304,7 @@ export class NotificationEntityService implements OnModuleInit {
this.cacheService.userProfileCache.fetch(meId).then(p => new Set(p.mutedInstances)), this.cacheService.userProfileCache.fetch(meId).then(p => new Set(p.mutedInstances)),
]); ]);
const notifierIds = notifications.map(notification => 'notifierId' in notification ? notification.notifierId : null).filter(isNotNull); const notifierIds = notifications.map(notification => 'notifierId' in notification ? notification.notifierId : null).filter(x => x != null);
const notifiers = notifierIds.length > 0 ? await this.usersRepository.find({ const notifiers = notifierIds.length > 0 ? await this.usersRepository.find({
where: { id: In(notifierIds) }, where: { id: In(notifierIds) },
}) : []; }) : [];
@ -313,7 +312,7 @@ export class NotificationEntityService implements OnModuleInit {
const filteredNotifications = ((await Promise.all(notifications.map(async (notification) => { const filteredNotifications = ((await Promise.all(notifications.map(async (notification) => {
const isValid = this.#validateNotifier(notification, userIdsWhoMeMuting, userMutedInstances, notifiers); const isValid = this.#validateNotifier(notification, userIdsWhoMeMuting, userMutedInstances, notifiers);
return isValid ? notification : null; return isValid ? notification : null;
}))) as [T | null] ).filter(isNotNull); }))) as [T | null] ).filter(x => x != null);
return filteredNotifications; return filteredNotifications;
} }

View File

@ -14,7 +14,6 @@ import type { MiPage } from '@/models/Page.js';
import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiDriveFile } from '@/models/DriveFile.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { UserEntityService } from './UserEntityService.js'; import { UserEntityService } from './UserEntityService.js';
import { DriveFileEntityService } from './DriveFileEntityService.js'; import { DriveFileEntityService } from './DriveFileEntityService.js';
@ -106,7 +105,7 @@ export class PageEntityService {
script: page.script, script: page.script,
eyeCatchingImageId: page.eyeCatchingImageId, eyeCatchingImageId: page.eyeCatchingImageId,
eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null, eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null,
attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter(isNotNull)), attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter(x => x != null)),
likedCount: page.likedCount, likedCount: page.likedCount,
isLiked: meId ? await this.pageLikesRepository.exists({ where: { pageId: page.id, userId: meId } }) : undefined, isLiked: meId ? await this.pageLikesRepository.exists({ where: { pageId: page.id, userId: meId } }) : undefined,
}); });

View File

@ -47,7 +47,6 @@ import { IdService } from '@/core/IdService.js';
import type { AnnouncementService } from '@/core/AnnouncementService.js'; import type { AnnouncementService } from '@/core/AnnouncementService.js';
import type { CustomEmojiService } from '@/core/CustomEmojiService.js'; import type { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
import type { NoteEntityService } from './NoteEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js';
import type { DriveFileEntityService } from './DriveFileEntityService.js'; import type { DriveFileEntityService } from './DriveFileEntityService.js';
@ -502,11 +501,15 @@ export class UserEntityService implements OnModuleInit {
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host), emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
onlineStatus: this.getOnlineStatus(user), onlineStatus: this.getOnlineStatus(user),
// パフォーマンス上の理由でローカルユーザーのみ // パフォーマンス上の理由でローカルユーザーのみ
badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then(rs => rs.sort((a, b) => b.displayOrder - a.displayOrder).map(r => ({ badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then((rs) => rs
name: r.name, .filter((r) => r.isPublic || iAmModerator)
iconUrl: r.iconUrl, .sort((a, b) => b.displayOrder - a.displayOrder)
displayOrder: r.displayOrder, .map((r) => ({
}))) : undefined, name: r.name,
iconUrl: r.iconUrl,
displayOrder: r.displayOrder,
}))
) : undefined,
...(isDetailed ? { ...(isDetailed ? {
url: profile!.url, url: profile!.url,
@ -514,7 +517,7 @@ export class UserEntityService implements OnModuleInit {
movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null, movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null,
alsoKnownAs: user.alsoKnownAs alsoKnownAs: user.alsoKnownAs
? Promise.all(user.alsoKnownAs.map(uri => this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null))) ? Promise.all(user.alsoKnownAs.map(uri => this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null)))
.then(xs => xs.length === 0 ? null : xs.filter(isNotNull)) .then(xs => xs.length === 0 ? null : xs.filter(x => x != null))
: null, : null,
createdAt: this.idService.parse(user.id).date.toISOString(), createdAt: this.idService.parse(user.id).date.toISOString(),
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null, updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,

View File

@ -1,8 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export function isNotNull<T extends NonNullable<unknown>>(input: T | undefined | null): input is T {
return input != null;
}

View File

@ -4,6 +4,10 @@
*/ */
export function isUserRelated(note: any, userIds: Set<string>, ignoreAuthor = false): boolean { export function isUserRelated(note: any, userIds: Set<string>, ignoreAuthor = false): boolean {
if (!note) {
return false;
}
if (userIds.has(note.userId) && !ignoreAuthor) { if (userIds.has(note.userId) && !ignoreAuthor) {
return true; return true;
} }

View File

@ -65,44 +65,6 @@ export function maximum(xs: number[]): number {
return Math.max(...xs); return Math.max(...xs);
} }
/**
* Splits an array based on the equivalence relation.
* The concatenation of the result is equal to the argument.
*/
export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] {
const groups = [] as T[][];
for (const x of xs) {
const lastGroup = groups.at(-1);
if (lastGroup !== undefined && f(lastGroup[0], x)) {
lastGroup.push(x);
} else {
groups.push([x]);
}
}
return groups;
}
/**
* Splits an array based on the equivalence relation induced by the function.
* The concatenation of the result is equal to the argument.
*/
export function groupOn<T, S>(f: (x: T) => S, xs: T[]): T[][] {
return groupBy((a, b) => f(a) === f(b), xs);
}
export function groupByX<T>(collections: T[], keySelector: (x: T) => string) {
return collections.reduce((obj: Record<string, T[]>, item: T) => {
const key = keySelector(item);
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
obj[key] = [];
}
obj[key].push(item);
return obj;
}, {});
}
/** /**
* Compare two arrays by lexicographical order * Compare two arrays by lexicographical order
*/ */

View File

@ -1,8 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export function gcd(a: number, b: number): number {
return b === 0 ? a : gcd(b, a % b);
}

View File

@ -1,25 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export interface IMaybe<T> {
isJust(): this is IJust<T>;
}
export interface IJust<T> extends IMaybe<T> {
get(): T;
}
export function just<T>(value: T): IJust<T> {
return {
isJust: () => true,
get: () => value,
};
}
export function nothing<T>(): IMaybe<T> {
return {
isJust: () => false,
};
}

View File

@ -1,20 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export function concat(xs: string[]): string {
return xs.join('');
}
export function capitalize(s: string): string {
return toUpperCase(s.charAt(0)) + toLowerCase(s.slice(1));
}
export function toUpperCase(s: string): string {
return s.toUpperCase();
}
export function toLowerCase(s: string): string {
return s.toLowerCase();
}

View File

@ -1,6 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const fallback = Symbol('fallback');

View File

@ -82,34 +82,14 @@ import { MiReversiGame } from '@/models/ReversiGame.js';
import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js'; import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
export interface MiRepository<T extends ObjectLiteral> { export interface MiRepository<T extends ObjectLiteral> {
createTableColumnNames(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>): string[]; createTableColumnNames(this: Repository<T> & MiRepository<T>): string[];
createTableColumnNamesWithPrimaryKey(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>): string[];
insertOne(this: Repository<T> & MiRepository<T>, entity: QueryDeepPartialEntity<T>, findOptions?: Pick<FindOneOptions<T>, 'relations'>): Promise<T>; insertOne(this: Repository<T> & MiRepository<T>, entity: QueryDeepPartialEntity<T>, findOptions?: Pick<FindOneOptions<T>, 'relations'>): Promise<T>;
selectAliasColumnNames(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>, builder: SelectQueryBuilder<T>): void; selectAliasColumnNames(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>, builder: SelectQueryBuilder<T>): void;
} }
export const miRepository = { export const miRepository = {
createTableColumnNames(queryBuilder) { createTableColumnNames() {
// @ts-expect-error -- protected return this.metadata.columns.filter(column => column.isSelect && !column.isVirtual).map(column => column.databaseName);
const insertedColumns = queryBuilder.getInsertedColumns();
if (insertedColumns.length) {
return insertedColumns.map(column => column.databaseName);
}
if (!queryBuilder.expressionMap.mainAlias?.hasMetadata && !queryBuilder.expressionMap.insertColumns.length) {
// @ts-expect-error -- protected
const valueSets = queryBuilder.getValueSets();
if (valueSets.length === 1) {
return Object.keys(valueSets[0]);
}
}
return queryBuilder.expressionMap.insertColumns;
},
createTableColumnNamesWithPrimaryKey(queryBuilder) {
const columnNames = this.createTableColumnNames(queryBuilder);
if (!columnNames.includes('id')) {
columnNames.unshift('id');
}
return columnNames;
}, },
async insertOne(entity, findOptions?) { async insertOne(entity, findOptions?) {
const queryBuilder = this.createQueryBuilder().insert().values(entity); const queryBuilder = this.createQueryBuilder().insert().values(entity);
@ -117,7 +97,7 @@ export const miRepository = {
const mainAlias = queryBuilder.expressionMap.mainAlias!; const mainAlias = queryBuilder.expressionMap.mainAlias!;
const name = mainAlias.name; const name = mainAlias.name;
mainAlias.name = 't'; mainAlias.name = 't';
const columnNames = this.createTableColumnNamesWithPrimaryKey(queryBuilder); const columnNames = this.createTableColumnNames();
queryBuilder.returning(columnNames.reduce((a, c) => `${a}, ${queryBuilder.escape(c)}`, '').slice(2)); queryBuilder.returning(columnNames.reduce((a, c) => `${a}, ${queryBuilder.escape(c)}`, '').slice(2));
const builder = this.createQueryBuilder().addCommonTableExpression(queryBuilder, 'cte', { columnNames }); const builder = this.createQueryBuilder().addCommonTableExpression(queryBuilder, 'cte', { columnNames });
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@ -138,7 +118,7 @@ export const miRepository = {
selectOrAddSelect = (selection, selectionAliasName) => builder.addSelect(selection, selectionAliasName); selectOrAddSelect = (selection, selectionAliasName) => builder.addSelect(selection, selectionAliasName);
return builder.select(selection, selectionAliasName); return builder.select(selection, selectionAliasName);
}; };
for (const columnName of this.createTableColumnNamesWithPrimaryKey(queryBuilder)) { for (const columnName of this.createTableColumnNames()) {
selectOrAddSelect(`${builder.alias}.${columnName}`, `${builder.alias}_${columnName}`); selectOrAddSelect(`${builder.alias}.${columnName}`, `${builder.alias}_${columnName}`);
} }
}, },

View File

@ -20,7 +20,7 @@ export const packedDriveFileSchema = {
name: { name: {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
example: 'lenna.jpg', example: '192.jpg',
}, },
type: { type: {
type: 'string', type: 'string',

View File

@ -228,6 +228,10 @@ export const packedRolePoliciesSchema = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
canUpdateBioMedia: {
type: 'boolean',
optional: false, nullable: false,
},
pinLimit: { pinLimit: {
type: 'integer', type: 'integer',
optional: false, nullable: false, optional: false, nullable: false,

View File

@ -109,6 +109,12 @@ export class DeliverProcessorService {
suspensionState: 'autoSuspendedForNotResponding', suspensionState: 'autoSuspendedForNotResponding',
}); });
} }
} else {
// isNotRespondingがtrueでnotRespondingSinceがnullの場合はnotRespondingSinceをセット
// notRespondingSinceは新たな機能なので、それ以前のデータにはnotRespondingSinceがない場合がある
this.federatedInstanceService.update(i.id, {
notRespondingSince: new Date(),
});
} }
this.apRequestChart.deliverFail(); this.apRequestChart.deliverFail();

View File

@ -73,6 +73,16 @@ export class ApiCallService implements OnApplicationShutdown {
reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="insufficient_scope", error_description="${err.message}"`); reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="insufficient_scope", error_description="${err.message}"`);
} }
statusCode = statusCode ?? 403; statusCode = statusCode ?? 403;
} else if (err.code === 'RATE_LIMIT_EXCEEDED') {
const info: unknown = err.info;
const unixEpochInSeconds = Date.now();
if (typeof(info) === 'object' && info && 'resetMs' in info && typeof(info.resetMs) === 'number') {
const cooldownInSeconds = Math.ceil((info.resetMs - unixEpochInSeconds) / 1000);
// もしかするとマイナスになる可能性がなくはないのでマイナスだったら0にしておく
reply.header('Retry-After', Math.max(cooldownInSeconds, 0).toString(10));
} else {
this.logger.warn(`rate limit information has unexpected type ${typeof(err.info?.reset)}`);
}
} else if (!statusCode) { } else if (!statusCode) {
statusCode = 500; statusCode = 500;
} }
@ -308,12 +318,17 @@ export class ApiCallService implements OnApplicationShutdown {
if (factor > 0) { if (factor > 0) {
// Rate limit // Rate limit
await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor).catch(err => { await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor).catch(err => {
throw new ApiError({ if ('info' in err) {
message: 'Rate limit exceeded. Please try again later.', // errはLimiter.LimiterInfoであることが期待される
code: 'RATE_LIMIT_EXCEEDED', throw new ApiError({
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', message: 'Rate limit exceeded. Please try again later.',
httpStatusCode: 429, code: 'RATE_LIMIT_EXCEEDED',
}); id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
httpStatusCode: 429,
}, err.info);
} else {
throw new TypeError('information must be a rate-limiter information.');
}
}); });
} }
} }

View File

@ -32,11 +32,13 @@ export class RateLimiterService {
@bindThis @bindThis
public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1) { public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1) {
return new Promise<void>((ok, reject) => { {
if (this.disabled) ok(); if (this.disabled) {
return Promise.resolve();
}
// Short-term limit // Short-term limit
const min = (): void => { const min = new Promise<void>((ok, reject) => {
const minIntervalLimiter = new Limiter({ const minIntervalLimiter = new Limiter({
id: `${actor}:${limitation.key}:min`, id: `${actor}:${limitation.key}:min`,
duration: limitation.minInterval! * factor, duration: limitation.minInterval! * factor,
@ -46,25 +48,25 @@ export class RateLimiterService {
minIntervalLimiter.get((err, info) => { minIntervalLimiter.get((err, info) => {
if (err) { if (err) {
return reject('ERR'); return reject({ code: 'ERR', info });
} }
this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`); this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`);
if (info.remaining === 0) { if (info.remaining === 0) {
reject('BRIEF_REQUEST_INTERVAL'); return reject({ code: 'BRIEF_REQUEST_INTERVAL', info });
} else { } else {
if (hasLongTermLimit) { if (hasLongTermLimit) {
max(); return max.then(ok, reject);
} else { } else {
ok(); return ok();
} }
} }
}); });
}; });
// Long term limit // Long term limit
const max = (): void => { const max = new Promise<void>((ok, reject) => {
const limiter = new Limiter({ const limiter = new Limiter({
id: `${actor}:${limitation.key}`, id: `${actor}:${limitation.key}`,
duration: limitation.duration! * factor, duration: limitation.duration! * factor,
@ -74,18 +76,18 @@ export class RateLimiterService {
limiter.get((err, info) => { limiter.get((err, info) => {
if (err) { if (err) {
return reject('ERR'); return reject({ code: 'ERR', info });
} }
this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`); this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`);
if (info.remaining === 0) { if (info.remaining === 0) {
reject('RATE_LIMIT_EXCEEDED'); return reject({ code: 'RATE_LIMIT_EXCEEDED', info });
} else { } else {
ok(); return ok();
} }
}); });
}; });
const hasShortTermLimit = typeof limitation.minInterval === 'number'; const hasShortTermLimit = typeof limitation.minInterval === 'number';
@ -94,12 +96,12 @@ export class RateLimiterService {
typeof limitation.max === 'number'; typeof limitation.max === 'number';
if (hasShortTermLimit) { if (hasShortTermLimit) {
min(); return min;
} else if (hasLongTermLimit) { } else if (hasLongTermLimit) {
max(); return max;
} else { } else {
ok(); return Promise.resolve();
} }
}); }
} }
} }

View File

@ -40,7 +40,7 @@ export const paramDef = {
startsAt: { type: 'integer' }, startsAt: { type: 'integer' },
dayOfWeek: { type: 'integer' }, dayOfWeek: { type: 'integer' },
}, },
required: ['id', 'memo', 'url', 'imageUrl', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'dayOfWeek'], required: ['id'],
} as const; } as const;
@Injectable() @Injectable()
@ -63,8 +63,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
ratio: ps.ratio, ratio: ps.ratio,
memo: ps.memo, memo: ps.memo,
imageUrl: ps.imageUrl, imageUrl: ps.imageUrl,
expiresAt: new Date(ps.expiresAt), expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : undefined,
startsAt: new Date(ps.startsAt), startsAt: ps.startsAt ? new Date(ps.startsAt) : undefined,
dayOfWeek: ps.dayOfWeek, dayOfWeek: ps.dayOfWeek,
}); });

View File

@ -61,7 +61,7 @@ export const meta = {
name: { name: {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
example: 'lenna.jpg', example: '192.jpg',
}, },
type: { type: {
type: 'string', type: 'string',

View File

@ -6,7 +6,6 @@
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 type { RolesRepository } from '@/models/_.js'; import type { RolesRepository } from '@/models/_.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js'; import { ApiError } from '@/server/api/error.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
@ -50,19 +49,6 @@ export const paramDef = {
}, },
required: [ required: [
'roleId', 'roleId',
'name',
'description',
'color',
'iconUrl',
'target',
'condFormula',
'isPublic',
'isModerator',
'isAdministrator',
'asBadge',
'canEditMembersByModerator',
'displayOrder',
'policies',
], ],
} as const; } as const;

View File

@ -93,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const currentAntennasCount = await this.antennasRepository.countBy({ const currentAntennasCount = await this.antennasRepository.countBy({
userId: me.id, userId: me.id,
}); });
if (currentAntennasCount > (await this.roleService.getUserPolicies(me.id)).antennaLimit) { if (currentAntennasCount >= (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
throw new ApiError(meta.errors.tooManyAntennas); throw new ApiError(meta.errors.tooManyAntennas);
} }

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
import { ClipService } from '@/core/ClipService.js'; import { ClipService } from '@/core/ClipService.js';
@ -41,7 +41,7 @@ export const paramDef = {
isPublic: { type: 'boolean' }, isPublic: { type: 'boolean' },
description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 }, description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
}, },
required: ['clipId', 'name'], required: ['clipId'],
} as const; } as const;
@Injectable() @Injectable()

View File

@ -95,15 +95,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Check if the circular reference will occur // Check if the circular reference will occur
const checkCircle = async (folderId: string): Promise<boolean> => { const checkCircle = async (folderId: string): Promise<boolean> => {
// Fetch folder const folder2 = await this.driveFoldersRepository.findOneByOrFail({
const folder2 = await this.driveFoldersRepository.findOneBy({
id: folderId, id: folderId,
}); });
if (folder2!.id === folder!.id) { if (folder2.id === folder.id) {
return true; return true;
} else if (folder2!.parentId) { } else if (folder2.parentId) {
return await checkCircle(folder2!.parentId); return await checkCircle(folder2.parentId);
} else { } else {
return false; return false;
} }

View File

@ -12,7 +12,6 @@ import type { MiDriveFile } from '@/models/DriveFile.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { isNotNull } from '@/misc/is-not-null.js';
export const meta = { export const meta = {
tags: ['gallery'], tags: ['gallery'],
@ -70,7 +69,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
id: fileId, id: fileId,
userId: me.id, userId: me.id,
}), }),
))).filter(isNotNull); ))).filter(x => x != null);
if (files.length === 0) { if (files.length === 0) {
throw new Error(); throw new Error();

View File

@ -10,7 +10,6 @@ import type { DriveFilesRepository, GalleryPostsRepository } from '@/models/_.js
import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiDriveFile } from '@/models/DriveFile.js';
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { isNotNull } from '@/misc/is-not-null.js';
export const meta = { export const meta = {
tags: ['gallery'], tags: ['gallery'],
@ -48,7 +47,7 @@ export const paramDef = {
} }, } },
isSensitive: { type: 'boolean', default: false }, isSensitive: { type: 'boolean', default: false },
}, },
required: ['postId', 'title', 'fileIds'], required: ['postId'],
} as const; } as const;
@Injectable() @Injectable()
@ -63,15 +62,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private galleryPostEntityService: GalleryPostEntityService, private galleryPostEntityService: GalleryPostEntityService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const files = (await Promise.all(ps.fileIds.map(fileId => let files: Array<MiDriveFile> | undefined;
this.driveFilesRepository.findOneBy({
id: fileId,
userId: me.id,
}),
))).filter(isNotNull);
if (files.length === 0) { if (ps.fileIds) {
throw new Error(); files = (await Promise.all(ps.fileIds.map(fileId =>
this.driveFilesRepository.findOneBy({
id: fileId,
userId: me.id,
}),
))).filter(x => x != null);
if (files.length === 0) {
throw new Error();
}
} }
await this.galleryPostsRepository.update({ await this.galleryPostsRepository.update({
@ -82,7 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
title: ps.title, title: ps.title,
description: ps.description, description: ps.description,
isSensitive: ps.isSensitive, isSensitive: ps.isSensitive,
fileIds: files.map(file => file.id), fileIds: files ? files.map(file => file.id) : undefined,
}); });
const post = await this.galleryPostsRepository.findOneByOrFail({ id: ps.postId }); const post = await this.galleryPostsRepository.findOneByOrFail({ id: ps.postId });

View File

@ -78,7 +78,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (file.size === 0) throw new ApiError(meta.errors.emptyFile); if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
const antennas: (_Antenna & { userListAccts: string[] | null })[] = JSON.parse(await this.downloadService.downloadTextFile(file.url)); const antennas: (_Antenna & { userListAccts: string[] | null })[] = JSON.parse(await this.downloadService.downloadTextFile(file.url));
const currentAntennasCount = await this.antennasRepository.countBy({ userId: me.id }); const currentAntennasCount = await this.antennasRepository.countBy({ userId: me.id });
if (currentAntennasCount + antennas.length > (await this.roleService.getUserPolicies(me.id)).antennaLimit) { if (currentAntennasCount + antennas.length >= (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
throw new ApiError(meta.errors.tooManyAntennas); throw new ApiError(meta.errors.tooManyAntennas);
} }
this.queueService.createImportAntennasJob(me, antennas); this.queueService.createImportAntennasJob(me, antennas);

View File

@ -25,7 +25,7 @@ import { UserFollowingService } from '@/core/UserFollowingService.js';
import { AccountUpdateService } from '@/core/AccountUpdateService.js'; import { AccountUpdateService } from '@/core/AccountUpdateService.js';
import { HashtagService } from '@/core/HashtagService.js'; import { HashtagService } from '@/core/HashtagService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js'; import { RolePolicies, RoleService } from '@/core/RoleService.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
@ -256,8 +256,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const profileUpdates = {} as Partial<MiUserProfile>; const profileUpdates = {} as Partial<MiUserProfile>;
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
let policies: RolePolicies | null = null;
if (ps.name !== undefined) updates.name = ps.name; if (ps.name !== undefined) {
if (ps.name === null) {
updates.name = null;
} else {
const trimmedName = ps.name.trim();
updates.name = trimmedName === '' ? null : trimmedName;
}
}
if (ps.description !== undefined) profileUpdates.description = ps.description; if (ps.description !== undefined) profileUpdates.description = ps.description;
if (ps.lang !== undefined) profileUpdates.lang = ps.lang; if (ps.lang !== undefined) profileUpdates.lang = ps.lang;
if (ps.location !== undefined) profileUpdates.location = ps.location; if (ps.location !== undefined) profileUpdates.location = ps.location;
@ -289,14 +297,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
if (ps.mutedWords !== undefined) { if (ps.mutedWords !== undefined) {
checkMuteWordCount(ps.mutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit); policies ??= await this.roleService.getUserPolicies(user.id);
checkMuteWordCount(ps.mutedWords, policies.wordMuteLimit);
validateMuteWordRegex(ps.mutedWords); validateMuteWordRegex(ps.mutedWords);
profileUpdates.mutedWords = ps.mutedWords; profileUpdates.mutedWords = ps.mutedWords;
profileUpdates.enableWordMute = ps.mutedWords.length > 0; profileUpdates.enableWordMute = ps.mutedWords.length > 0;
} }
if (ps.hardMutedWords !== undefined) { if (ps.hardMutedWords !== undefined) {
checkMuteWordCount(ps.hardMutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit); policies ??= await this.roleService.getUserPolicies(user.id);
checkMuteWordCount(ps.hardMutedWords, policies.wordMuteLimit);
validateMuteWordRegex(ps.hardMutedWords); validateMuteWordRegex(ps.hardMutedWords);
profileUpdates.hardMutedWords = ps.hardMutedWords; profileUpdates.hardMutedWords = ps.hardMutedWords;
} }
@ -315,13 +325,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail; if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
if (typeof ps.alwaysMarkNsfw === 'boolean') { if (typeof ps.alwaysMarkNsfw === 'boolean') {
if ((await roleService.getUserPolicies(user.id)).alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole); policies ??= await this.roleService.getUserPolicies(user.id);
if (policies.alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole);
profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw; profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
} }
if (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive; if (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive;
if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes; if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
if (ps.avatarId) { if (ps.avatarId) {
policies ??= await this.roleService.getUserPolicies(user.id);
if (!policies.canUpdateBioMedia) throw new ApiError(meta.errors.restrictedByRole);
const avatar = await this.driveFilesRepository.findOneBy({ id: ps.avatarId }); const avatar = await this.driveFilesRepository.findOneBy({ id: ps.avatarId });
if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar); if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar);
@ -337,6 +351,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
if (ps.bannerId) { if (ps.bannerId) {
policies ??= await this.roleService.getUserPolicies(user.id);
if (!policies.canUpdateBioMedia) throw new ApiError(meta.errors.restrictedByRole);
const banner = await this.driveFilesRepository.findOneBy({ id: ps.bannerId }); const banner = await this.driveFilesRepository.findOneBy({ id: ps.bannerId });
if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner); if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner);
@ -352,14 +369,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
if (ps.avatarDecorations) { if (ps.avatarDecorations) {
policies ??= await this.roleService.getUserPolicies(user.id);
const decorations = await this.avatarDecorationService.getAll(true); const decorations = await this.avatarDecorationService.getAll(true);
const [myRoles, myPolicies] = await Promise.all([this.roleService.getUserRoles(user.id), this.roleService.getUserPolicies(user.id)]); const myRoles = await this.roleService.getUserRoles(user.id);
const allRoles = await this.roleService.getRoles(); const allRoles = await this.roleService.getRoles();
const decorationIds = decorations const decorationIds = decorations
.filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id))) .filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id)))
.map(d => d.id); .map(d => d.id);
if (ps.avatarDecorations.length > myPolicies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole); if (ps.avatarDecorations.length > policies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole);
updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({ updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({
id: d.id, id: d.id,

View File

@ -85,7 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const currentWebhooksCount = await this.webhooksRepository.countBy({ const currentWebhooksCount = await this.webhooksRepository.countBy({
userId: me.id, userId: me.id,
}); });
if (currentWebhooksCount > (await this.roleService.getUserPolicies(me.id)).webhookLimit) { if (currentWebhooksCount >= (await this.roleService.getUserPolicies(me.id)).webhookLimit) {
throw new ApiError(meta.errors.tooManyWebhooks); throw new ApiError(meta.errors.tooManyWebhooks);
} }

Some files were not shown because too many files have changed in this diff Show More