1
1
mirror of https://github.com/kokonect-link/cherrypick synced 2024-11-23 14:46:44 +09:00

Merge remote-branch 'misskey/develop'

This commit is contained in:
NoriDev 2024-07-27 23:24:38 +09:00
commit 444fc93d8b
418 changed files with 11715 additions and 7303 deletions

View File

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

View File

@ -6,6 +6,7 @@
#───┘ 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
@ -38,9 +39,11 @@ db:
port: 5432
# Database name
# You can set db from an environment variable instead.
db: cherrypick
# Auth
# You can set user and pass from environment variables instead.
user: example-cherrypick-user
pass: example-cherrypick-pass

View File

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

View File

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

View File

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

View File

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

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: kokonect.link
* CherryPick: 4.x.x (Misskey: 2023.x.x)
* Server URL: cherrypick.example.com
* CherryPick: 4.x.x (Misskey: 2024.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, "CherryPick install shell script", development environment
* CherryPick: 4.x.x (Misskey: 2023.x.x)
* CherryPick: 4.x.x (Misskey: 2024.x.x)
* Node: 20.x.x
* PostgreSQL: 15.x.x
* Redis: 7.x.x
* OS and Architecture: Ubuntu 22.04.2 LTS aarch64
* OS and Architecture: Ubuntu 24.04.2 LTS aarch64
value: |
* Installation Method or Hosting Service:
* CherryPick:

View File

@ -2,6 +2,13 @@ contact_links:
- name: 💬 Misskey official Discord
url: https://discord.gg/Wp8gVStHW3
about: Chat freely about Misskey
# 仮
- name: 💬 Start discussion (Misskey)
url: https://github.com/misskey-dev/misskey/discussions
about: The official forum to join conversation and ask question
- name: 💬 CherryPick official Discord
url: https://discord.gg/V8qghB28Aj
about: Chat freely about CherryPick
- name: 💬 Start discussion (CherryPick)
url: https://github.com/kokonect-link/cherrypick/discussions
about: The official forum to join conversation and ask question

View File

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

View File

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

View File

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

View File

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

View File

@ -13,14 +13,16 @@ jobs:
runs-on: ubuntu-latest
env:
DOCKER_CONTENT_TRUST: 1
DOCKLE_VERSION: 0.4.14
steps:
- uses: actions/checkout@v4.1.1
- run: |
curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb"
- 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"
sudo dpkg -i dockle.deb
- run: |
cp .config/docker_example.env .config/docker.env
cp ./docker-compose_example.yml ./docker-compose.yml
cp ./compose_example.yml ./compose.yml
- run: |
docker compose up -d web
docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" cherrypick-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-cherrypick:
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.2
uses: actions/setup-node@v4.0.3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'

View File

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

View File

@ -4,10 +4,11 @@ on:
push:
paths:
- locales/**
- .github/workflows/locale.yml
pull_request:
paths:
- locales/**
- .github/workflows/locale.yml
jobs:
locale_verify:
runs-on: ubuntu-latest
@ -18,7 +19,7 @@ jobs:
fetch-depth: 0
submodules: true
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.0.2
- uses: actions/setup-node@v4.0.3
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.2
uses: actions/setup-node@v4.0.3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'

View File

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

View File

@ -33,18 +33,21 @@ jobs:
pr_number: ${{ steps.get_pr.outputs.pr_number }}
steps:
- uses: actions/checkout@v4
# headがrelease/かつopenのPRを1つ取得
# headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得
- name: Get PRs
run: |
echo "pr_number=$(gh pr list --limit 1 --search "head:release/ is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
echo "pr_number=$(gh pr list --limit 1 --search "head:$GITHUB_REF_NAME base:$STABLE_BRANCH is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
id: get_pr
env:
STABLE_BRANCH: ${{ vars.STABLE_BRANCH }}
merge:
uses: misskey-dev/release-manager-actions/.github/workflows/merge.yml@v1
uses: misskey-dev/release-manager-actions/.github/workflows/merge.yml@v2
needs: get-pr
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`
@ -65,15 +68,14 @@ 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@v1
uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v2
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 }}
@ -82,10 +84,11 @@ jobs:
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
create-target:
uses: misskey-dev/release-manager-actions/.github/workflows/create-target.yml@v1
uses: misskey-dev/release-manager-actions/.github/workflows/create-target.yml@v2
needs: get-pr
if: ${{ needs.get-pr.outputs.pr_number == '' }}
with:
user: 'github-actions[bot]'
# The script for version increment.
# process.env.CURRENT_VERSION: The current version.
#
@ -118,8 +121,7 @@ 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,23 +16,26 @@ jobs:
check:
runs-on: ubuntu-latest
outputs:
ref: ${{ steps.get_pr.outputs.ref }}
head: ${{ steps.get_pr.outputs.head }}
base: ${{ steps.get_pr.outputs.base }}
steps:
- uses: actions/checkout@v4
# PR情報を取得
- name: Get PR
run: |
pr_json=$(gh pr view "$PR_NUMBER" --json isDraft,headRefName)
echo "ref=$(echo $pr_json | jq -r '.headRefName')" >> $GITHUB_OUTPUT
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
id: get_pr
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
release:
uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v1
uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v2
needs: check
if: startsWith(needs.check.outputs.ref, 'release/')
if: needs.check.outputs.head == github.event.repository.default_branch && needs.check.outputs.base == vars.STABLE_BRANCH
with:
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.2
uses: actions/setup-node@v4.0.3
with:
node-version-file: '.node-version'
cache: 'pnpm'
@ -88,7 +88,7 @@ jobs:
if [ "$BRANCH" = "kokonect-link:$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,12 +9,13 @@ on:
- packages/backend/**
# for permissions
- packages/cherrypick-js/**
- .github/workflows/test-backend.yml
pull_request:
paths:
- packages/backend/**
# for permissions
- packages/cherrypick-js/**
- .github/workflows/test-backend.yml
jobs:
unit:
runs-on: ubuntu-latest
@ -45,7 +46,7 @@ jobs:
- name: Install FFmpeg
uses: FedericoCarboni/setup-ffmpeg@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
@ -92,7 +93,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'

View File

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

View File

@ -11,7 +11,7 @@ on:
- packages/cherrypick-js/**
# for e2e
- packages/backend/**
- .github/workflows/test-frontend.yml
pull_request:
paths:
- packages/frontend/**
@ -19,7 +19,7 @@ on:
- packages/cherrypick-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.2
uses: actions/setup-node@v4.0.3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
@ -90,7 +90,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
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.2
uses: actions/setup-node@v4.0.3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'

View File

@ -7,10 +7,11 @@ 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
@ -26,7 +27,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
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
docker-compose.yml
!/.devcontainer/docker-compose.yml
.devcontainer/compose.yml
!/.devcontainer/compose.yml
# cherrypick
/build
@ -59,6 +59,7 @@ ormconfig.json
temp
/packages/frontend/src/**/*.stories.ts
tsdoc-metadata.json
misskey-assets
# blender backups
*.blend1

3
.gitmodules vendored
View File

@ -1,6 +1,3 @@
[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,3 +1,88 @@
## 2024.7.0
### Note
- デッキUIの新着ートをサウンドで通知する機能の追加v2024.5.0)に伴い、以前から動作しなくなっていたクライアント設定内の「アンテナ受信」「チャンネル通知」サウンドを削除しました。
- Streaming APIにて入力が不正な場合にはそのメッセージを無視するようになりました。 #14251
### 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
### Note

View File

@ -1,7 +1,7 @@
# Contribution guide
We're glad you're interested in contributing CherryPick! In this document you will find the information you need to contribute to the project.
> **Note**
> [!NOTE]
> This project uses Japanese as its major language, **but you do not need to translate and write the Issues/PRs in Japanese.**
> Also, you might receive comments on your Issue/PR in Japanese, but you do not need to reply to them in Japanese as well.\
> The accuracy of machine translation into Japanese is not high, so it will be easier for us to understand if you write it in the original language.
@ -17,16 +17,31 @@ Before creating an issue, please check the following:
- Issues should only be used to feature requests, suggestions, and bug tracking.
- Please ask questions or troubleshooting in [GitHub Discussions](https://github.com/kokonect-link/cherrypick/discussions) or [Discord](https://discord.gg/V8qghB28Aj).
> **Warning**
> [!WARNING]
> Do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged.
## Before implementation
### Recommended discussing before implementation
We welcome your proposal.
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 another member to assign you). By expressing your intention to work the Issue, you can prevent conflicts in the work.
Also, when you start implementation, assign yourself to the Issue (if you cannot do it yourself, ask Committer 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 Committers 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 Decision rights including whether the project will implement feature and how to implement, these rights are not always exercised.
## Well-known branches
- **`master`** branch is tracking the latest release and used for production purposes.
@ -37,14 +52,14 @@ Also, when you start implementation, assign yourself to the Issue (if you cannot
## Creating a PR
Thank you for your PR! Before creating a PR, please check the following:
- If possible, prefix the title with a keyword that identifies the type of this PR, as shown below.
- `fix` / `refactor` / `feat` / `enhance` / `perf` / `chore` etc
- Also, make sure that the granularity of this PR is appropriate. Please do not include more than one type of change or interest in a single PR.
- `fix` / `refactor` / `feat` / `enhance` / `perf` / `chore` etc
- Also, make sure that the granularity of this PR is appropriate. Please do not include more than one type of change or interest in a single PR.
- If there is an Issue which will be resolved by this PR, please include a reference to the Issue in the text.
- Please add the summary of the changes to [`CHANGELOG_CHERRYPICK.md`](/CHANGELOG_CHERRYPICK.md). However, this is not necessary for changes that do not affect the users, such as refactoring.
- Check if there are any documents that need to be created or updated due to this change.
- If you have added a feature or fixed a bug, please add a test case if possible.
- Please make sure that tests and Lint are passed in advance.
- You can run it with `pnpm test` and `pnpm lint`. [See more info](#testing)
- You can run it with `pnpm test` and `pnpm lint`. [See more info](#testing)
- If this PR includes UI changes, please attach a screenshot in the text.
Thanks for your cooperation 🤗
@ -54,8 +69,8 @@ Be willing to comment on the good points and not just the things you want fixed
### Review perspective
- Scope
- Are the goals of the PR clear?
- Is the granularity of the PR appropriate?
- Are the goals of the PR clear?
- Is the granularity of the PR appropriate?
- Security
- Does merging this PR create a vulnerability?
- Performance
@ -77,7 +92,7 @@ An actual domain will be assigned so you can test the federation.
## Release
### Release Instructions
1. Commit version changes in the `develop` branch ([package.json](https://github.com/kokonect-link/cherrypick/blob/develop/package.json))
1. Commit version changes in the `develop` branch ([package.json](package.json))
2. Create a release PR.
- Into `master` from `develop` branch.
- The title must be in the format `Release: x.y.z`.
@ -88,7 +103,7 @@ An actual domain will be assigned so you can test the federation.
- The target branch must be `master`
- The tag name must be the version
> **Note**
> [!NOTE]
> Why this instruction is necessary:
> - To perform final QA checks
> - To distribute responsibility
@ -106,12 +121,42 @@ If your language is not listed in Crowdin, please open an issue.
![Crowdin](https://d322cqt584bo4o.cloudfront.net/misskey/localized.svg)
## Development
During development, it is useful to use the
### Setup
Before developing, you have to set up environment. CherryPick 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
```
pnpm dev
```
command.
- Server-side source files and automatically builds them if they are modified. Automatically start the server process(es).
@ -135,26 +180,6 @@ CP_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).
@ -165,7 +190,7 @@ cp .github/cherrypick/test.yml .config/
```
Prepare DB/Redis for testing.
```
docker compose -f packages/backend/test/docker-compose.yml up
docker compose -f packages/backend/test/compose.yml up
```
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.
@ -204,7 +229,7 @@ niraxは、CherryPickで使用しているオリジナルのフロントエン
### ルート定義
ルート定義は、以下の形式のオブジェクトの配列です。
``` ts
```ts
{
name?: string;
path: string;
@ -217,7 +242,7 @@ niraxは、CherryPickで使用しているオリジナルのフロントエン
}
```
> **Warning**
> [!WARNING]
> 現状、ルートは定義された順に評価されます。
> たとえば、`/foo/:id`ルート定義の次に`/foo/bar`ルート定義がされていた場合、後者がマッチすることはありません。
@ -279,7 +304,7 @@ export const Default = {
parameters: {
layout: 'centered',
},
} satisfies StoryObj<typeof MkAvatar>;
} satisfies StoryObj<typeof MyComponent>;
```
If you want to opt-out from the automatic generation, create a `MyComponent.stories.impl.ts` file and add the following line to the file.
@ -390,7 +415,7 @@ describe('test', () => {
})
.useMocker(...
.compile();
fooService = app.get<FooService>(FooService);
barService = app.get<BarService>(BarService) as jest.Mocked<BarService>;
@ -514,13 +539,13 @@ pnpm dlx typeorm migration:generate -d ormconfig.js -o <migration name>
- 作成されたスクリプトは不必要な変更を含むため除去してください
### JSON SchemaのobjectでanyOfを使うとき
JSON Schemaで、objectに対してanyOfを使う場合、anyOfの中でpropertiesを定義しないこと。
バリデーションが効かないため。SchemaTypeもそのように作られており、objectのanyOf内のpropertiesは捨てられます
JSON Schemaで、objectに対してanyOfを使う場合、anyOfの中でpropertiesを定義しないこと。
バリデーションが効かないため。SchemaTypeもそのように作られており、objectのanyOf内のpropertiesは捨てられます
https://github.com/misskey-dev/misskey/pull/10082
テキストhogeおよびfugaについて、片方を必須としつつ両方の指定もありうる場合:
```
```ts
export const paramDef = {
type: 'object',
properties: {

View File

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

View File

@ -28,7 +28,7 @@
## Thanks
<a href="https://sentry.io/"><img src="https://github.com/misskey-dev/misskey/assets/4439005/98576556-222f-467a-94be-e98dbda1d852" height="30" alt="Sentry" /></a>
<a href="https://sentry.io/"><img src="https://github.com/kokonect-link/cherrypick/assets/4439005/98576556-222f-467a-94be-e98dbda1d852" height="30" alt="Sentry" /></a>
Thanks to [Sentry](https://sentry.io/) for providing the error tracking platform that helps us catch unexpected errors.

View File

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

View File

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

54
locales/index.d.ts vendored
View File

@ -1062,6 +1062,22 @@ export interface Locale extends ILocale {
*
*/
"showOnRemote": string;
/**
*
*/
"continueOnRemote": string;
/**
* Misskey Hubからサーバーを選択
*/
"chooseServerOnMisskeyHub": string;
/**
*
*/
"specifyServerHost": string;
/**
*
*/
"inputHostName": string;
/**
*
*/
@ -2287,9 +2303,13 @@ export interface Locale extends ILocale {
*/
"onlyOneFileCanBeAttached": string;
/**
*
*
*/
"signinRequired": string;
/**
* 使
*/
"signinOrContinueOnRemote": string;
/**
*
*/
@ -5439,6 +5459,18 @@ export interface Locale extends ILocale {
*
*/
"inquiry": string;
/**
*
*/
"tryAgain": string;
/**
*
*/
"confirmWhenRevealingSensitiveMedia": string;
/**
*
*/
"sensitiveMediaRevealConfirm": string;
/**
*
*/
@ -7454,6 +7486,10 @@ export interface Locale extends ILocale {
* NSFWを常に付与
*/
"alwaysMarkNsfw": string;
/**
*
*/
"canUpdateBioMedia": string;
/**
*
*/
@ -8745,14 +8781,6 @@ export interface Locale extends ILocale {
* ()
*/
"chatBg": string;
/**
*
*/
"antenna": string;
/**
*
*/
"channel": string;
/**
*
*/
@ -11015,7 +11043,7 @@ export interface Locale extends ILocale {
"_dataSaver": {
"_media": {
/**
*
*
*/
"title": string;
/**
@ -11025,7 +11053,7 @@ export interface Locale extends ILocale {
};
"_avatar": {
/**
*
*
*/
"title": string;
/**
@ -11035,7 +11063,7 @@ export interface Locale extends ILocale {
};
"_urlPreview": {
/**
* URLプレビューのサムネイル
* URLプレビューのサムネイルを非表示
*/
"title": string;
/**
@ -11045,7 +11073,7 @@ export interface Locale extends ILocale {
};
"_code": {
/**
*
*
*/
"title": string;
/**

View File

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

View File

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

View File

@ -1188,7 +1188,7 @@ externalServices: "他のサイトのサービス"
sourceCode: "ソースコード"
sourceCodeIsNotYetProvided: "ソースコードはまだ提供されてへんで。問題の修正について管理者に問い合わせてみ。"
repositoryUrl: "リポジトリURL"
repositoryUrlDescription: "ソースコードが公開されているリポジトリがある場合、そのURLを記入するで。Misskeyをそのまんま(ソースコードにいかなる変更も加えずに)使っとる場合は https://github.com/misskey-dev/misskey と記入するで。"
repositoryUrlDescription: "ソースコードが公開されているリポジトリがある場合、そのURLを記入するで。CherryPickをそのまんま(ソースコードにいかなる変更も加えずに)使っとる場合は https://github.com/kokonect-link/cherrypick と記入するで。"
repositoryUrlOrTarballRequired: "リポジトリを公開してへんなら、代わりにtarballを提供する必要があるで。詳細は.config/example.ymlを参照してな。"
feedback: "フィードバック"
feedbackUrl: "フィードバックURL"
@ -1820,7 +1820,7 @@ _aboutMisskey:
allContributors: "全ての貢献者"
source: "ソースコード"
original: "オリジナル"
thisIsModifiedVersion: "{name}はオリジナルのMisskeyをいじったバージョンをつこうてるで。"
thisIsModifiedVersion: "{name}はオリジナルのCherryPickをいじったバージョンをつこうてるで。"
translation: "Misskeyを翻訳"
donate: "Misskeyに寄付"
morePatrons: "他にもぎょうさんの人からサポートしてもろてんねん。ほんまおおきに🥰"

@ -1 +0,0 @@
Subproject commit cf3ce27b2eb8417233072e3d6d2fb7c5356c2364

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@ -4,7 +4,7 @@
"private": true,
"type": "module",
"engines": {
"node": "^20.10.0"
"node": "^20.10.0 || ^22.0.0"
},
"scripts": {
"start": "node ./built/boot/entry.js",
@ -66,46 +66,46 @@
"utf-8-validate": "6.0.3"
},
"dependencies": {
"@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",
"@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",
"@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.2.0",
"@fastify/static": "7.0.3",
"@fastify/multipart": "8.3.0",
"@fastify/static": "7.0.4",
"@fastify/view": "9.1.0",
"@google-cloud/logging": "^10.5.0",
"@google-cloud/translate": "^7.2.1",
"@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.1.0",
"@napi-rs/canvas": "^0.1.52",
"@nestjs/common": "10.3.8",
"@nestjs/core": "10.3.8",
"@nestjs/testing": "10.3.8",
"@napi-rs/canvas": "^0.1.53",
"@nestjs/common": "10.3.10",
"@nestjs/core": "10.3.10",
"@nestjs/testing": "10.3.10",
"@peertube/http-signature": "1.7.0",
"@sentry/node": "^8.5.0",
"@sentry/profiling-node": "^8.5.0",
"@sentry/node": "8.13.0",
"@sentry/profiling-node": "8.13.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.4.17",
"@swc/core": "1.6.6",
"@twemoji/parser": "15.1.1",
"@vitalets/google-translate-api": "9.2.0",
"accepts": "1.3.8",
"ajv": "8.13.0",
"ajv": "8.16.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.7.8",
"bullmq": "5.8.3",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.2",
"chalk": "5.3.0",
@ -118,27 +118,27 @@
"content-disposition": "0.5.4",
"date-fns": "2.30.0",
"deep-email-validator": "0.1.21",
"fastify": "4.26.2",
"fastify": "4.28.1",
"fastify-raw-body": "4.3.0",
"feed": "4.2.2",
"file-type": "19.0.0",
"fluent-ffmpeg": "2.1.2",
"fluent-ffmpeg": "2.1.3",
"form-data": "4.0.0",
"got": "14.2.1",
"got": "14.4.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": "3.1.0",
"ip-cidr": "4.0.1",
"ipaddr.js": "2.2.0",
"is-svg": "5.0.0",
"is-svg": "5.0.1",
"js-yaml": "4.1.0",
"jsdom": "24.0.0",
"jsdom": "24.1.0",
"json5": "2.2.3",
"jsonld": "8.3.2",
"jsrsasign": "11.1.0",
"meilisearch": "0.38.0",
"meilisearch": "0.41.0",
"microformats-parser": "2.0.2",
"mime-types": "2.1.35",
"misskey-reversi": "workspace:*",
@ -146,24 +146,24 @@
"nanoid": "5.0.7",
"nested-property": "4.0.0",
"node-fetch": "3.3.2",
"nodemailer": "6.9.13",
"nodemailer": "6.9.14",
"nsfwjs": "2.4.2",
"oauth": "0.10.0",
"oauth2orize": "1.12.0",
"oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14",
"otpauth": "9.2.3",
"otpauth": "9.3.1",
"parse5": "7.1.2",
"pg": "8.11.5",
"pg": "8.12.0",
"pkce-challenge": "4.1.0",
"probe-image-size": "7.2.3",
"promise-limit": "2.7.0",
"pug": "3.0.2",
"pug": "3.0.3",
"punycode": "2.3.1",
"qrcode": "1.5.3",
"random-seed": "0.3.0",
"ratelimiter": "3.4.1",
"re2": "1.20.10",
"re2": "1.21.3",
"redis-lock": "0.1.4",
"reflect-metadata": "0.2.2",
"rename": "1.0.4",
@ -171,28 +171,27 @@
"rxjs": "7.8.1",
"sanitize-html": "2.13.0",
"secure-json-parse": "2.7.0",
"sharp": "0.33.3",
"sharp": "0.33.4",
"slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"strip-ansi": "^7.1.0",
"systeminformation": "5.22.7",
"systeminformation": "5.22.11",
"tinycolor2": "1.6.0",
"tmp": "0.2.3",
"tsc-alias": "1.8.8",
"tsc-alias": "1.8.10",
"tsconfig-paths": "4.2.0",
"typeorm": "0.3.20",
"typescript": "5.4.5",
"typescript": "5.5.3",
"ulid": "2.3.0",
"vary": "1.1.2",
"web-push": "3.6.7",
"ws": "8.17.0",
"ws": "8.17.1",
"xev": "3.0.2"
},
"devDependencies": {
"@jest/globals": "29.7.0",
"@misskey-dev/eslint-plugin": "1.0.0",
"@nestjs/platform-express": "10.3.8",
"@nestjs/platform-express": "10.3.10",
"@simplewebauthn/types": "10.0.0",
"@swc/jest": "0.2.36",
"@types/accepts": "1.3.7",
@ -202,22 +201,21 @@
"@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.5",
"@types/htmlescape": "1.1.3",
"@types/http-link-header": "1.0.7",
"@types/jest": "29.5.12",
"@types/js-yaml": "4.0.9",
"@types/jsdom": "21.1.6",
"@types/jsonld": "1.5.13",
"@types/jsdom": "21.1.7",
"@types/jsonld": "1.5.14",
"@types/jsrsasign": "10.5.14",
"@types/mime-types": "2.1.4",
"@types/ms": "0.7.34",
"@types/node": "20.12.7",
"@types/node-fetch": "3.0.3",
"@types/node": "20.14.9",
"@types/nodemailer": "6.4.15",
"@types/oauth": "0.9.4",
"@types/oauth": "0.9.5",
"@types/oauth2orize": "1.11.5",
"@types/oauth2orize-pkce": "0.1.2",
"@types/pg": "8.11.5",
"@types/pg": "8.11.6",
"@types/pug": "2.0.10",
"@types/punycode": "2.1.4",
"@types/qrcode": "1.5.5",
@ -233,18 +231,17 @@
"@types/vary": "1.1.3",
"@types/web-push": "3.6.3",
"@types/ws": "8.5.10",
"@typescript-eslint/eslint-plugin": "7.7.1",
"@typescript-eslint/parser": "7.7.1",
"aws-sdk-client-mock": "3.0.1",
"@typescript-eslint/eslint-plugin": "7.15.0",
"@typescript-eslint/parser": "7.15.0",
"aws-sdk-client-mock": "4.0.1",
"cross-env": "7.0.3",
"eslint": "8.57.0",
"eslint-plugin-import": "2.29.1",
"execa": "8.0.1",
"fkill": "^9.0.0",
"execa": "9.2.0",
"fkill": "9.0.0",
"jest": "29.7.0",
"jest-mock": "29.7.0",
"nodemon": "3.1.0",
"nodemon": "3.1.4",
"pid-port": "1.0.0",
"simple-oauth2": "5.0.0"
"simple-oauth2": "5.0.1"
}
}

View File

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

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 };
};
@ -219,7 +219,7 @@ 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);
const url = tryCreateUrl(config.url ?? process.env.CHERRYPICK_URL ?? '');
const version = meta.version;
const basedMisskeyVersion = meta.basedMisskeyVersion;
const host = url.host;
@ -227,6 +227,10 @@ export function loadConfig(): Config {
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;
@ -250,7 +254,7 @@ export function loadConfig(): Config {
apiUrl: `${scheme}://${host}/api`,
authUrl: `${scheme}://${host}/auth`,
driveUrl: `${scheme}://${host}/files`,
db: config.db,
db: { ...config.db, db: dbDb, user: dbUser, pass: dbPass },
dbReplications: config.dbReplications,
dbSlaves: config.dbSlaves,
meilisearch: config.meilisearch,
@ -278,7 +282,7 @@ export function loadConfig(): Config {
deliverJobMaxAttempts: config.deliverJobMaxAttempts,
inboxJobMaxAttempts: config.inboxJobMaxAttempts,
proxyRemoteFiles: config.proxyRemoteFiles,
signToActivityPubGet: config.signToActivityPubGet,
signToActivityPubGet: config.signToActivityPubGet ?? true,
apFileBaseUrl: config.apFileBaseUrl,
mediaProxy: externalMediaProxy ?? internalMediaProxy,
externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy,

View File

@ -3,6 +3,7 @@
* 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

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

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

@ -12,6 +12,7 @@ import {
} 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';
@ -62,6 +63,7 @@ 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';
@ -210,6 +212,8 @@ 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 };
@ -362,6 +366,8 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv
UserKeypairService,
UserListService,
UserMutingService,
UserRenoteMutingService,
UserSearchService,
UserSuspendService,
UserAuthService,
VideoProcessingService,
@ -510,6 +516,8 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv
$UserKeypairService,
$UserListService,
$UserMutingService,
$UserRenoteMutingService,
$UserSearchService,
$UserSuspendService,
$UserAuthService,
$VideoProcessingService,
@ -659,6 +667,8 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv
UserKeypairService,
UserListService,
UserMutingService,
UserRenoteMutingService,
UserSearchService,
UserSuspendService,
UserAuthService,
VideoProcessingService,
@ -806,6 +816,8 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv
$UserKeypairService,
$UserListService,
$UserMutingService,
$UserRenoteMutingService,
$UserSearchService,
$UserSuspendService,
$UserAuthService,
$VideoProcessingService,

View File

@ -56,9 +56,6 @@ 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([]);
@ -68,12 +65,11 @@ 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)));
const redisResultIds = Array.from(new Set(redisResult.flat(1))).sort(idCompare);
redisResultIds.sort(idCompare);
noteIds = redisResultIds.slice(0, ps.limit);
shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
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;
if (!shouldFallbackToDb) {
let filter = ps.noteFilter ?? (_note => true);

View File

@ -40,6 +40,7 @@ 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

@ -240,6 +240,10 @@ 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']; };
@ -278,27 +282,29 @@ 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: EventUnionFromDictionary<SerializedAll<InternalEventTypes>>;
payload: EventTypesToEventPayload<InternalEventTypes>;
};
broadcast: {
name: 'broadcast';
payload: EventUnionFromDictionary<SerializedAll<BroadcastTypes>>;
payload: EventTypesToEventPayload<BroadcastTypes>;
};
main: {
name: `mainStream:${MiUser['id']}`;
payload: EventUnionFromDictionary<SerializedAll<MainEventTypes>>;
payload: EventTypesToEventPayload<MainEventTypes>;
};
drive: {
name: `driveStream:${MiUser['id']}`;
payload: EventUnionFromDictionary<SerializedAll<DriveEventTypes>>;
payload: EventTypesToEventPayload<DriveEventTypes>;
};
note: {
name: `noteStream:${MiNote['id']}`;
payload: EventUnionFromDictionary<SerializedAll<NoteStreamEventTypes>>;
payload: EventTypesToEventPayload<NoteStreamEventTypes>;
};
channel: {
name: `channelStream:${MiChannel['id']}`;
@ -306,7 +312,7 @@ export type GlobalEvents = {
};
userList: {
name: `userListStream:${MiUserList['id']}`;
payload: EventUnionFromDictionary<SerializedAll<UserListEventTypes>>;
payload: EventTypesToEventPayload<UserListEventTypes>;
};
messaging: {
name: `messagingStream:${MiUser['id']}-${MiUser['id']}`;
@ -322,15 +328,15 @@ export type GlobalEvents = {
};
roleTimeline: {
name: `roleTimelineStream:${MiRole['id']}`;
payload: EventUnionFromDictionary<SerializedAll<RoleTimelineEventTypes>>;
payload: EventTypesToEventPayload<RoleTimelineEventTypes>;
};
antenna: {
name: `antennaStream:${MiAntenna['id']}`;
payload: EventUnionFromDictionary<SerializedAll<AntennaEventTypes>>;
payload: EventTypesToEventPayload<AntennaEventTypes>;
};
admin: {
name: `adminStream:${MiUser['id']}`;
payload: EventUnionFromDictionary<SerializedAll<AdminEventTypes>>;
payload: EventTypesToEventPayload<AdminEventTypes>;
};
notes: {
name: 'notesStream';
@ -338,11 +344,11 @@ export type GlobalEvents = {
};
reversi: {
name: `reversiStream:${MiUser['id']}`;
payload: EventUnionFromDictionary<SerializedAll<ReversiEventTypes>>;
payload: EventTypesToEventPayload<ReversiEventTypes>;
};
reversiGame: {
name: `reversiGameStream:${MiReversiGame['id']}`;
payload: EventUnionFromDictionary<SerializedAll<ReversiGameEventTypes>>;
payload: EventTypesToEventPayload<ReversiGameEventTypes>;
};
};

View File

@ -13,10 +13,12 @@ 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 * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js';
import type { DefaultTreeAdapterMap } from 'parse5';
import type * as mfm from 'cherrypick-mfm-js';
const treeAdapter = TreeAdapter.defaultTreeAdapter;
const treeAdapter = parse5.defaultTreeAdapter;
type Node = DefaultTreeAdapterMap['node'];
type ChildNode = DefaultTreeAdapterMap['childNode'];
const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
@ -46,7 +48,7 @@ export class MfmService {
return text.trim();
function getText(node: TreeAdapter.Node): string {
function getText(node: Node): string {
if (treeAdapter.isTextNode(node)) return node.value;
if (!treeAdapter.isElementNode(node)) return '';
if (node.nodeName === 'br') return '\n';
@ -58,7 +60,7 @@ export class MfmService {
return '';
}
function appendChildren(childNodes: TreeAdapter.ChildNode[]): void {
function appendChildren(childNodes: ChildNode[]): void {
if (childNodes) {
for (const n of childNodes) {
analyze(n);
@ -66,14 +68,16 @@ export class MfmService {
}
}
function analyze(node: TreeAdapter.Node) {
function analyze(node: 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': {
@ -81,8 +85,7 @@ 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');
@ -90,7 +93,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('@');
@ -102,7 +105,7 @@ export class MfmService {
} else if (part.length === 3) {
text += txt;
}
// その他
// その他
} else {
const generateLink = () => {
if (!href && !txt) {
@ -130,8 +133,7 @@ export class MfmService {
break;
}
case 'h1':
{
case 'h1': {
text += '【';
appendChildren(node.childNodes);
text += '】\n';
@ -139,16 +141,14 @@ 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,8 +156,7 @@ export class MfmService {
}
case 's':
case 'del':
{
case 'del': {
text += '~~';
appendChildren(node.childNodes);
text += '~~';
@ -165,8 +164,7 @@ export class MfmService {
}
case 'i':
case 'em':
{
case 'em': {
text += '<i>';
appendChildren(node.childNodes);
text += '</i>';
@ -207,8 +205,7 @@ export class MfmService {
case 'h3':
case 'h4':
case 'h5':
case 'h6':
{
case 'h6': {
text += '\n\n';
appendChildren(node.childNodes);
break;
@ -221,8 +218,7 @@ export class MfmService {
case 'article':
case 'li':
case 'dt':
case 'dd':
{
case 'dd': {
text += '\n';
appendChildren(node.childNodes);
break;

View File

@ -61,7 +61,6 @@ 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';
@ -864,7 +863,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(isNotNull);
))).filter(x => x != null);
// Drop duplicate users
mentionedUsers = mentionedUsers.filter((u, i, self) =>
@ -959,10 +958,13 @@ export class NoteCreateService implements OnApplicationShutdown {
}
}
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);
// 自分自身の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);
}
}
}

View File

@ -29,6 +29,7 @@ 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;
@ -117,11 +118,16 @@ 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) {
} else if (_reaction != null) {
const custom = reaction.match(isCustomEmojiRegexp);
if (custom) {
const reacterHost = this.utilityService.toPunyNullable(user.host);

View File

@ -48,6 +48,7 @@ export type RolePolicies = {
canHideAds: boolean;
driveCapacityMb: number;
alwaysMarkNsfw: boolean;
canUpdateBioMedia: boolean;
pinLimit: number;
antennaLimit: number;
wordMuteLimit: number;
@ -77,6 +78,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
canHideAds: false,
driveCapacityMb: 100,
alwaysMarkNsfw: false,
canUpdateBioMedia: true,
pinLimit: 5,
antennaLimit: 5,
wordMuteLimit: 200,
@ -379,6 +381,7 @@ 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)),
@ -505,14 +508,15 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
this.globalEventService.publishInternalEvent('userRoleAssigned', created);
if (role.isPublic) {
const user = await this.usersRepository.findOneByOrFail({ id: userId });
if (role.isPublic && user.host === null) {
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

@ -279,8 +279,10 @@ export class UserFollowingService implements OnModuleInit {
});
// 通知を作成
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
}, followee.id);
if (follower.host === null) {
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
}, followee.id);
}
}
if (alreadyFollowed) return;

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

@ -0,0 +1,52 @@
/*
* 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

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

View File

@ -8,7 +8,6 @@ import promiseLimit from 'promise-limit';
import type { MiRemoteUser, MiUser } from '@/models/User.js';
import { 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';
@ -41,7 +40,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(isNotNull);
)).filter(x => x != null);
if (toGroups.public.length > 0) {
return {

View File

@ -29,7 +29,6 @@ import { MessagingService } from '@/core/MessagingService.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, MessagingMessagesRepository, 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, isRead, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
@ -588,7 +587,7 @@ export class ApInboxService {
const userIds = uris
.filter(uri => uri.startsWith(this.config.url + '/users/'))
.map(uri => uri.split('/').at(-1))
.filter(isNotNull);
.filter(x => x != null);
const users = await this.usersRepository.findBy({
id: In(userIds),
});

View File

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

View File

@ -27,7 +27,6 @@ import type { MiUserKeypair } from '@/models/UserKeypair.js';
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository, EventsRepository } 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';
@ -322,7 +321,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(isNotNull);
return ids.map(id => items.find(item => item.id === id)).filter(x => x != null);
};
let inReplyTo;
@ -719,7 +718,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(isNotNull);
const emojis = names.map(name => allEmojis.get(name)).filter(x => x != null);
return emojis;
}

View File

@ -8,7 +8,6 @@ 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';
@ -28,7 +27,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(isNotNull);
)).filter(x => x != null);
return mentionedUsers;
}

View File

@ -26,7 +26,6 @@ import { bindThis } from '@/decorators.js';
import { checkHttps } from '@/misc/check-https.js';
import { NoteUpdateService } from '@/core/NoteUpdateService.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';
@ -278,7 +277,7 @@ export class ApNoteService {
}
};
const uris = unique([note._misskey_quote, note.quoteUrl].filter(isNotNull));
const uris = unique([note._misskey_quote, note.quoteUrl].filter(x => x != null));
const results = await Promise.all(uris.map(tryResolveNote));
quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);

View File

@ -34,12 +34,12 @@ 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 { AvatarDecorationService } from '@/core/AvatarDecorationService.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';
@ -103,6 +103,8 @@ export class ApPersonService implements OnModuleInit {
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
private roleService: RoleService,
private avatarDecorationService: AvatarDecorationService,
) {
}
@ -257,6 +259,11 @@ 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
@ -797,7 +804,7 @@ export class ApPersonService implements OnModuleInit {
// とりあえずidを別の時間で生成して順番を維持
let td = 0;
for (const note of featuredNotes.filter(isNotNull)) {
for (const note of featuredNotes.filter(x => x != null)) {
td -= 1000;
transactionalEntityManager.insert(MiUserNotePining, {
id: this.idService.gen(Date.now() + td),

View File

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

View File

@ -4,7 +4,6 @@
*/
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';
@ -16,7 +15,7 @@ export function extractApHashtags(tags: IObject | IObject[] | null | undefined):
return hashtags.map(tag => {
const m = tag.name.match(/^#(.+)/);
return m ? m[1] : null;
}).filter(isNotNull);
}).filter(x => x != null);
}
export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] {

View File

@ -11,7 +11,6 @@ import { bindThis } from '@/decorators.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { Packed } from '@/misc/json-schema.js';
import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
import { isNotNull } from '@/misc/is-not-null.js';
@Injectable()
export class AbuseReportNotificationRecipientEntityService {
@ -66,13 +65,13 @@ export class AbuseReportNotificationRecipientEntityService {
);
}
const userIds = objs.map(it => it.userId).filter(isNotNull);
const userIds = objs.map(it => it.userId).filter(x => x != null);
const users: Map<string, Packed<'UserLite'>> = (userIds.length > 0)
? 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(isNotNull);
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])))

View File

@ -10,7 +10,6 @@ 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';
@ -63,7 +62,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(isNotNull);
const _assignees = reports.map(({ assignee, assigneeId }) => assignee ?? assigneeId).filter(x => x != null);
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 ? await this.clipNotesRepository.countBy({ clipId: clip.id }) : undefined,
notesCount: (meId === clip.userId) ? await this.clipNotesRepository.countBy({ clipId: clip.id }) : undefined,
});
}

View File

@ -16,7 +16,6 @@ import { appendQuery, query } from '@/misc/prelude/url.js';
import { deepClone } from '@/misc/clone.js';
import { 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';
@ -271,11 +270,11 @@ export class DriveFileEntityService {
files: MiDriveFile[],
options?: PackOptions,
): Promise<Packed<'DriveFile'>[]> {
const _user = files.map(({ user, userId }) => user ?? userId).filter(isNotNull);
const _user = files.map(({ user, userId }) => user ?? userId).filter(x => x != null);
const _userMap = await this.userEntityService.packMany(_user)
.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(isNotNull);
return items.filter(x => x != null);
}
@bindThis
@ -300,6 +299,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(isNotNull);
return fileIds.map(id => filesMap.get(id)).filter(x => x != null);
}
}

View File

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

View File

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

View File

@ -14,7 +14,6 @@ import type { MiNote } from '@/models/Note.js';
import type { MiNoteReaction } from '@/models/NoteReaction.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, EventsRepository } 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';
@ -290,7 +289,7 @@ export class NoteEntityService implements OnModuleInit {
packedFiles.set(k, v);
}
}
return fileIds.map(id => packedFiles.get(id)).filter(isNotNull);
return fileIds.map(id => packedFiles.get(id)).filter(x => x != null);
}
@bindThis
@ -469,12 +468,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(isNotNull);
const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(x => x != null);
const packedFiles = fileIds.length > 0 ? await this.driveFileEntityService.packManyByIdsMap(fileIds) : new Map();
const users = [
...notes.map(({ user, userId }) => user ?? userId),
...notes.map(({ replyUserId }) => replyUserId).filter(isNotNull),
...notes.map(({ renoteUserId }) => renoteUserId).filter(isNotNull),
...notes.map(({ replyUserId }) => replyUserId).filter(x => x != null),
...notes.map(({ renoteUserId }) => renoteUserId).filter(x => x != null),
];
const packedUsers = await this.userEntityService.packMany(users, me)
.then(users => new Map(users.map(u => [u.id, u])));

View File

@ -13,7 +13,6 @@ import type { MiGroupedNotification, MiNotification } from '@/models/Notificatio
import type { MiNote } from '@/models/Note.js';
import type { 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';
@ -110,7 +109,7 @@ export class NotificationEntityService implements OnModuleInit {
user,
reaction: reaction.reaction,
};
}))).filter(r => isNotNull(r.user));
}))).filter(r => r.user != null);
// if all users have been deleted, don't show this notification
if (reactions.length === 0) {
return null;
@ -131,7 +130,7 @@ export class NotificationEntityService implements OnModuleInit {
}
return this.userEntityService.pack(userId, { id: meId });
}))).filter(isNotNull);
}))).filter(x => x != null);
// if all users have been deleted, don't show this notification
if (users.length === 0) {
return null;
@ -191,7 +190,7 @@ export class NotificationEntityService implements OnModuleInit {
validNotifications = await this.#filterValidNotifier(validNotifications, meId);
const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(isNotNull);
const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(x => x != null);
const notes = noteIds.length > 0 ? await this.notesRepository.find({
where: { id: In(noteIds) },
relations: ['user', 'reply', 'reply.user', 'renote', 'renote.user'],
@ -233,7 +232,7 @@ export class NotificationEntityService implements OnModuleInit {
);
});
return (await Promise.all(packPromises)).filter(isNotNull);
return (await Promise.all(packPromises)).filter(x => x != null);
}
@bindThis
@ -315,7 +314,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(isNotNull);
const notifierIds = notifications.map(notification => 'notifierId' in notification ? notification.notifierId : null).filter(x => x != null);
const notifiers = notifierIds.length > 0 ? await this.usersRepository.find({
where: { id: In(notifierIds) },
}) : [];
@ -323,7 +322,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(isNotNull);
}))) as [T | null] ).filter(x => x != null);
return filteredNotifications;
}

