Merge branch 'develop' into mahjong
This commit is contained in:
commit
f32b11ba12
|
@ -1,5 +1,11 @@
|
|||
# misskey settings
|
||||
# MISSKEY_URL=https://example.tld/
|
||||
|
||||
# db settings
|
||||
POSTGRES_PASSWORD=example-misskey-pass
|
||||
# DATABASE_PASSWORD=${POSTGRES_PASSWORD}
|
||||
POSTGRES_USER=example-misskey-user
|
||||
# DATABASE_USER=${POSTGRES_USER}
|
||||
POSTGRES_DB=misskey
|
||||
# DATABASE_DB=${POSTGRES_DB}
|
||||
DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}"
|
||||
|
|
|
@ -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: misskey
|
||||
|
||||
# Auth
|
||||
# You can set user and pass from environment variables instead.
|
||||
user: example-misskey-user
|
||||
pass: example-misskey-pass
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@ node_modules/
|
|||
packages/*/node_modules
|
||||
redis/
|
||||
files/
|
||||
misskey-assets/
|
||||
fluent-emojis/
|
||||
.pnp.*
|
||||
|
||||
|
|
|
@ -53,8 +53,8 @@ body:
|
|||
Examples:
|
||||
* Model and OS of the device(s): MacBook Pro (14inch, 2021), macOS Ventura 13.4
|
||||
* Browser: Chrome 113.0.5672.126
|
||||
* Server URL: misskey.io
|
||||
* Misskey: 13.x.x
|
||||
* Server URL: misskey.example.com
|
||||
* 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, "Misskey install shell script", development environment
|
||||
* Misskey: 13.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:
|
||||
* Misskey:
|
||||
|
|
|
@ -4,10 +4,11 @@ on:
|
|||
push:
|
||||
paths:
|
||||
- packages/misskey-js/**
|
||||
- .github/workflows/api-misskey-js.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- packages/misskey-js/**
|
||||
|
||||
- .github/workflows/api-misskey-js.yml
|
||||
jobs:
|
||||
report:
|
||||
|
||||
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -6,12 +6,13 @@ on:
|
|||
paths:
|
||||
- packages/misskey-js/package.json
|
||||
- package.json
|
||||
- .github/workflows/check-misskey-js-version.yml
|
||||
pull_request:
|
||||
branches: [ develop ]
|
||||
paths:
|
||||
- packages/misskey-js/package.json
|
||||
- package.json
|
||||
|
||||
- .github/workflows/check-misskey-js-version.yml
|
||||
jobs:
|
||||
check-version:
|
||||
# ルートの package.json と packages/misskey-js/package.json のバージョンが一致しているかを確認する
|
||||
|
|
|
@ -9,7 +9,7 @@ on:
|
|||
paths:
|
||||
- packages/backend/**
|
||||
- .github/workflows/get-api-diff.yml
|
||||
|
||||
- .github/workflows/get-api-diff.yml
|
||||
jobs:
|
||||
get-from-misskey:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -34,7 +34,7 @@ jobs:
|
|||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.0.2
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
|
|
@ -11,6 +11,7 @@ on:
|
|||
- packages/sw/**
|
||||
- packages/misskey-js/**
|
||||
- packages/shared/eslint.config.js
|
||||
- .github/workflows/lint.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- packages/backend/**
|
||||
|
@ -18,7 +19,7 @@ on:
|
|||
- packages/sw/**
|
||||
- packages/misskey-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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -9,12 +9,13 @@ on:
|
|||
- packages/backend/**
|
||||
# for permissions
|
||||
- packages/misskey-js/**
|
||||
- .github/workflows/test-backend.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- packages/backend/**
|
||||
# for permissions
|
||||
- packages/misskey-js/**
|
||||
|
||||
- .github/workflows/test-backend.yml
|
||||
jobs:
|
||||
unit:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -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'
|
||||
|
|
|
@ -11,7 +11,7 @@ on:
|
|||
- packages/misskey-js/**
|
||||
# for e2e
|
||||
- packages/backend/**
|
||||
|
||||
- .github/workflows/test-frontend.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- packages/frontend/**
|
||||
|
@ -19,7 +19,7 @@ on:
|
|||
- packages/misskey-js/**
|
||||
# for e2e
|
||||
- packages/backend/**
|
||||
|
||||
- .github/workflows/test-frontend.yml
|
||||
jobs:
|
||||
vitest:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -35,7 +35,7 @@ jobs:
|
|||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.0.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'
|
||||
|
|
|
@ -8,11 +8,12 @@ on:
|
|||
branches: [ develop ]
|
||||
paths:
|
||||
- packages/misskey-js/**
|
||||
- .github/workflows/test-misskey-js.yml
|
||||
pull_request:
|
||||
branches: [ develop ]
|
||||
paths:
|
||||
- packages/misskey-js/**
|
||||
|
||||
- .github/workflows/test-misskey-js.yml
|
||||
jobs:
|
||||
test:
|
||||
|
||||
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -59,6 +59,7 @@ ormconfig.json
|
|||
temp
|
||||
/packages/frontend/src/**/*.stories.ts
|
||||
tsdoc-metadata.json
|
||||
misskey-assets
|
||||
|
||||
# blender backups
|
||||
*.blend1
|
||||
|
|
|
@ -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
|
||||
|
|
56
CHANGELOG.md
56
CHANGELOG.md
|
@ -1,17 +1,50 @@
|
|||
## Unreleased
|
||||
## 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`ページにおいて絵文字ピッカーを開くことができない問題を修正
|
||||
- Fix: deck uiの通知音が重なる問題 (#14029)
|
||||
- Fix: ダイレクト投稿の"削除して編集"において、宛先が保持されていなかった問題を修正
|
||||
- Fix: 投稿フォームへのURL貼り付けによる引用が下書きに保存されていなかった問題を修正
|
||||
- Fix: "削除して編集"や下書きにおいて、リアクションの受け入れ設定が保持/保存されていなかった問題を修正
|
||||
|
||||
### Server
|
||||
- Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)
|
||||
|
@ -21,7 +54,8 @@
|
|||
- Enhance: エンドポイント`gallery/posts/update`の必須項目を`postId`のみに
|
||||
- Enhance: エンドポイント`i/webhook/update`の必須項目を`webhookId`のみに
|
||||
- Enhance: エンドポイント`admin/ad/update`の必須項目を`id`のみに
|
||||
- Fix: チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正
|
||||
- Enhance: `default.yml`内の`url`, `db.db`, `db.user`, `db.pass`を環境変数から読み込めるように
|
||||
- Fix: チャート生成時にinstance.suspensionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正
|
||||
- Fix: ユーザーのフィードページのMFMをHTMLに展開するように (#14006)
|
||||
- Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036)
|
||||
- Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059)
|
||||
|
@ -31,9 +65,27 @@
|
|||
- 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
|
||||
|
||||
|
|
107
CONTRIBUTING.md
107
CONTRIBUTING.md
|
@ -1,7 +1,7 @@
|
|||
# Contribution guide
|
||||
We're glad you're interested in contributing Misskey! 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/misskey-dev/misskey/discussions) or [Discord](https://discord.gg/Wp8gVStHW3).
|
||||
|
||||
> **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.md`](/CHANGELOG.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/misskey-dev/misskey/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. Misskey requires Redis, PostgreSQL, and FFmpeg.
|
||||
|
||||
You would want to install Meilisearch to experiment related features. Technically, meilisearch is not strict requirement, but some features and tests require it.
|
||||
|
||||
There are a few ways to proceed.
|
||||
|
||||
#### Use system-wide software
|
||||
You could install them in system-wide (such as from package manager).
|
||||
|
||||
#### Use `docker compose`
|
||||
You could obtain middleware container by typing `docker compose -f $PROJECT_ROOT/compose.local-db.yml up -d`.
|
||||
|
||||
#### Use Devcontainer
|
||||
Devcontainer also has necessary setting. This method can be done by connecting from VSCode.
|
||||
|
||||
Instead of running `pnpm` locally, you can use Dev Container to set up your development environment.
|
||||
To use Dev Container, open the project directory on VSCode with Dev Containers installed.
|
||||
**Note:** If you are using Windows, please clone the repository with WSL. Using Git for Windows will result in broken files due to the difference in how newlines are handled.
|
||||
|
||||
It will run the following command automatically inside the container.
|
||||
``` bash
|
||||
git submodule update --init
|
||||
pnpm install --frozen-lockfile
|
||||
cp .devcontainer/devcontainer.yml .config/default.yml
|
||||
pnpm build
|
||||
pnpm migrate
|
||||
```
|
||||
|
||||
After finishing the migration, you can proceed.
|
||||
|
||||
### Start developing
|
||||
During development, it is useful to use the
|
||||
```
|
||||
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 @@ MK_DEV_PREFER=backend pnpm dev
|
|||
- To change the port of Vite, specify with `VITE_PORT` environment variable.
|
||||
- HMR may not work in some environments such as Windows.
|
||||
|
||||
### Dev Container
|
||||
Instead of running `pnpm` locally, you can use Dev Container to set up your development environment.
|
||||
To use Dev Container, open the project directory on VSCode with Dev Containers installed.
|
||||
**Note:** If you are using Windows, please clone the repository with WSL. Using Git for Windows will result in broken files due to the difference in how newlines are handled.
|
||||
|
||||
It will run the following command automatically inside the container.
|
||||
``` bash
|
||||
git submodule update --init
|
||||
pnpm install --frozen-lockfile
|
||||
cp .devcontainer/devcontainer.yml .config/default.yml
|
||||
pnpm build
|
||||
pnpm migrate
|
||||
```
|
||||
|
||||
After finishing the migration, run the `pnpm dev` command to start the development server.
|
||||
|
||||
``` bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Testing
|
||||
- Test codes are located in [`/packages/backend/test`](/packages/backend/test).
|
||||
|
||||
|
@ -165,7 +190,7 @@ cp .github/misskey/test.yml .config/
|
|||
```
|
||||
Prepare DB/Redis for testing.
|
||||
```
|
||||
docker compose -f packages/backend/test/compose.yaml 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は、Misskeyで使用しているオリジナルのフロントエンド
|
|||
### ルート定義
|
||||
ルート定義は、以下の形式のオブジェクトの配列です。
|
||||
|
||||
``` ts
|
||||
```ts
|
||||
{
|
||||
name?: string;
|
||||
path: string;
|
||||
|
@ -217,7 +242,7 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド
|
|||
}
|
||||
```
|
||||
|
||||
> **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>;
|
||||
|
||||
|
@ -511,13 +536,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: {
|
||||
|
|
|
@ -17,6 +17,8 @@ services:
|
|||
networks:
|
||||
- internal_network
|
||||
- external_network
|
||||
# env_file:
|
||||
# - .config/docker.env
|
||||
volumes:
|
||||
- ./files:/misskey/files
|
||||
- ./.config:/misskey/.config:ro
|
||||
|
|
|
@ -736,6 +736,22 @@ export interface Locale extends ILocale {
|
|||
* リモートで表示
|
||||
*/
|
||||
"showOnRemote": string;
|
||||
/**
|
||||
* リモートで続行
|
||||
*/
|
||||
"continueOnRemote": string;
|
||||
/**
|
||||
* Misskey Hubからサーバーを選択
|
||||
*/
|
||||
"chooseServerOnMisskeyHub": string;
|
||||
/**
|
||||
* サーバーのドメインを直接指定
|
||||
*/
|
||||
"specifyServerHost": string;
|
||||
/**
|
||||
* ドメインを入力してください
|
||||
*/
|
||||
"inputHostName": string;
|
||||
/**
|
||||
* 全般
|
||||
*/
|
||||
|
@ -1921,9 +1937,13 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"onlyOneFileCanBeAttached": string;
|
||||
/**
|
||||
* 続行する前に、サインアップまたはサインインが必要です
|
||||
* 続行する前に、登録またはログインが必要です
|
||||
*/
|
||||
"signinRequired": string;
|
||||
/**
|
||||
* 続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります
|
||||
*/
|
||||
"signinOrContinueOnRemote": string;
|
||||
/**
|
||||
* 招待
|
||||
*/
|
||||
|
@ -4984,6 +5004,18 @@ export interface Locale extends ILocale {
|
|||
* お問い合わせ
|
||||
*/
|
||||
"inquiry": string;
|
||||
/**
|
||||
* もう一度お試しください。
|
||||
*/
|
||||
"tryAgain": string;
|
||||
/**
|
||||
* センシティブなメディアを表示するとき確認する
|
||||
*/
|
||||
"confirmWhenRevealingSensitiveMedia": string;
|
||||
/**
|
||||
* センシティブなメディアです。表示しますか?
|
||||
*/
|
||||
"sensitiveMediaRevealConfirm": string;
|
||||
"_delivery": {
|
||||
/**
|
||||
* 配信状態
|
||||
|
@ -6594,6 +6626,10 @@ export interface Locale extends ILocale {
|
|||
* ファイルにNSFWを常に付与
|
||||
*/
|
||||
"alwaysMarkNsfw": string;
|
||||
/**
|
||||
* アイコンとバナーの更新を許可
|
||||
*/
|
||||
"canUpdateBioMedia": string;
|
||||
/**
|
||||
* ノートのピン留めの最大数
|
||||
*/
|
||||
|
@ -7515,14 +7551,6 @@ export interface Locale extends ILocale {
|
|||
* 通知
|
||||
*/
|
||||
"notification": string;
|
||||
/**
|
||||
* アンテナ受信
|
||||
*/
|
||||
"antenna": string;
|
||||
/**
|
||||
* チャンネル通知
|
||||
*/
|
||||
"channel": string;
|
||||
/**
|
||||
* リアクション選択時
|
||||
*/
|
||||
|
|
|
@ -180,6 +180,10 @@ addAccount: "アカウントを追加"
|
|||
reloadAccountsList: "アカウントリストの情報を更新"
|
||||
loginFailed: "ログインに失敗しました"
|
||||
showOnRemote: "リモートで表示"
|
||||
continueOnRemote: "リモートで続行"
|
||||
chooseServerOnMisskeyHub: "Misskey Hubからサーバーを選択"
|
||||
specifyServerHost: "サーバーのドメインを直接指定"
|
||||
inputHostName: "ドメインを入力してください"
|
||||
general: "全般"
|
||||
wallpaper: "壁紙"
|
||||
setWallpaper: "壁紙を設定"
|
||||
|
@ -476,7 +480,8 @@ attachAsFileQuestion: "クリップボードのテキストが長いです。テ
|
|||
noMessagesYet: "まだチャットはありません"
|
||||
newMessageExists: "新しいメッセージがあります"
|
||||
onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです"
|
||||
signinRequired: "続行する前に、サインアップまたはサインインが必要です"
|
||||
signinRequired: "続行する前に、登録またはログインが必要です"
|
||||
signinOrContinueOnRemote: "続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります"
|
||||
invitations: "招待"
|
||||
invitationCode: "招待コード"
|
||||
checking: "確認しています"
|
||||
|
@ -1242,6 +1247,9 @@ keepOriginalFilenameDescription: "この設定をオフにすると、アップ
|
|||
noDescription: "説明文はありません"
|
||||
alwaysConfirmFollow: "フォローの際常に確認する"
|
||||
inquiry: "お問い合わせ"
|
||||
tryAgain: "もう一度お試しください。"
|
||||
confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示するとき確認する"
|
||||
sensitiveMediaRevealConfirm: "センシティブなメディアです。表示しますか?"
|
||||
|
||||
_delivery:
|
||||
status: "配信状態"
|
||||
|
@ -1705,6 +1713,7 @@ _role:
|
|||
canManageAvatarDecorations: "アバターデコレーションの管理"
|
||||
driveCapacity: "ドライブ容量"
|
||||
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
|
||||
canUpdateBioMedia: "アイコンとバナーの更新を許可"
|
||||
pinMax: "ノートのピン留めの最大数"
|
||||
antennaMax: "アンテナの作成可能数"
|
||||
wordMuteMax: "ワードミュートの最大文字数"
|
||||
|
@ -1971,8 +1980,6 @@ _sfx:
|
|||
note: "ノート"
|
||||
noteMy: "ノート(自分)"
|
||||
notification: "通知"
|
||||
antenna: "アンテナ受信"
|
||||
channel: "チャンネル通知"
|
||||
reaction: "リアクション選択時"
|
||||
|
||||
_soundSettings:
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 0179793ec891856d6f37a3be16ba4c22f67a81b5
|
30
package.json
30
package.json
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "2024.5.0",
|
||||
"version": "2024.7.0-beta.3",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/misskey-dev/misskey.git"
|
||||
},
|
||||
"packageManager": "pnpm@9.0.6",
|
||||
"packageManager": "pnpm@9.6.0",
|
||||
"workspaces": [
|
||||
"packages/frontend",
|
||||
"packages/backend",
|
||||
|
@ -22,7 +22,7 @@
|
|||
"build-assets": "node ./scripts/build-assets.mjs",
|
||||
"build": "pnpm build-pre && pnpm -r build && pnpm build-assets",
|
||||
"build-storybook": "pnpm --filter frontend build-storybook",
|
||||
"build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api",
|
||||
"build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api",
|
||||
"start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js",
|
||||
"start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js",
|
||||
"init": "pnpm migrate",
|
||||
|
@ -52,24 +52,24 @@
|
|||
"cssnano": "6.1.2",
|
||||
"execa": "8.0.1",
|
||||
"fast-glob": "3.3.2",
|
||||
"ignore-walk": "6.0.4",
|
||||
"ignore-walk": "6.0.5",
|
||||
"js-yaml": "4.1.0",
|
||||
"postcss": "8.4.38",
|
||||
"postcss": "8.4.40",
|
||||
"tar": "6.2.1",
|
||||
"terser": "5.31.1",
|
||||
"typescript": "5.5.3",
|
||||
"esbuild": "0.22.0",
|
||||
"glob": "10.3.12"
|
||||
"terser": "5.31.3",
|
||||
"typescript": "5.5.4",
|
||||
"esbuild": "0.23.0",
|
||||
"glob": "11.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/eslint-plugin": "2.0.2",
|
||||
"@types/node": "20.14.9",
|
||||
"@typescript-eslint/eslint-plugin": "7.15.0",
|
||||
"@typescript-eslint/parser": "7.15.0",
|
||||
"@types/node": "20.14.12",
|
||||
"@typescript-eslint/eslint-plugin": "7.17.0",
|
||||
"@typescript-eslint/parser": "7.17.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "13.13.0",
|
||||
"eslint": "9.6.0",
|
||||
"globals": "15.7.0",
|
||||
"cypress": "13.13.1",
|
||||
"eslint": "9.8.0",
|
||||
"globals": "15.8.0",
|
||||
"ncp": "2.0.0",
|
||||
"start-server-and-test": "2.0.4"
|
||||
},
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Misskey API</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script
|
||||
id="api-reference"
|
||||
data-url="/api.json"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,24 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Misskey API</title>
|
||||
<!-- needed for adaptive design -->
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
||||
|
||||
<!--
|
||||
ReDoc doesn't change outer page styles
|
||||
-->
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<redoc spec-url="/api.json" expand-responses="200" expand-single-schema-field="true"></redoc>
|
||||
<script src="https://cdn.redoc.ly/redoc/v2.1.3/bundles/redoc.standalone.js" integrity="sha256-u4DgqzYXoArvNF/Ymw3puKexfOC6lYfw0sfmeliBJ1I=" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -4,7 +4,7 @@ import sharedConfig from '../shared/eslint.config.js';
|
|||
export default [
|
||||
...sharedConfig,
|
||||
{
|
||||
ignores: ['**/node_modules', 'built', '@types/**/*'],
|
||||
ignores: ['**/node_modules', 'built', '@types/**/*', 'migration'],
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
|
||||
"test-and-coverage": "pnpm jest-and-coverage",
|
||||
"test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e",
|
||||
"generate-api-json": "pnpm build && node ./scripts/generate_api_json.js"
|
||||
"generate-api-json": "node ./scripts/generate_api_json.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-android-arm64": "1.3.11",
|
||||
|
@ -65,11 +65,11 @@
|
|||
"utf-8-validate": "6.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.600.0",
|
||||
"@aws-sdk/lib-storage": "3.600.0",
|
||||
"@bull-board/api": "5.20.5",
|
||||
"@bull-board/fastify": "5.20.5",
|
||||
"@bull-board/ui": "5.20.5",
|
||||
"@aws-sdk/client-s3": "3.620.0",
|
||||
"@aws-sdk/lib-storage": "3.620.0",
|
||||
"@bull-board/api": "5.21.1",
|
||||
"@bull-board/fastify": "5.21.1",
|
||||
"@bull-board/ui": "5.21.1",
|
||||
"@discordapp/twemoji": "15.0.3",
|
||||
"@fastify/accepts": "4.3.0",
|
||||
"@fastify/cookie": "9.3.1",
|
||||
|
@ -86,22 +86,22 @@
|
|||
"@nestjs/core": "10.3.10",
|
||||
"@nestjs/testing": "10.3.10",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@sentry/node": "8.13.0",
|
||||
"@sentry/profiling-node": "8.13.0",
|
||||
"@simplewebauthn/server": "10.0.0",
|
||||
"@sentry/node": "8.20.0",
|
||||
"@sentry/profiling-node": "8.20.0",
|
||||
"@simplewebauthn/server": "10.0.1",
|
||||
"@sinonjs/fake-timers": "11.2.2",
|
||||
"@smithy/node-http-handler": "2.5.0",
|
||||
"@swc/cli": "0.3.12",
|
||||
"@swc/core": "1.6.6",
|
||||
"@twemoji/parser": "15.1.1",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.16.0",
|
||||
"ajv": "8.17.1",
|
||||
"archiver": "7.0.1",
|
||||
"async-mutex": "0.5.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "2.0.5",
|
||||
"body-parser": "1.20.2",
|
||||
"bullmq": "5.8.3",
|
||||
"bullmq": "5.10.4",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"cbor": "9.0.2",
|
||||
"chalk": "5.3.0",
|
||||
|
@ -115,10 +115,10 @@
|
|||
"fastify": "4.28.1",
|
||||
"fastify-raw-body": "4.3.0",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "19.0.0",
|
||||
"file-type": "19.3.0",
|
||||
"fluent-ffmpeg": "2.1.3",
|
||||
"form-data": "4.0.0",
|
||||
"got": "14.4.1",
|
||||
"got": "14.4.2",
|
||||
"happy-dom": "10.0.3",
|
||||
"hpagent": "1.2.0",
|
||||
"htmlescape": "1.1.1",
|
||||
|
@ -128,7 +128,7 @@
|
|||
"ipaddr.js": "2.2.0",
|
||||
"is-svg": "5.0.1",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsdom": "24.1.0",
|
||||
"jsdom": "24.1.1",
|
||||
"json5": "2.2.3",
|
||||
"jsonld": "8.3.2",
|
||||
"jsrsasign": "11.1.0",
|
||||
|
@ -178,11 +178,11 @@
|
|||
"tsc-alias": "1.8.10",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typeorm": "0.3.20",
|
||||
"typescript": "5.5.3",
|
||||
"typescript": "5.5.4",
|
||||
"ulid": "2.3.0",
|
||||
"vary": "1.1.2",
|
||||
"web-push": "3.6.7",
|
||||
"ws": "8.17.1",
|
||||
"ws": "8.18.0",
|
||||
"xev": "3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -202,11 +202,11 @@
|
|||
"@types/jest": "29.5.12",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/jsdom": "21.1.7",
|
||||
"@types/jsonld": "1.5.14",
|
||||
"@types/jsonld": "1.5.15",
|
||||
"@types/jsrsasign": "10.5.14",
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/ms": "0.7.34",
|
||||
"@types/node": "20.14.9",
|
||||
"@types/node": "20.14.12",
|
||||
"@types/nodemailer": "6.4.15",
|
||||
"@types/oauth": "0.9.5",
|
||||
"@types/oauth2orize": "1.11.5",
|
||||
|
@ -226,18 +226,18 @@
|
|||
"@types/tmp": "0.2.6",
|
||||
"@types/vary": "1.1.3",
|
||||
"@types/web-push": "3.6.3",
|
||||
"@types/ws": "8.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "7.15.0",
|
||||
"@typescript-eslint/parser": "7.15.0",
|
||||
"@types/ws": "8.5.11",
|
||||
"@typescript-eslint/eslint-plugin": "7.17.0",
|
||||
"@typescript-eslint/parser": "7.17.0",
|
||||
"aws-sdk-client-mock": "4.0.1",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"execa": "9.2.0",
|
||||
"execa": "9.3.0",
|
||||
"fkill": "9.0.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-mock": "29.7.0",
|
||||
"nodemon": "3.1.4",
|
||||
"pid-port": "1.0.0",
|
||||
"simple-oauth2": "5.0.1"
|
||||
"simple-oauth2": "5.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,11 +3,34 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { loadConfig } from '../built/config.js'
|
||||
import { genOpenapiSpec } from '../built/server/api/openapi/gen-spec.js'
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { execa } from 'execa';
|
||||
import { writeFileSync, existsSync } from "node:fs";
|
||||
|
||||
const config = loadConfig();
|
||||
const spec = genOpenapiSpec(config, true);
|
||||
async function main() {
|
||||
if (!process.argv.includes('--no-build')) {
|
||||
await execa('pnpm', ['run', 'build'], {
|
||||
stdout: process.stdout,
|
||||
stderr: process.stderr,
|
||||
});
|
||||
}
|
||||
|
||||
writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8');
|
||||
if (!existsSync('./built')) {
|
||||
throw new Error('`built` directory does not exist.');
|
||||
}
|
||||
|
||||
/** @type {import('../src/config.js')} */
|
||||
const { loadConfig } = await import('../built/config.js');
|
||||
|
||||
/** @type {import('../src/server/api/openapi/gen-spec.js')} */
|
||||
const { genOpenapiSpec } = await import('../built/server/api/openapi/gen-spec.js');
|
||||
|
||||
const config = loadConfig();
|
||||
const spec = genOpenapiSpec(config, true);
|
||||
|
||||
writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8');
|
||||
}
|
||||
|
||||
main().catch(e => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
@ -23,7 +23,7 @@ type RedisOptionsSource = Partial<RedisOptions> & {
|
|||
* 設定ファイルの型
|
||||
*/
|
||||
type Source = {
|
||||
url: string;
|
||||
url?: string;
|
||||
port?: number;
|
||||
socket?: string;
|
||||
chmodSocket?: string;
|
||||
|
@ -31,9 +31,9 @@ type Source = {
|
|||
db: {
|
||||
host: string;
|
||||
port: number;
|
||||
db: string;
|
||||
user: string;
|
||||
pass: string;
|
||||
db?: string;
|
||||
user?: string;
|
||||
pass?: string;
|
||||
disableCache?: boolean;
|
||||
extra?: { [x: string]: string };
|
||||
};
|
||||
|
@ -202,13 +202,17 @@ 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.MISSKEY_URL ?? '');
|
||||
const version = meta.version;
|
||||
const host = url.host;
|
||||
const hostname = url.hostname;
|
||||
const scheme = url.protocol.replace(/:$/, '');
|
||||
const wsScheme = scheme.replace('http', 'ws');
|
||||
|
||||
const dbDb = config.db.db ?? process.env.DATABASE_DB ?? '';
|
||||
const dbUser = config.db.user ?? process.env.DATABASE_USER ?? '';
|
||||
const dbPass = config.db.pass ?? process.env.DATABASE_PASSWORD ?? '';
|
||||
|
||||
const externalMediaProxy = config.mediaProxy ?
|
||||
config.mediaProxy.endsWith('/') ? config.mediaProxy.substring(0, config.mediaProxy.length - 1) : config.mediaProxy
|
||||
: null;
|
||||
|
@ -231,7 +235,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,
|
||||
|
@ -259,7 +263,7 @@ export function loadConfig(): Config {
|
|||
deliverJobMaxAttempts: config.deliverJobMaxAttempts,
|
||||
inboxJobMaxAttempts: config.inboxJobMaxAttempts,
|
||||
proxyRemoteFiles: config.proxyRemoteFiles,
|
||||
signToActivityPubGet: config.signToActivityPubGet,
|
||||
signToActivityPubGet: config.signToActivityPubGet ?? true,
|
||||
mediaProxy: externalMediaProxy ?? internalMediaProxy,
|
||||
externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy,
|
||||
videoThumbnailGenerator: config.videoThumbnailGenerator ?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
|
@ -60,6 +61,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';
|
||||
|
@ -203,6 +205,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 };
|
||||
|
@ -350,6 +354,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
UserKeypairService,
|
||||
UserListService,
|
||||
UserMutingService,
|
||||
UserRenoteMutingService,
|
||||
UserSearchService,
|
||||
UserSuspendService,
|
||||
UserAuthService,
|
||||
VideoProcessingService,
|
||||
|
@ -493,6 +499,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$UserKeypairService,
|
||||
$UserListService,
|
||||
$UserMutingService,
|
||||
$UserRenoteMutingService,
|
||||
$UserSearchService,
|
||||
$UserSuspendService,
|
||||
$UserAuthService,
|
||||
$VideoProcessingService,
|
||||
|
@ -637,6 +645,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
UserKeypairService,
|
||||
UserListService,
|
||||
UserMutingService,
|
||||
UserRenoteMutingService,
|
||||
UserSearchService,
|
||||
UserSuspendService,
|
||||
UserAuthService,
|
||||
VideoProcessingService,
|
||||
|
@ -779,6 +789,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$UserKeypairService,
|
||||
$UserListService,
|
||||
$UserMutingService,
|
||||
$UserRenoteMutingService,
|
||||
$UserSearchService,
|
||||
$UserSuspendService,
|
||||
$UserAuthService,
|
||||
$VideoProcessingService,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -256,6 +256,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']; };
|
||||
|
@ -294,43 +298,45 @@ 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>;
|
||||
};
|
||||
userList: {
|
||||
name: `userListStream:${MiUserList['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<UserListEventTypes>>;
|
||||
payload: EventTypesToEventPayload<UserListEventTypes>;
|
||||
};
|
||||
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>;
|
||||
};
|
||||
mahjongRoom: {
|
||||
name: `mahjongRoomStream:${string}`;
|
||||
|
|
|
@ -933,10 +933,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -47,6 +47,7 @@ export type RolePolicies = {
|
|||
canHideAds: boolean;
|
||||
driveCapacityMb: number;
|
||||
alwaysMarkNsfw: boolean;
|
||||
canUpdateBioMedia: boolean;
|
||||
pinLimit: number;
|
||||
antennaLimit: number;
|
||||
wordMuteLimit: number;
|
||||
|
@ -75,6 +76,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
|||
canHideAds: false,
|
||||
driveCapacityMb: 100,
|
||||
alwaysMarkNsfw: false,
|
||||
canUpdateBioMedia: true,
|
||||
pinLimit: 5,
|
||||
antennaLimit: 5,
|
||||
wordMuteLimit: 200,
|
||||
|
@ -376,6 +378,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)),
|
||||
|
@ -502,14 +505,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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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が重複することはないが).
|
||||
* (1で既にヒットしていた場合、2, 3, 4でヒットしても無視される)
|
||||
* - ユーザ名とホスト名の検索条件はそれぞれ前方一致で検索される.
|
||||
* - ユーザ名の検索は大文字小文字を区別しない.
|
||||
* - ホスト名の検索は大文字小文字を区別しない.
|
||||
* - 検索結果は最大で {@link opts.limit} 件までとなる.
|
||||
*
|
||||
* ※一定期間とは {@link params.activeThreshold} で指定された日時から現在までの期間を指す.
|
||||
*
|
||||
* @param params 検索条件.
|
||||
* @param opts 関数の動作を制御するオプション.
|
||||
* @param me 検索を実行するユーザの情報. 未ログインの場合は指定しない.
|
||||
* @see {@link UserSearchService#buildSearchUserQueries}
|
||||
* @see {@link UserSearchService#buildSearchUserNoLoginQueries}
|
||||
*/
|
||||
@bindThis
|
||||
public async search(
|
||||
params: {
|
||||
username?: string | null,
|
||||
host?: string | null,
|
||||
activeThreshold?: Date,
|
||||
},
|
||||
opts?: {
|
||||
limit?: number,
|
||||
detail?: boolean,
|
||||
},
|
||||
me?: MiUser | null,
|
||||
): Promise<Packed<'User'>[]> {
|
||||
const queries = me ? this.buildSearchUserQueries(me, params) : this.buildSearchUserNoLoginQueries(params);
|
||||
|
||||
let resultSet = new Set<MiUser['id']>();
|
||||
const limit = opts?.limit ?? 10;
|
||||
for (const query of queries) {
|
||||
const ids = await query
|
||||
.select('user.id')
|
||||
.limit(limit - resultSet.size)
|
||||
.orderBy('user.usernameLower', 'ASC')
|
||||
.getRawMany<{ user_id: MiUser['id'] }>()
|
||||
.then(res => res.map(x => x.user_id));
|
||||
|
||||
resultSet = new Set([...resultSet, ...ids]);
|
||||
if (resultSet.size >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this.userEntityService.packMany<'UserLite' | 'UserDetailed'>(
|
||||
[...resultSet].slice(0, limit),
|
||||
me,
|
||||
{ schema: opts?.detail ? 'UserDetailed' : 'UserLite' },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ログイン済みユーザによる検索実行時のクエリ一覧を構築する.
|
||||
* @param me
|
||||
* @param params
|
||||
* @private
|
||||
*/
|
||||
@bindThis
|
||||
private buildSearchUserQueries(
|
||||
me: MiUser,
|
||||
params: {
|
||||
username?: string | null,
|
||||
host?: string | null,
|
||||
activeThreshold?: Date,
|
||||
},
|
||||
) {
|
||||
// デフォルト30日以内に更新されたユーザーをアクティブユーザーとする
|
||||
const activeThreshold = params.activeThreshold ?? defaultActiveThreshold();
|
||||
|
||||
const followingUserQuery = this.followingsRepository.createQueryBuilder('following')
|
||||
.select('following.followeeId')
|
||||
.where('following.followerId = :followerId', { followerId: me.id });
|
||||
|
||||
const activeFollowingUsersQuery = this.generateUserQueryBuilder(params)
|
||||
.andWhere(`user.id IN (${followingUserQuery.getQuery()})`)
|
||||
.andWhere('user.updatedAt > :activeThreshold', { activeThreshold });
|
||||
activeFollowingUsersQuery.setParameters(followingUserQuery.getParameters());
|
||||
|
||||
const inactiveFollowingUsersQuery = this.generateUserQueryBuilder(params)
|
||||
.andWhere(`user.id IN (${followingUserQuery.getQuery()})`)
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('user.updatedAt IS NULL')
|
||||
.orWhere('user.updatedAt <= :activeThreshold', { activeThreshold });
|
||||
}));
|
||||
inactiveFollowingUsersQuery.setParameters(followingUserQuery.getParameters());
|
||||
|
||||
// 自分自身がヒットするとしたらここ
|
||||
const activeUserQuery = this.generateUserQueryBuilder(params)
|
||||
.andWhere(`user.id NOT IN (${followingUserQuery.getQuery()})`)
|
||||
.andWhere('user.updatedAt > :activeThreshold', { activeThreshold });
|
||||
activeUserQuery.setParameters(followingUserQuery.getParameters());
|
||||
|
||||
const inactiveUserQuery = this.generateUserQueryBuilder(params)
|
||||
.andWhere(`user.id NOT IN (${followingUserQuery.getQuery()})`)
|
||||
.andWhere('user.updatedAt <= :activeThreshold', { activeThreshold });
|
||||
inactiveUserQuery.setParameters(followingUserQuery.getParameters());
|
||||
|
||||
return [activeFollowingUsersQuery, inactiveFollowingUsersQuery, activeUserQuery, inactiveUserQuery];
|
||||
}
|
||||
|
||||
/**
|
||||
* ログインしていないユーザによる検索実行時のクエリ一覧を構築する.
|
||||
* @param params
|
||||
* @private
|
||||
*/
|
||||
@bindThis
|
||||
private buildSearchUserNoLoginQueries(params: {
|
||||
username?: string | null,
|
||||
host?: string | null,
|
||||
activeThreshold?: Date,
|
||||
}) {
|
||||
// デフォルト30日以内に更新されたユーザーをアクティブユーザーとする
|
||||
const activeThreshold = params.activeThreshold ?? defaultActiveThreshold();
|
||||
|
||||
const activeUserQuery = this.generateUserQueryBuilder(params)
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('user.updatedAt IS NULL')
|
||||
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold });
|
||||
}));
|
||||
|
||||
const inactiveUserQuery = this.generateUserQueryBuilder(params)
|
||||
.andWhere('user.updatedAt <= :activeThreshold', { activeThreshold });
|
||||
|
||||
return [activeUserQuery, inactiveUserQuery];
|
||||
}
|
||||
|
||||
/**
|
||||
* ユーザ検索クエリで共通する抽出条件をあらかじめ設定したクエリビルダを生成する.
|
||||
* @param params
|
||||
* @private
|
||||
*/
|
||||
@bindThis
|
||||
private generateUserQueryBuilder(params: {
|
||||
username?: string | null,
|
||||
host?: string | null,
|
||||
}): SelectQueryBuilder<MiUser> {
|
||||
const userQuery = this.usersRepository.createQueryBuilder('user');
|
||||
|
||||
if (params.username) {
|
||||
userQuery.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(params.username.toLowerCase()) + '%' });
|
||||
}
|
||||
|
||||
if (params.host) {
|
||||
if (params.host === this.config.hostname || params.host === '.') {
|
||||
userQuery.andWhere('user.host IS NULL');
|
||||
} else {
|
||||
userQuery.andWhere('user.host LIKE :host', {
|
||||
host: sqlLikeEscape(params.host.toLowerCase()) + '%',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
userQuery.andWhere('user.isSuspended = FALSE');
|
||||
|
||||
return userQuery;
|
||||
}
|
||||
}
|
|
@ -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 ?? '');
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ 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';
|
||||
|
@ -100,6 +101,8 @@ export class ApPersonService implements OnModuleInit {
|
|||
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -238,6 +241,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
|
||||
|
|
|
@ -74,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
|
||||
|
|
|
@ -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,
|
||||
|
@ -90,9 +106,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,
|
||||
|
|
|
@ -501,11 +501,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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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[];
|
|
@ -204,6 +204,7 @@ export const packedNoteSchema = {
|
|||
reactionAcceptance: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
enum: ['likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'],
|
||||
},
|
||||
reactionEmojis: {
|
||||
type: 'object',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ import { UserFollowingService } from '@/core/UserFollowingService.js';
|
|||
import { AccountUpdateService } from '@/core/AccountUpdateService.js';
|
||||
import { HashtagService } from '@/core/HashtagService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { RolePolicies, RoleService } from '@/core/RoleService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||
|
@ -256,6 +256,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
const profileUpdates = {} as Partial<MiUserProfile>;
|
||||
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
||||
let policies: RolePolicies | null = null;
|
||||
|
||||
if (ps.name !== undefined) {
|
||||
if (ps.name === null) {
|
||||
|
@ -296,14 +297,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}
|
||||
|
||||
if (ps.mutedWords !== undefined) {
|
||||
checkMuteWordCount(ps.mutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
|
||||
policies ??= await this.roleService.getUserPolicies(user.id);
|
||||
checkMuteWordCount(ps.mutedWords, policies.wordMuteLimit);
|
||||
validateMuteWordRegex(ps.mutedWords);
|
||||
|
||||
profileUpdates.mutedWords = ps.mutedWords;
|
||||
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
|
||||
}
|
||||
if (ps.hardMutedWords !== undefined) {
|
||||
checkMuteWordCount(ps.hardMutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
|
||||
policies ??= await this.roleService.getUserPolicies(user.id);
|
||||
checkMuteWordCount(ps.hardMutedWords, policies.wordMuteLimit);
|
||||
validateMuteWordRegex(ps.hardMutedWords);
|
||||
profileUpdates.hardMutedWords = ps.hardMutedWords;
|
||||
}
|
||||
|
@ -322,13 +325,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
|
||||
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
|
||||
if (typeof ps.alwaysMarkNsfw === 'boolean') {
|
||||
if ((await roleService.getUserPolicies(user.id)).alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole);
|
||||
policies ??= await this.roleService.getUserPolicies(user.id);
|
||||
if (policies.alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole);
|
||||
profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
|
||||
}
|
||||
if (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive;
|
||||
if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
|
||||
|
||||
if (ps.avatarId) {
|
||||
policies ??= await this.roleService.getUserPolicies(user.id);
|
||||
if (!policies.canUpdateBioMedia) throw new ApiError(meta.errors.restrictedByRole);
|
||||
|
||||
const avatar = await this.driveFilesRepository.findOneBy({ id: ps.avatarId });
|
||||
|
||||
if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar);
|
||||
|
@ -344,6 +351,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}
|
||||
|
||||
if (ps.bannerId) {
|
||||
policies ??= await this.roleService.getUserPolicies(user.id);
|
||||
if (!policies.canUpdateBioMedia) throw new ApiError(meta.errors.restrictedByRole);
|
||||
|
||||
const banner = await this.driveFilesRepository.findOneBy({ id: ps.bannerId });
|
||||
|
||||
if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner);
|
||||
|
@ -359,14 +369,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}
|
||||
|
||||
if (ps.avatarDecorations) {
|
||||
policies ??= await this.roleService.getUserPolicies(user.id);
|
||||
const decorations = await this.avatarDecorationService.getAll(true);
|
||||
const [myRoles, myPolicies] = await Promise.all([this.roleService.getUserRoles(user.id), this.roleService.getUserPolicies(user.id)]);
|
||||
const myRoles = await this.roleService.getUserRoles(user.id);
|
||||
const allRoles = await this.roleService.getRoles();
|
||||
const decorationIds = decorations
|
||||
.filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id)))
|
||||
.map(d => d.id);
|
||||
|
||||
if (ps.avatarDecorations.length > myPolicies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole);
|
||||
if (ps.avatarDecorations.length > policies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole);
|
||||
|
||||
updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({
|
||||
id: d.id,
|
||||
|
|
|
@ -139,6 +139,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
timelineConfig = [
|
||||
`homeTimeline:${me.id}`,
|
||||
'localTimeline',
|
||||
`localTimelineWithReplyTo:${me.id}`,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -6,12 +6,11 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { RenoteMutingsRepository } from '@/models/_.js';
|
||||
import type { MiRenoteMuting } from '@/models/RenoteMuting.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { UserRenoteMutingService } from "@/core/UserRenoteMutingService.js";
|
||||
import type { RenoteMutingsRepository } from '@/models/_.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
|
@ -62,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private renoteMutingsRepository: RenoteMutingsRepository,
|
||||
|
||||
private getterService: GetterService,
|
||||
private idService: IdService,
|
||||
private userRenoteMutingService: UserRenoteMutingService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const muter = me;
|
||||
|
@ -79,21 +78,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
});
|
||||
|
||||
// Check if already muting
|
||||
const exist = await this.renoteMutingsRepository.findOneBy({
|
||||
muterId: muter.id,
|
||||
muteeId: mutee.id,
|
||||
const exist = await this.renoteMutingsRepository.exists({
|
||||
where: {
|
||||
muterId: muter.id,
|
||||
muteeId: mutee.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (exist === true) {
|
||||
throw new ApiError(meta.errors.alreadyMuting);
|
||||
}
|
||||
|
||||
// Create mute
|
||||
await this.renoteMutingsRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
muterId: muter.id,
|
||||
muteeId: mutee.id,
|
||||
} as MiRenoteMuting);
|
||||
await this.userRenoteMutingService.mute(muter, mutee);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,10 +5,11 @@
|
|||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RenoteMutingsRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { UserRenoteMutingService } from "@/core/UserRenoteMutingService.js";
|
||||
import type { RenoteMutingsRepository } from '@/models/_.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
|
@ -53,6 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private renoteMutingsRepository: RenoteMutingsRepository,
|
||||
|
||||
private getterService: GetterService,
|
||||
private userRenoteMutingService: UserRenoteMutingService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const muter = me;
|
||||
|
@ -79,9 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}
|
||||
|
||||
// Delete mute
|
||||
await this.renoteMutingsRepository.delete({
|
||||
id: exist.id,
|
||||
});
|
||||
await this.userRenoteMutingService.unmute([exist]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { DI } from '@/di-symbols.js';
|
|||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
|
@ -74,6 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const userIdsWhoBlockingMe = me ? await this.cacheService.userBlockedCache.fetch(me.id) : new Set<string>();
|
||||
const iAmModerator = me ? await this.roleService.isModerator(me) : false; // Moderators can see reactions of all users
|
||||
if (!iAmModerator) {
|
||||
const user = await this.cacheService.findUserById(ps.userId);
|
||||
|
@ -85,8 +87,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
if ((me == null || me.id !== ps.userId) && !profile.publicReactions) {
|
||||
throw new ApiError(meta.errors.reactionsNotPublic);
|
||||
}
|
||||
|
||||
// early return if me is blocked by requesting user
|
||||
if (userIdsWhoBlockingMe.has(ps.userId)) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const userIdsWhoMeMuting = me ? await this.cacheService.userMutingsCache.fetch(me.id) : new Set<string>();
|
||||
|
||||
const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'),
|
||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('reaction.userId = :userId', { userId: ps.userId })
|
||||
|
@ -94,9 +103,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
|
||||
const reactions = await query
|
||||
const reactions = (await query
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
.getMany()).filter(reaction => {
|
||||
if (reaction.note?.userId === ps.userId) return true; // we can see reactions to note of requesting user
|
||||
if (me && isUserRelated(reaction.note, userIdsWhoBlockingMe)) return false;
|
||||
if (me && isUserRelated(reaction.note, userIdsWhoMeMuting)) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return await this.noteReactionEntityService.packMany(reactions, me, { withNote: true });
|
||||
});
|
||||
|
|
|
@ -3,15 +3,9 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Brackets } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UsersRepository, FollowingsRepository } from '@/models/_.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||
import { UserSearchService } from '@/core/UserSearchService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
|
@ -49,89 +43,16 @@ export const paramDef = {
|
|||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private userSearchService: UserSearchService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const setUsernameAndHostQuery = (query = this.usersRepository.createQueryBuilder('user')) => {
|
||||
if (ps.username) {
|
||||
query.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' });
|
||||
}
|
||||
|
||||
if (ps.host) {
|
||||
if (ps.host === this.config.hostname || ps.host === '.') {
|
||||
query.andWhere('user.host IS NULL');
|
||||
} else {
|
||||
query.andWhere('user.host LIKE :host', {
|
||||
host: sqlLikeEscape(ps.host.toLowerCase()) + '%',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
|
||||
|
||||
let users: MiUser[] = [];
|
||||
|
||||
if (me) {
|
||||
const followingQuery = this.followingsRepository.createQueryBuilder('following')
|
||||
.select('following.followeeId')
|
||||
.where('following.followerId = :followerId', { followerId: me.id });
|
||||
|
||||
const query = setUsernameAndHostQuery()
|
||||
.andWhere(`user.id IN (${ followingQuery.getQuery() })`)
|
||||
.andWhere('user.id != :meId', { meId: me.id })
|
||||
.andWhere('user.isSuspended = FALSE')
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('user.updatedAt IS NULL')
|
||||
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
|
||||
}));
|
||||
|
||||
query.setParameters(followingQuery.getParameters());
|
||||
|
||||
users = await query
|
||||
.orderBy('user.usernameLower', 'ASC')
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
if (users.length < ps.limit) {
|
||||
const otherQuery = setUsernameAndHostQuery()
|
||||
.andWhere(`user.id NOT IN (${ followingQuery.getQuery() })`)
|
||||
.andWhere('user.isSuspended = FALSE')
|
||||
.andWhere('user.updatedAt IS NOT NULL');
|
||||
|
||||
otherQuery.setParameters(followingQuery.getParameters());
|
||||
|
||||
const otherUsers = await otherQuery
|
||||
.orderBy('user.updatedAt', 'DESC')
|
||||
.limit(ps.limit - users.length)
|
||||
.getMany();
|
||||
|
||||
users = users.concat(otherUsers);
|
||||
}
|
||||
} else {
|
||||
const query = setUsernameAndHostQuery()
|
||||
.andWhere('user.isSuspended = FALSE')
|
||||
.andWhere('user.updatedAt IS NOT NULL');
|
||||
|
||||
users = await query
|
||||
.orderBy('user.updatedAt', 'DESC')
|
||||
.limit(ps.limit - users.length)
|
||||
.getMany();
|
||||
}
|
||||
|
||||
return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });
|
||||
super(meta, paramDef, (ps, me) => {
|
||||
return this.userSearchService.search({
|
||||
username: ps.username,
|
||||
host: ps.host,
|
||||
}, {
|
||||
limit: ps.limit,
|
||||
detail: ps.detail,
|
||||
}, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ export class OpenApiServerService {
|
|||
public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) {
|
||||
fastify.get('/api-doc', async (_request, reply) => {
|
||||
reply.header('Cache-Control', 'public, max-age=86400');
|
||||
return await reply.sendFile('/redoc.html', staticAssets);
|
||||
return await reply.sendFile('/api-doc.html', staticAssets);
|
||||
});
|
||||
fastify.get('/api.json', (_request, reply) => {
|
||||
reply.header('Cache-Control', 'public, max-age=600');
|
||||
|
|
|
@ -15,7 +15,6 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) {
|
|||
info: {
|
||||
version: config.version,
|
||||
title: 'Misskey API',
|
||||
'x-logo': { url: '/static-assets/api-doc.png' },
|
||||
},
|
||||
|
||||
externalDocs: {
|
||||
|
|
|
@ -14,6 +14,7 @@ import { CacheService } from '@/core/CacheService.js';
|
|||
import { MiFollowing, MiUserProfile } from '@/models/_.js';
|
||||
import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import type { ChannelsService } from './ChannelsService.js';
|
||||
import type { EventEmitter } from 'events';
|
||||
import type Channel from './channel.js';
|
||||
|
@ -28,7 +29,7 @@ export default class Connection {
|
|||
private wsConnection: WebSocket.WebSocket;
|
||||
public subscriber: StreamEventEmitter;
|
||||
private channels: Channel[] = [];
|
||||
private subscribingNotes: any = {};
|
||||
private subscribingNotes: Partial<Record<string, number>> = {};
|
||||
private cachedNotes: Packed<'Note'>[] = [];
|
||||
public userProfile: MiUserProfile | null = null;
|
||||
public following: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {};
|
||||
|
@ -101,7 +102,7 @@ export default class Connection {
|
|||
*/
|
||||
@bindThis
|
||||
private async onWsConnectionMessage(data: WebSocket.RawData) {
|
||||
let obj: Record<string, any>;
|
||||
let obj: JsonObject;
|
||||
|
||||
try {
|
||||
obj = JSON.parse(data.toString());
|
||||
|
@ -111,6 +112,8 @@ export default class Connection {
|
|||
|
||||
const { type, body } = obj;
|
||||
|
||||
if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
|
||||
|
||||
switch (type) {
|
||||
case 'readNotification': this.onReadNotification(body); break;
|
||||
case 'subNote': this.onSubscribeNote(body); break;
|
||||
|
@ -151,7 +154,7 @@ export default class Connection {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private readNote(body: any) {
|
||||
private readNote(body: JsonObject) {
|
||||
const id = body.id;
|
||||
|
||||
const note = this.cachedNotes.find(n => n.id === id);
|
||||
|
@ -163,7 +166,7 @@ export default class Connection {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private onReadNotification(payload: any) {
|
||||
private onReadNotification(payload: JsonObject) {
|
||||
this.notificationService.readAllNotification(this.user!.id);
|
||||
}
|
||||
|
||||
|
@ -171,16 +174,14 @@ export default class Connection {
|
|||
* 投稿購読要求時
|
||||
*/
|
||||
@bindThis
|
||||
private onSubscribeNote(payload: any) {
|
||||
if (!payload.id) return;
|
||||
private onSubscribeNote(payload: JsonObject) {
|
||||
if (!payload.id || typeof payload.id !== 'string') return;
|
||||
|
||||
if (this.subscribingNotes[payload.id] == null) {
|
||||
this.subscribingNotes[payload.id] = 0;
|
||||
}
|
||||
const current = this.subscribingNotes[payload.id] ?? 0;
|
||||
const updated = current + 1;
|
||||
this.subscribingNotes[payload.id] = updated;
|
||||
|
||||
this.subscribingNotes[payload.id]++;
|
||||
|
||||
if (this.subscribingNotes[payload.id] === 1) {
|
||||
if (updated === 1) {
|
||||
this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage);
|
||||
}
|
||||
}
|
||||
|
@ -189,11 +190,14 @@ export default class Connection {
|
|||
* 投稿購読解除要求時
|
||||
*/
|
||||
@bindThis
|
||||
private onUnsubscribeNote(payload: any) {
|
||||
if (!payload.id) return;
|
||||
private onUnsubscribeNote(payload: JsonObject) {
|
||||
if (!payload.id || typeof payload.id !== 'string') return;
|
||||
|
||||
this.subscribingNotes[payload.id]--;
|
||||
if (this.subscribingNotes[payload.id] <= 0) {
|
||||
const current = this.subscribingNotes[payload.id];
|
||||
if (current == null) return;
|
||||
const updated = current - 1;
|
||||
this.subscribingNotes[payload.id] = updated;
|
||||
if (updated <= 0) {
|
||||
delete this.subscribingNotes[payload.id];
|
||||
this.subscriber.off(`noteStream:${payload.id}`, this.onNoteStreamMessage);
|
||||
}
|
||||
|
@ -212,17 +216,22 @@ export default class Connection {
|
|||
* チャンネル接続要求時
|
||||
*/
|
||||
@bindThis
|
||||
private onChannelConnectRequested(payload: any) {
|
||||
private onChannelConnectRequested(payload: JsonObject) {
|
||||
const { channel, id, params, pong } = payload;
|
||||
this.connectChannel(id, params, channel, pong);
|
||||
if (typeof id !== 'string') return;
|
||||
if (typeof channel !== 'string') return;
|
||||
if (typeof pong !== 'boolean' && typeof pong !== 'undefined' && pong !== null) return;
|
||||
if (typeof params !== 'undefined' && (typeof params !== 'object' || params === null || Array.isArray(params))) return;
|
||||
this.connectChannel(id, params, channel, pong ?? undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* チャンネル切断要求時
|
||||
*/
|
||||
@bindThis
|
||||
private onChannelDisconnectRequested(payload: any) {
|
||||
private onChannelDisconnectRequested(payload: JsonObject) {
|
||||
const { id } = payload;
|
||||
if (typeof id !== 'string') return;
|
||||
this.disconnectChannel(id);
|
||||
}
|
||||
|
||||
|
@ -230,7 +239,7 @@ export default class Connection {
|
|||
* クライアントにメッセージ送信
|
||||
*/
|
||||
@bindThis
|
||||
public sendMessageToWs(type: string, payload: any) {
|
||||
public sendMessageToWs(type: string, payload: JsonObject) {
|
||||
this.wsConnection.send(JSON.stringify({
|
||||
type: type,
|
||||
body: payload,
|
||||
|
@ -241,7 +250,7 @@ export default class Connection {
|
|||
* チャンネルに接続
|
||||
*/
|
||||
@bindThis
|
||||
public connectChannel(id: string, params: any, channel: string, pong = false) {
|
||||
public connectChannel(id: string, params: JsonObject | undefined, channel: string, pong = false) {
|
||||
const channelService = this.channelsService.getChannelService(channel);
|
||||
|
||||
if (channelService.requireCredential && this.user == null) {
|
||||
|
@ -288,7 +297,11 @@ export default class Connection {
|
|||
* @param data メッセージ
|
||||
*/
|
||||
@bindThis
|
||||
private onChannelMessageRequested(data: any) {
|
||||
private onChannelMessageRequested(data: JsonObject) {
|
||||
if (typeof data.id !== 'string') return;
|
||||
if (typeof data.type !== 'string') return;
|
||||
if (typeof data.body === 'undefined') return;
|
||||
|
||||
const channel = this.channels.find(c => c.id === data.id);
|
||||
if (channel != null && channel.onMessage != null) {
|
||||
channel.onMessage(data.type, data.body);
|
||||
|
|
|
@ -8,6 +8,7 @@ import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
|||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
|
||||
import type Connection from './Connection.js';
|
||||
|
||||
/**
|
||||
|
@ -81,10 +82,12 @@ export default abstract class Channel {
|
|||
this.connection = connection;
|
||||
}
|
||||
|
||||
public send(payload: { type: string, body: JsonValue }): void
|
||||
public send(type: string, payload: JsonValue): void
|
||||
@bindThis
|
||||
public send(typeOrPayload: any, payload?: any) {
|
||||
const type = payload === undefined ? typeOrPayload.type : typeOrPayload;
|
||||
const body = payload === undefined ? typeOrPayload.body : payload;
|
||||
public send(typeOrPayload: { type: string, body: JsonValue } | string, payload?: JsonValue) {
|
||||
const type = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).type : (typeOrPayload as string);
|
||||
const body = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).body : payload;
|
||||
|
||||
this.connection.sendMessageToWs('channel', {
|
||||
id: this.id,
|
||||
|
@ -93,11 +96,11 @@ export default abstract class Channel {
|
|||
});
|
||||
}
|
||||
|
||||
public abstract init(params: any): void;
|
||||
public abstract init(params: JsonObject): void;
|
||||
|
||||
public dispose?(): void;
|
||||
|
||||
public onMessage?(type: string, body: any): void;
|
||||
public onMessage?(type: string, body: JsonValue): void;
|
||||
}
|
||||
|
||||
export type MiChannelService<T extends boolean> = {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class AdminChannel extends Channel {
|
||||
|
@ -14,7 +15,7 @@ class AdminChannel extends Channel {
|
|||
public static kind = 'read:admin:stream';
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
public async init(params: JsonObject) {
|
||||
// Subscribe admin stream
|
||||
this.subscriber.on(`adminStream:${this.user!.id}`, data => {
|
||||
this.send(data);
|
||||
|
|
|
@ -7,6 +7,7 @@ import { Injectable } from '@nestjs/common';
|
|||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class AntennaChannel extends Channel {
|
||||
|
@ -27,8 +28,9 @@ class AntennaChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
this.antennaId = params.antennaId as string;
|
||||
public async init(params: JsonObject) {
|
||||
if (typeof params.antennaId !== 'string') return;
|
||||
this.antennaId = params.antennaId;
|
||||
|
||||
// Subscribe stream
|
||||
this.subscriber.on(`antennaStream:${this.antennaId}`, this.onEvent);
|
||||
|
|
|
@ -8,6 +8,7 @@ import type { Packed } from '@/misc/json-schema.js';
|
|||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class ChannelChannel extends Channel {
|
||||
|
@ -27,8 +28,9 @@ class ChannelChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
this.channelId = params.channelId as string;
|
||||
public async init(params: JsonObject) {
|
||||
if (typeof params.channelId !== 'string') return;
|
||||
this.channelId = params.channelId;
|
||||
|
||||
// Subscribe stream
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class DriveChannel extends Channel {
|
||||
|
@ -14,7 +15,7 @@ class DriveChannel extends Channel {
|
|||
public static kind = 'read:account';
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
public async init(params: JsonObject) {
|
||||
// Subscribe drive stream
|
||||
this.subscriber.on(`driveStream:${this.user!.id}`, data => {
|
||||
this.send(data);
|
||||
|
|
|
@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class GlobalTimelineChannel extends Channel {
|
||||
|
@ -32,12 +33,12 @@ class GlobalTimelineChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
public async init(params: JsonObject) {
|
||||
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
|
||||
if (!policies.gtlAvailable) return;
|
||||
|
||||
this.withRenotes = params.withRenotes ?? true;
|
||||
this.withFiles = params.withFiles ?? false;
|
||||
this.withRenotes = !!(params.withRenotes ?? true);
|
||||
this.withFiles = !!(params.withFiles ?? false);
|
||||
|
||||
// Subscribe events
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
|
|
|
@ -9,6 +9,7 @@ import type { Packed } from '@/misc/json-schema.js';
|
|||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class HashtagChannel extends Channel {
|
||||
|
@ -28,11 +29,11 @@ class HashtagChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
public async init(params: JsonObject) {
|
||||
if (!Array.isArray(params.q)) return;
|
||||
if (!params.q.every(x => Array.isArray(x) && x.every(y => typeof y === 'string'))) return;
|
||||
this.q = params.q;
|
||||
|
||||
if (this.q == null) return;
|
||||
|
||||
// Subscribe stream
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import type { Packed } from '@/misc/json-schema.js';
|
|||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class HomeTimelineChannel extends Channel {
|
||||
|
@ -29,9 +30,9 @@ class HomeTimelineChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
this.withRenotes = params.withRenotes ?? true;
|
||||
this.withFiles = params.withFiles ?? false;
|
||||
public async init(params: JsonObject) {
|
||||
this.withRenotes = !!(params.withRenotes ?? true);
|
||||
this.withFiles = !!(params.withFiles ?? false);
|
||||
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class HybridTimelineChannel extends Channel {
|
||||
|
@ -34,13 +35,13 @@ class HybridTimelineChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any): Promise<void> {
|
||||
public async init(params: JsonObject): Promise<void> {
|
||||
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
|
||||
if (!policies.ltlAvailable) return;
|
||||
|
||||
this.withRenotes = params.withRenotes ?? true;
|
||||
this.withReplies = params.withReplies ?? false;
|
||||
this.withFiles = params.withFiles ?? false;
|
||||
this.withRenotes = !!(params.withRenotes ?? true);
|
||||
this.withReplies = !!(params.withReplies ?? false);
|
||||
this.withFiles = !!(params.withFiles ?? false);
|
||||
|
||||
// Subscribe events
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
|
|
|
@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class LocalTimelineChannel extends Channel {
|
||||
|
@ -33,13 +34,13 @@ class LocalTimelineChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
public async init(params: JsonObject) {
|
||||
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
|
||||
if (!policies.ltlAvailable) return;
|
||||
|
||||
this.withRenotes = params.withRenotes ?? true;
|
||||
this.withReplies = params.withReplies ?? false;
|
||||
this.withFiles = params.withFiles ?? false;
|
||||
this.withRenotes = !!(params.withRenotes ?? true);
|
||||
this.withReplies = !!(params.withReplies ?? false);
|
||||
this.withFiles = !!(params.withFiles ?? false);
|
||||
|
||||
// Subscribe events
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
|
|
|
@ -7,6 +7,7 @@ import { Injectable } from '@nestjs/common';
|
|||
import { isInstanceMuted, isUserFromMutedInstance } from '@/misc/is-instance-muted.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class MainChannel extends Channel {
|
||||
|
@ -25,7 +26,7 @@ class MainChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
public async init(params: JsonObject) {
|
||||
// Subscribe main stream channel
|
||||
this.subscriber.on(`mainStream:${this.user!.id}`, async data => {
|
||||
switch (data.type) {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import Xev from 'xev';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
const ev = new Xev();
|
||||
|
@ -22,19 +23,22 @@ class QueueStatsChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
public async init(params: JsonObject) {
|
||||
ev.addListener('queueStats', this.onStats);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private onStats(stats: any) {
|
||||
private onStats(stats: JsonObject) {
|
||||
this.send('stats', stats);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onMessage(type: string, body: any) {
|
||||
public onMessage(type: string, body: JsonValue) {
|
||||
switch (type) {
|
||||
case 'requestLog':
|
||||
if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
|
||||
if (typeof body.id !== 'string') return;
|
||||
if (typeof body.length !== 'number') return;
|
||||
ev.once(`queueStatsLog:${body.id}`, statsLog => {
|
||||
this.send('statsLog', statsLog);
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ import { DI } from '@/di-symbols.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { ReversiService } from '@/core/ReversiService.js';
|
||||
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
|
||||
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class ReversiGameChannel extends Channel {
|
||||
|
@ -28,25 +29,41 @@ class ReversiGameChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
this.gameId = params.gameId as string;
|
||||
public async init(params: JsonObject) {
|
||||
if (typeof params.gameId !== 'string') return;
|
||||
this.gameId = params.gameId;
|
||||
|
||||
this.subscriber.on(`reversiGameStream:${this.gameId}`, this.send);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onMessage(type: string, body: any) {
|
||||
public onMessage(type: string, body: JsonValue) {
|
||||
switch (type) {
|
||||
case 'ready': this.ready(body); break;
|
||||
case 'updateSettings': this.updateSettings(body.key, body.value); break;
|
||||
case 'cancel': this.cancelGame(); break;
|
||||
case 'putStone': this.putStone(body.pos, body.id); break;
|
||||
case 'ready':
|
||||
if (typeof body !== 'boolean') return;
|
||||
this.ready(body);
|
||||
break;
|
||||
case 'updateSettings':
|
||||
if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
|
||||
if (typeof body.key !== 'string') return;
|
||||
if (typeof body.value !== 'object' || body.value === null || Array.isArray(body.value)) return;
|
||||
this.updateSettings(body.key, body.value);
|
||||
break;
|
||||
case 'cancel':
|
||||
this.cancelGame();
|
||||
break;
|
||||
case 'putStone':
|
||||
if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
|
||||
if (typeof body.pos !== 'number') return;
|
||||
if (typeof body.id !== 'string') return;
|
||||
this.putStone(body.pos, body.id);
|
||||
break;
|
||||
case 'claimTimeIsUp': this.claimTimeIsUp(); break;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async updateSettings(key: string, value: any) {
|
||||
private async updateSettings(key: string, value: JsonObject) {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.reversiService.updateSettings(this.gameId!, this.user, key, value);
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class ReversiChannel extends Channel {
|
||||
|
@ -21,7 +22,7 @@ class ReversiChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
public async init(params: JsonObject) {
|
||||
this.subscriber.on(`reversiStream:${this.user!.id}`, this.send);
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class RoleTimelineChannel extends Channel {
|
||||
|
@ -28,8 +29,9 @@ class RoleTimelineChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
this.roleId = params.roleId as string;
|
||||
public async init(params: JsonObject) {
|
||||
if (typeof params.roleId !== 'string') return;
|
||||
this.roleId = params.roleId;
|
||||
|
||||
this.subscriber.on(`roleTimelineStream:${this.roleId}`, this.onEvent);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import Xev from 'xev';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
const ev = new Xev();
|
||||
|
@ -22,19 +23,20 @@ class ServerStatsChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
public async init(params: JsonObject) {
|
||||
ev.addListener('serverStats', this.onStats);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private onStats(stats: any) {
|
||||
private onStats(stats: JsonObject) {
|
||||
this.send('stats', stats);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onMessage(type: string, body: any) {
|
||||
public onMessage(type: string, body: JsonValue) {
|
||||
switch (type) {
|
||||
case 'requestLog':
|
||||
if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
|
||||
ev.once(`serverStatsLog:${body.id}`, statsLog => {
|
||||
this.send('statsLog', statsLog);
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
|||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class UserListChannel extends Channel {
|
||||
|
@ -36,10 +37,11 @@ class UserListChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
this.listId = params.listId as string;
|
||||
this.withFiles = params.withFiles ?? false;
|
||||
this.withRenotes = params.withRenotes ?? true;
|
||||
public async init(params: JsonObject) {
|
||||
if (typeof params.listId !== 'string') return;
|
||||
this.listId = params.listId;
|
||||
this.withFiles = !!(params.withFiles ?? false);
|
||||
this.withRenotes = !!(params.withRenotes ?? true);
|
||||
|
||||
// Check existence and owner
|
||||
const listExist = await this.userListsRepository.exists({
|
||||
|
|
|
@ -206,7 +206,7 @@ describe('2要素認証', () => {
|
|||
username,
|
||||
}, alice);
|
||||
assert.strictEqual(usersShowResponse.status, 200);
|
||||
assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true);
|
||||
assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true);
|
||||
|
||||
const signinResponse = await api('signin', {
|
||||
...signinParam(),
|
||||
|
@ -248,7 +248,7 @@ describe('2要素認証', () => {
|
|||
keyName,
|
||||
credentialId,
|
||||
creationOptions: registerKeyResponse.body,
|
||||
}) as any, alice);
|
||||
} as any) as any, alice);
|
||||
assert.strictEqual(keyDoneResponse.status, 200);
|
||||
assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url'));
|
||||
assert.strictEqual(keyDoneResponse.body.name, keyName);
|
||||
|
@ -257,22 +257,22 @@ describe('2要素認証', () => {
|
|||
username,
|
||||
});
|
||||
assert.strictEqual(usersShowResponse.status, 200);
|
||||
assert.strictEqual(usersShowResponse.body.securityKeys, true);
|
||||
assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, true);
|
||||
|
||||
const signinResponse = await api('signin', {
|
||||
...signinParam(),
|
||||
});
|
||||
assert.strictEqual(signinResponse.status, 200);
|
||||
assert.strictEqual(signinResponse.body.i, undefined);
|
||||
assert.notEqual(signinResponse.body.challenge, undefined);
|
||||
assert.notEqual(signinResponse.body.allowCredentials, undefined);
|
||||
assert.strictEqual(signinResponse.body.allowCredentials[0].id, credentialId.toString('base64url'));
|
||||
assert.notEqual((signinResponse.body as unknown as { challenge: unknown | undefined }).challenge, undefined);
|
||||
assert.notEqual((signinResponse.body as unknown as { allowCredentials: unknown | undefined }).allowCredentials, undefined);
|
||||
assert.strictEqual((signinResponse.body as unknown as { allowCredentials: {id: string}[] }).allowCredentials[0].id, credentialId.toString('base64url'));
|
||||
|
||||
const signinResponse2 = await api('signin', signinWithSecurityKeyParam({
|
||||
keyName,
|
||||
credentialId,
|
||||
requestOptions: signinResponse.body,
|
||||
}));
|
||||
} as any));
|
||||
assert.strictEqual(signinResponse2.status, 200);
|
||||
assert.notEqual(signinResponse2.body.i, undefined);
|
||||
|
||||
|
@ -307,7 +307,7 @@ describe('2要素認証', () => {
|
|||
keyName,
|
||||
credentialId,
|
||||
creationOptions: registerKeyResponse.body,
|
||||
}) as any, alice);
|
||||
} as any) as any, alice);
|
||||
assert.strictEqual(keyDoneResponse.status, 200);
|
||||
|
||||
const passwordLessResponse = await api('i/2fa/password-less', {
|
||||
|
@ -319,7 +319,7 @@ describe('2要素認証', () => {
|
|||
username,
|
||||
});
|
||||
assert.strictEqual(usersShowResponse.status, 200);
|
||||
assert.strictEqual(usersShowResponse.body.usePasswordLessLogin, true);
|
||||
assert.strictEqual((usersShowResponse.body as unknown as { usePasswordLessLogin: boolean }).usePasswordLessLogin, true);
|
||||
|
||||
const signinResponse = await api('signin', {
|
||||
...signinParam(),
|
||||
|
@ -333,7 +333,7 @@ describe('2要素認証', () => {
|
|||
keyName,
|
||||
credentialId,
|
||||
requestOptions: signinResponse.body,
|
||||
}),
|
||||
} as any),
|
||||
password: '',
|
||||
});
|
||||
assert.strictEqual(signinResponse2.status, 200);
|
||||
|
@ -370,7 +370,7 @@ describe('2要素認証', () => {
|
|||
keyName,
|
||||
credentialId,
|
||||
creationOptions: registerKeyResponse.body,
|
||||
}) as any, alice);
|
||||
} as any) as any, alice);
|
||||
assert.strictEqual(keyDoneResponse.status, 200);
|
||||
|
||||
const renamedKey = 'other-key';
|
||||
|
@ -383,6 +383,7 @@ describe('2要素認証', () => {
|
|||
const iResponse = await api('i', {
|
||||
}, alice);
|
||||
assert.strictEqual(iResponse.status, 200);
|
||||
assert.ok(iResponse.body.securityKeysList);
|
||||
const securityKeys = iResponse.body.securityKeysList.filter((s: { id: string; }) => s.id === credentialId.toString('base64url'));
|
||||
assert.strictEqual(securityKeys.length, 1);
|
||||
assert.strictEqual(securityKeys[0].name, renamedKey);
|
||||
|
@ -419,13 +420,14 @@ describe('2要素認証', () => {
|
|||
keyName,
|
||||
credentialId,
|
||||
creationOptions: registerKeyResponse.body,
|
||||
}) as any, alice);
|
||||
} as any) as any, alice);
|
||||
assert.strictEqual(keyDoneResponse.status, 200);
|
||||
|
||||
// テストの実行順によっては複数残ってるので全部消す
|
||||
const iResponse = await api('i', {
|
||||
}, alice);
|
||||
assert.strictEqual(iResponse.status, 200);
|
||||
assert.ok(iResponse.body.securityKeysList);
|
||||
for (const key of iResponse.body.securityKeysList) {
|
||||
const removeKeyResponse = await api('i/2fa/remove-key', {
|
||||
token: otpToken(registerResponse.body.secret),
|
||||
|
@ -439,7 +441,7 @@ describe('2要素認証', () => {
|
|||
username,
|
||||
});
|
||||
assert.strictEqual(usersShowResponse.status, 200);
|
||||
assert.strictEqual(usersShowResponse.body.securityKeys, false);
|
||||
assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, false);
|
||||
|
||||
const signinResponse = await api('signin', {
|
||||
...signinParam(),
|
||||
|
@ -470,7 +472,7 @@ describe('2要素認証', () => {
|
|||
username,
|
||||
});
|
||||
assert.strictEqual(usersShowResponse.status, 200);
|
||||
assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true);
|
||||
assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true);
|
||||
|
||||
const unregisterResponse = await api('i/2fa/unregister', {
|
||||
token: otpToken(registerResponse.body.secret),
|
||||
|
|
|
@ -410,21 +410,21 @@ describe('API visibility', () => {
|
|||
test('[HTL] public-post が 自分が見れる', async () => {
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter((n: any) => n.id === pub.id);
|
||||
const notes = res.body.filter(n => n.id === pub.id);
|
||||
assert.strictEqual(notes[0].text, 'x');
|
||||
});
|
||||
|
||||
test('[HTL] public-post が 非フォロワーから見れない', async () => {
|
||||
const res = await api('notes/timeline', { limit: 100 }, other);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter((n: any) => n.id === pub.id);
|
||||
const notes = res.body.filter(n => n.id === pub.id);
|
||||
assert.strictEqual(notes.length, 0);
|
||||
});
|
||||
|
||||
test('[HTL] followers-post が フォロワーから見れる', async () => {
|
||||
const res = await api('notes/timeline', { limit: 100 }, follower);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter((n: any) => n.id === fol.id);
|
||||
const notes = res.body.filter(n => n.id === fol.id);
|
||||
assert.strictEqual(notes[0].text, 'x');
|
||||
});
|
||||
//#endregion
|
||||
|
@ -433,21 +433,21 @@ describe('API visibility', () => {
|
|||
test('[replies] followers-reply が フォロワーから見れる', async () => {
|
||||
const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, follower);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter((n: any) => n.id === folR.id);
|
||||
const notes = res.body.filter(n => n.id === folR.id);
|
||||
assert.strictEqual(notes[0].text, 'x');
|
||||
});
|
||||
|
||||
test('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => {
|
||||
const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, other);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter((n: any) => n.id === folR.id);
|
||||
const notes = res.body.filter(n => n.id === folR.id);
|
||||
assert.strictEqual(notes.length, 0);
|
||||
});
|
||||
|
||||
test('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
|
||||
const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, target);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter((n: any) => n.id === folR.id);
|
||||
const notes = res.body.filter(n => n.id === folR.id);
|
||||
assert.strictEqual(notes[0].text, 'x');
|
||||
});
|
||||
//#endregion
|
||||
|
@ -456,14 +456,14 @@ describe('API visibility', () => {
|
|||
test('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
|
||||
const res = await api('notes/mentions', { limit: 100 }, target);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter((n: any) => n.id === folR.id);
|
||||
const notes = res.body.filter(n => n.id === folR.id);
|
||||
assert.strictEqual(notes[0].text, 'x');
|
||||
});
|
||||
|
||||
test('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async () => {
|
||||
const res = await api('notes/mentions', { limit: 100 }, target);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter((n: any) => n.id === folM.id);
|
||||
const notes = res.body.filter(n => n.id === folM.id);
|
||||
assert.strictEqual(notes[0].text, '@target x');
|
||||
});
|
||||
//#endregion
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { api, post, signup } from '../utils.js';
|
||||
import { api, castAsError, post, signup } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
describe('Block', () => {
|
||||
|
@ -33,7 +33,7 @@ describe('Block', () => {
|
|||
const res = await api('following/create', { userId: alice.id }, bob);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0');
|
||||
assert.strictEqual(castAsError(res.body).error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0');
|
||||
});
|
||||
|
||||
test('ブロックされているユーザーにリアクションできない', async () => {
|
||||
|
@ -42,7 +42,8 @@ describe('Block', () => {
|
|||
const res = await api('notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec');
|
||||
assert.ok(res.body);
|
||||
assert.strictEqual(castAsError(res.body).error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec');
|
||||
});
|
||||
|
||||
test('ブロックされているユーザーに返信できない', async () => {
|
||||
|
@ -51,7 +52,8 @@ describe('Block', () => {
|
|||
const res = await api('notes/create', { replyId: note.id, text: 'yo' }, bob);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
|
||||
assert.ok(res.body);
|
||||
assert.strictEqual(castAsError(res.body).error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
|
||||
});
|
||||
|
||||
test('ブロックされているユーザーのノートをRenoteできない', async () => {
|
||||
|
@ -60,7 +62,7 @@ describe('Block', () => {
|
|||
const res = await api('notes/create', { renoteId: note.id, text: 'yo' }, bob);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
|
||||
assert.strictEqual(castAsError(res.body).error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
|
||||
});
|
||||
|
||||
// TODO: ユーザーリストに入れられないテスト
|
||||
|
|
|
@ -79,14 +79,14 @@ describe('クリップ', () => {
|
|||
};
|
||||
|
||||
const deleteClip = async (parameters: Misskey.entities.ClipsDeleteRequest, request: Partial<ApiRequest<'clips/delete'>> = {}): Promise<void> => {
|
||||
return await successfulApiCall({
|
||||
await successfulApiCall({
|
||||
endpoint: 'clips/delete',
|
||||
parameters,
|
||||
user: alice,
|
||||
...request,
|
||||
}, {
|
||||
status: 204,
|
||||
}) as any as void;
|
||||
});
|
||||
};
|
||||
|
||||
const show = async (parameters: Misskey.entities.ClipsShowRequest, request: Partial<ApiRequest<'clips/show'>> = {}): Promise<Misskey.entities.Clip> => {
|
||||
|
@ -454,25 +454,25 @@ describe('クリップ', () => {
|
|||
let aliceClip: Misskey.entities.Clip;
|
||||
|
||||
const favorite = async (parameters: Misskey.entities.ClipsFavoriteRequest, request: Partial<ApiRequest<'clips/favorite'>> = {}): Promise<void> => {
|
||||
return successfulApiCall({
|
||||
await successfulApiCall({
|
||||
endpoint: 'clips/favorite',
|
||||
parameters,
|
||||
user: alice,
|
||||
...request,
|
||||
}, {
|
||||
status: 204,
|
||||
}) as any as void;
|
||||
});
|
||||
};
|
||||
|
||||
const unfavorite = async (parameters: Misskey.entities.ClipsUnfavoriteRequest, request: Partial<ApiRequest<'clips/unfavorite'>> = {}): Promise<void> => {
|
||||
return successfulApiCall({
|
||||
await successfulApiCall({
|
||||
endpoint: 'clips/unfavorite',
|
||||
parameters,
|
||||
user: alice,
|
||||
...request,
|
||||
}, {
|
||||
status: 204,
|
||||
}) as any as void;
|
||||
});
|
||||
};
|
||||
|
||||
const myFavorites = async (request: Partial<ApiRequest<'clips/my-favorites'>> = {}): Promise<Misskey.entities.Clip[]> => {
|
||||
|
|
|
@ -10,7 +10,7 @@ import * as assert from 'assert';
|
|||
// https://github.com/node-fetch/node-fetch/pull/1664
|
||||
import { Blob } from 'node-fetch';
|
||||
import { MiUser } from '@/models/_.js';
|
||||
import { api, initTestDb, post, signup, simpleGet, uploadFile } from '../utils.js';
|
||||
import { api, castAsError, initTestDb, post, signup, simpleGet, uploadFile } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
describe('Endpoints', () => {
|
||||
|
@ -164,7 +164,7 @@ describe('Endpoints', () => {
|
|||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.id, alice.id);
|
||||
assert.strictEqual((res.body as unknown as { id: string }).id, alice.id);
|
||||
});
|
||||
|
||||
test('ユーザーが存在しなかったら怒る', async () => {
|
||||
|
@ -285,7 +285,8 @@ describe('Endpoints', () => {
|
|||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
assert.strictEqual(res.body.error.code, 'CANNOT_REACT_TO_RENOTE');
|
||||
assert.ok(res.body);
|
||||
assert.strictEqual(castAsError(res.body).error.code, 'CANNOT_REACT_TO_RENOTE');
|
||||
});
|
||||
|
||||
test('引用にリアクションできる', async () => {
|
||||
|
@ -1063,7 +1064,7 @@ describe('Endpoints', () => {
|
|||
userId: bob.id,
|
||||
}, alice);
|
||||
assert.strictEqual(res1.status, 204);
|
||||
assert.strictEqual(res2.body?.memo, memo);
|
||||
assert.strictEqual((res2.body as unknown as { memo: string })?.memo, memo);
|
||||
});
|
||||
|
||||
test('自分に関するメモを更新できる', async () => {
|
||||
|
@ -1078,7 +1079,7 @@ describe('Endpoints', () => {
|
|||
userId: alice.id,
|
||||
}, alice);
|
||||
assert.strictEqual(res1.status, 204);
|
||||
assert.strictEqual(res2.body?.memo, memo);
|
||||
assert.strictEqual((res2.body as unknown as { memo: string })?.memo, memo);
|
||||
});
|
||||
|
||||
test('メモを削除できる', async () => {
|
||||
|
@ -1099,7 +1100,7 @@ describe('Endpoints', () => {
|
|||
}, alice);
|
||||
|
||||
// memoには常に文字列かnullが入っている(5cac151)
|
||||
assert.strictEqual(res.body.memo, null);
|
||||
assert.strictEqual((res.body as unknown as { memo: string | null }).memo, null);
|
||||
});
|
||||
|
||||
test('メモは個人ごとに独立して保存される', async () => {
|
||||
|
@ -1126,8 +1127,8 @@ describe('Endpoints', () => {
|
|||
}, carol),
|
||||
]);
|
||||
|
||||
assert.strictEqual(resAlice.body.memo, memoAliceToBob);
|
||||
assert.strictEqual(resCarol.body.memo, memoCarolToBob);
|
||||
assert.strictEqual((resAlice.body as unknown as { memo: string }).memo, memoAliceToBob);
|
||||
assert.strictEqual((resCarol.body as unknown as { memo: string }).memo, memoCarolToBob);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -61,14 +61,14 @@ describe('export-clips', () => {
|
|||
});
|
||||
|
||||
test('basic export', async () => {
|
||||
let res = await api('clips/create', {
|
||||
const res1 = await api('clips/create', {
|
||||
name: 'foo',
|
||||
description: 'bar',
|
||||
}, alice);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res1.status, 200);
|
||||
|
||||
res = await api('i/export-clips', {}, alice);
|
||||
assert.strictEqual(res.status, 204);
|
||||
const res2 = await api('i/export-clips', {}, alice);
|
||||
assert.strictEqual(res2.status, 204);
|
||||
|
||||
const exported = await pollFirstDriveFile();
|
||||
assert.strictEqual(exported[0].name, 'foo');
|
||||
|
@ -77,7 +77,7 @@ describe('export-clips', () => {
|
|||
});
|
||||
|
||||
test('export with notes', async () => {
|
||||
let res = await api('clips/create', {
|
||||
const res = await api('clips/create', {
|
||||
name: 'foo',
|
||||
description: 'bar',
|
||||
}, alice);
|
||||
|
@ -96,15 +96,15 @@ describe('export-clips', () => {
|
|||
});
|
||||
|
||||
for (const note of [note1, note2]) {
|
||||
res = await api('clips/add-note', {
|
||||
const res2 = await api('clips/add-note', {
|
||||
clipId: clip.id,
|
||||
noteId: note.id,
|
||||
}, alice);
|
||||
assert.strictEqual(res.status, 204);
|
||||
assert.strictEqual(res2.status, 204);
|
||||
}
|
||||
|
||||
res = await api('i/export-clips', {}, alice);
|
||||
assert.strictEqual(res.status, 204);
|
||||
const res3 = await api('i/export-clips', {}, alice);
|
||||
assert.strictEqual(res3.status, 204);
|
||||
|
||||
const exported = await pollFirstDriveFile();
|
||||
assert.strictEqual(exported[0].name, 'foo');
|
||||
|
@ -116,19 +116,19 @@ describe('export-clips', () => {
|
|||
});
|
||||
|
||||
test('multiple clips', async () => {
|
||||
let res = await api('clips/create', {
|
||||
const res1 = await api('clips/create', {
|
||||
name: 'kawaii',
|
||||
description: 'kawaii',
|
||||
}, alice);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const clip1 = res.body;
|
||||
assert.strictEqual(res1.status, 200);
|
||||
const clip1 = res1.body;
|
||||
|
||||
res = await api('clips/create', {
|
||||
const res2 = await api('clips/create', {
|
||||
name: 'yuri',
|
||||
description: 'yuri',
|
||||
}, alice);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const clip2 = res.body;
|
||||
assert.strictEqual(res2.status, 200);
|
||||
const clip2 = res2.body;
|
||||
|
||||
const note1 = await post(alice, {
|
||||
text: 'baz1',
|
||||
|
@ -138,20 +138,26 @@ describe('export-clips', () => {
|
|||
text: 'baz2',
|
||||
});
|
||||
|
||||
res = await api('clips/add-note', {
|
||||
clipId: clip1.id,
|
||||
noteId: note1.id,
|
||||
}, alice);
|
||||
assert.strictEqual(res.status, 204);
|
||||
{
|
||||
const res = await api('clips/add-note', {
|
||||
clipId: clip1.id,
|
||||
noteId: note1.id,
|
||||
}, alice);
|
||||
assert.strictEqual(res.status, 204);
|
||||
}
|
||||
|
||||
res = await api('clips/add-note', {
|
||||
clipId: clip2.id,
|
||||
noteId: note2.id,
|
||||
}, alice);
|
||||
assert.strictEqual(res.status, 204);
|
||||
{
|
||||
const res = await api('clips/add-note', {
|
||||
clipId: clip2.id,
|
||||
noteId: note2.id,
|
||||
}, alice);
|
||||
assert.strictEqual(res.status, 204);
|
||||
}
|
||||
|
||||
res = await api('i/export-clips', {}, alice);
|
||||
assert.strictEqual(res.status, 204);
|
||||
{
|
||||
const res = await api('i/export-clips', {}, alice);
|
||||
assert.strictEqual(res.status, 204);
|
||||
}
|
||||
|
||||
const exported = await pollFirstDriveFile();
|
||||
assert.strictEqual(exported[0].name, 'kawaii');
|
||||
|
@ -163,7 +169,7 @@ describe('export-clips', () => {
|
|||
});
|
||||
|
||||
test('Clipping other user\'s note', async () => {
|
||||
let res = await api('clips/create', {
|
||||
const res = await api('clips/create', {
|
||||
name: 'kawaii',
|
||||
description: 'kawaii',
|
||||
}, alice);
|
||||
|
@ -175,14 +181,14 @@ describe('export-clips', () => {
|
|||
visibility: 'followers',
|
||||
});
|
||||
|
||||
res = await api('clips/add-note', {
|
||||
const res2 = await api('clips/add-note', {
|
||||
clipId: clip.id,
|
||||
noteId: note.id,
|
||||
}, alice);
|
||||
assert.strictEqual(res.status, 204);
|
||||
assert.strictEqual(res2.status, 204);
|
||||
|
||||
res = await api('i/export-clips', {}, alice);
|
||||
assert.strictEqual(res.status, 204);
|
||||
const res3 = await api('i/export-clips', {}, alice);
|
||||
assert.strictEqual(res3.status, 204);
|
||||
|
||||
const exported = await pollFirstDriveFile();
|
||||
assert.strictEqual(exported[0].name, 'kawaii');
|
||||
|
|
|
@ -13,14 +13,14 @@ import { loadConfig } from '@/config.js';
|
|||
import { MiRepository, MiUser, UsersRepository, miRepository } from '@/models/_.js';
|
||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
import { jobQueue } from '@/boot/common.js';
|
||||
import { api, initTestDb, signup, successfulApiCall, uploadFile } from '../utils.js';
|
||||
import { api, castAsError, initTestDb, signup, successfulApiCall, uploadFile } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
describe('Account Move', () => {
|
||||
let jq: INestApplicationContext;
|
||||
let url: URL;
|
||||
|
||||
let root: any;
|
||||
let root: misskey.entities.SignupResponse;
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let bob: misskey.entities.SignupResponse;
|
||||
let carol: misskey.entities.SignupResponse;
|
||||
|
@ -93,8 +93,8 @@ describe('Account Move', () => {
|
|||
}, bob);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
assert.strictEqual(res.body.error.code, 'NO_SUCH_USER');
|
||||
assert.strictEqual(res.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
||||
assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_USER');
|
||||
assert.strictEqual(castAsError(res.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
||||
});
|
||||
|
||||
test('Unable to add duplicated aliases to alsoKnownAs', async () => {
|
||||
|
@ -103,8 +103,8 @@ describe('Account Move', () => {
|
|||
}, bob);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
assert.strictEqual(res.body.error.code, 'INVALID_PARAM');
|
||||
assert.strictEqual(res.body.error.id, '3d81ceae-475f-4600-b2a8-2bc116157532');
|
||||
assert.strictEqual(castAsError(res.body).error.code, 'INVALID_PARAM');
|
||||
assert.strictEqual(castAsError(res.body).error.id, '3d81ceae-475f-4600-b2a8-2bc116157532');
|
||||
});
|
||||
|
||||
test('Unable to add itself', async () => {
|
||||
|
@ -113,8 +113,8 @@ describe('Account Move', () => {
|
|||
}, bob);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
assert.strictEqual(res.body.error.code, 'FORBIDDEN_TO_SET_YOURSELF');
|
||||
assert.strictEqual(res.body.error.id, '25c90186-4ab0-49c8-9bba-a1fa6c202ba4');
|
||||
assert.strictEqual(castAsError(res.body).error.code, 'FORBIDDEN_TO_SET_YOURSELF');
|
||||
assert.strictEqual(castAsError(res.body).error.id, '25c90186-4ab0-49c8-9bba-a1fa6c202ba4');
|
||||
});
|
||||
|
||||
test('Unable to add a nonexisting local account to alsoKnownAs', async () => {
|
||||
|
@ -123,16 +123,16 @@ describe('Account Move', () => {
|
|||
}, bob);
|
||||
|
||||
assert.strictEqual(res1.status, 400);
|
||||
assert.strictEqual(res1.body.error.code, 'NO_SUCH_USER');
|
||||
assert.strictEqual(res1.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
||||
assert.strictEqual(castAsError(res1.body).error.code, 'NO_SUCH_USER');
|
||||
assert.strictEqual(castAsError(res1.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
||||
|
||||
const res2 = await api('i/update', {
|
||||
alsoKnownAs: ['@alice', 'nonexist'],
|
||||
}, bob);
|
||||
|
||||
assert.strictEqual(res2.status, 400);
|
||||
assert.strictEqual(res2.body.error.code, 'NO_SUCH_USER');
|
||||
assert.strictEqual(res2.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
||||
assert.strictEqual(castAsError(res2.body).error.code, 'NO_SUCH_USER');
|
||||
assert.strictEqual(castAsError(res2.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
||||
});
|
||||
|
||||
test('Able to add two existing local account to alsoKnownAs', async () => {
|
||||
|
@ -241,8 +241,8 @@ describe('Account Move', () => {
|
|||
}, root);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
assert.strictEqual(res.body.error.code, 'NOT_ROOT_FORBIDDEN');
|
||||
assert.strictEqual(res.body.error.id, '4362e8dc-731f-4ad8-a694-be2a88922a24');
|
||||
assert.strictEqual(castAsError(res.body).error.code, 'NOT_ROOT_FORBIDDEN');
|
||||
assert.strictEqual(castAsError(res.body).error.id, '4362e8dc-731f-4ad8-a694-be2a88922a24');
|
||||
});
|
||||
|
||||
test('Unable to move to a nonexisting local account', async () => {
|
||||
|
@ -251,8 +251,8 @@ describe('Account Move', () => {
|
|||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
assert.strictEqual(res.body.error.code, 'NO_SUCH_USER');
|
||||
assert.strictEqual(res.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
||||
assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_USER');
|
||||
assert.strictEqual(castAsError(res.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
||||
});
|
||||
|
||||
test('Unable to move if alsoKnownAs is invalid', async () => {
|
||||
|
@ -261,8 +261,8 @@ describe('Account Move', () => {
|
|||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
assert.strictEqual(res.body.error.code, 'DESTINATION_ACCOUNT_FORBIDS');
|
||||
assert.strictEqual(res.body.error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4');
|
||||
assert.strictEqual(castAsError(res.body).error.code, 'DESTINATION_ACCOUNT_FORBIDS');
|
||||
assert.strictEqual(castAsError(res.body).error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4');
|
||||
});
|
||||
|
||||
test('Relationships have been properly migrated', async () => {
|
||||
|
@ -279,36 +279,44 @@ describe('Account Move', () => {
|
|||
userId: alice.id,
|
||||
}, alice);
|
||||
assert.strictEqual(aliceFollowings.status, 200);
|
||||
assert.ok(aliceFollowings);
|
||||
assert.strictEqual(aliceFollowings.body.length, 3);
|
||||
|
||||
const carolFollowings = await api('users/following', {
|
||||
userId: carol.id,
|
||||
}, carol);
|
||||
assert.strictEqual(carolFollowings.status, 200);
|
||||
assert.ok(carolFollowings);
|
||||
assert.strictEqual(carolFollowings.body.length, 2);
|
||||
assert.strictEqual(carolFollowings.body[0].followeeId, bob.id);
|
||||
assert.strictEqual(carolFollowings.body[1].followeeId, alice.id);
|
||||
|
||||
const blockings = await api('blocking/list', {}, dave);
|
||||
assert.strictEqual(blockings.status, 200);
|
||||
assert.ok(blockings);
|
||||
assert.strictEqual(blockings.body.length, 2);
|
||||
assert.strictEqual(blockings.body[0].blockeeId, bob.id);
|
||||
assert.strictEqual(blockings.body[1].blockeeId, alice.id);
|
||||
|
||||
const mutings = await api('mute/list', {}, dave);
|
||||
assert.strictEqual(mutings.status, 200);
|
||||
assert.ok(mutings);
|
||||
assert.strictEqual(mutings.body.length, 2);
|
||||
assert.strictEqual(mutings.body[0].muteeId, bob.id);
|
||||
assert.strictEqual(mutings.body[1].muteeId, alice.id);
|
||||
|
||||
const rootLists = await api('users/lists/list', {}, root);
|
||||
assert.strictEqual(rootLists.status, 200);
|
||||
assert.ok(rootLists);
|
||||
assert.ok(rootLists.body[0].userIds);
|
||||
assert.strictEqual(rootLists.body[0].userIds.length, 2);
|
||||
assert.ok(rootLists.body[0].userIds.find((id: string) => id === bob.id));
|
||||
assert.ok(rootLists.body[0].userIds.find((id: string) => id === alice.id));
|
||||
|
||||
const eveLists = await api('users/lists/list', {}, eve);
|
||||
assert.strictEqual(eveLists.status, 200);
|
||||
assert.ok(eveLists);
|
||||
assert.ok(eveLists.body[0].userIds);
|
||||
assert.strictEqual(eveLists.body[0].userIds.length, 1);
|
||||
assert.ok(eveLists.body[0].userIds.find((id: string) => id === bob.id));
|
||||
});
|
||||
|
@ -347,8 +355,8 @@ describe('Account Move', () => {
|
|||
}, bob);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
assert.strictEqual(res.body.error.code, 'DESTINATION_ACCOUNT_FORBIDS');
|
||||
assert.strictEqual(res.body.error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4');
|
||||
assert.strictEqual(castAsError(res.body).error.code, 'DESTINATION_ACCOUNT_FORBIDS');
|
||||
assert.strictEqual(castAsError(res.body).error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4');
|
||||
});
|
||||
|
||||
test('Follow and follower counts are properly adjusted', async () => {
|
||||
|
@ -419,8 +427,9 @@ describe('Account Move', () => {
|
|||
] as const)('Prohibit access after moving: %s', async (endpoint) => {
|
||||
const res = await api(endpoint, {}, alice);
|
||||
assert.strictEqual(res.status, 403);
|
||||
assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED');
|
||||
assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
|
||||
assert.ok(res.body);
|
||||
assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED');
|
||||
assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
|
||||
});
|
||||
|
||||
test('Prohibit access after moving: /antennas/update', async () => {
|
||||
|
@ -438,16 +447,19 @@ describe('Account Move', () => {
|
|||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 403);
|
||||
assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED');
|
||||
assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
|
||||
assert.ok(res.body);
|
||||
assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED');
|
||||
assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
|
||||
});
|
||||
|
||||
test('Prohibit access after moving: /drive/files/create', async () => {
|
||||
// FIXME: 一旦逃げておく
|
||||
const res = await uploadFile(alice);
|
||||
|
||||
assert.strictEqual(res.status, 403);
|
||||
assert.strictEqual((res.body! as any as { error: misskey.api.APIError }).error.code, 'YOUR_ACCOUNT_MOVED');
|
||||
assert.strictEqual((res.body! as any as { error: misskey.api.APIError }).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
|
||||
assert.ok(res.body);
|
||||
assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED');
|
||||
assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
|
||||
});
|
||||
|
||||
test('Prohibit updating alsoKnownAs after moving', async () => {
|
||||
|
@ -456,8 +468,8 @@ describe('Account Move', () => {
|
|||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 403);
|
||||
assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED');
|
||||
assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
|
||||
assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED');
|
||||
assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -47,8 +47,8 @@ describe('Mute', () => {
|
|||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||
});
|
||||
|
||||
test('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => {
|
||||
|
@ -92,9 +92,9 @@ describe('Mute', () => {
|
|||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||
});
|
||||
|
||||
test('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async () => {
|
||||
|
@ -108,9 +108,9 @@ describe('Mute', () => {
|
|||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -124,8 +124,8 @@ describe('Mute', () => {
|
|||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||
});
|
||||
|
||||
test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
|
||||
|
@ -138,8 +138,8 @@ describe('Mute', () => {
|
|||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||
});
|
||||
|
||||
test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
|
||||
|
@ -152,8 +152,8 @@ describe('Mute', () => {
|
|||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||
});
|
||||
|
||||
test('通知にミュートしているユーザーからの引用リノートが含まれない', async () => {
|
||||
|
@ -166,8 +166,8 @@ describe('Mute', () => {
|
|||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||
});
|
||||
|
||||
test('通知にミュートしているユーザーからのリノートが含まれない', async () => {
|
||||
|
@ -180,8 +180,8 @@ describe('Mute', () => {
|
|||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||
});
|
||||
|
||||
test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => {
|
||||
|
@ -193,8 +193,8 @@ describe('Mute', () => {
|
|||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||
|
||||
await api('following/delete', { userId: alice.id }, bob);
|
||||
await api('following/delete', { userId: alice.id }, carol);
|
||||
|
@ -210,8 +210,8 @@ describe('Mute', () => {
|
|||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||
|
||||
await api('following/delete', { userId: alice.id }, bob);
|
||||
await api('following/delete', { userId: alice.id }, carol);
|
||||
|
@ -228,8 +228,8 @@ describe('Mute', () => {
|
|||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||
});
|
||||
test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
|
||||
const aliceNote = await post(alice, { text: 'hi' });
|
||||
|
@ -241,8 +241,8 @@ describe('Mute', () => {
|
|||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||
});
|
||||
|
||||
test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
|
||||
|
@ -255,8 +255,8 @@ describe('Mute', () => {
|
|||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||
});
|
||||
|
||||
test('通知にミュートしているユーザーからの引用リノートが含まれない', async () => {
|
||||
|
@ -269,8 +269,8 @@ describe('Mute', () => {
|
|||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||
});
|
||||
|
||||
test('通知にミュートしているユーザーからのリノートが含まれない', async () => {
|
||||
|
@ -283,8 +283,8 @@ describe('Mute', () => {
|
|||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||
});
|
||||
|
||||
test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => {
|
||||
|
@ -296,8 +296,8 @@ describe('Mute', () => {
|
|||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||
|
||||
await api('following/delete', { userId: alice.id }, bob);
|
||||
await api('following/delete', { userId: alice.id }, carol);
|
||||
|
@ -313,8 +313,8 @@ describe('Mute', () => {
|
|||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,16 +3,18 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Repository } from "typeorm";
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { MiNote } from '@/models/Note.js';
|
||||
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||
import { api, initTestDb, post, role, signup, uploadFile, uploadUrl } from '../utils.js';
|
||||
import { api, castAsError, initTestDb, post, role, signup, uploadFile, uploadUrl } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
describe('Note', () => {
|
||||
let Notes: any;
|
||||
let Notes: Repository<MiNote>;
|
||||
|
||||
let root: misskey.entities.SignupResponse;
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
|
@ -61,8 +63,8 @@ describe('Note', () => {
|
|||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE');
|
||||
assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
|
||||
assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_FILE');
|
||||
assert.strictEqual(castAsError(res.body).error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
|
||||
}, 1000 * 10);
|
||||
|
||||
test('存在しないファイルで怒られる', async () => {
|
||||
|
@ -72,8 +74,8 @@ describe('Note', () => {
|
|||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE');
|
||||
assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
|
||||
assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_FILE');
|
||||
assert.strictEqual(castAsError(res.body).error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
|
||||
});
|
||||
|
||||
test('不正なファイルIDで怒られる', async () => {
|
||||
|
@ -81,8 +83,8 @@ describe('Note', () => {
|
|||
fileIds: ['kyoppie'],
|
||||
}, alice);
|
||||
assert.strictEqual(res.status, 400);
|
||||
assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE');
|
||||
assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
|
||||
assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_FILE');
|
||||
assert.strictEqual(castAsError(res.body).error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
|
||||
});
|
||||
|
||||
test('返信できる', async () => {
|
||||
|
@ -101,6 +103,7 @@ describe('Note', () => {
|
|||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.createdNote.text, alicePost.text);
|
||||
assert.strictEqual(res.body.createdNote.replyId, alicePost.replyId);
|
||||
assert.ok(res.body.createdNote.reply);
|
||||
assert.strictEqual(res.body.createdNote.reply.text, bobPost.text);
|
||||
});
|
||||
|
||||
|
@ -118,6 +121,7 @@ describe('Note', () => {
|
|||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
|
||||
assert.ok(res.body.createdNote.renote);
|
||||
assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
|
||||
});
|
||||
|
||||
|
@ -137,6 +141,7 @@ describe('Note', () => {
|
|||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.createdNote.text, alicePost.text);
|
||||
assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
|
||||
assert.ok(res.body.createdNote.renote);
|
||||
assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
|
||||
});
|
||||
|
||||
|
@ -218,7 +223,7 @@ describe('Note', () => {
|
|||
}, bob);
|
||||
|
||||
assert.strictEqual(bobReply.status, 400);
|
||||
assert.strictEqual(bobReply.body.error.code, 'CANNOT_REPLY_TO_AN_INVISIBLE_NOTE');
|
||||
assert.strictEqual(castAsError(bobReply.body).error.code, 'CANNOT_REPLY_TO_AN_INVISIBLE_NOTE');
|
||||
});
|
||||
|
||||
test('visibility: specifiedなノートに対してvisibility: specifiedで返信できる', async () => {
|
||||
|
@ -256,7 +261,7 @@ describe('Note', () => {
|
|||
}, bob);
|
||||
|
||||
assert.strictEqual(bobReply.status, 400);
|
||||
assert.strictEqual(bobReply.body.error.code, 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY');
|
||||
assert.strictEqual(castAsError(bobReply.body).error.code, 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY');
|
||||
});
|
||||
|
||||
test('文字数ぎりぎりで怒られない', async () => {
|
||||
|
@ -333,6 +338,7 @@ describe('Note', () => {
|
|||
assert.strictEqual(res.body.createdNote.text, post.text);
|
||||
|
||||
const noteDoc = await Notes.findOneBy({ id: res.body.createdNote.id });
|
||||
assert.ok(noteDoc);
|
||||
assert.deepStrictEqual(noteDoc.mentions, [bob.id]);
|
||||
});
|
||||
|
||||
|
@ -345,6 +351,7 @@ describe('Note', () => {
|
|||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||
assert.ok(res.body.createdNote.files);
|
||||
assert.strictEqual(res.body.createdNote.files.length, 1);
|
||||
assert.strictEqual(res.body.createdNote.files[0].id, file.body!.id);
|
||||
});
|
||||
|
@ -363,8 +370,9 @@ describe('Note', () => {
|
|||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
const myNote = res.body.find((note: { id: string; files: { id: string }[] }) => note.id === createdNote.body.createdNote.id);
|
||||
assert.notEqual(myNote, null);
|
||||
const myNote = res.body.find(note => note.id === createdNote.body.createdNote.id);
|
||||
assert.ok(myNote);
|
||||
assert.ok(myNote.files);
|
||||
assert.strictEqual(myNote.files.length, 1);
|
||||
assert.strictEqual(myNote.files[0].id, file.body!.id);
|
||||
});
|
||||
|
@ -389,7 +397,9 @@ describe('Note', () => {
|
|||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id);
|
||||
assert.notEqual(myNote, null);
|
||||
assert.ok(myNote);
|
||||
assert.ok(myNote.renote);
|
||||
assert.ok(myNote.renote.files);
|
||||
assert.strictEqual(myNote.renote.files.length, 1);
|
||||
assert.strictEqual(myNote.renote.files[0].id, file.body!.id);
|
||||
});
|
||||
|
@ -415,7 +425,9 @@ describe('Note', () => {
|
|||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
const myNote = res.body.find((note: { id: string }) => note.id === reply.body.createdNote.id);
|
||||
assert.notEqual(myNote, null);
|
||||
assert.ok(myNote);
|
||||
assert.ok(myNote.reply);
|
||||
assert.ok(myNote.reply.files);
|
||||
assert.strictEqual(myNote.reply.files.length, 1);
|
||||
assert.strictEqual(myNote.reply.files[0].id, file.body!.id);
|
||||
});
|
||||
|
@ -446,7 +458,10 @@ describe('Note', () => {
|
|||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id);
|
||||
assert.notEqual(myNote, null);
|
||||
assert.ok(myNote);
|
||||
assert.ok(myNote.renote);
|
||||
assert.ok(myNote.renote.reply);
|
||||
assert.ok(myNote.renote.reply.files);
|
||||
assert.strictEqual(myNote.renote.reply.files.length, 1);
|
||||
assert.strictEqual(myNote.renote.reply.files[0].id, file.body!.id);
|
||||
});
|
||||
|
@ -474,7 +489,7 @@ describe('Note', () => {
|
|||
priority: 0,
|
||||
value: true,
|
||||
},
|
||||
} as any,
|
||||
},
|
||||
}, root);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
|
@ -498,7 +513,7 @@ describe('Note', () => {
|
|||
}, alice);
|
||||
|
||||
assert.strictEqual(liftnsfw.status, 400);
|
||||
assert.strictEqual(liftnsfw.body.error.code, 'RESTRICTED_BY_ROLE');
|
||||
assert.strictEqual(castAsError(liftnsfw.body).error.code, 'RESTRICTED_BY_ROLE');
|
||||
|
||||
const oldaddnsfw = await api('drive/files/update', {
|
||||
fileId: file.body!.id,
|
||||
|
@ -710,7 +725,7 @@ describe('Note', () => {
|
|||
}, alice);
|
||||
|
||||
assert.strictEqual(note1.status, 400);
|
||||
assert.strictEqual(note1.body.error.code, 'CONTAINS_PROHIBITED_WORDS');
|
||||
assert.strictEqual(castAsError(note1.body).error.code, 'CONTAINS_PROHIBITED_WORDS');
|
||||
});
|
||||
|
||||
test('禁止ワードを含む投稿はエラーになる (正規表現)', async () => {
|
||||
|
@ -727,7 +742,7 @@ describe('Note', () => {
|
|||
}, alice);
|
||||
|
||||
assert.strictEqual(note2.status, 400);
|
||||
assert.strictEqual(note2.body.error.code, 'CONTAINS_PROHIBITED_WORDS');
|
||||
assert.strictEqual(castAsError(note2.body).error.code, 'CONTAINS_PROHIBITED_WORDS');
|
||||
});
|
||||
|
||||
test('禁止ワードを含む投稿はエラーになる (スペースアンド)', async () => {
|
||||
|
@ -744,7 +759,7 @@ describe('Note', () => {
|
|||
}, alice);
|
||||
|
||||
assert.strictEqual(note2.status, 400);
|
||||
assert.strictEqual(note2.body.error.code, 'CONTAINS_PROHIBITED_WORDS');
|
||||
assert.strictEqual(castAsError(note2.body).error.code, 'CONTAINS_PROHIBITED_WORDS');
|
||||
});
|
||||
|
||||
test('禁止ワードを含んでるリモートノートもエラーになる', async () => {
|
||||
|
@ -786,7 +801,7 @@ describe('Note', () => {
|
|||
priority: 1,
|
||||
value: 0,
|
||||
},
|
||||
} as any,
|
||||
},
|
||||
}, root);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
|
@ -807,7 +822,7 @@ describe('Note', () => {
|
|||
}, alice);
|
||||
|
||||
assert.strictEqual(note.status, 400);
|
||||
assert.strictEqual(note.body.error.code, 'CONTAINS_TOO_MANY_MENTIONS');
|
||||
assert.strictEqual(castAsError(note.body).error.code, 'CONTAINS_TOO_MANY_MENTIONS');
|
||||
|
||||
await api('admin/roles/unassign', {
|
||||
userId: alice.id,
|
||||
|
@ -840,7 +855,7 @@ describe('Note', () => {
|
|||
priority: 1,
|
||||
value: 0,
|
||||
},
|
||||
} as any,
|
||||
},
|
||||
}, root);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
|
@ -863,7 +878,7 @@ describe('Note', () => {
|
|||
}, alice);
|
||||
|
||||
assert.strictEqual(note.status, 400);
|
||||
assert.strictEqual(note.body.error.code, 'CONTAINS_TOO_MANY_MENTIONS');
|
||||
assert.strictEqual(castAsError(note.body).error.code, 'CONTAINS_TOO_MANY_MENTIONS');
|
||||
|
||||
await api('admin/roles/unassign', {
|
||||
userId: alice.id,
|
||||
|
@ -896,7 +911,7 @@ describe('Note', () => {
|
|||
priority: 1,
|
||||
value: 1,
|
||||
},
|
||||
} as any,
|
||||
},
|
||||
}, root);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
|
@ -951,6 +966,7 @@ describe('Note', () => {
|
|||
|
||||
assert.strictEqual(deleteOneRes.status, 204);
|
||||
let mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id });
|
||||
assert.ok(mainNote);
|
||||
assert.strictEqual(mainNote.repliesCount, 1);
|
||||
|
||||
const deleteTwoRes = await api('notes/delete', {
|
||||
|
@ -959,6 +975,7 @@ describe('Note', () => {
|
|||
|
||||
assert.strictEqual(deleteTwoRes.status, 204);
|
||||
mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id });
|
||||
assert.ok(mainNote);
|
||||
assert.strictEqual(mainNote.repliesCount, 0);
|
||||
});
|
||||
});
|
||||
|
@ -980,7 +997,7 @@ describe('Note', () => {
|
|||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
assert.strictEqual(res.body.error.code, 'UNAVAILABLE');
|
||||
assert.strictEqual(castAsError(res.body).error.code, 'UNAVAILABLE');
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
@ -992,7 +1009,7 @@ describe('Note', () => {
|
|||
const res = await api('notes/translate', { noteId: 'foo', targetLang: 'ja' }, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
assert.strictEqual(res.body.error.code, 'NO_SUCH_NOTE');
|
||||
assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_NOTE');
|
||||
});
|
||||
|
||||
test('不可視なノートは翻訳できない', async () => {
|
||||
|
@ -1000,7 +1017,7 @@ describe('Note', () => {
|
|||
const bobTranslateAttempt = await api('notes/translate', { noteId: aliceNote.id, targetLang: 'ja' }, bob);
|
||||
|
||||
assert.strictEqual(bobTranslateAttempt.status, 400);
|
||||
assert.strictEqual(bobTranslateAttempt.body.error.code, 'CANNOT_TRANSLATE_INVISIBLE_NOTE');
|
||||
assert.strictEqual(castAsError(bobTranslateAttempt.body).error.code, 'CANNOT_TRANSLATE_INVISIBLE_NOTE');
|
||||
});
|
||||
|
||||
test('text: null なノートを翻訳すると空のレスポンスが返ってくる', async () => {
|
||||
|
@ -1016,7 +1033,7 @@ describe('Note', () => {
|
|||
|
||||
// NOTE: デフォルトでは登録されていないので落ちる
|
||||
assert.strictEqual(res.status, 400);
|
||||
assert.strictEqual(res.body.error.code, 'UNAVAILABLE');
|
||||
assert.strictEqual(castAsError(res.body).error.code, 'UNAVAILABLE');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -42,9 +42,9 @@ describe('Renote Mute', () => {
|
|||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolRenote.id), false);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolRenote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
|
||||
});
|
||||
|
||||
test('タイムラインにリノートミュートしているユーザーの引用が含まれる', async () => {
|
||||
|
@ -59,9 +59,9 @@ describe('Renote Mute', () => {
|
|||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolRenote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolRenote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
|
||||
});
|
||||
|
||||
// #12956
|
||||
|
@ -76,8 +76,8 @@ describe('Renote Mute', () => {
|
|||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobRenote.id), true);
|
||||
});
|
||||
|
||||
test('ストリームにリノートミュートしているユーザーのリノートが流れない', async () => {
|
||||
|
|
|
@ -33,9 +33,9 @@ describe('Note thread mute', () => {
|
|||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolReply.id), false);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolReplyWithoutMention.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolReply.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolReplyWithoutMention.id), false);
|
||||
});
|
||||
|
||||
test('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => {
|
||||
|
@ -93,8 +93,8 @@ describe('Note thread mute', () => {
|
|||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReply.id), false);
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReplyWithoutMention.id), false);
|
||||
assert.strictEqual(res.body.some(notification => 'note' in notification && notification.note.id === carolReply.id), false);
|
||||
assert.strictEqual(res.body.some(notification => 'note' in notification && notification.note.id === carolReplyWithoutMention.id), false);
|
||||
|
||||
// NOTE: bobの投稿はスレッドミュート前に行われたため通知に含まれていてもよい
|
||||
});
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
import * as assert from 'assert';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { Redis } from 'ioredis';
|
||||
import { loadConfig } from '@/config.js';
|
||||
import { api, post, randomString, sendEnvUpdateRequest, signup, uploadUrl } from '../utils.js';
|
||||
import { loadConfig } from '@/config.js';
|
||||
|
||||
function genHost() {
|
||||
return randomString() + '.example.com';
|
||||
|
@ -37,8 +37,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi');
|
||||
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi');
|
||||
});
|
||||
|
||||
test.concurrent('フォローしているユーザーのノートが含まれる', async () => {
|
||||
|
@ -53,8 +53,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => {
|
||||
|
@ -69,9 +69,9 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi');
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('withReplies: false でフォローしているユーザーの他人への返信が含まれない', async () => {
|
||||
|
@ -86,8 +86,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('withReplies: true でフォローしているユーザーの他人への返信が含まれる', async () => {
|
||||
|
@ -103,8 +103,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('withReplies: true でフォローしているユーザーの他人へのDM返信が含まれない', async () => {
|
||||
|
@ -120,8 +120,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => {
|
||||
|
@ -137,8 +137,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの visibility: followers な投稿への返信が含まれる', async () => {
|
||||
|
@ -156,9 +156,9 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
|
||||
assert.strictEqual(res.body.find((note: any) => note.id === carolNote.id).text, 'hi');
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
|
||||
assert.strictEqual(res.body.find(note => note.id === carolNote.id)?.text, 'hi');
|
||||
});
|
||||
|
||||
test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの投稿への visibility: specified な返信が含まれない', async () => {
|
||||
|
@ -175,8 +175,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('withReplies: false でフォローしているユーザーのそのユーザー自身への返信が含まれる', async () => {
|
||||
|
@ -191,8 +191,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
|
||||
|
@ -207,8 +207,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('自分の他人への返信が含まれる', async () => {
|
||||
|
@ -221,8 +221,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('フォローしているユーザーの他人の投稿のリノートが含まれる', async () => {
|
||||
|
@ -237,8 +237,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('[withRenotes: false] フォローしているユーザーの他人の投稿のリノートが含まれない', async () => {
|
||||
|
@ -255,8 +255,8 @@ describe('Timelines', () => {
|
|||
withRenotes: false,
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('[withRenotes: false] フォローしているユーザーの他人の投稿の引用が含まれる', async () => {
|
||||
|
@ -273,8 +273,8 @@ describe('Timelines', () => {
|
|||
withRenotes: false,
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('フォローしているユーザーの他人への visibility: specified なノートが含まれない', async () => {
|
||||
|
@ -288,7 +288,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => {
|
||||
|
@ -304,8 +304,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => {
|
||||
|
@ -322,8 +322,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
|
||||
|
@ -338,7 +338,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => {
|
||||
|
@ -353,7 +353,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('[withFiles: true] フォローしているユーザーのファイル付きノートのみ含まれる', async () => {
|
||||
|
@ -374,10 +374,10 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/timeline', { limit: 100, withFiles: true }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote1.id), false);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote2.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote1.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote2.id), false);
|
||||
}, 1000 * 10);
|
||||
|
||||
test.concurrent('フォローしているユーザーのチャンネル投稿が含まれない', async () => {
|
||||
|
@ -392,7 +392,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('自分の visibility: specified なノートが含まれる', async () => {
|
||||
|
@ -404,8 +404,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi');
|
||||
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi');
|
||||
});
|
||||
|
||||
test.concurrent('フォローしているユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれる', async () => {
|
||||
|
@ -419,8 +419,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi');
|
||||
});
|
||||
|
||||
test.concurrent('フォローしていないユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれない', async () => {
|
||||
|
@ -432,7 +432,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('フォローしているユーザーの自身を visibleUserIds に指定していない visibility: specified なノートが含まれない', async () => {
|
||||
|
@ -446,7 +446,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('フォローしていないユーザーからの visibility: specified なノートに返信したときの自身のノートが含まれる', async () => {
|
||||
|
@ -459,8 +459,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'ok');
|
||||
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'ok');
|
||||
});
|
||||
|
||||
/* TODO
|
||||
|
@ -474,8 +474,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'ok');
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.find(note => note.id === bobNote.id).text, 'ok');
|
||||
});
|
||||
*/
|
||||
|
||||
|
@ -490,7 +490,45 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('FTT: ローカルユーザーの HTL にはプッシュされる', async () => {
|
||||
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||
|
||||
await api('following/create', {
|
||||
userId: alice.id,
|
||||
}, bob);
|
||||
|
||||
const aliceNote = await post(alice, { text: 'I\'m Alice.' });
|
||||
const bobNote = await post(bob, { text: 'I\'m Bob.' });
|
||||
const carolNote = await post(carol, { text: 'I\'m Carol.' });
|
||||
|
||||
await waitForPushToTl();
|
||||
|
||||
// NOTE: notes/timeline だと DB へのフォールバックが効くので Redis を直接見て確かめる
|
||||
assert.strictEqual(await redisForTimelines.exists(`list:homeTimeline:${bob.id}`), 1);
|
||||
|
||||
const bobHTL = await redisForTimelines.lrange(`list:homeTimeline:${bob.id}`, 0, -1);
|
||||
assert.strictEqual(bobHTL.includes(aliceNote.id), true);
|
||||
assert.strictEqual(bobHTL.includes(bobNote.id), true);
|
||||
assert.strictEqual(bobHTL.includes(carolNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('FTT: リモートユーザーの HTL にはプッシュされない', async () => {
|
||||
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
|
||||
|
||||
await api('following/create', {
|
||||
userId: alice.id,
|
||||
}, bob);
|
||||
|
||||
await post(alice, { text: 'I\'m Alice.' });
|
||||
await post(bob, { text: 'I\'m Bob.' });
|
||||
|
||||
await waitForPushToTl();
|
||||
|
||||
// NOTE: notes/timeline だと DB へのフォールバックが効くので Redis を直接見て確かめる
|
||||
assert.strictEqual(await redisForTimelines.exists(`list:homeTimeline:${bob.id}`), 0);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -505,8 +543,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('他人の他人への返信が含まれない', async () => {
|
||||
|
@ -519,8 +557,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('他人のその人自身への返信が含まれる', async () => {
|
||||
|
@ -533,8 +571,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('チャンネル投稿が含まれない', async () => {
|
||||
|
@ -547,7 +585,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('リモートユーザーのノートが含まれない', async () => {
|
||||
|
@ -559,7 +597,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
});
|
||||
|
||||
// 含まれても良いと思うけど実装が面倒なので含まれない
|
||||
|
@ -575,8 +613,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('ミュートしているユーザーのノートが含まれない', async () => {
|
||||
|
@ -591,8 +629,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => {
|
||||
|
@ -608,8 +646,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => {
|
||||
|
@ -626,8 +664,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
|
||||
|
@ -642,8 +680,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('[withReplies: true] 他人の他人への返信が含まれる', async () => {
|
||||
|
@ -656,7 +694,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/local-timeline', { limit: 100, withReplies: true }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
|
||||
|
@ -670,8 +708,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/local-timeline', { limit: 100, withFiles: true }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
|
||||
}, 1000 * 10);
|
||||
});
|
||||
|
||||
|
@ -685,7 +723,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('ローカルユーザーの visibility: home なノートが含まれない', async () => {
|
||||
|
@ -697,7 +735,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('フォローしているローカルユーザーの visibility: home なノートが含まれる', async () => {
|
||||
|
@ -711,7 +749,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
|
||||
|
@ -726,8 +764,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('他人の他人への返信が含まれない', async () => {
|
||||
|
@ -740,8 +778,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('リモートユーザーのノートが含まれない', async () => {
|
||||
|
@ -753,7 +791,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/local-timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
|
||||
|
@ -768,7 +806,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => {
|
||||
|
@ -783,7 +821,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('[withReplies: true] 他人の他人への返信が含まれる', async () => {
|
||||
|
@ -796,7 +834,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/hybrid-timeline', { limit: 100, withReplies: true }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
|
||||
|
@ -810,8 +848,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/hybrid-timeline', { limit: 100, withFiles: true }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
|
||||
}, 1000 * 10);
|
||||
});
|
||||
|
||||
|
@ -828,7 +866,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('リスインしているフォローしていないユーザーの visibility: home なノートが含まれる', async () => {
|
||||
|
@ -843,7 +881,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('リスインしているフォローしていないユーザーの visibility: followers なノートが含まれない', async () => {
|
||||
|
@ -858,7 +896,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('リスインしているフォローしていないユーザーの他人への返信が含まれない', async () => {
|
||||
|
@ -874,7 +912,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('リスインしているフォローしていないユーザーのユーザー自身への返信が含まれる', async () => {
|
||||
|
@ -890,8 +928,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('withReplies: false でリスインしているフォローしていないユーザーからの自分への返信が含まれる', async () => {
|
||||
|
@ -908,7 +946,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('withReplies: false でリスインしているフォローしていないユーザーの他人への返信が含まれない', async () => {
|
||||
|
@ -925,7 +963,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('withReplies: true でリスインしているフォローしていないユーザーの他人への返信が含まれる', async () => {
|
||||
|
@ -942,7 +980,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('リスインしているフォローしているユーザーの visibility: home なノートが含まれる', async () => {
|
||||
|
@ -958,7 +996,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('リスインしているフォローしているユーザーの visibility: followers なノートが含まれる', async () => {
|
||||
|
@ -974,8 +1012,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi');
|
||||
});
|
||||
|
||||
test.concurrent('リスインしている自分の visibility: followers なノートが含まれる', async () => {
|
||||
|
@ -990,8 +1028,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi');
|
||||
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi');
|
||||
});
|
||||
|
||||
test.concurrent('リスインしているユーザーのチャンネルノートが含まれない', async () => {
|
||||
|
@ -1007,7 +1045,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('[withFiles: true] リスインしているユーザーのファイル付きノートのみ含まれる', async () => {
|
||||
|
@ -1023,8 +1061,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/user-list-timeline', { listId: list.id, withFiles: true }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
|
||||
}, 1000 * 10);
|
||||
|
||||
test.concurrent('リスインしているユーザーの自身宛ての visibility: specified なノートが含まれる', async () => {
|
||||
|
@ -1039,8 +1077,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi');
|
||||
});
|
||||
|
||||
test.concurrent('リスインしているユーザーの自身宛てではない visibility: specified なノートが含まれない', async () => {
|
||||
|
@ -1056,7 +1094,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1070,7 +1108,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('users/notes', { userId: bob.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('フォローしていないユーザーの visibility: followers なノートが含まれない', async () => {
|
||||
|
@ -1082,7 +1120,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('users/notes', { userId: bob.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => {
|
||||
|
@ -1096,8 +1134,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('users/notes', { userId: bob.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi');
|
||||
});
|
||||
|
||||
test.concurrent('自身の visibility: followers なノートが含まれる', async () => {
|
||||
|
@ -1109,8 +1147,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('users/notes', { userId: alice.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi');
|
||||
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi');
|
||||
});
|
||||
|
||||
test.concurrent('チャンネル投稿が含まれない', async () => {
|
||||
|
@ -1123,7 +1161,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('users/notes', { userId: bob.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('[withReplies: false] 他人への返信が含まれない', async () => {
|
||||
|
@ -1137,8 +1175,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('users/notes', { userId: bob.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('[withReplies: true] 他人への返信が含まれる', async () => {
|
||||
|
@ -1152,8 +1190,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('[withReplies: true] 他人への visibility: specified な返信が含まれない', async () => {
|
||||
|
@ -1167,8 +1205,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
|
||||
|
@ -1182,8 +1220,8 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('users/notes', { userId: bob.id, withFiles: true }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
|
||||
}, 1000 * 10);
|
||||
|
||||
test.concurrent('[withChannelNotes: true] チャンネル投稿が含まれる', async () => {
|
||||
|
@ -1196,7 +1234,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('[withChannelNotes: true] 他人が取得した場合センシティブチャンネル投稿が含まれない', async () => {
|
||||
|
@ -1209,7 +1247,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('[withChannelNotes: true] 自分が取得した場合センシティブチャンネル投稿が含まれる', async () => {
|
||||
|
@ -1222,7 +1260,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, bob);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('ミュートしているユーザーに関連する投稿が含まれない', async () => {
|
||||
|
@ -1237,7 +1275,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('users/notes', { userId: bob.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('ミュートしていても userId に指定したユーザーの投稿が含まれる', async () => {
|
||||
|
@ -1253,9 +1291,9 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('users/notes', { userId: bob.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote3.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote3.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('自身の visibility: specified なノートが含まれる', async () => {
|
||||
|
@ -1267,7 +1305,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('users/notes', { userId: alice.id, withReplies: true }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||
});
|
||||
|
||||
test.concurrent('visibleUserIds に指定されてない visibility: specified なノートが含まれない', async () => {
|
||||
|
@ -1279,7 +1317,7 @@ describe('Timelines', () => {
|
|||
|
||||
const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
});
|
||||
|
||||
/** @see https://github.com/misskey-dev/misskey/issues/14000 */
|
||||
|
|
|
@ -231,7 +231,7 @@ describe('ユーザー', () => {
|
|||
rolePublic = await role(root, { isPublic: true, name: 'Public Role' });
|
||||
await api('admin/roles/assign', { userId: userRolePublic.id, roleId: rolePublic.id }, root);
|
||||
userRoleBadge = await signup({ username: 'userRoleBadge' });
|
||||
roleBadge = await role(root, { asBadge: true, name: 'Badge Role' });
|
||||
roleBadge = await role(root, { asBadge: true, name: 'Badge Role', isPublic: true });
|
||||
await api('admin/roles/assign', { userId: userRoleBadge.id, roleId: roleBadge.id }, root);
|
||||
userSilenced = await signup({ username: 'userSilenced' });
|
||||
await post(userSilenced, { text: 'test' });
|
||||
|
@ -655,7 +655,16 @@ describe('ユーザー', () => {
|
|||
iconUrl: roleBadge.iconUrl,
|
||||
displayOrder: roleBadge.displayOrder,
|
||||
}]);
|
||||
assert.deepStrictEqual(response.roles, []); // バッヂだからといってrolesが取れるとは限らない
|
||||
assert.deepStrictEqual(response.roles, [{
|
||||
id: roleBadge.id,
|
||||
name: roleBadge.name,
|
||||
color: roleBadge.color,
|
||||
iconUrl: roleBadge.iconUrl,
|
||||
description: roleBadge.description,
|
||||
isModerator: roleBadge.isModerator,
|
||||
isAdministrator: roleBadge.isAdministrator,
|
||||
displayOrder: roleBadge.displayOrder,
|
||||
}]);
|
||||
});
|
||||
test('をID指定のリスト形式で取得することができる(空)', async () => {
|
||||
const parameters = { userIds: [] };
|
||||
|
|
|
@ -23,10 +23,10 @@ describe('ApMfmService', () => {
|
|||
|
||||
describe('getNoteHtml', () => {
|
||||
test('Do not provide _misskey_content for simple text', () => {
|
||||
const note: MiNote = {
|
||||
const note = {
|
||||
text: 'テキスト #タグ @mention 🍊 :emoji: https://example.com',
|
||||
mentionedRemoteUsers: '[]',
|
||||
} as any;
|
||||
};
|
||||
|
||||
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
|
||||
|
||||
|
@ -35,10 +35,10 @@ describe('ApMfmService', () => {
|
|||
});
|
||||
|
||||
test('Provide _misskey_content for MFM', () => {
|
||||
const note: MiNote = {
|
||||
const note = {
|
||||
text: '$[tada foo]',
|
||||
mentionedRemoteUsers: '[]',
|
||||
} as any;
|
||||
};
|
||||
|
||||
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import { ModuleMocker } from 'jest-mock';
|
|||
import { Test } from '@nestjs/testing';
|
||||
import { afterAll, beforeAll, describe, test } from '@jest/globals';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { FileInfoService } from '@/core/FileInfoService.js';
|
||||
import { FileInfo, FileInfoService } from '@/core/FileInfoService.js';
|
||||
//import { DI } from '@/di-symbols.js';
|
||||
import { AiService } from '@/core/AiService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
|
@ -28,6 +28,15 @@ const moduleMocker = new ModuleMocker(global);
|
|||
describe('FileInfoService', () => {
|
||||
let app: TestingModule;
|
||||
let fileInfoService: FileInfoService;
|
||||
const strip = (fileInfo: FileInfo): Omit<Partial<FileInfo>, 'warnings' | 'blurhash' | 'sensitive' | 'porn'> => {
|
||||
const fi: Partial<FileInfo> = fileInfo;
|
||||
delete fi.warnings;
|
||||
delete fi.sensitive;
|
||||
delete fi.blurhash;
|
||||
delete fi.porn;
|
||||
|
||||
return fi;
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await Test.createTestingModule({
|
||||
|
@ -63,11 +72,7 @@ describe('FileInfoService', () => {
|
|||
|
||||
test('Empty file', async () => {
|
||||
const path = `${resources}/emptyfile`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 0,
|
||||
md5: 'd41d8cd98f00b204e9800998ecf8427e',
|
||||
|
@ -84,11 +89,7 @@ describe('FileInfoService', () => {
|
|||
describe('IMAGE', () => {
|
||||
test('Generic JPEG', async () => {
|
||||
const path = `${resources}/192.jpg`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 5131,
|
||||
md5: '8c9ed0677dd2b8f9f7472c3af247e5e3',
|
||||
|
@ -104,11 +105,7 @@ describe('FileInfoService', () => {
|
|||
|
||||
test('Generic APNG', async () => {
|
||||
const path = `${resources}/anime.png`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 1868,
|
||||
md5: '08189c607bea3b952704676bb3c979e0',
|
||||
|
@ -124,11 +121,7 @@ describe('FileInfoService', () => {
|
|||
|
||||
test('Generic AGIF', async () => {
|
||||
const path = `${resources}/anime.gif`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 2248,
|
||||
md5: '32c47a11555675d9267aee1a86571e7e',
|
||||
|
@ -144,11 +137,7 @@ describe('FileInfoService', () => {
|
|||
|
||||
test('PNG with alpha', async () => {
|
||||
const path = `${resources}/with-alpha.png`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 3772,
|
||||
md5: 'f73535c3e1e27508885b69b10cf6e991',
|
||||
|
@ -164,11 +153,7 @@ describe('FileInfoService', () => {
|
|||
|
||||
test('Generic SVG', async () => {
|
||||
const path = `${resources}/image.svg`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 505,
|
||||
md5: 'b6f52b4b021e7b92cdd04509c7267965',
|
||||
|
@ -185,11 +170,7 @@ describe('FileInfoService', () => {
|
|||
test('SVG with XML definition', async () => {
|
||||
// https://github.com/misskey-dev/misskey/issues/4413
|
||||
const path = `${resources}/with-xml-def.svg`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 544,
|
||||
md5: '4b7a346cde9ccbeb267e812567e33397',
|
||||
|
@ -205,11 +186,7 @@ describe('FileInfoService', () => {
|
|||
|
||||
test('Dimension limit', async () => {
|
||||
const path = `${resources}/25000x25000.png`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 75933,
|
||||
md5: '268c5dde99e17cf8fe09f1ab3f97df56',
|
||||
|
@ -225,11 +202,7 @@ describe('FileInfoService', () => {
|
|||
|
||||
test('Rotate JPEG', async () => {
|
||||
const path = `${resources}/rotate.jpg`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 12624,
|
||||
md5: '68d5b2d8d1d1acbbce99203e3ec3857e',
|
||||
|
@ -247,11 +220,7 @@ describe('FileInfoService', () => {
|
|||
describe('AUDIO', () => {
|
||||
test('MP3', async () => {
|
||||
const path = `${resources}/kick_gaba7.mp3`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
delete info.width;
|
||||
delete info.height;
|
||||
delete info.orientation;
|
||||
|
@ -267,11 +236,7 @@ describe('FileInfoService', () => {
|
|||
|
||||
test('WAV', async () => {
|
||||
const path = `${resources}/kick_gaba7.wav`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
delete info.width;
|
||||
delete info.height;
|
||||
delete info.orientation;
|
||||
|
@ -287,11 +252,7 @@ describe('FileInfoService', () => {
|
|||
|
||||
test('AAC', async () => {
|
||||
const path = `${resources}/kick_gaba7.aac`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
delete info.width;
|
||||
delete info.height;
|
||||
delete info.orientation;
|
||||
|
@ -307,11 +268,7 @@ describe('FileInfoService', () => {
|
|||
|
||||
test('FLAC', async () => {
|
||||
const path = `${resources}/kick_gaba7.flac`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
delete info.width;
|
||||
delete info.height;
|
||||
delete info.orientation;
|
||||
|
@ -327,11 +284,7 @@ describe('FileInfoService', () => {
|
|||
|
||||
test('MPEG-4 AUDIO (M4A)', async () => {
|
||||
const path = `${resources}/kick_gaba7.m4a`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
delete info.width;
|
||||
delete info.height;
|
||||
delete info.orientation;
|
||||
|
@ -347,11 +300,7 @@ describe('FileInfoService', () => {
|
|||
|
||||
test('WEBM AUDIO', async () => {
|
||||
const path = `${resources}/kick_gaba7.webm`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
delete info.width;
|
||||
delete info.height;
|
||||
delete info.orientation;
|
||||
|
|
|
@ -0,0 +1,265 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { describe, jest, test } from '@jest/globals';
|
||||
import { In } from 'typeorm';
|
||||
import { UserSearchService } from '@/core/UserSearchService.js';
|
||||
import { FollowingsRepository, MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
|
||||
describe('UserSearchService', () => {
|
||||
let app: TestingModule;
|
||||
let service: UserSearchService;
|
||||
|
||||
let usersRepository: UsersRepository;
|
||||
let followingsRepository: FollowingsRepository;
|
||||
let idService: IdService;
|
||||
let userProfilesRepository: UserProfilesRepository;
|
||||
|
||||
let root: MiUser;
|
||||
let alice: MiUser;
|
||||
let alyce: MiUser;
|
||||
let alycia: MiUser;
|
||||
let alysha: MiUser;
|
||||
let alyson: MiUser;
|
||||
let alyssa: MiUser;
|
||||
let bob: MiUser;
|
||||
let bobbi: MiUser;
|
||||
let bobbie: MiUser;
|
||||
let bobby: MiUser;
|
||||
|
||||
async function createUser(data: Partial<MiUser> = {}) {
|
||||
const user = await usersRepository
|
||||
.insert({
|
||||
id: idService.gen(),
|
||||
...data,
|
||||
})
|
||||
.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
await userProfilesRepository.insert({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async function createFollowings(follower: MiUser, followees: MiUser[]) {
|
||||
for (const followee of followees) {
|
||||
await followingsRepository.insert({
|
||||
id: idService.gen(),
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function setActive(users: MiUser[]) {
|
||||
for (const user of users) {
|
||||
await usersRepository.update(user.id, {
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function setInactive(users: MiUser[]) {
|
||||
for (const user of users) {
|
||||
await usersRepository.update(user.id, {
|
||||
updatedAt: new Date(0),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function setSuspended(users: MiUser[]) {
|
||||
for (const user of users) {
|
||||
await usersRepository.update(user.id, {
|
||||
isSuspended: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await Test
|
||||
.createTestingModule({
|
||||
imports: [
|
||||
GlobalModule,
|
||||
],
|
||||
providers: [
|
||||
UserSearchService,
|
||||
{
|
||||
provide: UserEntityService, useFactory: jest.fn(() => ({
|
||||
// とりあえずIDが返れば確認が出来るので
|
||||
packMany: (value: any) => value,
|
||||
})),
|
||||
},
|
||||
IdService,
|
||||
],
|
||||
})
|
||||
.compile();
|
||||
|
||||
await app.init();
|
||||
|
||||
usersRepository = app.get(DI.usersRepository);
|
||||
userProfilesRepository = app.get(DI.userProfilesRepository);
|
||||
followingsRepository = app.get(DI.followingsRepository);
|
||||
|
||||
service = app.get(UserSearchService);
|
||||
idService = app.get(IdService);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true });
|
||||
alice = await createUser({ username: 'Alice', usernameLower: 'alice' });
|
||||
alyce = await createUser({ username: 'Alyce', usernameLower: 'alyce' });
|
||||
alycia = await createUser({ username: 'Alycia', usernameLower: 'alycia' });
|
||||
alysha = await createUser({ username: 'Alysha', usernameLower: 'alysha' });
|
||||
alyson = await createUser({ username: 'Alyson', usernameLower: 'alyson', host: 'example.com' });
|
||||
alyssa = await createUser({ username: 'Alyssa', usernameLower: 'alyssa', host: 'example.com' });
|
||||
bob = await createUser({ username: 'Bob', usernameLower: 'bob' });
|
||||
bobbi = await createUser({ username: 'Bobbi', usernameLower: 'bobbi' });
|
||||
bobbie = await createUser({ username: 'Bobbie', usernameLower: 'bobbie', host: 'example.com' });
|
||||
bobby = await createUser({ username: 'Bobby', usernameLower: 'bobby', host: 'example.com' });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await usersRepository.delete({});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe('search', () => {
|
||||
test('フォロー中のアクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
|
||||
await createFollowings(root, [alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
await setActive([alice, alyce, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
await setInactive([alycia, alysha, alyson]);
|
||||
|
||||
const result = await service.search(
|
||||
{ username: 'al' },
|
||||
{ limit: 100 },
|
||||
root,
|
||||
);
|
||||
|
||||
// alycia, alysha, alysonは非アクティブなので後ろに行く
|
||||
expect(result).toEqual([alice, alyce, alyssa, alycia, alysha, alyson].map(x => x.id));
|
||||
});
|
||||
|
||||
test('フォロー中の非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
|
||||
await createFollowings(root, [alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
|
||||
const result = await service.search(
|
||||
{ username: 'al' },
|
||||
{ limit: 100 },
|
||||
root,
|
||||
);
|
||||
|
||||
// alice, alyceはフォローしていないので後ろに行く
|
||||
expect(result).toEqual([alycia, alysha, alyson, alyssa, alice, alyce].map(x => x.id));
|
||||
});
|
||||
|
||||
test('フォローしていないアクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
|
||||
await setActive([alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
await setInactive([alice, alyce, alycia]);
|
||||
|
||||
const result = await service.search(
|
||||
{ username: 'al' },
|
||||
{ limit: 100 },
|
||||
root,
|
||||
);
|
||||
|
||||
// alice, alyce, alyciaは非アクティブなので後ろに行く
|
||||
expect(result).toEqual([alysha, alyson, alyssa, alice, alyce, alycia].map(x => x.id));
|
||||
});
|
||||
|
||||
test('フォローしていない非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
|
||||
await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
|
||||
const result = await service.search(
|
||||
{ username: 'al' },
|
||||
{ limit: 100 },
|
||||
root,
|
||||
);
|
||||
|
||||
expect(result).toEqual([alice, alyce, alycia, alysha, alyson, alyssa].map(x => x.id));
|
||||
});
|
||||
|
||||
test('フォロー(アクティブ)、フォロー(非アクティブ)、非フォロー(アクティブ)、非フォロー(非アクティブ)混在時の優先順位度確認', async () => {
|
||||
await createFollowings(root, [alyson, alyssa, bob, bobbi, bobbie]);
|
||||
await setActive([root, alyssa, bob, bobbi, alyce, alycia]);
|
||||
await setInactive([alyson, alice, alysha, bobbie, bobby]);
|
||||
|
||||
const result = await service.search(
|
||||
{ },
|
||||
{ limit: 100 },
|
||||
root,
|
||||
);
|
||||
|
||||
// 見る用
|
||||
// const users = await usersRepository.findBy({ id: In(result) }).then(it => new Map(it.map(x => [x.id, x])));
|
||||
// console.log(result.map(x => users.get(x as any)).map(it => it?.username));
|
||||
|
||||
// フォローしててアクティブなので先頭: alyssa, bob, bobbi
|
||||
// フォローしてて非アクティブなので次: alyson, bobbie
|
||||
// フォローしてないけどアクティブなので次: alyce, alycia, root(アルファベット順的にここになる)
|
||||
// フォローしてないし非アクティブなので最後: alice, alysha, bobby
|
||||
expect(result).toEqual([alyssa, bob, bobbi, alyson, bobbie, alyce, alycia, root, alice, alysha, bobby].map(x => x.id));
|
||||
});
|
||||
|
||||
test('[非ログイン] アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
|
||||
await setActive([alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
await setInactive([alice, alyce, alycia]);
|
||||
|
||||
const result = await service.search(
|
||||
{ username: 'al' },
|
||||
{ limit: 100 },
|
||||
);
|
||||
|
||||
// alice, alyce, alyciaは非アクティブなので後ろに行く
|
||||
expect(result).toEqual([alysha, alyson, alyssa, alice, alyce, alycia].map(x => x.id));
|
||||
});
|
||||
|
||||
test('[非ログイン] 非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
|
||||
await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
|
||||
const result = await service.search(
|
||||
{ username: 'al' },
|
||||
{ limit: 100 },
|
||||
);
|
||||
|
||||
expect(result).toEqual([alice, alyce, alycia, alysha, alyson, alyssa].map(x => x.id));
|
||||
});
|
||||
|
||||
test('フォロー中のアクティブユーザのうち、"al"から始まり"example.com"にいる人が全員ヒットする', async () => {
|
||||
await createFollowings(root, [alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
await setActive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
|
||||
const result = await service.search(
|
||||
{ username: 'al', host: 'exam' },
|
||||
{ limit: 100 },
|
||||
root,
|
||||
);
|
||||
|
||||
expect(result).toEqual([alyson, alyssa].map(x => x.id));
|
||||
});
|
||||
|
||||
test('サスペンド済みユーザは出ない', async () => {
|
||||
await setActive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
await setSuspended([alice, alyce, alycia]);
|
||||
|
||||
const result = await service.search(
|
||||
{ username: 'al' },
|
||||
{ limit: 100 },
|
||||
root,
|
||||
);
|
||||
|
||||
expect(result).toEqual([alysha, alyson, alyssa].map(x => x.id));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -18,6 +18,7 @@ import { entities } from '../src/postgres.js';
|
|||
import { loadConfig } from '../src/config.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
import { type Response } from 'node-fetch';
|
||||
import { ApiError } from "@/server/api/error.js";
|
||||
|
||||
export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js';
|
||||
|
||||
|
@ -48,27 +49,28 @@ export const successfulApiCall = async <E extends keyof misskey.Endpoints, P ext
|
|||
const res = await api(endpoint, parameters, user);
|
||||
const status = assertion.status ?? (res.body == null ? 204 : 200);
|
||||
assert.strictEqual(res.status, status, inspect(res.body, { depth: 5, colors: true }));
|
||||
return res.body;
|
||||
|
||||
return res.body as misskey.api.SwitchCaseResponseType<E, P>;
|
||||
};
|
||||
|
||||
export const failedApiCall = async <T, E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req']>(request: ApiRequest<E, P>, assertion: {
|
||||
export const failedApiCall = async <E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req']>(request: ApiRequest<E, P>, assertion: {
|
||||
status: number,
|
||||
code: string,
|
||||
id: string
|
||||
}): Promise<T> => {
|
||||
}): Promise<void> => {
|
||||
const { endpoint, parameters, user } = request;
|
||||
const { status, code, id } = assertion;
|
||||
const res = await api(endpoint, parameters, user);
|
||||
assert.strictEqual(res.status, status, inspect(res.body));
|
||||
assert.strictEqual(res.body.error.code, code, inspect(res.body));
|
||||
assert.strictEqual(res.body.error.id, id, inspect(res.body));
|
||||
return res.body;
|
||||
assert.ok(res.body);
|
||||
assert.strictEqual(castAsError(res.body as any).error.code, code, inspect(res.body));
|
||||
assert.strictEqual(castAsError(res.body as any).error.id, id, inspect(res.body));
|
||||
};
|
||||
|
||||
export const api = async <E extends keyof misskey.Endpoints>(path: E, params: misskey.Endpoints[E]['req'], me?: UserToken): Promise<{
|
||||
export const api = async <E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req']>(path: E, params: P, me?: UserToken): Promise<{
|
||||
status: number,
|
||||
headers: Headers,
|
||||
body: any
|
||||
body: misskey.api.SwitchCaseResponseType<E, P>
|
||||
}> => {
|
||||
const bodyAuth: Record<string, string> = {};
|
||||
const headers: Record<string, string> = {
|
||||
|
@ -89,13 +91,14 @@ export const api = async <E extends keyof misskey.Endpoints>(path: E, params: mi
|
|||
});
|
||||
|
||||
const body = res.headers.get('content-type') === 'application/json; charset=utf-8'
|
||||
? await res.json()
|
||||
? await res.json() as misskey.api.SwitchCaseResponseType<E, P>
|
||||
: null;
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
headers: res.headers,
|
||||
body,
|
||||
// FIXME: removing this non-null assertion: requires better typing around empty response.
|
||||
body: body!,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -141,7 +144,8 @@ export const post = async (user: UserToken, params: misskey.Endpoints['notes/cre
|
|||
|
||||
const res = await api('notes/create', q, user);
|
||||
|
||||
return res.body ? res.body.createdNote : null;
|
||||
// FIXME: the return type should reflect this fact.
|
||||
return (res.body ? res.body.createdNote : null)!;
|
||||
};
|
||||
|
||||
export const createAppToken = async (user: UserToken, permissions: (typeof misskey.permissions)[number][]) => {
|
||||
|
@ -635,3 +639,9 @@ export async function sendEnvResetRequest() {
|
|||
throw new Error('server env update failed.');
|
||||
}
|
||||
}
|
||||
|
||||
// 与えられた値を強制的にエラーとみなす。この関数は型安全性を破壊するため、異常系のアサーション以外で用いられるべきではない。
|
||||
// FIXME(misskey-js): misskey-jsがエラー情報を公開するようになったらこの関数を廃止する
|
||||
export function castAsError(obj: Record<string, unknown>): { error: ApiError } {
|
||||
return obj as { error: ApiError };
|
||||
}
|
||||
|
|
|
@ -53,7 +53,6 @@ await fs.readFile(
|
|||
'../../assets/**',
|
||||
'../../fluent-emojis/**',
|
||||
'../../locales/ja-JP.yml',
|
||||
'../../misskey-assets/**',
|
||||
'assets/**',
|
||||
'public/**',
|
||||
'../../pnpm-lock.yaml',
|
||||
|
|
|
@ -24,12 +24,12 @@
|
|||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/plugin-replace": "5.0.7",
|
||||
"@rollup/pluginutils": "5.1.0",
|
||||
"@syuilo/aiscript": "0.18.0",
|
||||
"@syuilo/aiscript": "0.19.0",
|
||||
"@tabler/icons-webfont": "3.3.0",
|
||||
"@twemoji/parser": "15.1.1",
|
||||
"@vitejs/plugin-vue": "5.0.5",
|
||||
"@vue/compiler-sfc": "3.4.31",
|
||||
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.9",
|
||||
"@vitejs/plugin-vue": "5.1.0",
|
||||
"@vue/compiler-sfc": "3.4.34",
|
||||
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.11",
|
||||
"astring": "1.8.6",
|
||||
"broadcast-channel": "7.0.0",
|
||||
"buraha": "0.0.1",
|
||||
|
@ -39,9 +39,9 @@
|
|||
"chartjs-chart-matrix": "2.0.1",
|
||||
"chartjs-plugin-gradient": "0.6.1",
|
||||
"chartjs-plugin-zoom": "2.0.1",
|
||||
"chromatic": "11.5.4",
|
||||
"compare-versions": "6.1.0",
|
||||
"cropperjs": "2.0.0-beta.5",
|
||||
"chromatic": "11.5.6",
|
||||
"compare-versions": "6.1.1",
|
||||
"cropperjs": "2.0.0-rc.1",
|
||||
"date-fns": "2.30.0",
|
||||
"escape-regexp": "0.0.1",
|
||||
"estree-walker": "3.0.3",
|
||||
|
@ -58,85 +58,85 @@
|
|||
"misskey-reversi": "workspace:*",
|
||||
"photoswipe": "5.4.4",
|
||||
"punycode": "2.3.1",
|
||||
"rollup": "4.18.0",
|
||||
"rollup": "4.19.1",
|
||||
"sanitize-html": "2.13.0",
|
||||
"sass": "1.77.6",
|
||||
"shiki": "1.10.0",
|
||||
"sass": "1.77.8",
|
||||
"shiki": "1.12.0",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.165.0",
|
||||
"three": "0.167.0",
|
||||
"throttle-debounce": "5.0.2",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tsc-alias": "1.8.10",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typescript": "5.5.3",
|
||||
"typescript": "5.5.4",
|
||||
"uuid": "10.0.0",
|
||||
"v-code-diff": "1.12.0",
|
||||
"vite": "5.3.2",
|
||||
"vue": "3.4.31",
|
||||
"vite": "5.3.5",
|
||||
"vue": "3.4.34",
|
||||
"vuedraggable": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/summaly": "5.1.0",
|
||||
"@storybook/addon-actions": "8.1.11",
|
||||
"@storybook/addon-essentials": "8.1.11",
|
||||
"@storybook/addon-interactions": "8.1.11",
|
||||
"@storybook/addon-links": "8.1.11",
|
||||
"@storybook/addon-mdx-gfm": "8.1.11",
|
||||
"@storybook/addon-storysource": "8.1.11",
|
||||
"@storybook/blocks": "8.1.11",
|
||||
"@storybook/components": "8.1.11",
|
||||
"@storybook/core-events": "8.1.11",
|
||||
"@storybook/manager-api": "8.1.11",
|
||||
"@storybook/preview-api": "8.1.11",
|
||||
"@storybook/react": "8.1.11",
|
||||
"@storybook/react-vite": "8.1.11",
|
||||
"@storybook/test": "8.1.11",
|
||||
"@storybook/theming": "8.1.11",
|
||||
"@storybook/types": "8.1.11",
|
||||
"@storybook/vue3": "8.1.11",
|
||||
"@storybook/addon-actions": "8.2.6",
|
||||
"@storybook/addon-essentials": "8.2.6",
|
||||
"@storybook/addon-interactions": "8.2.6",
|
||||
"@storybook/addon-links": "8.2.6",
|
||||
"@storybook/addon-mdx-gfm": "8.2.6",
|
||||
"@storybook/addon-storysource": "8.2.6",
|
||||
"@storybook/blocks": "8.2.6",
|
||||
"@storybook/components": "8.2.6",
|
||||
"@storybook/core-events": "8.2.6",
|
||||
"@storybook/manager-api": "8.2.6",
|
||||
"@storybook/preview-api": "8.2.6",
|
||||
"@storybook/react": "8.2.6",
|
||||
"@storybook/react-vite": "8.2.6",
|
||||
"@storybook/test": "8.2.6",
|
||||
"@storybook/theming": "8.2.6",
|
||||
"@storybook/types": "8.2.6",
|
||||
"@storybook/vue3": "8.2.6",
|
||||
"@storybook/vue3-vite": "8.1.11",
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/escape-regexp": "0.0.3",
|
||||
"@types/estree": "1.0.5",
|
||||
"@types/matter-js": "0.19.6",
|
||||
"@types/matter-js": "0.19.7",
|
||||
"@types/micromatch": "4.0.9",
|
||||
"@types/node": "20.14.9",
|
||||
"@types/node": "20.14.12",
|
||||
"@types/punycode": "2.1.4",
|
||||
"@types/sanitize-html": "2.11.0",
|
||||
"@types/seedrandom": "3.0.8",
|
||||
"@types/throttle-debounce": "5.0.2",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/uuid": "10.0.0",
|
||||
"@types/ws": "8.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "7.15.0",
|
||||
"@typescript-eslint/parser": "7.15.0",
|
||||
"@types/ws": "8.5.11",
|
||||
"@typescript-eslint/eslint-plugin": "7.17.0",
|
||||
"@typescript-eslint/parser": "7.17.0",
|
||||
"@vitest/coverage-v8": "1.6.0",
|
||||
"@vue/runtime-core": "3.4.31",
|
||||
"acorn": "8.12.0",
|
||||
"@vue/runtime-core": "3.4.34",
|
||||
"acorn": "8.12.1",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "13.13.0",
|
||||
"cypress": "13.13.1",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-vue": "9.26.0",
|
||||
"eslint-plugin-vue": "9.27.0",
|
||||
"fast-glob": "3.3.2",
|
||||
"happy-dom": "10.0.3",
|
||||
"intersection-observer": "0.12.2",
|
||||
"micromatch": "4.0.7",
|
||||
"msw": "2.3.1",
|
||||
"msw-storybook-addon": "2.0.2",
|
||||
"msw": "2.3.4",
|
||||
"msw-storybook-addon": "2.0.3",
|
||||
"nodemon": "3.1.4",
|
||||
"prettier": "3.3.2",
|
||||
"prettier": "3.3.3",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"seedrandom": "3.0.5",
|
||||
"start-server-and-test": "2.0.4",
|
||||
"storybook": "8.1.11",
|
||||
"storybook": "8.2.6",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vitest": "1.6.0",
|
||||
"vitest-fetch-mock": "0.2.2",
|
||||
"vue-component-type-helpers": "2.0.24",
|
||||
"vue-component-type-helpers": "2.0.29",
|
||||
"vue-eslint-parser": "9.4.3",
|
||||
"vue-tsc": "2.0.24"
|
||||
"vue-tsc": "2.0.29"
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue