Compare commits

..

4 Commits

Author SHA1 Message Date
Acid Chicken (硫酸鶏)
d02198417b chore(backend): polyfill position 2024-06-01 16:51:07 +09:00
Acid Chicken (硫酸鶏)
9366515565 test(backend): polyfill position 2024-06-01 16:39:34 +09:00
Acid Chicken (硫酸鶏)
623eb410bb test(backend): polyfill erm 2024-06-01 16:28:07 +09:00
Acid Chicken (硫酸鶏)
b233444ed6 fix(backend): explicitly set query runner on insertOne 2024-06-01 16:12:02 +09:00
487 changed files with 7878 additions and 19476 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,11 +7,12 @@ Dockerfile
build/
built/
db/
.devcontainer/compose.yml
docker-compose.yml
node_modules/
packages/*/node_modules
redis/
files/
misskey-assets/
fluent-emojis/
.pnp.*
@@ -27,4 +28,4 @@ fluent-emojis/
.idea/
packages/*/.vscode/
packages/backend/test/compose.yml
packages/backend/test/docker-compose.yml

View File

@@ -53,8 +53,8 @@ body:
Examples:
* Model and OS of the device(s): MacBook Pro (14inch, 2021), macOS Ventura 13.4
* Browser: Chrome 113.0.5672.126
* Server URL: misskey.example.com
* Misskey: 2024.x.x
* Server URL: misskey.io
* Misskey: 13.x.x
value: |
* Model and OS of the device(s):
* Browser:
@@ -74,11 +74,11 @@ body:
Examples:
* Installation Method or Hosting Service: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment
* Misskey: 2024.x.x
* Misskey: 13.x.x
* Node: 20.x.x
* PostgreSQL: 15.x.x
* Redis: 7.x.x
* OS and Architecture: Ubuntu 24.04.2 LTS aarch64
* OS and Architecture: Ubuntu 22.04.2 LTS aarch64
value: |
* Installation Method or Hosting Service:
* Misskey:

View File

@@ -2,7 +2,3 @@ contact_links:
- name: 💬 Misskey official Discord
url: https://discord.gg/Wp8gVStHW3
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,11 +4,10 @@ on:
push:
paths:
- packages/misskey-js/**
- .github/workflows/api-misskey-js.yml
pull_request:
paths:
- packages/misskey-js/**
- .github/workflows/api-misskey-js.yml
jobs:
report:
@@ -21,7 +20,7 @@ jobs:
- run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@v4.0.3
uses: actions/setup-node@v4.0.2
with:
node-version-file: '.node-version'
cache: 'pnpm'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,7 +36,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js 20.x
uses: actions/setup-node@v4.0.3
uses: actions/setup-node@v4.0.2
with:
node-version-file: '.node-version'
cache: 'pnpm'
@@ -88,7 +88,7 @@ jobs:
if [ "$BRANCH" = "misskey-dev:$HEAD_REF" ]; then
BRANCH="$HEAD_REF"
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:
HEAD_REF: ${{ github.event.pull_request.head.ref }}
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}

View File

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

View File

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

View File

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

View File

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

View File

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

5
.gitignore vendored
View File

@@ -35,8 +35,8 @@ coverage
!/.config/example.yml
!/.config/docker_example.yml
!/.config/docker_example.env
.devcontainer/compose.yml
!/.devcontainer/compose.yml
docker-compose.yml
!/.devcontainer/docker-compose.yml
# misskey
/build
@@ -59,7 +59,6 @@ ormconfig.json
temp
/packages/frontend/src/**/*.stories.ts
tsdoc-metadata.json
misskey-assets
# blender backups
*.blend1

3
.gitmodules vendored
View File

@@ -1,3 +1,6 @@
[submodule "misskey-assets"]
path = misskey-assets
url = https://github.com/misskey-dev/assets.git
[submodule "fluent-emojis"]
path = fluent-emojis
url = https://github.com/misskey-dev/emojis.git

View File

@@ -1,87 +1,14 @@
## 2024.7.0
### Note
- デッキUIの新着ートをサウンドで通知する機能の追加v2024.5.0)に伴い、以前から動作しなくなっていたクライアント設定内の「アンテナ受信」「チャンネル通知」サウンドを削除しました。
- Streaming APIにて入力が不正な場合にはそのメッセージを無視するようになりました。 #14251
## Unreleased
### General
- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
- Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に
- 変更不可となっていても、設定済みのものを解除してデフォルト画像に戻すことは出来ます
- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
- Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正
-
### 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)
- Enhance: AiScriptを0.19.0にアップデート
- Enhance: Allow negative delay for MFM animation elements (`tada`, `jelly`, `twitch`, `shake`, `spin`, `jump`, `bounce`, `rainbow`)
- Enhance: センシティブなメディアを開く際に確認ダイアログを出せるように
- 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認証処理に渡ることを避けるため
- Fix: 「アニメーション画像を再生しない」がオンのときでもサーバーのバナー画像・背景画像がアニメーションしてしまう問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/574)
- Fix: Twitchの埋め込みが開けない問題を修正
- Fix: 子メニューの高さがウィンドウからはみ出ることがある問題を修正
- Fix: 個人宛てのダイアログ形式のお知らせが即時表示されない問題を修正
- Fix: 一部の画像がセンシティブ指定されているときに画面に何も表示されないことがあるのを修正
- Fix: リアクションしたユーザー一覧のユーザー名がはみ出る問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/672)
- Fix: `/share`ページにおいて絵文字ピッカーを開くことができない問題を修正
-
### Server
- 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.suspensionStateに置き換えられた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: FTT有効時にリモートユーザーのートがHTLにキャッシュされる問題を修正
- Fix: 一部の通知がローカル上のリモートユーザーに対して行われていた問題を修正
- Fix: エラーメッセージの誤字を修正 (#14213)
- Fix: ソーシャルタイムラインにローカルタイムラインに表示される自分へのリプライが表示されない問題を修正
- Fix: リノートのミュートが適用されるまでに時間がかかることがある問題を修正
(Cherry-picked from https://github.com/Type4ny-Project/Type4ny/commit/e9601029b52e0ad43d9131b555b614e56c84ebc1)
- Fix: Steaming APIが不正なデータを受けた場合の動作が不安定である問題 #14251
### Misskey.js
- Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応)
- Feat: `/admin/role/create` のロールポリシーの型を修正
## 2024.5.0

View File

@@ -20,29 +20,13 @@ Before creating an issue, please check the following:
> **Warning**
> Do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged.
### Recommended discussing before implementation
We welcome your purposal.
## Before implementation
When you want to add a feature or fix a bug, **first have the design and policy reviewed in an Issue** (if it is not there, please make one). Without this step, there is a high possibility that the PR will not be merged even if it is implemented.
At this point, you also need to clarify the goals of the PR you will create, and make sure that the other members of the team are aware of them.
PRs that do not have a clear set of do's and don'ts tend to be bloated and difficult to review.
Also, when you start implementation, assign yourself to the Issue (if you cannot do it yourself, ask Commiter to assign you).
By expressing your intention to work on the Issue, you can prevent conflicts in the work.
To the Committers: you should not assign someone on it before the Final Decision.
### How issues are triaged
The Commiters may:
* close an issue that is not reproducible on latest stable release,
* merge an issue into another issue,
* split an issue into multiple issues,
* or re-open that has been closed for some reason which is not applicable anymore.
@syuilo reserves the Final Desicion rights including whether the project will implement feature and how to implement, these rights are not always exercised.
Also, when you start implementation, assign yourself to the Issue (if you cannot do it yourself, ask another member to assign you). By expressing your intention to work the Issue, you can prevent conflicts in the work.
## Well-known branches
- **`master`** branch is tracking the latest release and used for production purposes.
@@ -122,38 +106,6 @@ If your language is not listed in Crowdin, please open an issue.
![Crowdin](https://d322cqt584bo4o.cloudfront.net/misskey/localized.svg)
## Development
### Setup
Before developing, you have to set up environment. Misskey requires Redis, PostgreSQL, and FFmpeg.
You would want to install Meilisearch to experiment related features. Technically, meilisearch is not strict requirement, but some features and tests require it.
There are a few ways to proceed.
#### Use system-wide software
You could install them in system-wide (such as from package manager).
#### Use `docker compose`
You could obtain middleware container by typing `docker compose -f $PROJECT_ROOT/compose.local-db.yml up -d`.
#### Use Devcontainer
Devcontainer also has necessary setting. This method can be done by connecting from VSCode.
Instead of running `pnpm` locally, you can use Dev Container to set up your development environment.
To use Dev Container, open the project directory on VSCode with Dev Containers installed.
**Note:** If you are using Windows, please clone the repository with WSL. Using Git for Windows will result in broken files due to the difference in how newlines are handled.
It will run the following command automatically inside the container.
``` bash
git submodule update --init
pnpm install --frozen-lockfile
cp .devcontainer/devcontainer.yml .config/default.yml
pnpm build
pnpm migrate
```
After finishing the migration, you can proceed.
### Start developing
During development, it is useful to use the
```
@@ -183,6 +135,26 @@ MK_DEV_PREFER=backend pnpm dev
- To change the port of Vite, specify with `VITE_PORT` environment variable.
- HMR may not work in some environments such as Windows.
### Dev Container
Instead of running `pnpm` locally, you can use Dev Container to set up your development environment.
To use Dev Container, open the project directory on VSCode with Dev Containers installed.
**Note:** If you are using Windows, please clone the repository with WSL. Using Git for Windows will result in broken files due to the difference in how newlines are handled.
It will run the following command automatically inside the container.
``` bash
git submodule update --init
pnpm install --frozen-lockfile
cp .devcontainer/devcontainer.yml .config/default.yml
pnpm build
pnpm migrate
```
After finishing the migration, run the `pnpm dev` command to start the development server.
``` bash
pnpm dev
```
## Testing
- Test codes are located in [`/packages/backend/test`](/packages/backend/test).
@@ -193,7 +165,7 @@ cp .github/misskey/test.yml .config/
```
Prepare DB/Redis for testing.
```
docker compose -f packages/backend/test/compose.yml up
docker compose -f packages/backend/test/docker-compose.yml up
```
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.

View File

@@ -82,10 +82,6 @@ RUN apt-get update \
USER 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/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

View File

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

View File

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

148
locales/index.d.ts vendored
View File

@@ -736,22 +736,6 @@ export interface Locale extends ILocale {
* リモートで表示
*/
"showOnRemote": string;
/**
* リモートで続行
*/
"continueOnRemote": string;
/**
* Misskey Hubからサーバーを選択
*/
"chooseServerOnMisskeyHub": string;
/**
* サーバーのドメインを直接指定
*/
"specifyServerHost": string;
/**
* ドメインを入力してください
*/
"inputHostName": string;
/**
* 全般
*/
@@ -1937,13 +1921,9 @@ export interface Locale extends ILocale {
*/
"onlyOneFileCanBeAttached": string;
/**
* 続行する前に、登録またはログインが必要です
* 続行する前に、サインアップまたはサインインが必要です
*/
"signinRequired": string;
/**
* 続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります
*/
"signinOrContinueOnRemote": string;
/**
* 招待
*/
@@ -5004,18 +4984,6 @@ export interface Locale extends ILocale {
* お問い合わせ
*/
"inquiry": string;
/**
* もう一度お試しください。
*/
"tryAgain": string;
/**
* センシティブなメディアを表示するとき確認する
*/
"confirmWhenRevealingSensitiveMedia": string;
/**
* センシティブなメディアです。表示しますか?
*/
"sensitiveMediaRevealConfirm": string;
"_delivery": {
/**
* 配信状態
@@ -6626,10 +6594,6 @@ export interface Locale extends ILocale {
* ファイルにNSFWを常に付与
*/
"alwaysMarkNsfw": string;
/**
* アイコンとバナーの更新を許可
*/
"canUpdateBioMedia": string;
/**
* ノートのピン留めの最大数
*/
@@ -7551,6 +7515,14 @@ export interface Locale extends ILocale {
* 通知
*/
"notification": string;
/**
* アンテナ受信
*/
"antenna": string;
/**
* チャンネル通知
*/
"channel": string;
/**
* リアクション選択時
*/
@@ -9333,10 +9305,6 @@ export interface Locale extends ILocale {
* Webhookを作成
*/
"createWebhook": string;
/**
* Webhookを編集
*/
"modifyWebhook": string;
/**
* 名前
*/
@@ -9383,72 +9351,6 @@ export interface Locale extends ILocale {
*/
"mention": string;
};
"_systemEvents": {
/**
* ユーザーから通報があったとき
*/
"abuseReport": string;
/**
* ユーザーからの通報を処理したとき
*/
"abuseReportResolved": string;
};
/**
* Webhookを削除しますか
*/
"deleteConfirm": string;
};
"_abuseReport": {
"_notificationRecipient": {
/**
* 通報の通知先を追加
*/
"createRecipient": string;
/**
* 通報の通知先を編集
*/
"modifyRecipient": string;
/**
* 通知先の種類
*/
"recipientType": string;
"_recipientType": {
/**
* メール
*/
"mail": string;
/**
* Webhook
*/
"webhook": string;
"_captions": {
/**
* モデレーター権限を持つユーザーのメールアドレスに通知を送ります(通報を受けた時のみ)
*/
"mail": string;
/**
* 指定したSystemWebhookに通知を送ります(通報を受けた時と通報を解決した時にそれぞれ発信)
*/
"webhook": string;
};
};
/**
* キーワード
*/
"keywords": string;
/**
* 通知先ユーザー
*/
"notifiedUser": string;
/**
* 使用するWebhook
*/
"notifiedWebhook": string;
/**
* 通知先を削除しますか?
*/
"deleteConfirm": string;
};
};
"_moderationLogTypes": {
/**
@@ -9595,30 +9497,6 @@ export interface Locale extends ILocale {
* ユーザーのバナーを解除
*/
"unsetUserBanner": string;
/**
* SystemWebhookを作成
*/
"createSystemWebhook": string;
/**
* SystemWebhookを更新
*/
"updateSystemWebhook": string;
/**
* SystemWebhookを削除
*/
"deleteSystemWebhook": string;
/**
* 通報の通知先を作成
*/
"createAbuseReportNotificationRecipient": string;
/**
* 通報の通知先を更新
*/
"updateAbuseReportNotificationRecipient": string;
/**
* 通報の通知先を削除
*/
"deleteAbuseReportNotificationRecipient": string;
};
"_fileViewer": {
/**
@@ -9789,7 +9667,7 @@ export interface Locale extends ILocale {
"_dataSaver": {
"_media": {
/**
* メディアの読み込みを無効化
* メディアの読み込み
*/
"title": string;
/**
@@ -9799,7 +9677,7 @@ export interface Locale extends ILocale {
};
"_avatar": {
/**
* アイコン画像のアニメーションを無効化
* アイコン画像
*/
"title": string;
/**
@@ -9809,7 +9687,7 @@ export interface Locale extends ILocale {
};
"_urlPreview": {
/**
* URLプレビューのサムネイルを非表示
* URLプレビューのサムネイル
*/
"title": string;
/**
@@ -9819,7 +9697,7 @@ export interface Locale extends ILocale {
};
"_code": {
/**
* コードハイライトを非表示
* コードハイライト
*/
"title": string;
/**

View File

@@ -52,11 +52,7 @@ const primaries = {
const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
export function build() {
// 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 locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {});
// 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す
const removeEmpty = (obj) => {

View File

@@ -180,10 +180,6 @@ addAccount: "アカウントを追加"
reloadAccountsList: "アカウントリストの情報を更新"
loginFailed: "ログインに失敗しました"
showOnRemote: "リモートで表示"
continueOnRemote: "リモートで続行"
chooseServerOnMisskeyHub: "Misskey Hubからサーバーを選択"
specifyServerHost: "サーバーのドメインを直接指定"
inputHostName: "ドメインを入力してください"
general: "全般"
wallpaper: "壁紙"
setWallpaper: "壁紙を設定"
@@ -480,8 +476,7 @@ attachAsFileQuestion: "クリップボードのテキストが長いです。テ
noMessagesYet: "まだチャットはありません"
newMessageExists: "新しいメッセージがあります"
onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです"
signinRequired: "続行する前に、登録またはログインが必要です"
signinOrContinueOnRemote: "続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります"
signinRequired: "続行する前に、サインアップまたはサインインが必要です"
invitations: "招待"
invitationCode: "招待コード"
checking: "確認しています"
@@ -1247,9 +1242,6 @@ keepOriginalFilenameDescription: "この設定をオフにすると、アップ
noDescription: "説明文はありません"
alwaysConfirmFollow: "フォローの際常に確認する"
inquiry: "お問い合わせ"
tryAgain: "もう一度お試しください。"
confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示するとき確認する"
sensitiveMediaRevealConfirm: "センシティブなメディアです。表示しますか?"
_delivery:
status: "配信状態"
@@ -1713,7 +1705,6 @@ _role:
canManageAvatarDecorations: "アバターデコレーションの管理"
driveCapacity: "ドライブ容量"
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
canUpdateBioMedia: "アイコンとバナーの更新を許可"
pinMax: "ノートのピン留めの最大数"
antennaMax: "アンテナの作成可能数"
wordMuteMax: "ワードミュートの最大文字数"
@@ -1980,6 +1971,8 @@ _sfx:
note: "ノート"
noteMy: "ノート(自分)"
notification: "通知"
antenna: "アンテナ受信"
channel: "チャンネル通知"
reaction: "リアクション選択時"
_soundSettings:
@@ -2475,7 +2468,6 @@ _drivecleaner:
_webhookSettings:
createWebhook: "Webhookを作成"
modifyWebhook: "Webhookを編集"
name: "名前"
secret: "シークレット"
events: "Webhookを実行するタイミング"
@@ -2488,26 +2480,6 @@ _webhookSettings:
renote: "Renoteされたとき"
reaction: "リアクションがあったとき"
mention: "メンションされたとき"
_systemEvents:
abuseReport: "ユーザーから通報があったとき"
abuseReportResolved: "ユーザーからの通報を処理したとき"
deleteConfirm: "Webhookを削除しますか"
_abuseReport:
_notificationRecipient:
createRecipient: "通報の通知先を追加"
modifyRecipient: "通報の通知先を編集"
recipientType: "通知先の種類"
_recipientType:
mail: "メール"
webhook: "Webhook"
_captions:
mail: "モデレーター権限を持つユーザーのメールアドレスに通知を送ります(通報を受けた時のみ)"
webhook: "指定したSystemWebhookに通知を送ります(通報を受けた時と通報を解決した時にそれぞれ発信)"
keywords: "キーワード"
notifiedUser: "通知先ユーザー"
notifiedWebhook: "使用するWebhook"
deleteConfirm: "通知先を削除しますか?"
_moderationLogTypes:
createRole: "ロールを作成"
@@ -2546,12 +2518,6 @@ _moderationLogTypes:
deleteAvatarDecoration: "アイコンデコレーションを削除"
unsetUserAvatar: "ユーザーのアイコンを解除"
unsetUserBanner: "ユーザーのバナーを解除"
createSystemWebhook: "SystemWebhookを作成"
updateSystemWebhook: "SystemWebhookを更新"
deleteSystemWebhook: "SystemWebhookを削除"
createAbuseReportNotificationRecipient: "通報の通知先を作成"
updateAbuseReportNotificationRecipient: "通報の通知先を更新"
deleteAbuseReportNotificationRecipient: "通報の通知先を削除"
_fileViewer:
title: "ファイルの詳細"
@@ -2606,16 +2572,16 @@ _externalResourceInstaller:
_dataSaver:
_media:
title: "メディアの読み込みを無効化"
title: "メディアの読み込み"
description: "画像・動画が自動で読み込まれるのを防止します。隠れている画像・動画はタップすると読み込まれます。"
_avatar:
title: "アイコン画像のアニメーションを無効化"
title: "アイコン画像"
description: "アイコン画像のアニメーションが停止します。アニメーション画像は通常の画像よりファイルサイズが大きいことがあるので、データ通信量をさらに削減できます。"
_urlPreview:
title: "URLプレビューのサムネイルを非表示"
title: "URLプレビューのサムネイル"
description: "URLプレビューのサムネイル画像が読み込まれなくなります。"
_code:
title: "コードハイライトを非表示"
title: "コードハイライト"
description: "MFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。"
_hemisphere:

1
misskey-assets Submodule

Submodule misskey-assets added at 0179793ec8

View File

@@ -1,12 +1,12 @@
{
"name": "misskey",
"version": "2024.7.0-beta.2",
"version": "2024.5.0",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
},
"packageManager": "pnpm@9.5.0",
"packageManager": "pnpm@9.0.6",
"workspaces": [
"packages/frontend",
"packages/backend",
@@ -55,22 +55,20 @@
"js-yaml": "4.1.0",
"postcss": "8.4.38",
"tar": "6.2.1",
"terser": "5.31.1",
"typescript": "5.5.3",
"esbuild": "0.22.0",
"terser": "5.30.3",
"typescript": "5.4.5",
"esbuild": "0.20.2",
"glob": "10.3.12"
},
"devDependencies": {
"@misskey-dev/eslint-plugin": "2.0.2",
"@types/node": "20.14.9",
"@typescript-eslint/eslint-plugin": "7.15.0",
"@typescript-eslint/parser": "7.15.0",
"@types/node": "20.12.7",
"@typescript-eslint/eslint-plugin": "7.7.1",
"@typescript-eslint/parser": "7.7.1",
"cross-env": "7.0.3",
"cypress": "13.13.0",
"eslint": "9.6.0",
"globals": "15.7.0",
"cypress": "13.7.3",
"eslint": "8.57.0",
"ncp": "2.0.0",
"start-server-and-test": "2.0.4"
"start-server-and-test": "2.0.3"
},
"optionalDependencies": {
"@tensorflow/tfjs-core": "4.4.0"

View File

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

View File

@@ -0,0 +1,32 @@
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

@@ -1,20 +0,0 @@
<!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

@@ -0,0 +1,24 @@
<!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

@@ -1,46 +0,0 @@
import tsParser from '@typescript-eslint/parser';
import sharedConfig from '../shared/eslint.config.js';
export default [
...sharedConfig,
{
ignores: ['**/node_modules', 'built', '@types/**/*', 'migration'],
},
{
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

@@ -1,62 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class AbuseReportNotification1713656541000 {
name = 'AbuseReportNotification1713656541000'
async up(queryRunner) {
await queryRunner.query(`
CREATE TABLE "system_webhook" (
"id" varchar(32) NOT NULL,
"isActive" boolean NOT NULL DEFAULT true,
"updatedAt" timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
"latestSentAt" timestamp with time zone NULL DEFAULT NULL,
"latestStatus" integer NULL DEFAULT NULL,
"name" varchar(255) NOT NULL,
"on" varchar(128) [] NOT NULL DEFAULT '{}'::character varying[],
"url" varchar(1024) NOT NULL,
"secret" varchar(1024) NOT NULL,
CONSTRAINT "PK_system_webhook_id" PRIMARY KEY ("id")
);
CREATE INDEX "IDX_system_webhook_isActive" ON "system_webhook" ("isActive");
CREATE INDEX "IDX_system_webhook_on" ON "system_webhook" USING gin ("on");
CREATE TABLE "abuse_report_notification_recipient" (
"id" varchar(32) NOT NULL,
"isActive" boolean NOT NULL DEFAULT true,
"updatedAt" timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
"name" varchar(255) NOT NULL,
"method" varchar(64) NOT NULL,
"userId" varchar(32) NULL DEFAULT NULL,
"systemWebhookId" varchar(32) NULL DEFAULT NULL,
CONSTRAINT "PK_abuse_report_notification_recipient_id" PRIMARY KEY ("id"),
CONSTRAINT "FK_abuse_report_notification_recipient_userId1" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION,
CONSTRAINT "FK_abuse_report_notification_recipient_userId2" FOREIGN KEY ("userId") REFERENCES "user_profile"("userId") ON DELETE CASCADE ON UPDATE NO ACTION,
CONSTRAINT "FK_abuse_report_notification_recipient_systemWebhookId" FOREIGN KEY ("systemWebhookId") REFERENCES "system_webhook"("id") ON DELETE CASCADE ON UPDATE NO ACTION
);
CREATE INDEX "IDX_abuse_report_notification_recipient_isActive" ON "abuse_report_notification_recipient" ("isActive");
CREATE INDEX "IDX_abuse_report_notification_recipient_method" ON "abuse_report_notification_recipient" ("method");
CREATE INDEX "IDX_abuse_report_notification_recipient_userId" ON "abuse_report_notification_recipient" ("userId");
CREATE INDEX "IDX_abuse_report_notification_recipient_systemWebhookId" ON "abuse_report_notification_recipient" ("systemWebhookId");
`);
}
async down(queryRunner) {
await queryRunner.query(`
ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_userId1";
ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_userId2";
ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_systemWebhookId";
DROP INDEX "IDX_abuse_report_notification_recipient_isActive";
DROP INDEX "IDX_abuse_report_notification_recipient_method";
DROP INDEX "IDX_abuse_report_notification_recipient_userId";
DROP INDEX "IDX_abuse_report_notification_recipient_systemWebhookId";
DROP TABLE "abuse_report_notification_recipient";
DROP INDEX "IDX_system_webhook_isActive";
DROP INDEX "IDX_system_webhook_on";
DROP TABLE "system_webhook";
`);
}
}

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ import { LoggerService } from '@nestjs/common';
import Logger from '@/logger.js';
const logger = new Logger('core', 'cyan');
const nestLogger = logger.createSubLogger('nest', 'green');
const nestLogger = logger.createSubLogger('nest', 'green', false);
export class NestLogger implements LoggerService {
/**

View File

@@ -25,7 +25,7 @@ Error.stackTraceLimit = Infinity;
EventEmitter.defaultMaxListeners = 128;
const logger = new Logger('core', 'cyan');
const clusterLogger = logger.createSubLogger('cluster', 'orange');
const clusterLogger = logger.createSubLogger('cluster', 'orange', false);
const ev = new Xev();
//#region Events

View File

@@ -25,7 +25,7 @@ const _dirname = dirname(_filename);
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
const logger = new Logger('core', 'cyan');
const bootLogger = logger.createSubLogger('boot', 'magenta');
const bootLogger = logger.createSubLogger('boot', 'magenta', false);
const themeColor = chalk.hex('#86b300');

View File

@@ -4,36 +4,13 @@
*/
import cluster from 'node:cluster';
import * as Sentry from '@sentry/node';
import { nodeProfilingIntegration } from '@sentry/profiling-node';
import { envOption } from '@/env.js';
import { loadConfig } from '@/config.js';
import { jobQueue, server } from './common.js';
/**
* Init worker process
*/
export async function workerMain() {
const config = loadConfig();
if (config.sentryForBackend) {
Sentry.init({
integrations: [
...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []),
],
// Performance Monitoring
tracesSampleRate: 1.0, // Capture 100% of the transactions
// Set sampling rate for profiling - this is relative to tracesSampleRate
profilesSampleRate: 1.0,
maxBreadcrumbs: 0,
...config.sentryForBackend.options,
});
}
if (envOption.onlyServer) {
await server();
} else if (envOption.onlyQueue) {

View File

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

View File

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

View File

@@ -1,405 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable, type OnApplicationShutdown } from '@nestjs/common';
import { Brackets, In, IsNull, Not } from 'typeorm';
import * as Redis from 'ioredis';
import sanitizeHtml from 'sanitize-html';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
import type {
AbuseReportNotificationRecipientRepository,
MiAbuseReportNotificationRecipient,
MiAbuseUserReport,
MiUser,
} from '@/models/_.js';
import { EmailService } from '@/core/EmailService.js';
import { MetaService } from '@/core/MetaService.js';
import { RoleService } from '@/core/RoleService.js';
import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { IdService } from './IdService.js';
@Injectable()
export class AbuseReportNotificationService implements OnApplicationShutdown {
constructor(
@Inject(DI.abuseReportNotificationRecipientRepository)
private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository,
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
private idService: IdService,
private roleService: RoleService,
private systemWebhookService: SystemWebhookService,
private emailService: EmailService,
private metaService: MetaService,
private moderationLogService: ModerationLogService,
private globalEventService: GlobalEventService,
) {
this.redisForSub.on('message', this.onMessage);
}
/**
* 管理者用Redisイベントを用いて{@link abuseReports}の内容を管理者各位に通知する.
* 通知先ユーザは{@link RoleService.getModeratorIds}の取得結果に依る.
*
* @see RoleService.getModeratorIds
* @see GlobalEventService.publishAdminStream
*/
@bindThis
public async notifyAdminStream(abuseReports: MiAbuseUserReport[]) {
if (abuseReports.length <= 0) {
return;
}
const moderatorIds = await this.roleService.getModeratorIds(true, true);
for (const moderatorId of moderatorIds) {
for (const abuseReport of abuseReports) {
this.globalEventService.publishAdminStream(
moderatorId,
'newAbuseUserReport',
{
id: abuseReport.id,
targetUserId: abuseReport.targetUserId,
reporterId: abuseReport.reporterId,
comment: abuseReport.comment,
},
);
}
}
}
/**
* Mailを用いて{@link abuseReports}の内容を管理者各位に通知する.
* メールアドレスの送信先は以下の通り.
* - モデレータ権限所有者ユーザ(設定画面からメールアドレスの設定を行っているユーザに限る)
* - metaテーブルに設定されているメールアドレス
*
* @see EmailService.sendEmail
*/
@bindThis
public async notifyMail(abuseReports: MiAbuseUserReport[]) {
if (abuseReports.length <= 0) {
return;
}
const recipientEMailAddresses = await this.fetchEMailRecipients().then(it => it
.filter(it => it.isActive && it.userProfile?.emailVerified)
.map(it => it.userProfile?.email)
.filter(x => x != null),
);
// 送信先の鮮度を保つため、毎回取得する
const meta = await this.metaService.fetch(true);
recipientEMailAddresses.push(
...(meta.email ? [meta.email] : []),
);
if (recipientEMailAddresses.length <= 0) {
return;
}
for (const mailAddress of recipientEMailAddresses) {
await Promise.all(
abuseReports.map(it => {
// TODO: 送信処理はJobQueue化したい
return this.emailService.sendEmail(
mailAddress,
'New Abuse Report',
sanitizeHtml(it.comment),
sanitizeHtml(it.comment),
);
}),
);
}
}
/**
* SystemWebhookを用いて{@link abuseReports}の内容を管理者各位に通知する.
* ここではJobQueueへのエンキューのみを行うため、即時実行されない.
*
* @see SystemWebhookService.enqueueSystemWebhook
*/
@bindThis
public async notifySystemWebhook(
abuseReports: MiAbuseUserReport[],
type: 'abuseReport' | 'abuseReportResolved',
) {
if (abuseReports.length <= 0) {
return;
}
const recipientWebhookIds = await this.fetchWebhookRecipients()
.then(it => it
.filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook')
.map(it => it.systemWebhookId)
.filter(x => x != null));
for (const webhookId of recipientWebhookIds) {
await Promise.all(
abuseReports.map(it => {
return this.systemWebhookService.enqueueSystemWebhook(
webhookId,
type,
it,
);
}),
);
}
}
/**
* 通報の通知先一覧を取得する.
*
* @param {Object} [params] クエリの取得条件
* @param {Object} [params.method] 取得する通知先の通知方法
* @param {Object} [opts] 動作時の詳細なオプション
* @param {boolean} [opts.removeUnauthorized] 副作用としてモデレータ権限を持たない送信先ユーザをDBから削除するかどうか(default: true)
* @param {boolean} [opts.joinUser] 通知先のユーザ情報をJOINするかどうか(default: false)
* @param {boolean} [opts.joinSystemWebhook] 通知先のSystemWebhook情報をJOINするかどうか(default: false)
* @see removeUnauthorizedRecipientUsers
*/
@bindThis
public async fetchRecipients(
params?: {
ids?: MiAbuseReportNotificationRecipient['id'][],
method?: RecipientMethod[],
},
opts?: {
removeUnauthorized?: boolean,
joinUser?: boolean,
joinSystemWebhook?: boolean,
},
): Promise<MiAbuseReportNotificationRecipient[]> {
const query = this.abuseReportNotificationRecipientRepository.createQueryBuilder('recipient');
if (opts?.joinUser) {
query.innerJoinAndSelect('user', 'user', 'recipient.userId = user.id');
query.innerJoinAndSelect('recipient.userProfile', 'userProfile');
}
if (opts?.joinSystemWebhook) {
query.innerJoinAndSelect('recipient.systemWebhook', 'systemWebhook');
}
if (params?.ids) {
query.andWhere({ id: In(params.ids) });
}
if (params?.method) {
query.andWhere(new Brackets(qb => {
if (params.method?.includes('email')) {
qb.orWhere({ method: 'email', userId: Not(IsNull()) });
}
if (params.method?.includes('webhook')) {
qb.orWhere({ method: 'webhook', userId: IsNull() });
}
}));
}
const recipients = await query.getMany();
if (recipients.length <= 0) {
return [];
}
// アサイン有効期限切れはイベントで拾えないので、このタイミングでチェック及び削除(オプション)
return (opts?.removeUnauthorized ?? true)
? await this.removeUnauthorizedRecipientUsers(recipients)
: recipients;
}
/**
* EMailの通知先一覧を取得する.
* リレーション先の{@link MiUser}および{@link MiUserProfile}も同時に取得する.
*
* @param {Object} [opts]
* @param {boolean} [opts.removeUnauthorized] 副作用としてモデレータ権限を持たない送信先ユーザをDBから削除するかどうか(default: true)
* @see removeUnauthorizedRecipientUsers
*/
@bindThis
public async fetchEMailRecipients(opts?: {
removeUnauthorized?: boolean
}): Promise<MiAbuseReportNotificationRecipient[]> {
return this.fetchRecipients({ method: ['email'] }, { joinUser: true, ...opts });
}
/**
* Webhookの通知先一覧を取得する.
* リレーション先の{@link MiSystemWebhook}も同時に取得する.
*/
@bindThis
public fetchWebhookRecipients(): Promise<MiAbuseReportNotificationRecipient[]> {
return this.fetchRecipients({ method: ['webhook'] }, { joinSystemWebhook: true });
}
/**
* 通知先を作成する.
*/
@bindThis
public async createRecipient(
params: {
isActive: MiAbuseReportNotificationRecipient['isActive'];
name: MiAbuseReportNotificationRecipient['name'];
method: MiAbuseReportNotificationRecipient['method'];
userId: MiAbuseReportNotificationRecipient['userId'];
systemWebhookId: MiAbuseReportNotificationRecipient['systemWebhookId'];
},
updater: MiUser,
): Promise<MiAbuseReportNotificationRecipient> {
const id = this.idService.gen();
await this.abuseReportNotificationRecipientRepository.insert({
...params,
id,
});
const created = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: id });
this.moderationLogService
.log(updater, 'createAbuseReportNotificationRecipient', {
recipientId: id,
recipient: created,
})
.then();
return created;
}
/**
* 通知先を更新する.
*/
@bindThis
public async updateRecipient(
params: {
id: MiAbuseReportNotificationRecipient['id'];
isActive: MiAbuseReportNotificationRecipient['isActive'];
name: MiAbuseReportNotificationRecipient['name'];
method: MiAbuseReportNotificationRecipient['method'];
userId: MiAbuseReportNotificationRecipient['userId'];
systemWebhookId: MiAbuseReportNotificationRecipient['systemWebhookId'];
},
updater: MiUser,
): Promise<MiAbuseReportNotificationRecipient> {
const beforeEntity = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: params.id });
await this.abuseReportNotificationRecipientRepository.update(params.id, {
isActive: params.isActive,
updatedAt: new Date(),
name: params.name,
method: params.method,
userId: params.userId,
systemWebhookId: params.systemWebhookId,
});
const afterEntity = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: params.id });
this.moderationLogService
.log(updater, 'updateAbuseReportNotificationRecipient', {
recipientId: params.id,
before: beforeEntity,
after: afterEntity,
})
.then();
return afterEntity;
}
/**
* 通知先を削除する.
*/
@bindThis
public async deleteRecipient(
id: MiAbuseReportNotificationRecipient['id'],
updater: MiUser,
) {
const entity = await this.abuseReportNotificationRecipientRepository.findBy({ id });
await this.abuseReportNotificationRecipientRepository.delete(id);
this.moderationLogService
.log(updater, 'deleteAbuseReportNotificationRecipient', {
recipientId: id,
recipient: entity,
})
.then();
}
/**
* モデレータ権限を持たない(*1)通知先ユーザを削除する.
*
* *1: 以下の両方を満たすものの事を言う
* - 通知先にユーザIDが設定されている
* - 付与ロールにモデレータ権限がない or アサインの有効期限が切れている
*
* @param recipients 通知先一覧の配列
* @returns {@lisk recipients}からモデレータ権限を持たない通知先を削除した配列
*/
@bindThis
private async removeUnauthorizedRecipientUsers(recipients: MiAbuseReportNotificationRecipient[]): Promise<MiAbuseReportNotificationRecipient[]> {
const userRecipients = recipients.filter(it => it.userId !== null);
const recipientUserIds = new Set(userRecipients.map(it => it.userId).filter(x => x != null));
if (recipientUserIds.size <= 0) {
// ユーザが通知先として設定されていない場合、この関数での処理を行うべきレコードが無い
return recipients;
}
// モデレータ権限の有無で通知先設定を振り分ける
const authorizedUserIds = await this.roleService.getModeratorIds(true, true);
const authorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
const unauthorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
for (const recipient of userRecipients) {
// eslint-disable-next-line
if (authorizedUserIds.includes(recipient.userId!)) {
authorizedUserRecipients.push(recipient);
} else {
unauthorizedUserRecipients.push(recipient);
}
}
// モデレータ権限を持たない通知先をDBから削除する
if (unauthorizedUserRecipients.length > 0) {
await this.abuseReportNotificationRecipientRepository.delete(unauthorizedUserRecipients.map(it => it.id));
}
const nonUserRecipients = recipients.filter(it => it.userId === null);
return [...nonUserRecipients, ...authorizedUserRecipients].sort((a, b) => a.id.localeCompare(b.id));
}
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
if (obj.channel !== 'internal') {
return;
}
const { type } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'roleUpdated':
case 'roleDeleted':
case 'userRoleUnassigned': {
// 場合によってはキャッシュ更新よりも先にここが呼ばれてしまう可能性があるのでnextTickで遅延実行
process.nextTick(async () => {
const recipients = await this.abuseReportNotificationRecipientRepository.findBy({
userId: Not(IsNull()),
});
await this.removeUnauthorizedRecipientUsers(recipients);
});
break;
}
default: {
break;
}
}
}
@bindThis
public dispose(): void {
this.redisForSub.off('message', this.onMessage);
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
}

View File

@@ -1,128 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import type { AbuseUserReportsRepository, MiAbuseUserReport, MiUser, UsersRepository } from '@/models/_.js';
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
import { QueueService } from '@/core/QueueService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { IdService } from './IdService.js';
@Injectable()
export class AbuseReportService {
constructor(
@Inject(DI.abuseUserReportsRepository)
private abuseUserReportsRepository: AbuseUserReportsRepository,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private idService: IdService,
private abuseReportNotificationService: AbuseReportNotificationService,
private queueService: QueueService,
private instanceActorService: InstanceActorService,
private apRendererService: ApRendererService,
private moderationLogService: ModerationLogService,
) {
}
/**
* ユーザからの通報をDBに記録し、その内容を下記の手段で管理者各位に通知する.
* - 管理者用Redisイベント
* - EMailモデレータ権限所有者ユーザmetaテーブルに設定されているメールアドレス
* - SystemWebhook
*
* @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
* @see AbuseReportNotificationService.notify
*/
@bindThis
public async report(params: {
targetUserId: MiAbuseUserReport['targetUserId'],
targetUserHost: MiAbuseUserReport['targetUserHost'],
reporterId: MiAbuseUserReport['reporterId'],
reporterHost: MiAbuseUserReport['reporterHost'],
comment: string,
}[]) {
const entities = params.map(param => {
return {
id: this.idService.gen(),
targetUserId: param.targetUserId,
targetUserHost: param.targetUserHost,
reporterId: param.reporterId,
reporterHost: param.reporterHost,
comment: param.comment,
};
});
const reports = Array.of<MiAbuseUserReport>();
for (const entity of entities) {
const report = await this.abuseUserReportsRepository.insertOne(entity);
reports.push(report);
}
return Promise.all([
this.abuseReportNotificationService.notifyAdminStream(reports),
this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReport'),
this.abuseReportNotificationService.notifyMail(reports),
]);
}
/**
* 通報を解決し、その内容を下記の手段で管理者各位に通知する.
* - SystemWebhook
*
* @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
* @param operator 通報を処理したユーザ
* @see AbuseReportNotificationService.notify
*/
@bindThis
public async resolve(
params: {
reportId: string;
forward: boolean;
}[],
operator: MiUser,
) {
const paramsMap = new Map(params.map(it => [it.reportId, it]));
const reports = await this.abuseUserReportsRepository.findBy({
id: In(params.map(it => it.reportId)),
});
for (const report of reports) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const ps = paramsMap.get(report.id)!;
await this.abuseUserReportsRepository.update(report.id, {
resolved: true,
assigneeId: operator.id,
forwarded: ps.forward && report.targetUserHost !== null,
});
if (ps.forward && report.targetUserHost != null) {
const actor = await this.instanceActorService.getInstanceActor();
const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
// eslint-disable-next-line
const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment);
const contextAssignedFlag = this.apRendererService.addContext(flag);
this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false);
}
this.moderationLogService
.log(operator, 'resolveAbuseReport', {
reportId: report.id,
report: report,
forwarded: ps.forward && report.targetUserHost !== null,
})
.then();
}
return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) })
.then(reports => this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReportResolved'));
}
}

View File

@@ -41,7 +41,7 @@ export class ClipService {
const currentCount = await this.clipsRepository.countBy({
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();
}
@@ -102,7 +102,7 @@ export class ClipService {
const currentCount = await this.clipNotesRepository.countBy({
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();
}

View File

@@ -5,14 +5,6 @@
import { Module } from '@nestjs/common';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { AbuseReportService } from '@/core/AbuseReportService.js';
import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
import {
AbuseReportNotificationRecipientEntityService,
} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { UserSearchService } from '@/core/UserSearchService.js';
import { AccountMoveService } from './AccountMoveService.js';
import { AccountUpdateService } from './AccountUpdateService.js';
import { AiService } from './AiService.js';
@@ -61,11 +53,10 @@ import { UserFollowingService } from './UserFollowingService.js';
import { UserKeypairService } from './UserKeypairService.js';
import { UserListService } from './UserListService.js';
import { UserMutingService } from './UserMutingService.js';
import { UserRenoteMutingService } from './UserRenoteMutingService.js';
import { UserSuspendService } from './UserSuspendService.js';
import { UserAuthService } from './UserAuthService.js';
import { VideoProcessingService } from './VideoProcessingService.js';
import { UserWebhookService } from './UserWebhookService.js';
import { WebhookService } from './WebhookService.js';
import { ProxyAccountService } from './ProxyAccountService.js';
import { UtilityService } from './UtilityService.js';
import { FileInfoService } from './FileInfoService.js';
@@ -153,8 +144,6 @@ import type { Provider } from '@nestjs/common';
//#region 文字列ベースでのinjection用(循環参照対応のため)
const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService };
const $AbuseReportService: Provider = { provide: 'AbuseReportService', useExisting: AbuseReportService };
const $AbuseReportNotificationService: Provider = { provide: 'AbuseReportNotificationService', useExisting: AbuseReportNotificationService };
const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService };
const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService };
const $AiService: Provider = { provide: 'AiService', useExisting: AiService };
@@ -204,13 +193,10 @@ const $UserFollowingService: Provider = { provide: 'UserFollowingService', useEx
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
const $UserRenoteMutingService: Provider = { provide: 'UserRenoteMutingService', useExisting: UserRenoteMutingService };
const $UserSearchService: Provider = { provide: 'UserSearchService', useExisting: UserSearchService };
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
const $WebhookService: Provider = { provide: 'WebhookService', useExisting: WebhookService };
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
@@ -239,7 +225,6 @@ const $ChartManagementService: Provider = { provide: 'ChartManagementService', u
const $AbuseUserReportEntityService: Provider = { provide: 'AbuseUserReportEntityService', useExisting: AbuseUserReportEntityService };
const $AnnouncementEntityService: Provider = { provide: 'AnnouncementEntityService', useExisting: AnnouncementEntityService };
const $AbuseReportNotificationRecipientEntityService: Provider = { provide: 'AbuseReportNotificationRecipientEntityService', useExisting: AbuseReportNotificationRecipientEntityService };
const $AntennaEntityService: Provider = { provide: 'AntennaEntityService', useExisting: AntennaEntityService };
const $AppEntityService: Provider = { provide: 'AppEntityService', useExisting: AppEntityService };
const $AuthSessionEntityService: Provider = { provide: 'AuthSessionEntityService', useExisting: AuthSessionEntityService };
@@ -273,7 +258,6 @@ const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', u
const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService };
const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService };
const $MetaEntityService: Provider = { provide: 'MetaEntityService', useExisting: MetaEntityService };
const $SystemWebhookEntityService: Provider = { provide: 'SystemWebhookEntityService', useExisting: SystemWebhookEntityService };
const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService };
const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService };
@@ -301,8 +285,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
],
providers: [
LoggerService,
AbuseReportService,
AbuseReportNotificationService,
AccountMoveService,
AccountUpdateService,
AiService,
@@ -352,13 +334,10 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
UserKeypairService,
UserListService,
UserMutingService,
UserRenoteMutingService,
UserSearchService,
UserSuspendService,
UserAuthService,
VideoProcessingService,
UserWebhookService,
SystemWebhookService,
WebhookService,
UtilityService,
FileInfoService,
SearchService,
@@ -387,7 +366,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AbuseUserReportEntityService,
AnnouncementEntityService,
AbuseReportNotificationRecipientEntityService,
AntennaEntityService,
AppEntityService,
AuthSessionEntityService,
@@ -421,7 +399,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
RoleEntityService,
ReversiGameEntityService,
MetaEntityService,
SystemWebhookEntityService,
ApAudienceService,
ApDbResolverService,
@@ -445,8 +422,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
//#region 文字列ベースでのinjection用(循環参照対応のため)
$LoggerService,
$AbuseReportService,
$AbuseReportNotificationService,
$AccountMoveService,
$AccountUpdateService,
$AiService,
@@ -496,13 +471,10 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$UserKeypairService,
$UserListService,
$UserMutingService,
$UserRenoteMutingService,
$UserSearchService,
$UserSuspendService,
$UserAuthService,
$VideoProcessingService,
$UserWebhookService,
$SystemWebhookService,
$WebhookService,
$UtilityService,
$FileInfoService,
$SearchService,
@@ -531,7 +503,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AbuseUserReportEntityService,
$AnnouncementEntityService,
$AbuseReportNotificationRecipientEntityService,
$AntennaEntityService,
$AppEntityService,
$AuthSessionEntityService,
@@ -565,7 +536,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$RoleEntityService,
$ReversiGameEntityService,
$MetaEntityService,
$SystemWebhookEntityService,
$ApAudienceService,
$ApDbResolverService,
@@ -590,8 +560,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
exports: [
QueueModule,
LoggerService,
AbuseReportService,
AbuseReportNotificationService,
AccountMoveService,
AccountUpdateService,
AiService,
@@ -641,13 +609,10 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
UserKeypairService,
UserListService,
UserMutingService,
UserRenoteMutingService,
UserSearchService,
UserSuspendService,
UserAuthService,
VideoProcessingService,
UserWebhookService,
SystemWebhookService,
WebhookService,
UtilityService,
FileInfoService,
SearchService,
@@ -675,7 +640,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AbuseUserReportEntityService,
AnnouncementEntityService,
AbuseReportNotificationRecipientEntityService,
AntennaEntityService,
AppEntityService,
AuthSessionEntityService,
@@ -709,7 +673,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
RoleEntityService,
ReversiGameEntityService,
MetaEntityService,
SystemWebhookEntityService,
ApAudienceService,
ApDbResolverService,
@@ -733,8 +696,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
//#region 文字列ベースでのinjection用(循環参照対応のため)
$LoggerService,
$AbuseReportService,
$AbuseReportNotificationService,
$AccountMoveService,
$AccountUpdateService,
$AiService,
@@ -784,13 +745,10 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$UserKeypairService,
$UserListService,
$UserMutingService,
$UserRenoteMutingService,
$UserSearchService,
$UserSuspendService,
$UserAuthService,
$VideoProcessingService,
$UserWebhookService,
$SystemWebhookService,
$WebhookService,
$UtilityService,
$FileInfoService,
$SearchService,
@@ -818,7 +776,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AbuseUserReportEntityService,
$AnnouncementEntityService,
$AbuseReportNotificationRecipientEntityService,
$AntennaEntityService,
$AppEntityService,
$AuthSessionEntityService,
@@ -852,7 +809,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$RoleEntityService,
$ReversiGameEntityService,
$MetaEntityService,
$SystemWebhookEntityService,
$ApAudienceService,
$ApDbResolverService,

View File

@@ -16,7 +16,6 @@ import type { UserProfilesRepository } from '@/models/_.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { QueueService } from '@/core/QueueService.js';
@Injectable()
export class EmailService {
@@ -33,7 +32,6 @@ export class EmailService {
private loggerService: LoggerService,
private utilityService: UtilityService,
private httpRequestService: HttpRequestService,
private queueService: QueueService,
) {
this.logger = this.loggerService.getLogger('email');
}

View File

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

View File

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

View File

@@ -18,7 +18,6 @@ import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import type { MiSignin } from '@/models/Signin.js';
import type { MiPage } from '@/models/Page.js';
import type { MiWebhook } from '@/models/Webhook.js';
import type { MiSystemWebhook } from '@/models/SystemWebhook.js';
import type { MiMeta } from '@/models/Meta.js';
import { MiAvatarDecoration, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js';
@@ -209,10 +208,6 @@ type SerializedAll<T> = {
[K in keyof T]: Serialized<T[K]>;
};
type UndefinedAsNullAll<T> = {
[K in keyof T]: T[K] extends undefined ? null : T[K];
}
export interface InternalEventTypes {
userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; };
userChangeDeletedState: { id: MiUser['id']; isDeleted: MiUser['isDeleted']; };
@@ -232,9 +227,6 @@ export interface InternalEventTypes {
webhookCreated: MiWebhook;
webhookDeleted: MiWebhook;
webhookUpdated: MiWebhook;
systemWebhookCreated: MiSystemWebhook;
systemWebhookDeleted: MiSystemWebhook;
systemWebhookUpdated: MiSystemWebhook;
antennaCreated: MiAntenna;
antennaDeleted: MiAntenna;
antennaUpdated: MiAntenna;
@@ -251,45 +243,43 @@ export interface InternalEventTypes {
userListMemberRemoved: { userListId: MiUserList['id']; memberId: MiUser['id']; };
}
type EventTypesToEventPayload<T> = EventUnionFromDictionary<UndefinedAsNullAll<SerializedAll<T>>>;
// name/messages(spec) pairs dictionary
export type GlobalEvents = {
internal: {
name: 'internal';
payload: EventTypesToEventPayload<InternalEventTypes>;
payload: EventUnionFromDictionary<SerializedAll<InternalEventTypes>>;
};
broadcast: {
name: 'broadcast';
payload: EventTypesToEventPayload<BroadcastTypes>;
payload: EventUnionFromDictionary<SerializedAll<BroadcastTypes>>;
};
main: {
name: `mainStream:${MiUser['id']}`;
payload: EventTypesToEventPayload<MainEventTypes>;
payload: EventUnionFromDictionary<SerializedAll<MainEventTypes>>;
};
drive: {
name: `driveStream:${MiUser['id']}`;
payload: EventTypesToEventPayload<DriveEventTypes>;
payload: EventUnionFromDictionary<SerializedAll<DriveEventTypes>>;
};
note: {
name: `noteStream:${MiNote['id']}`;
payload: EventTypesToEventPayload<NoteStreamEventTypes>;
payload: EventUnionFromDictionary<SerializedAll<NoteStreamEventTypes>>;
};
userList: {
name: `userListStream:${MiUserList['id']}`;
payload: EventTypesToEventPayload<UserListEventTypes>;
payload: EventUnionFromDictionary<SerializedAll<UserListEventTypes>>;
};
roleTimeline: {
name: `roleTimelineStream:${MiRole['id']}`;
payload: EventTypesToEventPayload<RoleTimelineEventTypes>;
payload: EventUnionFromDictionary<SerializedAll<RoleTimelineEventTypes>>;
};
antenna: {
name: `antennaStream:${MiAntenna['id']}`;
payload: EventTypesToEventPayload<AntennaEventTypes>;
payload: EventUnionFromDictionary<SerializedAll<AntennaEventTypes>>;
};
admin: {
name: `adminStream:${MiUser['id']}`;
payload: EventTypesToEventPayload<AdminEventTypes>;
payload: EventUnionFromDictionary<SerializedAll<AdminEventTypes>>;
};
notes: {
name: 'notesStream';
@@ -297,11 +287,11 @@ export type GlobalEvents = {
};
reversi: {
name: `reversiStream:${MiUser['id']}`;
payload: EventTypesToEventPayload<ReversiEventTypes>;
payload: EventUnionFromDictionary<SerializedAll<ReversiEventTypes>>;
};
reversiGame: {
name: `reversiGameStream:${MiReversiGame['id']}`;
payload: EventTypesToEventPayload<ReversiGameEventTypes>;
payload: EventUnionFromDictionary<SerializedAll<ReversiGameEventTypes>>;
};
};

View File

@@ -15,7 +15,7 @@ export class LoggerService {
}
@bindThis
public getLogger(domain: string, color?: KEYWORD | undefined) {
return new Logger(domain, color);
public getLogger(domain: string, color?: KEYWORD | undefined, store?: boolean) {
return new Logger(domain, color, store);
}
}

View File

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

View File

@@ -38,7 +38,7 @@ import InstanceChart from '@/core/chart/charts/instance.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { UserWebhookService } from '@/core/UserWebhookService.js';
import { WebhookService } from '@/core/WebhookService.js';
import { HashtagService } from '@/core/HashtagService.js';
import { AntennaService } from '@/core/AntennaService.js';
import { QueueService } from '@/core/QueueService.js';
@@ -59,6 +59,7 @@ import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import { isReply } from '@/misc/is-reply.js';
import { trackPromise } from '@/misc/promise-tracker.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@@ -204,7 +205,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private federatedInstanceService: FederatedInstanceService,
private hashtagService: HashtagService,
private antennaService: AntennaService,
private webhookService: UserWebhookService,
private webhookService: WebhookService,
private featuredService: FeaturedService,
private remoteUserResolveService: RemoteUserResolveService,
private apDeliverManagerService: ApDeliverManagerService,
@@ -605,7 +606,7 @@ export class NoteCreateService implements OnApplicationShutdown {
this.webhookService.getActiveWebhooks().then(webhooks => {
webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
for (const webhook of webhooks) {
this.queueService.userWebhookDeliver(webhook, 'note', {
this.queueService.webhookDeliver(webhook, 'note', {
note: noteObj,
});
}
@@ -632,7 +633,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply'));
for (const webhook of webhooks) {
this.queueService.userWebhookDeliver(webhook, 'reply', {
this.queueService.webhookDeliver(webhook, 'reply', {
note: noteObj,
});
}
@@ -655,7 +656,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote'));
for (const webhook of webhooks) {
this.queueService.userWebhookDeliver(webhook, 'renote', {
this.queueService.webhookDeliver(webhook, 'renote', {
note: noteObj,
});
}
@@ -787,7 +788,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention'));
for (const webhook of webhooks) {
this.queueService.userWebhookDeliver(webhook, 'mention', {
this.queueService.webhookDeliver(webhook, 'mention', {
note: detailPackedNote,
});
}
@@ -838,7 +839,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const mentions = extractMentions(tokens);
let mentionedUsers = (await Promise.all(mentions.map(m =>
this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null),
))).filter(x => x != null);
))).filter(isNotNull);
// Drop duplicate users
mentionedUsers = mentionedUsers.filter((u, i, self) =>
@@ -933,13 +934,10 @@ export class NoteCreateService implements OnApplicationShutdown {
}
}
// 自分自身のHTL
if (note.userHost == null) {
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) {
this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
if (note.fileIds.length > 0) {
this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
}
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL
this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
if (note.fileIds.length > 0) {
this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
}
}

View File

@@ -7,17 +7,10 @@ import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
import * as Bull from 'bullmq';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { baseQueueOptions, QUEUE } from '@/queue/const.js';
import { QUEUE, baseQueueOptions } from '@/queue/const.js';
import { allSettled } from '@/misc/promise-tracker.js';
import {
DeliverJobData,
EndedPollNotificationJobData,
InboxJobData,
RelationshipJobData,
UserWebhookDeliverJobData,
SystemWebhookDeliverJobData,
} from '../queue/types.js';
import type { Provider } from '@nestjs/common';
import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js';
export type SystemQueue = Bull.Queue<Record<string, unknown>>;
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
@@ -26,8 +19,7 @@ export type InboxQueue = Bull.Queue<InboxJobData>;
export type DbQueue = Bull.Queue;
export type RelationshipQueue = Bull.Queue<RelationshipJobData>;
export type ObjectStorageQueue = Bull.Queue;
export type UserWebhookDeliverQueue = Bull.Queue<UserWebhookDeliverJobData>;
export type SystemWebhookDeliverQueue = Bull.Queue<SystemWebhookDeliverJobData>;
export type WebhookDeliverQueue = Bull.Queue<WebhookDeliverJobData>;
const $system: Provider = {
provide: 'queue:system',
@@ -71,15 +63,9 @@ const $objectStorage: Provider = {
inject: [DI.config],
};
const $userWebhookDeliver: Provider = {
provide: 'queue:userWebhookDeliver',
useFactory: (config: Config) => new Bull.Queue(QUEUE.USER_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.USER_WEBHOOK_DELIVER)),
inject: [DI.config],
};
const $systemWebhookDeliver: Provider = {
provide: 'queue:systemWebhookDeliver',
useFactory: (config: Config) => new Bull.Queue(QUEUE.SYSTEM_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.SYSTEM_WEBHOOK_DELIVER)),
const $webhookDeliver: Provider = {
provide: 'queue:webhookDeliver',
useFactory: (config: Config) => new Bull.Queue(QUEUE.WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.WEBHOOK_DELIVER)),
inject: [DI.config],
};
@@ -94,8 +80,7 @@ const $systemWebhookDeliver: Provider = {
$db,
$relationship,
$objectStorage,
$userWebhookDeliver,
$systemWebhookDeliver,
$webhookDeliver,
],
exports: [
$system,
@@ -105,8 +90,7 @@ const $systemWebhookDeliver: Provider = {
$db,
$relationship,
$objectStorage,
$userWebhookDeliver,
$systemWebhookDeliver,
$webhookDeliver,
],
})
export class QueueModule implements OnApplicationShutdown {
@@ -118,8 +102,7 @@ export class QueueModule implements OnApplicationShutdown {
@Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
) {}
public async dispose(): Promise<void> {
@@ -134,8 +117,7 @@ export class QueueModule implements OnApplicationShutdown {
this.dbQueue.close(),
this.relationshipQueue.close(),
this.objectStorageQueue.close(),
this.userWebhookDeliverQueue.close(),
this.systemWebhookDeliverQueue.close(),
this.webhookDeliverQueue.close(),
]);
}

View File

@@ -8,33 +8,15 @@ import { Inject, Injectable } from '@nestjs/common';
import type { IActivity } from '@/core/activitypub/type.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js';
import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
import type {
DbJobData,
DeliverJobData,
RelationshipJobData,
SystemWebhookDeliverJobData,
ThinUser,
UserWebhookDeliverJobData,
} from '../queue/types.js';
import type {
DbQueue,
DeliverQueue,
EndedPollNotificationQueue,
InboxQueue,
ObjectStorageQueue,
RelationshipQueue,
SystemQueue,
UserWebhookDeliverQueue,
SystemWebhookDeliverQueue,
} from './QueueModule.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
import type httpSignature from '@peertube/http-signature';
import type * as Bull from 'bullmq';
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
@Injectable()
export class QueueService {
@@ -49,8 +31,7 @@ export class QueueService {
@Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
) {
this.systemQueue.add('tickCharts', {
}, {
@@ -450,13 +431,9 @@ export class QueueService {
});
}
/**
* @see UserWebhookDeliverJobData
* @see WebhookDeliverProcessorService
*/
@bindThis
public userWebhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
const data: UserWebhookDeliverJobData = {
public webhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
const data = {
type,
content,
webhookId: webhook.id,
@@ -467,33 +444,7 @@ export class QueueService {
eventId: randomUUID(),
};
return this.userWebhookDeliverQueue.add(webhook.id, data, {
attempts: 4,
backoff: {
type: 'custom',
},
removeOnComplete: true,
removeOnFail: true,
});
}
/**
* @see SystemWebhookDeliverJobData
* @see WebhookDeliverProcessorService
*/
@bindThis
public systemWebhookDeliver(webhook: MiSystemWebhook, type: SystemWebhookEventType, content: unknown) {
const data: SystemWebhookDeliverJobData = {
type,
content,
webhookId: webhook.id,
to: webhook.url,
secret: webhook.secret,
createdAt: Date.now(),
eventId: randomUUID(),
};
return this.systemWebhookDeliverQueue.add(webhook.id, data, {
return this.webhookDeliverQueue.add(webhook.id, data, {
attempts: 4,
backoff: {
type: 'custom',

View File

@@ -29,7 +29,6 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { RoleService } from '@/core/RoleService.js';
import { FeaturedService } from '@/core/FeaturedService.js';
import { trackPromise } from '@/misc/promise-tracker.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
const FALLBACK = '\u2764';
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
@@ -118,16 +117,11 @@ export class ReactionService {
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;
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) {
reaction = '\u2764';
} else if (_reaction != null) {
} else if (_reaction) {
const custom = reaction.match(isCustomEmojiRegexp);
if (custom) {
const reacterHost = this.utilityService.toPunyNullable(user.host);

View File

@@ -47,7 +47,6 @@ export type RolePolicies = {
canHideAds: boolean;
driveCapacityMb: number;
alwaysMarkNsfw: boolean;
canUpdateBioMedia: boolean;
pinLimit: number;
antennaLimit: number;
wordMuteLimit: number;
@@ -76,7 +75,6 @@ export const DEFAULT_POLICIES: RolePolicies = {
canHideAds: false,
driveCapacityMb: 100,
alwaysMarkNsfw: false,
canUpdateBioMedia: true,
pinLimit: 5,
antennaLimit: 5,
wordMuteLimit: 200,
@@ -378,7 +376,6 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
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)),
antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)),
@@ -413,32 +410,14 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
}
@bindThis
public async getModeratorIds(includeAdmins = true, excludeExpire = false): Promise<MiUser['id'][]> {
public async getModeratorIds(includeAdmins = true): Promise<MiUser['id'][]> {
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const moderatorRoles = includeAdmins
? roles.filter(r => r.isModerator || r.isAdministrator)
: roles.filter(r => r.isModerator);
const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator);
const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
roleId: In(moderatorRoles.map(r => r.id)),
}) : [];
// TODO: isRootなアカウントも含める
const assigns = moderatorRoles.length > 0
? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) })
: [];
const now = Date.now();
const result = [
// Setを経由して重複を除去ユーザIDは重複する可能性があるので
...new Set(
assigns
.filter(it =>
(excludeExpire)
? (it.expiresAt == null || it.expiresAt.getTime() > now)
: true,
)
.map(a => a.userId),
),
];
return result.sort((x, y) => x.localeCompare(y));
return assigns.map(a => a.userId);
}
@bindThis
@@ -505,15 +484,14 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
this.globalEventService.publishInternalEvent('userRoleAssigned', created);
const user = await this.usersRepository.findOneByOrFail({ id: userId });
if (role.isPublic && user.host === null) {
if (role.isPublic) {
this.notificationService.createNotification(userId, 'roleAssigned', {
roleId: roleId,
});
}
if (moderator) {
const user = await this.usersRepository.findOneByOrFail({ id: userId });
this.moderationLogService.log(moderator, 'assignRole', {
roleId: roleId,
roleName: role.name,

View File

@@ -1,233 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import type { MiUser, SystemWebhooksRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
import { IdService } from '@/core/IdService.js';
import { QueueService } from '@/core/QueueService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { LoggerService } from '@/core/LoggerService.js';
import Logger from '@/logger.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
export class SystemWebhookService implements OnApplicationShutdown {
private logger: Logger;
private activeSystemWebhooksFetched = false;
private activeSystemWebhooks: MiSystemWebhook[] = [];
constructor(
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.systemWebhooksRepository)
private systemWebhooksRepository: SystemWebhooksRepository,
private idService: IdService,
private queueService: QueueService,
private moderationLogService: ModerationLogService,
private loggerService: LoggerService,
private globalEventService: GlobalEventService,
) {
this.redisForSub.on('message', this.onMessage);
this.logger = this.loggerService.getLogger('webhook');
}
@bindThis
public async fetchActiveSystemWebhooks() {
if (!this.activeSystemWebhooksFetched) {
this.activeSystemWebhooks = await this.systemWebhooksRepository.findBy({
isActive: true,
});
this.activeSystemWebhooksFetched = true;
}
return this.activeSystemWebhooks;
}
/**
* SystemWebhook の一覧を取得する.
*/
@bindThis
public async fetchSystemWebhooks(params?: {
ids?: MiSystemWebhook['id'][];
isActive?: MiSystemWebhook['isActive'];
on?: MiSystemWebhook['on'];
}): Promise<MiSystemWebhook[]> {
const query = this.systemWebhooksRepository.createQueryBuilder('systemWebhook');
if (params) {
if (params.ids && params.ids.length > 0) {
query.andWhere('systemWebhook.id IN (:...ids)', { ids: params.ids });
}
if (params.isActive !== undefined) {
query.andWhere('systemWebhook.isActive = :isActive', { isActive: params.isActive });
}
if (params.on && params.on.length > 0) {
query.andWhere(':on <@ systemWebhook.on', { on: params.on });
}
}
return query.getMany();
}
/**
* SystemWebhook を作成する.
*/
@bindThis
public async createSystemWebhook(
params: {
isActive: MiSystemWebhook['isActive'];
name: MiSystemWebhook['name'];
on: MiSystemWebhook['on'];
url: MiSystemWebhook['url'];
secret: MiSystemWebhook['secret'];
},
updater: MiUser,
): Promise<MiSystemWebhook> {
const id = this.idService.gen();
await this.systemWebhooksRepository.insert({
...params,
id,
});
const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id });
this.globalEventService.publishInternalEvent('systemWebhookCreated', webhook);
this.moderationLogService
.log(updater, 'createSystemWebhook', {
systemWebhookId: webhook.id,
webhook: webhook,
})
.then();
return webhook;
}
/**
* SystemWebhook を更新する.
*/
@bindThis
public async updateSystemWebhook(
params: {
id: MiSystemWebhook['id'];
isActive: MiSystemWebhook['isActive'];
name: MiSystemWebhook['name'];
on: MiSystemWebhook['on'];
url: MiSystemWebhook['url'];
secret: MiSystemWebhook['secret'];
},
updater: MiUser,
): Promise<MiSystemWebhook> {
const beforeEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: params.id });
await this.systemWebhooksRepository.update(beforeEntity.id, {
updatedAt: new Date(),
isActive: params.isActive,
name: params.name,
on: params.on,
url: params.url,
secret: params.secret,
});
const afterEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: beforeEntity.id });
this.globalEventService.publishInternalEvent('systemWebhookUpdated', afterEntity);
this.moderationLogService
.log(updater, 'updateSystemWebhook', {
systemWebhookId: beforeEntity.id,
before: beforeEntity,
after: afterEntity,
})
.then();
return afterEntity;
}
/**
* SystemWebhook を削除する.
*/
@bindThis
public async deleteSystemWebhook(id: MiSystemWebhook['id'], updater: MiUser) {
const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id });
await this.systemWebhooksRepository.delete(id);
this.globalEventService.publishInternalEvent('systemWebhookDeleted', webhook);
this.moderationLogService
.log(updater, 'deleteSystemWebhook', {
systemWebhookId: webhook.id,
webhook,
})
.then();
}
/**
* SystemWebhook をWebhook配送キューに追加する
* @see QueueService.systemWebhookDeliver
*/
@bindThis
public async enqueueSystemWebhook(webhook: MiSystemWebhook | MiSystemWebhook['id'], type: SystemWebhookEventType, content: unknown) {
const webhookEntity = typeof webhook === 'string'
? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)
: webhook;
if (!webhookEntity || !webhookEntity.isActive) {
this.logger.info(`Webhook is not active or not found : ${webhook}`);
return;
}
if (!webhookEntity.on.includes(type)) {
this.logger.info(`Webhook ${webhookEntity.id} is not listening to ${type}`);
return;
}
return this.queueService.systemWebhookDeliver(webhookEntity, type, content);
}
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
if (obj.channel !== 'internal') {
return;
}
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'systemWebhookCreated': {
if (body.isActive) {
this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body));
}
break;
}
case 'systemWebhookUpdated': {
if (body.isActive) {
const i = this.activeSystemWebhooks.findIndex(a => a.id === body.id);
if (i > -1) {
this.activeSystemWebhooks[i] = MiSystemWebhook.deserialize(body);
} else {
this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body));
}
} else {
this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id);
}
break;
}
case 'systemWebhookDeleted': {
this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id);
break;
}
default:
break;
}
}
@bindThis
public dispose(): void {
this.redisForSub.off('message', this.onMessage);
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
}

View File

@@ -16,7 +16,7 @@ import Logger from '@/logger.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { UserWebhookService } from '@/core/UserWebhookService.js';
import { WebhookService } from '@/core/WebhookService.js';
import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
@@ -46,7 +46,7 @@ export class UserBlockingService implements OnModuleInit {
private idService: IdService,
private queueService: QueueService,
private globalEventService: GlobalEventService,
private webhookService: UserWebhookService,
private webhookService: WebhookService,
private apRendererService: ApRendererService,
private loggerService: LoggerService,
) {
@@ -121,7 +121,7 @@ export class UserBlockingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) {
this.queueService.userWebhookDeliver(webhook, 'unfollow', {
this.queueService.webhookDeliver(webhook, 'unfollow', {
user: packed,
});
}

View File

@@ -16,7 +16,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js
import type { Packed } from '@/misc/json-schema.js';
import InstanceChart from '@/core/chart/charts/instance.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { UserWebhookService } from '@/core/UserWebhookService.js';
import { WebhookService } from '@/core/WebhookService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { DI } from '@/di-symbols.js';
import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
@@ -82,7 +82,7 @@ export class UserFollowingService implements OnModuleInit {
private metaService: MetaService,
private notificationService: NotificationService,
private federatedInstanceService: FederatedInstanceService,
private webhookService: UserWebhookService,
private webhookService: WebhookService,
private apRendererService: ApRendererService,
private accountMoveService: AccountMoveService,
private fanoutTimelineService: FanoutTimelineService,
@@ -279,10 +279,8 @@ export class UserFollowingService implements OnModuleInit {
});
// 通知を作成
if (follower.host === null) {
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
}, followee.id);
}
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
}, followee.id);
}
if (alreadyFollowed) return;
@@ -333,7 +331,7 @@ export class UserFollowingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
for (const webhook of webhooks) {
this.queueService.userWebhookDeliver(webhook, 'follow', {
this.queueService.webhookDeliver(webhook, 'follow', {
user: packed,
});
}
@@ -347,7 +345,7 @@ export class UserFollowingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed'));
for (const webhook of webhooks) {
this.queueService.userWebhookDeliver(webhook, 'followed', {
this.queueService.webhookDeliver(webhook, 'followed', {
user: packed,
});
}
@@ -400,7 +398,7 @@ export class UserFollowingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) {
this.queueService.userWebhookDeliver(webhook, 'unfollow', {
this.queueService.webhookDeliver(webhook, 'unfollow', {
user: packed,
});
}
@@ -742,7 +740,7 @@ export class UserFollowingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) {
this.queueService.userWebhookDeliver(webhook, 'unfollow', {
this.queueService.webhookDeliver(webhook, 'unfollow', {
user: packedFollowee,
});
}

View File

@@ -95,7 +95,7 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit {
const currentCount = await this.userListMembershipsRepository.countBy({
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();
}

View File

@@ -1,52 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import type { RenoteMutingsRepository } from '@/models/_.js';
import type { MiRenoteMuting } from '@/models/RenoteMuting.js';
import { IdService } from '@/core/IdService.js';
import type { MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
@Injectable()
export class UserRenoteMutingService {
constructor(
@Inject(DI.renoteMutingsRepository)
private renoteMutingsRepository: RenoteMutingsRepository,
private idService: IdService,
private cacheService: CacheService,
) {
}
@bindThis
public async mute(user: MiUser, target: MiUser, expiresAt: Date | null = null): Promise<void> {
await this.renoteMutingsRepository.insert({
id: this.idService.gen(),
muterId: user.id,
muteeId: target.id,
});
await this.cacheService.renoteMutingsCache.refresh(user.id);
}
@bindThis
public async unmute(mutings: MiRenoteMuting[]): Promise<void> {
if (mutings.length === 0) return;
await this.renoteMutingsRepository.delete({
id: In(mutings.map(m => m.id)),
});
const muterIds = [...new Set(mutings.map(m => m.muterId))];
for (const muterId of muterIds) {
await this.cacheService.renoteMutingsCache.refresh(muterId);
}
}
}

View File

@@ -1,205 +0,0 @@
/*
* 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が重複することはないが).
* 1で既にヒットしていた場合、2, 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

@@ -1,99 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import type { WebhooksRepository } from '@/models/_.js';
import type { MiWebhook } from '@/models/Webhook.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { GlobalEvents } from '@/core/GlobalEventService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
export class UserWebhookService implements OnApplicationShutdown {
private activeWebhooksFetched = false;
private activeWebhooks: MiWebhook[] = [];
constructor(
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.webhooksRepository)
private webhooksRepository: WebhooksRepository,
) {
this.redisForSub.on('message', this.onMessage);
}
@bindThis
public async getActiveWebhooks() {
if (!this.activeWebhooksFetched) {
this.activeWebhooks = await this.webhooksRepository.findBy({
active: true,
});
this.activeWebhooksFetched = true;
}
return this.activeWebhooks;
}
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
if (obj.channel !== 'internal') {
return;
}
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'webhookCreated': {
if (body.active) {
this.activeWebhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
...body,
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
user: null, // joinなカラムは通常取ってこないので
});
}
break;
}
case 'webhookUpdated': {
if (body.active) {
const i = this.activeWebhooks.findIndex(a => a.id === body.id);
if (i > -1) {
this.activeWebhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
...body,
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
user: null, // joinなカラムは通常取ってこないので
};
} else {
this.activeWebhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
...body,
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
user: null, // joinなカラムは通常取ってこないので
});
}
} else {
this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id);
}
break;
}
case 'webhookDeleted': {
this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id);
break;
}
default:
break;
}
}
@bindThis
public dispose(): void {
this.redisForSub.off('message', this.onMessage);
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
}

View File

@@ -0,0 +1,97 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import type { WebhooksRepository } from '@/models/_.js';
import type { MiWebhook } from '@/models/Webhook.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
export class WebhookService implements OnApplicationShutdown {
private webhooksFetched = false;
private webhooks: MiWebhook[] = [];
constructor(
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.webhooksRepository)
private webhooksRepository: WebhooksRepository,
) {
//this.onMessage = this.onMessage.bind(this);
this.redisForSub.on('message', this.onMessage);
}
@bindThis
public async getActiveWebhooks() {
if (!this.webhooksFetched) {
this.webhooks = await this.webhooksRepository.findBy({
active: true,
});
this.webhooksFetched = true;
}
return this.webhooks;
}
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'webhookCreated':
if (body.active) {
this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
...body,
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
user: null, // joinなカラムは通常取ってこないので
});
}
break;
case 'webhookUpdated':
if (body.active) {
const i = this.webhooks.findIndex(a => a.id === body.id);
if (i > -1) {
this.webhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
...body,
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
user: null, // joinなカラムは通常取ってこないので
};
} else {
this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
...body,
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
user: null, // joinなカラムは通常取ってこないので
});
}
} else {
this.webhooks = this.webhooks.filter(a => a.id !== body.id);
}
break;
case 'webhookDeleted':
this.webhooks = this.webhooks.filter(a => a.id !== body.id);
break;
default:
break;
}
}
}
@bindThis
public dispose(): void {
this.redisForSub.off('message', this.onMessage);
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
}

View File

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

View File

@@ -27,8 +27,8 @@ import { QueueService } from '@/core/QueueService.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import type { MiRemoteUser } from '@/models/User.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { GlobalEventService } from '@/core/GlobalEventService.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 { ApNoteService } from './models/ApNoteService.js';
import { ApLoggerService } from './ApLoggerService.js';
@@ -57,6 +57,9 @@ export class ApInboxService {
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.abuseUserReportsRepository)
private abuseUserReportsRepository: AbuseUserReportsRepository,
@Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository,
@@ -65,7 +68,6 @@ export class ApInboxService {
private utilityService: UtilityService,
private idService: IdService,
private metaService: MetaService,
private abuseReportService: AbuseReportService,
private userFollowingService: UserFollowingService,
private apAudienceService: ApAudienceService,
private reactionService: ReactionService,
@@ -537,19 +539,20 @@ export class ApInboxService {
const userIds = uris
.filter(uri => uri.startsWith(this.config.url + '/users/'))
.map(uri => uri.split('/').at(-1))
.filter(x => x != null);
.filter(isNotNull);
const users = await this.usersRepository.findBy({
id: In(userIds),
});
if (users.length < 1) return 'skip';
await this.abuseReportService.report([{
await this.abuseUserReportsRepository.insert({
id: this.idService.gen(),
targetUserId: users[0].id,
targetUserHost: users[0].host,
reporterId: actor.id,
reporterHost: actor.host,
comment: `${activity.content}\n${JSON.stringify(uris, null, 2)}`,
}]);
});
return 'ok';
}

View File

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

View File

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

View File

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

View File

@@ -24,6 +24,7 @@ import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { checkHttps } from '@/misc/check-https.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 { ApLoggerService } from '../ApLoggerService.js';
import { ApMfmService } from '../ApMfmService.js';
@@ -252,7 +253,7 @@ export class ApNoteService {
}
};
const uris = unique([note._misskey_quote, note.quoteUrl].filter(x => x != null));
const uris = unique([note._misskey_quote, note.quoteUrl].filter(isNotNull));
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);

View File

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

View File

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

View File

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

View File

@@ -14,6 +14,6 @@ export class ChartLoggerService {
constructor(
private loggerService: LoggerService,
) {
this.logger = this.loggerService.getLogger('chart', 'white');
this.logger = this.loggerService.getLogger('chart', 'white', process.env.NODE_ENV !== 'test');
}
}

View File

@@ -47,7 +47,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
const suspendedInstancesQuery = this.instancesRepository.createQueryBuilder('instance')
.select('instance.host')
.where('instance.suspensionState != \'none\'');
.where('instance.isSuspended = true');
const pubsubSubQuery = this.followingsRepository.createQueryBuilder('f')
.select('f.followerHost')
@@ -89,7 +89,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
.select('COUNT(instance.id)')
.where(`instance.host IN (${ subInstancesQuery.getQuery() })`)
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere('instance.suspensionState = \'none\'')
.andWhere('instance.isSuspended = false')
.andWhere('instance.isNotResponding = false')
.getRawOne()
.then(x => parseInt(x.count, 10)),
@@ -97,7 +97,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
.select('COUNT(instance.id)')
.where(`instance.host IN (${ pubInstancesQuery.getQuery() })`)
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere('instance.suspensionState = \'none\'')
.andWhere('instance.isSuspended = false')
.andWhere('instance.isNotResponding = false')
.getRawOne()
.then(x => parseInt(x.count, 10)),

View File

@@ -1,87 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { AbuseReportNotificationRecipientRepository, MiAbuseReportNotificationRecipient } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { Packed } from '@/misc/json-schema.js';
import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
@Injectable()
export class AbuseReportNotificationRecipientEntityService {
constructor(
@Inject(DI.abuseReportNotificationRecipientRepository)
private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository,
private userEntityService: UserEntityService,
private systemWebhookEntityService: SystemWebhookEntityService,
) {
}
@bindThis
public async pack(
src: MiAbuseReportNotificationRecipient['id'] | MiAbuseReportNotificationRecipient,
opts?: {
users: Map<string, Packed<'UserLite'>>,
webhooks: Map<string, Packed<'SystemWebhook'>>,
},
): Promise<Packed<'AbuseReportNotificationRecipient'>> {
const recipient = typeof src === 'object'
? src
: await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: src });
const user = recipient.userId
? (opts?.users.get(recipient.userId) ?? await this.userEntityService.pack<'UserLite'>(recipient.userId))
: undefined;
const webhook = recipient.systemWebhookId
? (opts?.webhooks.get(recipient.systemWebhookId) ?? await this.systemWebhookEntityService.pack(recipient.systemWebhookId))
: undefined;
return {
id: recipient.id,
isActive: recipient.isActive,
updatedAt: recipient.updatedAt.toISOString(),
name: recipient.name,
method: recipient.method,
userId: recipient.userId ?? undefined,
user: user,
systemWebhookId: recipient.systemWebhookId ?? undefined,
systemWebhook: webhook,
};
}
@bindThis
public async packMany(
src: MiAbuseReportNotificationRecipient['id'][] | MiAbuseReportNotificationRecipient[],
): Promise<Packed<'AbuseReportNotificationRecipient'>[]> {
const objs = src.filter((it): it is MiAbuseReportNotificationRecipient => typeof it === 'object');
const ids = src.filter((it): it is MiAbuseReportNotificationRecipient['id'] => typeof it === 'string');
if (ids.length > 0) {
objs.push(
...await this.abuseReportNotificationRecipientRepository.findBy({ id: In(ids) }),
);
}
const userIds = objs.map(it => it.userId).filter(x => x != null);
const users: Map<string, Packed<'UserLite'>> = (userIds.length > 0)
? await this.userEntityService.packMany(userIds)
.then(it => new Map(it.map(it => [it.id, it])))
: new Map();
const systemWebhookIds = objs.map(it => it.systemWebhookId).filter(x => x != null);
const systemWebhooks: Map<string, Packed<'SystemWebhook'>> = (systemWebhookIds.length > 0)
? await this.systemWebhookEntityService.packMany(systemWebhookIds)
.then(it => new Map(it.map(it => [it.id, it])))
: new Map();
return Promise
.all(
objs.map(it => this.pack(it, { users: users, webhooks: systemWebhooks })),
)
.then(it => it.sort((a, b) => a.id.localeCompare(b.id)));
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,74 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { MiSystemWebhook, SystemWebhooksRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { Packed } from '@/misc/json-schema.js';
@Injectable()
export class SystemWebhookEntityService {
constructor(
@Inject(DI.systemWebhooksRepository)
private systemWebhooksRepository: SystemWebhooksRepository,
) {
}
@bindThis
public async pack(
src: MiSystemWebhook['id'] | MiSystemWebhook,
opts?: {
webhooks: Map<string, MiSystemWebhook>
},
): Promise<Packed<'SystemWebhook'>> {
const webhook = typeof src === 'object'
? src
: opts?.webhooks.get(src) ?? await this.systemWebhooksRepository.findOneByOrFail({ id: src });
return {
id: webhook.id,
isActive: webhook.isActive,
updatedAt: webhook.updatedAt.toISOString(),
latestSentAt: webhook.latestSentAt?.toISOString() ?? null,
latestStatus: webhook.latestStatus,
name: webhook.name,
on: webhook.on,
url: webhook.url,
secret: webhook.secret,
};
}
@bindThis
public async packMany(src: MiSystemWebhook['id'][] | MiSystemWebhook[]): Promise<Packed<'SystemWebhook'>[]> {
if (src.length === 0) {
return [];
}
const webhooks = Array.of<MiSystemWebhook>();
webhooks.push(
...src.filter((it): it is MiSystemWebhook => typeof it === 'object'),
);
const ids = src.filter((it): it is MiSystemWebhook['id'] => typeof it === 'string');
if (ids.length > 0) {
webhooks.push(
...await this.systemWebhooksRepository.findBy({ id: In(ids) }),
);
}
return Promise
.all(
webhooks.map(x =>
this.pack(x, {
webhooks: new Map(webhooks.map(x => [x.id, x])),
}),
),
)
.then(it => it.sort((a, b) => a.id.localeCompare(b.id)));
}
}

View File

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

View File

@@ -49,7 +49,6 @@ export const DI = {
swSubscriptionsRepository: Symbol('swSubscriptionsRepository'),
hashtagsRepository: Symbol('hashtagsRepository'),
abuseUserReportsRepository: Symbol('abuseUserReportsRepository'),
abuseReportNotificationRecipientRepository: Symbol('abuseReportNotificationRecipientRepository'),
registrationTicketsRepository: Symbol('registrationTicketsRepository'),
authSessionsRepository: Symbol('authSessionsRepository'),
accessTokensRepository: Symbol('accessTokensRepository'),
@@ -71,7 +70,6 @@ export const DI = {
channelFavoritesRepository: Symbol('channelFavoritesRepository'),
registryItemsRepository: Symbol('registryItemsRepository'),
webhooksRepository: Symbol('webhooksRepository'),
systemWebhooksRepository: Symbol('systemWebhooksRepository'),
adsRepository: Symbol('adsRepository'),
passwordResetRequestsRepository: Symbol('passwordResetRequestsRepository'),
retentionAggregationsRepository: Symbol('retentionAggregationsRepository'),

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