View File

@ -14,7 +14,6 @@ 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';
@ -106,7 +105,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(isNotNull)),
attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter(x => x != null)),
likedCount: page.likedCount,
isLiked: meId ? await this.pageLikesRepository.exists({ where: { pageId: page.id, userId: meId } }) : undefined,
});

View File

@ -49,7 +49,6 @@ 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';
@ -542,11 +541,15 @@ 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.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
.filter((r) => r.isPublic || iAmModerator)
.sort((a, b) => b.displayOrder - a.displayOrder)
.map((r) => ({
name: r.name,
iconUrl: r.iconUrl,
displayOrder: r.displayOrder,
}))
) : undefined,
...(isDetailed ? {
url: profile!.url,
@ -554,7 +557,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(isNotNull))
.then(xs => xs.length === 0 ? null : xs.filter(x => x != null))
: null,
createdAt: this.idService.parse(user.id).date.toISOString(),
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,

View File

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

View File

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

View File

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export type JsonValue = JsonArray | JsonObject | string | number | boolean | null;
export type JsonObject = {[K in string]?: JsonValue};
export type JsonArray = JsonValue[];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { PollVotesRepository, NotesRepository } from '@/models/_.js';
import type Logger from '@/logger.js';
import { CacheService } from '@/core/CacheService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
@ -24,6 +25,7 @@ export class EndedPollNotificationProcessorService {
@Inject(DI.pollVotesRepository)
private pollVotesRepository: PollVotesRepository,
private cacheService: CacheService,
private notificationService: NotificationService,
private queueLoggerService: QueueLoggerService,
) {
@ -47,9 +49,12 @@ export class EndedPollNotificationProcessorService {
const userIds = [...new Set([note.userId, ...votes.map(v => v.userId)])];
for (const userId of userIds) {
this.notificationService.createNotification(userId, 'pollEnded', {
noteId: note.id,
});
const profile = await this.cacheService.userProfileCache.fetch(userId);
if (profile.userHost === null) {
this.notificationService.createNotification(userId, 'pollEnded', {
noteId: note.id,
});
}
}
}
}

View File

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

View File

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

View File

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

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