Merge remote-tracking branch 'misskey-dev/develop' into develop
# Conflicts: # .config/docker_example.env # .config/docker_example.yml # CHANGELOG.md # locales/index.d.ts # locales/ja-JP.yml # packages/backend/package.json # packages/backend/src/core/entities/UserEntityService.ts # packages/backend/src/server/api/endpoints/i/update.ts # packages/frontend/package.json # packages/frontend/src/components/MkChannelFollowButton.vue # packages/frontend/src/components/MkFollowButton.vue # packages/frontend/src/components/MkMediaAudio.vue # packages/frontend/src/components/MkMenu.vue # packages/frontend/src/components/MkNote.vue # packages/frontend/src/components/MkNoteDetailed.vue # packages/frontend/src/components/MkPostForm.vue # packages/frontend/src/components/MkRadio.vue # packages/frontend/src/components/MkSuperMenu.vue # packages/frontend/src/components/MkSwitch.vue # packages/frontend/src/components/MkVisitorDashboard.vue # packages/frontend/src/os.ts # packages/frontend/src/pages/about.vue # packages/frontend/src/pages/admin/roles.editor.vue # packages/frontend/src/pages/admin/roles.vue # packages/frontend/src/pages/custom-emojis-manager.vue # packages/frontend/src/pages/follow.vue # packages/frontend/src/pages/timeline.vue # packages/frontend/src/pages/user/home.vue # packages/frontend/src/scripts/get-note-menu.ts # packages/frontend/src/scripts/keycode.ts # packages/frontend/src/ui/_common_/navbar-for-mobile.vue # packages/frontend/src/ui/_common_/navbar.vue # pnpm-lock.yaml
This commit is contained in:
commit
15005c4266
|
@ -1,5 +1,12 @@
|
||||||
|
# type4ny settings
|
||||||
|
# TYPE4NY_URL=https://example.tld/
|
||||||
|
|
||||||
# db settings
|
# db settings
|
||||||
POSTGRES_PASSWORD=example-type4ny-pass
|
POSTGRES_PASSWORD=example-type4ny-pass
|
||||||
|
# DATABASE_PASSWORD=${POSTGRES_PASSWORD}
|
||||||
POSTGRES_USER=example-type4ny-user
|
POSTGRES_USER=example-type4ny-user
|
||||||
|
# DATABASE_USER=${POSTGRES_USER}
|
||||||
POSTGRES_DB=type4ny
|
POSTGRES_DB=type4ny
|
||||||
|
# DATABASE_DB=${POSTGRES_DB}
|
||||||
|
|
||||||
DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}"
|
DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}"
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
#───┘ URL └─────────────────────────────────────────────────────
|
#───┘ URL └─────────────────────────────────────────────────────
|
||||||
|
|
||||||
# Final accessible URL seen by a user.
|
# Final accessible URL seen by a user.
|
||||||
|
# You can set url from an environment variable instead.
|
||||||
url: https://example.tld/
|
url: https://example.tld/
|
||||||
|
|
||||||
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||||
|
@ -38,9 +39,11 @@ db:
|
||||||
port: 5432
|
port: 5432
|
||||||
|
|
||||||
# Database name
|
# Database name
|
||||||
|
# You can set db from an environment variable instead.
|
||||||
db: type4ny
|
db: type4ny
|
||||||
|
|
||||||
# Auth
|
# Auth
|
||||||
|
# You can set user and pass from environment variables instead.
|
||||||
user: example-type4ny-user
|
user: example-type4ny-user
|
||||||
pass: example-type4ny-pass
|
pass: example-type4ny-pass
|
||||||
|
|
||||||
|
|
5
.github/workflows/api-misskey-js.yml
vendored
5
.github/workflows/api-misskey-js.yml
vendored
|
@ -4,10 +4,11 @@ on:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- packages/misskey-js/**
|
- packages/misskey-js/**
|
||||||
|
- .github/workflows/api-misskey-js.yml
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- packages/misskey-js/**
|
- packages/misskey-js/**
|
||||||
|
- .github/workflows/api-misskey-js.yml
|
||||||
jobs:
|
jobs:
|
||||||
report:
|
report:
|
||||||
|
|
||||||
|
@ -20,7 +21,7 @@ jobs:
|
||||||
- run: corepack enable
|
- run: corepack enable
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.3
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
2
.github/workflows/changelog-check.yml
vendored
2
.github/workflows/changelog-check.yml
vendored
|
@ -14,7 +14,7 @@ jobs:
|
||||||
- name: Checkout head
|
- name: Checkout head
|
||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.1
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.3
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ jobs:
|
||||||
|
|
||||||
- name: setup node
|
- name: setup node
|
||||||
id: setup-node
|
id: setup-node
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.3
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
|
|
|
@ -6,12 +6,13 @@ on:
|
||||||
paths:
|
paths:
|
||||||
- packages/misskey-js/package.json
|
- packages/misskey-js/package.json
|
||||||
- package.json
|
- package.json
|
||||||
|
- .github/workflows/check-misskey-js-version.yml
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ develop ]
|
branches: [ develop ]
|
||||||
paths:
|
paths:
|
||||||
- packages/misskey-js/package.json
|
- packages/misskey-js/package.json
|
||||||
- package.json
|
- package.json
|
||||||
|
- .github/workflows/check-misskey-js-version.yml
|
||||||
jobs:
|
jobs:
|
||||||
check-version:
|
check-version:
|
||||||
# ルートの package.json と packages/misskey-js/package.json のバージョンが一致しているかを確認する
|
# ルートの package.json と packages/misskey-js/package.json のバージョンが一致しているかを確認する
|
||||||
|
|
4
.github/workflows/get-api-diff.yml
vendored
4
.github/workflows/get-api-diff.yml
vendored
|
@ -9,7 +9,7 @@ on:
|
||||||
paths:
|
paths:
|
||||||
- packages/backend/**
|
- packages/backend/**
|
||||||
- .github/workflows/get-api-diff.yml
|
- .github/workflows/get-api-diff.yml
|
||||||
|
- .github/workflows/get-api-diff.yml
|
||||||
jobs:
|
jobs:
|
||||||
get-from-misskey:
|
get-from-misskey:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -34,7 +34,7 @@ jobs:
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
24
.github/workflows/lint.yml
vendored
24
.github/workflows/lint.yml
vendored
|
@ -10,15 +10,16 @@ on:
|
||||||
- packages/frontend/**
|
- packages/frontend/**
|
||||||
- packages/sw/**
|
- packages/sw/**
|
||||||
- packages/misskey-js/**
|
- packages/misskey-js/**
|
||||||
- packages/shared/.eslintrc.js
|
- packages/shared/eslint.config.js
|
||||||
|
- .github/workflows/lint.yml
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- packages/backend/**
|
- packages/backend/**
|
||||||
- packages/frontend/**
|
- packages/frontend/**
|
||||||
- packages/sw/**
|
- packages/sw/**
|
||||||
- packages/misskey-js/**
|
- packages/misskey-js/**
|
||||||
- packages/shared/.eslintrc.js
|
- packages/shared/eslint.config.js
|
||||||
|
- .github/workflows/lint.yml
|
||||||
jobs:
|
jobs:
|
||||||
pnpm_install:
|
pnpm_install:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -28,7 +29,7 @@ jobs:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: true
|
submodules: true
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v4.0.2
|
- uses: actions/setup-node@v4.0.3
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
@ -39,6 +40,8 @@ jobs:
|
||||||
needs: [pnpm_install]
|
needs: [pnpm_install]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
env:
|
||||||
|
eslint-cache-version: v1
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
workspace:
|
workspace:
|
||||||
|
@ -52,13 +55,20 @@ jobs:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: true
|
submodules: true
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v4.0.2
|
- uses: actions/setup-node@v4.0.3
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
- run: corepack enable
|
- run: corepack enable
|
||||||
- run: pnpm i --frozen-lockfile
|
- run: pnpm i --frozen-lockfile
|
||||||
- run: pnpm --filter ${{ matrix.workspace }} run eslint
|
- name: Restore eslint cache
|
||||||
|
uses: actions/cache@v4.0.2
|
||||||
|
with:
|
||||||
|
path: node_modules/.cache/eslint
|
||||||
|
key: eslint-${{ env.eslint-cache-version }}-${{ hashFiles('/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
eslint-${{ env.eslint-cache-version }}-${{ hashFiles('/pnpm-lock.yaml') }}-
|
||||||
|
- run: pnpm --filter ${{ matrix.workspace }} run eslint --cache --cache-location node_modules/.cache/eslint --cache-strategy content
|
||||||
|
|
||||||
typecheck:
|
typecheck:
|
||||||
needs: [pnpm_install]
|
needs: [pnpm_install]
|
||||||
|
@ -75,7 +85,7 @@ jobs:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: true
|
submodules: true
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v4.0.2
|
- uses: actions/setup-node@v4.0.3
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
5
.github/workflows/locale.yml
vendored
5
.github/workflows/locale.yml
vendored
|
@ -4,10 +4,11 @@ on:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- locales/**
|
- locales/**
|
||||||
|
- .github/workflows/locale.yml
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- locales/**
|
- locales/**
|
||||||
|
- .github/workflows/locale.yml
|
||||||
jobs:
|
jobs:
|
||||||
locale_verify:
|
locale_verify:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -18,7 +19,7 @@ jobs:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: true
|
submodules: true
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v4.0.2
|
- uses: actions/setup-node@v4.0.3
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
2
.github/workflows/on-release-created.yml
vendored
2
.github/workflows/on-release-created.yml
vendored
|
@ -26,7 +26,7 @@ jobs:
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
19
.github/workflows/release-edit-with-push.yml
vendored
19
.github/workflows/release-edit-with-push.yml
vendored
|
@ -3,10 +3,10 @@ name: "Release Manager: sync changelog with PR"
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- release/**
|
- develop
|
||||||
paths:
|
paths:
|
||||||
- 'CHANGELOG.md'
|
- 'CHANGELOG.md'
|
||||||
|
# - .github/workflows/release-edit-with-push.yml
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
@ -20,24 +20,29 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
# headがrelease/かつopenのPRを1つ取得
|
# headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得
|
||||||
- name: Get PR
|
- name: Get PR
|
||||||
run: |
|
run: |
|
||||||
echo "pr_number=$(gh pr list --limit 1 --head "$GITHUB_REF_NAME" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
|
echo "pr_number=$(gh pr list --limit 1 --search "head:$GITHUB_REF_NAME base:$STABLE_BRANCH is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
|
||||||
id: get_pr
|
id: get_pr
|
||||||
|
env:
|
||||||
|
STABLE_BRANCH: ${{ vars.STABLE_BRANCH }}
|
||||||
- name: Get target version
|
- name: Get target version
|
||||||
uses: misskey-dev/release-manager-actions/.github/actions/get-target-version@v1
|
if: steps.get_pr.outputs.pr_number != ''
|
||||||
|
uses: misskey-dev/release-manager-actions/.github/actions/get-target-version@v2
|
||||||
id: v
|
id: v
|
||||||
# CHANGELOG.mdの内容を取得
|
# CHANGELOG.mdの内容を取得
|
||||||
- name: Get changelog
|
- name: Get changelog
|
||||||
uses: misskey-dev/release-manager-actions/.github/actions/get-changelog@v1
|
if: steps.get_pr.outputs.pr_number != ''
|
||||||
|
uses: misskey-dev/release-manager-actions/.github/actions/get-changelog@v2
|
||||||
with:
|
with:
|
||||||
version: ${{ steps.v.outputs.target_version }}
|
version: ${{ steps.v.outputs.target_version }}
|
||||||
id: changelog
|
id: changelog
|
||||||
# PRのnotesを更新
|
# PRのnotesを更新
|
||||||
- name: Update PR
|
- name: Update PR
|
||||||
|
if: steps.get_pr.outputs.pr_number != ''
|
||||||
run: |
|
run: |
|
||||||
gh pr edit "$PR_NUMBER" --body "$CHANGELOG"
|
gh pr edit "$PR_NUMBER" --body "$CHANGELOG"
|
||||||
env:
|
env:
|
||||||
CHANGELOG: ${{ steps.changelog.outputs.changelog }}
|
|
||||||
PR_NUMBER: ${{ steps.get_pr.outputs.pr_number }}
|
PR_NUMBER: ${{ steps.get_pr.outputs.pr_number }}
|
||||||
|
CHANGELOG: ${{ steps.changelog.outputs.changelog }}
|
||||||
|
|
20
.github/workflows/release-with-dispatch.yml
vendored
20
.github/workflows/release-with-dispatch.yml
vendored
|
@ -33,18 +33,21 @@ jobs:
|
||||||
pr_number: ${{ steps.get_pr.outputs.pr_number }}
|
pr_number: ${{ steps.get_pr.outputs.pr_number }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
# headがrelease/かつopenのPRを1つ取得
|
# headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得
|
||||||
- name: Get PRs
|
- name: Get PRs
|
||||||
run: |
|
run: |
|
||||||
echo "pr_number=$(gh pr list --limit 1 --search "head:release/ is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
|
echo "pr_number=$(gh pr list --limit 1 --search "head:$GITHUB_REF_NAME base:$STABLE_BRANCH is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
|
||||||
id: get_pr
|
id: get_pr
|
||||||
|
env:
|
||||||
|
STABLE_BRANCH: ${{ vars.STABLE_BRANCH }}
|
||||||
|
|
||||||
merge:
|
merge:
|
||||||
uses: misskey-dev/release-manager-actions/.github/workflows/merge.yml@v1
|
uses: misskey-dev/release-manager-actions/.github/workflows/merge.yml@v2
|
||||||
needs: get-pr
|
needs: get-pr
|
||||||
if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge == true }}
|
if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge == true }}
|
||||||
with:
|
with:
|
||||||
pr_number: ${{ needs.get-pr.outputs.pr_number }}
|
pr_number: ${{ needs.get-pr.outputs.pr_number }}
|
||||||
|
user: 'github-actions[bot]'
|
||||||
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
|
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
|
||||||
# Text to prepend to the changelog
|
# Text to prepend to the changelog
|
||||||
# The first line must be `## Unreleased`
|
# The first line must be `## Unreleased`
|
||||||
|
@ -65,15 +68,14 @@ jobs:
|
||||||
secrets:
|
secrets:
|
||||||
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
|
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
|
||||||
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
||||||
RULESET_EDIT_APP_ID: ${{ secrets.RULESET_EDIT_APP_ID }}
|
|
||||||
RULESET_EDIT_APP_PRIVATE_KEY: ${{ secrets.RULESET_EDIT_APP_PRIVATE_KEY }}
|
|
||||||
|
|
||||||
create-prerelease:
|
create-prerelease:
|
||||||
uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v1
|
uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v2
|
||||||
needs: get-pr
|
needs: get-pr
|
||||||
if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge != true }}
|
if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge != true }}
|
||||||
with:
|
with:
|
||||||
pr_number: ${{ needs.get-pr.outputs.pr_number }}
|
pr_number: ${{ needs.get-pr.outputs.pr_number }}
|
||||||
|
user: 'github-actions[bot]'
|
||||||
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
|
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
|
||||||
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
|
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
|
||||||
indent: ${{ vars.INDENT }}
|
indent: ${{ vars.INDENT }}
|
||||||
|
@ -82,10 +84,11 @@ jobs:
|
||||||
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
create-target:
|
create-target:
|
||||||
uses: misskey-dev/release-manager-actions/.github/workflows/create-target.yml@v1
|
uses: misskey-dev/release-manager-actions/.github/workflows/create-target.yml@v2
|
||||||
needs: get-pr
|
needs: get-pr
|
||||||
if: ${{ needs.get-pr.outputs.pr_number == '' }}
|
if: ${{ needs.get-pr.outputs.pr_number == '' }}
|
||||||
with:
|
with:
|
||||||
|
user: 'github-actions[bot]'
|
||||||
# The script for version increment.
|
# The script for version increment.
|
||||||
# process.env.CURRENT_VERSION: The current version.
|
# process.env.CURRENT_VERSION: The current version.
|
||||||
#
|
#
|
||||||
|
@ -118,8 +121,7 @@ jobs:
|
||||||
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
|
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
|
||||||
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
|
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
|
||||||
indent: ${{ vars.INDENT }}
|
indent: ${{ vars.INDENT }}
|
||||||
|
stable_branch: ${{ vars.STABLE_BRANCH }}
|
||||||
secrets:
|
secrets:
|
||||||
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
|
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
|
||||||
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
||||||
RULESET_EDIT_APP_ID: ${{ secrets.RULESET_EDIT_APP_ID }}
|
|
||||||
RULESET_EDIT_APP_PRIVATE_KEY: ${{ secrets.RULESET_EDIT_APP_PRIVATE_KEY }}
|
|
||||||
|
|
13
.github/workflows/release-with-ready.yml
vendored
13
.github/workflows/release-with-ready.yml
vendored
|
@ -16,23 +16,26 @@ jobs:
|
||||||
check:
|
check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
ref: ${{ steps.get_pr.outputs.ref }}
|
head: ${{ steps.get_pr.outputs.head }}
|
||||||
|
base: ${{ steps.get_pr.outputs.base }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
# PR情報を取得
|
# PR情報を取得
|
||||||
- name: Get PR
|
- name: Get PR
|
||||||
run: |
|
run: |
|
||||||
pr_json=$(gh pr view "$PR_NUMBER" --json isDraft,headRefName)
|
pr_json=$(gh pr view "$PR_NUMBER" --json isDraft,headRefName,baseRefName)
|
||||||
echo "ref=$(echo $pr_json | jq -r '.headRefName')" >> $GITHUB_OUTPUT
|
echo "head=$(echo $pr_json | jq -r '.headRefName')" >> $GITHUB_OUTPUT
|
||||||
|
echo "base=$(echo $pr_json | jq -r '.baseRefName')" >> $GITHUB_OUTPUT
|
||||||
id: get_pr
|
id: get_pr
|
||||||
env:
|
env:
|
||||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
release:
|
release:
|
||||||
uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v1
|
uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v2
|
||||||
needs: check
|
needs: check
|
||||||
if: startsWith(needs.check.outputs.ref, 'release/')
|
if: needs.check.outputs.head == github.event.repository.default_branch && needs.check.outputs.base == vars.STABLE_BRANCH
|
||||||
with:
|
with:
|
||||||
pr_number: ${{ github.event.pull_request.number }}
|
pr_number: ${{ github.event.pull_request.number }}
|
||||||
|
user: 'github-actions[bot]'
|
||||||
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
|
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
|
||||||
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
|
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
|
||||||
indent: ${{ vars.INDENT }}
|
indent: ${{ vars.INDENT }}
|
||||||
|
|
2
.github/workflows/storybook.yml
vendored
2
.github/workflows/storybook.yml
vendored
|
@ -36,7 +36,7 @@ jobs:
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
- name: Use Node.js 20.x
|
- name: Use Node.js 20.x
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.3
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
7
.github/workflows/test-backend.yml
vendored
7
.github/workflows/test-backend.yml
vendored
|
@ -9,12 +9,13 @@ on:
|
||||||
- packages/backend/**
|
- packages/backend/**
|
||||||
# for permissions
|
# for permissions
|
||||||
- packages/misskey-js/**
|
- packages/misskey-js/**
|
||||||
|
- .github/workflows/test-backend.yml
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- packages/backend/**
|
- packages/backend/**
|
||||||
# for permissions
|
# for permissions
|
||||||
- packages/misskey-js/**
|
- packages/misskey-js/**
|
||||||
|
- .github/workflows/test-backend.yml
|
||||||
jobs:
|
jobs:
|
||||||
unit:
|
unit:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -45,7 +46,7 @@ jobs:
|
||||||
- name: Install FFmpeg
|
- name: Install FFmpeg
|
||||||
uses: FedericoCarboni/setup-ffmpeg@v3
|
uses: FedericoCarboni/setup-ffmpeg@v3
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
@ -92,7 +93,7 @@ jobs:
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
8
.github/workflows/test-frontend.yml
vendored
8
.github/workflows/test-frontend.yml
vendored
|
@ -11,7 +11,7 @@ on:
|
||||||
- packages/misskey-js/**
|
- packages/misskey-js/**
|
||||||
# for e2e
|
# for e2e
|
||||||
- packages/backend/**
|
- packages/backend/**
|
||||||
|
- .github/workflows/test-frontend.yml
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- packages/frontend/**
|
- packages/frontend/**
|
||||||
|
@ -19,7 +19,7 @@ on:
|
||||||
- packages/misskey-js/**
|
- packages/misskey-js/**
|
||||||
# for e2e
|
# for e2e
|
||||||
- packages/backend/**
|
- packages/backend/**
|
||||||
|
- .github/workflows/test-frontend.yml
|
||||||
jobs:
|
jobs:
|
||||||
vitest:
|
vitest:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -35,7 +35,7 @@ jobs:
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
@ -90,7 +90,7 @@ jobs:
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
5
.github/workflows/test-misskey-js.yml
vendored
5
.github/workflows/test-misskey-js.yml
vendored
|
@ -8,11 +8,12 @@ on:
|
||||||
branches: [ develop ]
|
branches: [ develop ]
|
||||||
paths:
|
paths:
|
||||||
- packages/misskey-js/**
|
- packages/misskey-js/**
|
||||||
|
- .github/workflows/test-misskey-js.yml
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ develop ]
|
branches: [ develop ]
|
||||||
paths:
|
paths:
|
||||||
- packages/misskey-js/**
|
- packages/misskey-js/**
|
||||||
|
- .github/workflows/test-misskey-js.yml
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
|
|
||||||
|
@ -30,7 +31,7 @@ jobs:
|
||||||
- run: corepack enable
|
- run: corepack enable
|
||||||
|
|
||||||
- name: Setup Node.js ${{ matrix.node-version }}
|
- name: Setup Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
2
.github/workflows/test-production.yml
vendored
2
.github/workflows/test-production.yml
vendored
|
@ -25,7 +25,7 @@ jobs:
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
5
.github/workflows/validate-api-json.yml
vendored
5
.github/workflows/validate-api-json.yml
vendored
|
@ -7,10 +7,11 @@ on:
|
||||||
- develop
|
- develop
|
||||||
paths:
|
paths:
|
||||||
- packages/backend/**
|
- packages/backend/**
|
||||||
|
- .github/workflows/validate-api-json.yml
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- packages/backend/**
|
- packages/backend/**
|
||||||
|
- .github/workflows/validate-api-json.yml
|
||||||
jobs:
|
jobs:
|
||||||
validate-api-json:
|
validate-api-json:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -26,7 +27,7 @@ jobs:
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
46
CHANGELOG.md
46
CHANGELOG.md
|
@ -1,22 +1,66 @@
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Note
|
||||||
|
- デッキUIの新着ノートをサウンドで通知する機能の追加(v2024.5.0)に伴い、以前から動作しなくなっていたクライアント設定内の「アンテナ受信」「チャンネル通知」サウンドを削除しました。
|
||||||
|
|
||||||
### General
|
### General
|
||||||
- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
|
- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
|
||||||
|
- Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に
|
||||||
|
- 変更不可となっていても、設定済みのものを解除してデフォルト画像に戻すことは出来ます
|
||||||
- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
|
- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
|
||||||
- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
|
- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
|
||||||
|
- Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正
|
||||||
|
- Enhance: Allow negative delay for MFM animation elements (`tada`, `jelly`, `twitch`, `shake`, `spin`, `jump`, `bounce`, `rainbow`)
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
|
- Enhance: 内蔵APIドキュメントのデザイン・パフォーマンスを改善
|
||||||
|
- Enhance: 非ログイン時に他サーバーに遷移するアクションを追加
|
||||||
|
- Enhance: 非ログイン時のハイライトTLのデザインを改善
|
||||||
|
- Enhance: フロントエンドのアクセシビリティ改善
|
||||||
|
(Based on https://github.com/taiyme/misskey/pull/226)
|
||||||
|
- Enhance: サーバー情報ページ・お問い合わせページを改善
|
||||||
|
(Cherry-picked from https://github.com/taiyme/misskey/pull/238)
|
||||||
- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
|
- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
|
||||||
- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
|
- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
|
||||||
- Fix: リバーシの対局を正しく共有できないことがある問題を修正
|
- Fix: リバーシの対局を正しく共有できないことがある問題を修正
|
||||||
- Fix: コントロールパネルでベースロールのポリシーを編集してもUI上では変更が反映されない問題を修正
|
- Fix: コントロールパネルでベースロールのポリシーを編集してもUI上では変更が反映されない問題を修正
|
||||||
- Fix: アンテナの編集画面のボタンに隙間を追加
|
- Fix: アンテナの編集画面のボタンに隙間を追加
|
||||||
- Fix: テーマプレビューが見れない問題を修正
|
- Fix: テーマプレビューが見れない問題を修正
|
||||||
|
- Fix: ショートカットキーが連打できる問題を修正
|
||||||
|
(Cherry-picked from https://github.com/taiyme/misskey/pull/234)
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
- チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正
|
|
||||||
- Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)
|
- Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)
|
||||||
|
- Enhance: エンドポイント`clips/update`の必須項目を`clipId`のみに
|
||||||
|
- Enhance: エンドポイント`admin/roles/update`の必須項目を`roleId`のみに
|
||||||
|
- Enhance: エンドポイント`pages/update`の必須項目を`pageId`のみに
|
||||||
|
- Enhance: エンドポイント`gallery/posts/update`の必須項目を`postId`のみに
|
||||||
|
- Enhance: エンドポイント`i/webhook/update`の必須項目を`webhookId`のみに
|
||||||
|
- Enhance: エンドポイント`admin/ad/update`の必須項目を`id`のみに
|
||||||
|
- Enhance: `default.yml`内の`url`, `db.db`, `db.user`, `db.pass`を環境変数から読み込めるように
|
||||||
|
- Fix: チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正
|
||||||
|
- Fix: ユーザーのフィードページのMFMをHTMLに展開するように (#14006)
|
||||||
- Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036)
|
- Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036)
|
||||||
|
- Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059)
|
||||||
|
- Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正
|
||||||
|
- Fix: 自分以外のクリップ内のノート個数が見えることがあるのを修正
|
||||||
|
- Fix: 空文字列のリアクションはフォールバックされるように
|
||||||
|
- Fix: リノートにリアクションできないように
|
||||||
|
- Fix: ユーザー名の前後に空白文字列がある場合は省略するように
|
||||||
|
- Fix: プロフィール編集時に名前を空白文字列のみにできる問題を修正
|
||||||
|
- Fix: ユーザ名のサジェスト時に表示される内容と順番を調整(以下の順番になります) #14149
|
||||||
|
1. フォロー中かつアクティブなユーザ
|
||||||
|
2. フォロー中かつ非アクティブなユーザ
|
||||||
|
3. フォローしていないアクティブなユーザ
|
||||||
|
4. フォローしていない非アクティブなユーザ
|
||||||
|
- Fix: 一般ユーザーから見たユーザーのバッジの一覧に公開されていないものが含まれることがある問題を修正
|
||||||
|
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/652)
|
||||||
|
- Fix: ユーザーのリアクション一覧でミュート/ブロックが機能していなかった問題を修正
|
||||||
|
- Fix: エラーメッセージの誤字を修正 (#14213)
|
||||||
|
|
||||||
|
### Misskey.js
|
||||||
|
- Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応)
|
||||||
|
- Feat: `/admin/role/create` のロールポリシーの型を修正
|
||||||
|
|
||||||
## 2024.5.0
|
## 2024.5.0
|
||||||
|
|
||||||
|
|
|
@ -165,7 +165,7 @@ cp .github/misskey/test.yml .config/
|
||||||
```
|
```
|
||||||
Prepare DB/Redis for testing.
|
Prepare DB/Redis for testing.
|
||||||
```
|
```
|
||||||
docker compose -f packages/backend/test/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`.
|
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,8 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- internal_network
|
- internal_network
|
||||||
- external_network
|
- external_network
|
||||||
|
# env_file:
|
||||||
|
# - .config/docker.env
|
||||||
volumes:
|
volumes:
|
||||||
- ./files:/type4ny/files
|
- ./files:/type4ny/files
|
||||||
- ./.config:/type4ny/.config:ro
|
- ./.config:/type4ny/.config:ro
|
||||||
|
|
38
locales/index.d.ts
vendored
38
locales/index.d.ts
vendored
|
@ -848,6 +848,22 @@ export interface Locale extends ILocale {
|
||||||
* リモートで表示
|
* リモートで表示
|
||||||
*/
|
*/
|
||||||
"showOnRemote": string;
|
"showOnRemote": string;
|
||||||
|
/**
|
||||||
|
* リモートで続行
|
||||||
|
*/
|
||||||
|
"continueOnRemote": string;
|
||||||
|
/**
|
||||||
|
* Misskey Hubからサーバーを選択
|
||||||
|
*/
|
||||||
|
"chooseServerOnMisskeyHub": string;
|
||||||
|
/**
|
||||||
|
* サーバーのドメインを直接指定
|
||||||
|
*/
|
||||||
|
"specifyServerHost": string;
|
||||||
|
/**
|
||||||
|
* ドメインを入力してください
|
||||||
|
*/
|
||||||
|
"inputHostName": string;
|
||||||
/**
|
/**
|
||||||
* 全般
|
* 全般
|
||||||
*/
|
*/
|
||||||
|
@ -2061,9 +2077,13 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"onlyOneFileCanBeAttached": string;
|
"onlyOneFileCanBeAttached": string;
|
||||||
/**
|
/**
|
||||||
* 続行する前に、サインアップまたはサインインが必要です
|
* 続行する前に、登録またはログインが必要です
|
||||||
*/
|
*/
|
||||||
"signinRequired": string;
|
"signinRequired": string;
|
||||||
|
/**
|
||||||
|
* 続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります
|
||||||
|
*/
|
||||||
|
"signinOrContinueOnRemote": string;
|
||||||
/**
|
/**
|
||||||
* 招待
|
* 招待
|
||||||
*/
|
*/
|
||||||
|
@ -5304,6 +5324,10 @@ export interface Locale extends ILocale {
|
||||||
* 下書きを適用すると現在入力されている内容はリセットされます。よろしいですか?
|
* 下書きを適用すると現在入力されている内容はリセットされます。よろしいですか?
|
||||||
*/
|
*/
|
||||||
"draftOverwriteConfirm": string;
|
"draftOverwriteConfirm": string;
|
||||||
|
/**
|
||||||
|
* もう一度お試しください。
|
||||||
|
*/
|
||||||
|
"tryAgain": string;
|
||||||
"_delivery": {
|
"_delivery": {
|
||||||
/**
|
/**
|
||||||
* 配信状態
|
* 配信状態
|
||||||
|
@ -6930,6 +6954,10 @@ export interface Locale extends ILocale {
|
||||||
* ファイルにNSFWを常に付与
|
* ファイルにNSFWを常に付与
|
||||||
*/
|
*/
|
||||||
"alwaysMarkNsfw": string;
|
"alwaysMarkNsfw": string;
|
||||||
|
/**
|
||||||
|
* アイコンとバナーの更新を許可
|
||||||
|
*/
|
||||||
|
"canUpdateBioMedia": string;
|
||||||
/**
|
/**
|
||||||
* ノートのピン留めの最大数
|
* ノートのピン留めの最大数
|
||||||
*/
|
*/
|
||||||
|
@ -7867,14 +7895,6 @@ export interface Locale extends ILocale {
|
||||||
* 通知
|
* 通知
|
||||||
*/
|
*/
|
||||||
"notification": string;
|
"notification": string;
|
||||||
/**
|
|
||||||
* アンテナ受信
|
|
||||||
*/
|
|
||||||
"antenna": string;
|
|
||||||
/**
|
|
||||||
* チャンネル通知
|
|
||||||
*/
|
|
||||||
"channel": string;
|
|
||||||
/**
|
/**
|
||||||
* リアクション選択時
|
* リアクション選択時
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -52,7 +52,11 @@ const primaries = {
|
||||||
const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
|
const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
|
||||||
|
|
||||||
export function build() {
|
export function build() {
|
||||||
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {});
|
// vitestの挙動を調整するため、一度ローカル変数化する必要がある
|
||||||
|
// https://github.com/vitest-dev/vitest/issues/3988#issuecomment-1686599577
|
||||||
|
// https://github.com/misskey-dev/misskey/pull/14057#issuecomment-2192833785
|
||||||
|
const metaUrl = import.meta.url;
|
||||||
|
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, metaUrl), 'utf-8'))) || {}, a), {});
|
||||||
|
|
||||||
// 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す
|
// 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す
|
||||||
const removeEmpty = (obj) => {
|
const removeEmpty = (obj) => {
|
||||||
|
|
|
@ -209,6 +209,10 @@ addAccount: "アカウントを追加"
|
||||||
reloadAccountsList: "アカウントリストの情報を更新"
|
reloadAccountsList: "アカウントリストの情報を更新"
|
||||||
loginFailed: "ログインに失敗しました"
|
loginFailed: "ログインに失敗しました"
|
||||||
showOnRemote: "リモートで表示"
|
showOnRemote: "リモートで表示"
|
||||||
|
continueOnRemote: "リモートで続行"
|
||||||
|
chooseServerOnMisskeyHub: "Misskey Hubからサーバーを選択"
|
||||||
|
specifyServerHost: "サーバーのドメインを直接指定"
|
||||||
|
inputHostName: "ドメインを入力してください"
|
||||||
general: "全般"
|
general: "全般"
|
||||||
wallpaper: "壁紙"
|
wallpaper: "壁紙"
|
||||||
setWallpaper: "壁紙を設定"
|
setWallpaper: "壁紙を設定"
|
||||||
|
@ -512,7 +516,8 @@ attachAsFileQuestion: "クリップボードのテキストが長いです。テ
|
||||||
noMessagesYet: "まだチャットはありません"
|
noMessagesYet: "まだチャットはありません"
|
||||||
newMessageExists: "新しいメッセージがあります"
|
newMessageExists: "新しいメッセージがあります"
|
||||||
onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです"
|
onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです"
|
||||||
signinRequired: "続行する前に、サインアップまたはサインインが必要です"
|
signinRequired: "続行する前に、登録またはログインが必要です"
|
||||||
|
signinOrContinueOnRemote: "続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります"
|
||||||
invitations: "招待"
|
invitations: "招待"
|
||||||
invitationCode: "招待コード"
|
invitationCode: "招待コード"
|
||||||
checking: "確認しています"
|
checking: "確認しています"
|
||||||
|
@ -1323,6 +1328,7 @@ drafts: "下書き"
|
||||||
draftSavingBehavior: "下書きの保存に関する動作"
|
draftSavingBehavior: "下書きの保存に関する動作"
|
||||||
saveAsDraft: "下書きとして保存"
|
saveAsDraft: "下書きとして保存"
|
||||||
draftOverwriteConfirm: "下書きを適用すると現在入力されている内容はリセットされます。よろしいですか?"
|
draftOverwriteConfirm: "下書きを適用すると現在入力されている内容はリセットされます。よろしいですか?"
|
||||||
|
tryAgain: "もう一度お試しください。"
|
||||||
|
|
||||||
_delivery:
|
_delivery:
|
||||||
status: "配信状態"
|
status: "配信状態"
|
||||||
|
@ -1790,6 +1796,7 @@ _role:
|
||||||
canManageAvatarDecorations: "アバターデコレーションの管理"
|
canManageAvatarDecorations: "アバターデコレーションの管理"
|
||||||
driveCapacity: "ドライブ容量"
|
driveCapacity: "ドライブ容量"
|
||||||
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
|
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
|
||||||
|
canUpdateBioMedia: "アイコンとバナーの更新を許可"
|
||||||
pinMax: "ノートのピン留めの最大数"
|
pinMax: "ノートのピン留めの最大数"
|
||||||
antennaMax: "アンテナの作成可能数"
|
antennaMax: "アンテナの作成可能数"
|
||||||
wordMuteMax: "ワードミュートの最大文字数"
|
wordMuteMax: "ワードミュートの最大文字数"
|
||||||
|
@ -2060,8 +2067,6 @@ _sfx:
|
||||||
note: "ノート"
|
note: "ノート"
|
||||||
noteMy: "ノート(自分)"
|
noteMy: "ノート(自分)"
|
||||||
notification: "通知"
|
notification: "通知"
|
||||||
antenna: "アンテナ受信"
|
|
||||||
channel: "チャンネル通知"
|
|
||||||
reaction: "リアクション選択時"
|
reaction: "リアクション選択時"
|
||||||
|
|
||||||
_soundSettings:
|
_soundSettings:
|
||||||
|
|
20
package.json
20
package.json
|
@ -55,20 +55,22 @@
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"postcss": "8.4.38",
|
"postcss": "8.4.38",
|
||||||
"tar": "6.2.1",
|
"tar": "6.2.1",
|
||||||
"terser": "5.30.3",
|
"terser": "5.31.1",
|
||||||
"typescript": "5.5.2",
|
"typescript": "5.5.3",
|
||||||
"esbuild": "0.20.2",
|
"esbuild": "0.22.0",
|
||||||
"glob": "10.3.12"
|
"glob": "10.3.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "20.12.7",
|
"@misskey-dev/eslint-plugin": "2.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "7.7.1",
|
"@types/node": "20.14.9",
|
||||||
"@typescript-eslint/parser": "7.7.1",
|
"@typescript-eslint/eslint-plugin": "7.15.0",
|
||||||
|
"@typescript-eslint/parser": "7.15.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "13.7.3",
|
"cypress": "13.13.0",
|
||||||
"eslint": "8.57.0",
|
"eslint": "9.6.0",
|
||||||
|
"globals": "15.7.0",
|
||||||
"ncp": "2.0.0",
|
"ncp": "2.0.0",
|
||||||
"start-server-and-test": "2.0.3"
|
"start-server-and-test": "2.0.4"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@tensorflow/tfjs-core": "4.4.0"
|
"@tensorflow/tfjs-core": "4.4.0"
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
node_modules
|
|
||||||
/built
|
|
||||||
/.eslintrc.js
|
|
||||||
/@types/**/*
|
|
|
@ -1,32 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
parserOptions: {
|
|
||||||
tsconfigRootDir: __dirname,
|
|
||||||
project: ['./tsconfig.json', './test/tsconfig.json'],
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
'../shared/.eslintrc.js',
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
'import/order': ['warn', {
|
|
||||||
'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
|
|
||||||
'pathGroups': [
|
|
||||||
{
|
|
||||||
'pattern': '@/**',
|
|
||||||
'group': 'external',
|
|
||||||
'position': 'after'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}],
|
|
||||||
'no-restricted-globals': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
'name': '__dirname',
|
|
||||||
'message': 'Not in ESModule. Use `import.meta.url` instead.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': '__filename',
|
|
||||||
'message': 'Not in ESModule. Use `import.meta.url` instead.'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
};
|
|
20
packages/backend/assets/api-doc.html
Normal file
20
packages/backend/assets/api-doc.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Misskey API</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script
|
||||||
|
id="api-reference"
|
||||||
|
data-url="/api.json"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -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>
|
|
46
packages/backend/eslint.config.js
Normal file
46
packages/backend/eslint.config.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import tsParser from '@typescript-eslint/parser';
|
||||||
|
import sharedConfig from '../shared/eslint.config.js';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...sharedConfig,
|
||||||
|
{
|
||||||
|
ignores: ['**/node_modules', 'built', '@types/**/*'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
project: ['./tsconfig.json', './test/tsconfig.json'],
|
||||||
|
sourceType: 'module',
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'import/order': ['warn', {
|
||||||
|
groups: [
|
||||||
|
'builtin',
|
||||||
|
'external',
|
||||||
|
'internal',
|
||||||
|
'parent',
|
||||||
|
'sibling',
|
||||||
|
'index',
|
||||||
|
'object',
|
||||||
|
'type',
|
||||||
|
],
|
||||||
|
pathGroups: [{
|
||||||
|
pattern: '@/**',
|
||||||
|
group: 'external',
|
||||||
|
position: 'after',
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
'no-restricted-globals': ['error', {
|
||||||
|
name: '__dirname',
|
||||||
|
message: 'Not in ESModule. Use `import.meta.url` instead.',
|
||||||
|
}, {
|
||||||
|
name: '__filename',
|
||||||
|
message: 'Not in ESModule. Use `import.meta.url` instead.',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
|
@ -65,43 +65,43 @@
|
||||||
"utf-8-validate": "6.0.3"
|
"utf-8-validate": "6.0.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "3.412.0",
|
"@aws-sdk/client-s3": "3.600.0",
|
||||||
"@aws-sdk/lib-storage": "3.412.0",
|
"@aws-sdk/lib-storage": "3.600.0",
|
||||||
"@bull-board/api": "5.17.0",
|
"@bull-board/api": "5.20.5",
|
||||||
"@bull-board/fastify": "5.17.0",
|
"@bull-board/fastify": "5.20.5",
|
||||||
"@bull-board/ui": "5.17.0",
|
"@bull-board/ui": "5.20.5",
|
||||||
"@discordapp/twemoji": "15.0.3",
|
"@discordapp/twemoji": "15.0.3",
|
||||||
"@fastify/accepts": "4.3.0",
|
"@fastify/accepts": "4.3.0",
|
||||||
"@fastify/cookie": "9.3.1",
|
"@fastify/cookie": "9.3.1",
|
||||||
"@fastify/cors": "9.0.1",
|
"@fastify/cors": "9.0.1",
|
||||||
"@fastify/express": "3.0.0",
|
"@fastify/express": "3.0.0",
|
||||||
"@fastify/http-proxy": "9.5.0",
|
"@fastify/http-proxy": "9.5.0",
|
||||||
"@fastify/multipart": "8.2.0",
|
"@fastify/multipart": "8.3.0",
|
||||||
"@fastify/static": "7.0.3",
|
"@fastify/static": "7.0.4",
|
||||||
"@fastify/view": "9.1.0",
|
"@fastify/view": "9.1.0",
|
||||||
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
||||||
"@misskey-dev/summaly": "5.1.0",
|
"@misskey-dev/summaly": "5.1.0",
|
||||||
"@napi-rs/canvas": "^0.1.52",
|
"@napi-rs/canvas": "^0.1.53",
|
||||||
"@nestjs/common": "10.3.8",
|
"@nestjs/common": "10.3.10",
|
||||||
"@nestjs/core": "10.3.8",
|
"@nestjs/core": "10.3.10",
|
||||||
"@nestjs/testing": "10.3.8",
|
"@nestjs/testing": "10.3.10",
|
||||||
"@peertube/http-signature": "1.7.0",
|
"@peertube/http-signature": "1.7.0",
|
||||||
"@sentry/node": "^8.5.0",
|
"@sentry/node": "8.13.0",
|
||||||
"@sentry/profiling-node": "^8.5.0",
|
"@sentry/profiling-node": "8.13.0",
|
||||||
"@simplewebauthn/server": "10.0.0",
|
"@simplewebauthn/server": "10.0.0",
|
||||||
"@sinonjs/fake-timers": "11.2.2",
|
"@sinonjs/fake-timers": "11.2.2",
|
||||||
"@smithy/node-http-handler": "2.5.0",
|
"@smithy/node-http-handler": "2.5.0",
|
||||||
"@swc/cli": "0.3.12",
|
"@swc/cli": "0.3.12",
|
||||||
"@swc/core": "1.4.17",
|
"@swc/core": "1.6.6",
|
||||||
"@twemoji/parser": "15.1.1",
|
"@twemoji/parser": "15.1.1",
|
||||||
"accepts": "1.3.8",
|
"accepts": "1.3.8",
|
||||||
"ajv": "8.13.0",
|
"ajv": "8.16.0",
|
||||||
"archiver": "7.0.1",
|
"archiver": "7.0.1",
|
||||||
"async-mutex": "0.5.0",
|
"async-mutex": "0.5.0",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
"body-parser": "1.20.2",
|
"body-parser": "1.20.2",
|
||||||
"bullmq": "5.7.8",
|
"bullmq": "5.8.3",
|
||||||
"cacheable-lookup": "7.0.0",
|
"cacheable-lookup": "7.0.0",
|
||||||
"cbor": "9.0.2",
|
"cbor": "9.0.2",
|
||||||
"chalk": "5.3.0",
|
"chalk": "5.3.0",
|
||||||
|
@ -112,27 +112,27 @@
|
||||||
"content-disposition": "0.5.4",
|
"content-disposition": "0.5.4",
|
||||||
"date-fns": "2.30.0",
|
"date-fns": "2.30.0",
|
||||||
"deep-email-validator": "0.1.21",
|
"deep-email-validator": "0.1.21",
|
||||||
"fastify": "4.26.2",
|
"fastify": "4.28.1",
|
||||||
"fastify-raw-body": "4.3.0",
|
"fastify-raw-body": "4.3.0",
|
||||||
"feed": "4.2.2",
|
"feed": "4.2.2",
|
||||||
"file-type": "19.0.0",
|
"file-type": "19.0.0",
|
||||||
"fluent-ffmpeg": "2.1.2",
|
"fluent-ffmpeg": "2.1.3",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"got": "14.2.1",
|
"got": "14.4.1",
|
||||||
"happy-dom": "10.0.3",
|
"happy-dom": "10.0.3",
|
||||||
"hpagent": "1.2.0",
|
"hpagent": "1.2.0",
|
||||||
"htmlescape": "1.1.1",
|
"htmlescape": "1.1.1",
|
||||||
"http-link-header": "1.1.3",
|
"http-link-header": "1.1.3",
|
||||||
"ioredis": "5.4.1",
|
"ioredis": "5.4.1",
|
||||||
"ip-cidr": "3.1.0",
|
"ip-cidr": "4.0.1",
|
||||||
"ipaddr.js": "2.2.0",
|
"ipaddr.js": "2.2.0",
|
||||||
"is-svg": "5.0.0",
|
"is-svg": "5.0.1",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"jsdom": "24.0.0",
|
"jsdom": "24.1.0",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"jsonld": "8.3.2",
|
"jsonld": "8.3.2",
|
||||||
"jsrsasign": "11.1.0",
|
"jsrsasign": "11.1.0",
|
||||||
"meilisearch": "0.38.0",
|
"meilisearch": "0.41.0",
|
||||||
"mfm-js": "0.24.0",
|
"mfm-js": "0.24.0",
|
||||||
"microformats-parser": "2.0.2",
|
"microformats-parser": "2.0.2",
|
||||||
"mime-types": "2.1.35",
|
"mime-types": "2.1.35",
|
||||||
|
@ -142,25 +142,25 @@
|
||||||
"nanoid": "5.0.7",
|
"nanoid": "5.0.7",
|
||||||
"nested-property": "4.0.0",
|
"nested-property": "4.0.0",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
"nodemailer": "6.9.13",
|
"nodemailer": "6.9.14",
|
||||||
"nsfwjs": "2.4.2",
|
"nsfwjs": "2.4.2",
|
||||||
"oauth": "0.10.0",
|
"oauth": "0.10.0",
|
||||||
"oauth2orize": "1.12.0",
|
"oauth2orize": "1.12.0",
|
||||||
"oauth2orize-pkce": "0.1.2",
|
"oauth2orize-pkce": "0.1.2",
|
||||||
"os-utils": "0.0.14",
|
"os-utils": "0.0.14",
|
||||||
"otpauth": "9.2.3",
|
"otpauth": "9.3.1",
|
||||||
"parse5": "7.1.2",
|
"parse5": "7.1.2",
|
||||||
"pg": "8.11.5",
|
"pg": "8.12.0",
|
||||||
"pkce-challenge": "4.1.0",
|
"pkce-challenge": "4.1.0",
|
||||||
"probe-image-size": "7.2.3",
|
"probe-image-size": "7.2.3",
|
||||||
"promise-limit": "2.7.0",
|
"promise-limit": "2.7.0",
|
||||||
"proxycheck-ts": "^0.0.9",
|
"proxycheck-ts": "^0.0.9",
|
||||||
"pug": "3.0.2",
|
"pug": "3.0.3",
|
||||||
"punycode": "2.3.1",
|
"punycode": "2.3.1",
|
||||||
"qrcode": "1.5.3",
|
"qrcode": "1.5.3",
|
||||||
"random-seed": "0.3.0",
|
"random-seed": "0.3.0",
|
||||||
"ratelimiter": "3.4.1",
|
"ratelimiter": "3.4.1",
|
||||||
"re2": "1.21.2",
|
"re2": "1.21.3",
|
||||||
"redis-lock": "0.1.4",
|
"redis-lock": "0.1.4",
|
||||||
"reflect-metadata": "0.2.2",
|
"reflect-metadata": "0.2.2",
|
||||||
"rename": "1.0.4",
|
"rename": "1.0.4",
|
||||||
|
@ -168,28 +168,27 @@
|
||||||
"rxjs": "7.8.1",
|
"rxjs": "7.8.1",
|
||||||
"sanitize-html": "2.13.0",
|
"sanitize-html": "2.13.0",
|
||||||
"secure-json-parse": "2.7.0",
|
"secure-json-parse": "2.7.0",
|
||||||
"sharp": "0.33.3",
|
"sharp": "0.33.4",
|
||||||
"slacc": "0.0.10",
|
"slacc": "0.0.10",
|
||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"stringz": "2.1.0",
|
"stringz": "2.1.0",
|
||||||
"systeminformation": "5.22.7",
|
"systeminformation": "5.22.11",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"tmp": "0.2.3",
|
"tmp": "0.2.3",
|
||||||
"tsc-alias": "1.8.8",
|
"tsc-alias": "1.8.10",
|
||||||
"tsconfig-paths": "4.2.0",
|
"tsconfig-paths": "4.2.0",
|
||||||
"typeorm": "0.3.20",
|
"typeorm": "0.3.20",
|
||||||
"typescript": "5.5.2",
|
"typescript": "5.5.3",
|
||||||
"ulid": "2.3.0",
|
"ulid": "2.3.0",
|
||||||
"vary": "1.1.2",
|
"vary": "1.1.2",
|
||||||
"w3c-xmlserializer": "^5.0.0",
|
"w3c-xmlserializer": "^5.0.0",
|
||||||
"web-push": "3.6.7",
|
"web-push": "3.6.7",
|
||||||
"ws": "8.17.0",
|
"ws": "8.17.1",
|
||||||
"xev": "3.0.2"
|
"xev": "3.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/globals": "29.7.0",
|
"@jest/globals": "29.7.0",
|
||||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
"@nestjs/platform-express": "10.3.10",
|
||||||
"@nestjs/platform-express": "10.3.8",
|
|
||||||
"@simplewebauthn/types": "10.0.0",
|
"@simplewebauthn/types": "10.0.0",
|
||||||
"@swc/jest": "0.2.36",
|
"@swc/jest": "0.2.36",
|
||||||
"@types/accepts": "1.3.7",
|
"@types/accepts": "1.3.7",
|
||||||
|
@ -199,21 +198,21 @@
|
||||||
"@types/color-convert": "2.0.3",
|
"@types/color-convert": "2.0.3",
|
||||||
"@types/content-disposition": "0.5.8",
|
"@types/content-disposition": "0.5.8",
|
||||||
"@types/fluent-ffmpeg": "2.1.24",
|
"@types/fluent-ffmpeg": "2.1.24",
|
||||||
"@types/htmlescape": "^1.1.3",
|
"@types/htmlescape": "1.1.3",
|
||||||
"@types/http-link-header": "1.0.5",
|
"@types/http-link-header": "1.0.7",
|
||||||
"@types/jest": "29.5.12",
|
"@types/jest": "29.5.12",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/jsdom": "21.1.6",
|
"@types/jsdom": "21.1.7",
|
||||||
"@types/jsonld": "1.5.13",
|
"@types/jsonld": "1.5.14",
|
||||||
"@types/jsrsasign": "10.5.14",
|
"@types/jsrsasign": "10.5.14",
|
||||||
"@types/mime-types": "2.1.4",
|
"@types/mime-types": "2.1.4",
|
||||||
"@types/ms": "0.7.34",
|
"@types/ms": "0.7.34",
|
||||||
"@types/node": "20.12.7",
|
"@types/node": "20.14.9",
|
||||||
"@types/nodemailer": "6.4.15",
|
"@types/nodemailer": "6.4.15",
|
||||||
"@types/oauth": "0.9.4",
|
"@types/oauth": "0.9.5",
|
||||||
"@types/oauth2orize": "1.11.5",
|
"@types/oauth2orize": "1.11.5",
|
||||||
"@types/oauth2orize-pkce": "0.1.2",
|
"@types/oauth2orize-pkce": "0.1.2",
|
||||||
"@types/pg": "8.11.5",
|
"@types/pg": "8.11.6",
|
||||||
"@types/pug": "2.0.10",
|
"@types/pug": "2.0.10",
|
||||||
"@types/punycode": "2.1.4",
|
"@types/punycode": "2.1.4",
|
||||||
"@types/qrcode": "1.5.5",
|
"@types/qrcode": "1.5.5",
|
||||||
|
@ -230,18 +229,17 @@
|
||||||
"@types/w3c-xmlserializer": "^2.0.4",
|
"@types/w3c-xmlserializer": "^2.0.4",
|
||||||
"@types/web-push": "3.6.3",
|
"@types/web-push": "3.6.3",
|
||||||
"@types/ws": "8.5.10",
|
"@types/ws": "8.5.10",
|
||||||
"@typescript-eslint/eslint-plugin": "7.7.1",
|
"@typescript-eslint/eslint-plugin": "7.15.0",
|
||||||
"@typescript-eslint/parser": "7.7.1",
|
"@typescript-eslint/parser": "7.15.0",
|
||||||
"aws-sdk-client-mock": "3.0.1",
|
"aws-sdk-client-mock": "4.0.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint": "8.57.0",
|
|
||||||
"eslint-plugin-import": "2.29.1",
|
"eslint-plugin-import": "2.29.1",
|
||||||
"execa": "8.0.1",
|
"execa": "9.2.0",
|
||||||
"fkill": "^9.0.0",
|
"fkill": "9.0.0",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-mock": "29.7.0",
|
"jest-mock": "29.7.0",
|
||||||
"nodemon": "3.1.0",
|
"nodemon": "3.1.4",
|
||||||
"pid-port": "1.0.0",
|
"pid-port": "1.0.0",
|
||||||
"simple-oauth2": "5.0.0"
|
"simple-oauth2": "5.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ function execStart() {
|
||||||
|
|
||||||
async function killProc() {
|
async function killProc() {
|
||||||
if (backendProcess) {
|
if (backendProcess) {
|
||||||
|
backendProcess.catch(() => {}); // backendProcess.kill()によって発生する例外を無視するためにcatch()を呼び出す
|
||||||
backendProcess.kill();
|
backendProcess.kill();
|
||||||
await new Promise(resolve => backendProcess.on('exit', resolve));
|
await new Promise(resolve => backendProcess.on('exit', resolve));
|
||||||
backendProcess = undefined;
|
backendProcess = undefined;
|
||||||
|
@ -46,6 +47,7 @@ async function killProc() {
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
stdio: [process.stdin, process.stdout, process.stderr, 'ipc'],
|
stdio: [process.stdin, process.stdout, process.stderr, 'ipc'],
|
||||||
|
serialization: "json",
|
||||||
})
|
})
|
||||||
.on('message', async (message) => {
|
.on('message', async (message) => {
|
||||||
if (message.type === 'exit') {
|
if (message.type === 'exit') {
|
||||||
|
|
|
@ -23,7 +23,7 @@ type RedisOptionsSource = Partial<RedisOptions> & {
|
||||||
* 設定ファイルの型
|
* 設定ファイルの型
|
||||||
*/
|
*/
|
||||||
type Source = {
|
type Source = {
|
||||||
url: string;
|
url?: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
socket?: string;
|
socket?: string;
|
||||||
chmodSocket?: string;
|
chmodSocket?: string;
|
||||||
|
@ -31,9 +31,9 @@ type Source = {
|
||||||
db: {
|
db: {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
db: string;
|
db?: string;
|
||||||
user: string;
|
user?: string;
|
||||||
pass: string;
|
pass?: string;
|
||||||
disableCache?: boolean;
|
disableCache?: boolean;
|
||||||
extra?: { [x: string]: string };
|
extra?: { [x: string]: string };
|
||||||
};
|
};
|
||||||
|
@ -202,13 +202,17 @@ export function loadConfig(): Config {
|
||||||
: { 'src/_boot_.ts': { file: 'src/_boot_.ts' } };
|
: { 'src/_boot_.ts': { file: 'src/_boot_.ts' } };
|
||||||
const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
|
const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
|
||||||
|
|
||||||
const url = tryCreateUrl(config.url);
|
const url = tryCreateUrl(config.url ?? process.env.MISSKEY_URL ?? '');
|
||||||
const version = meta.version;
|
const version = meta.version;
|
||||||
const host = url.host;
|
const host = url.host;
|
||||||
const hostname = url.hostname;
|
const hostname = url.hostname;
|
||||||
const scheme = url.protocol.replace(/:$/, '');
|
const scheme = url.protocol.replace(/:$/, '');
|
||||||
const wsScheme = scheme.replace('http', 'ws');
|
const wsScheme = scheme.replace('http', 'ws');
|
||||||
|
|
||||||
|
const dbDb = config.db.db ?? process.env.DATABASE_DB ?? '';
|
||||||
|
const dbUser = config.db.user ?? process.env.DATABASE_USER ?? '';
|
||||||
|
const dbPass = config.db.pass ?? process.env.DATABASE_PASSWORD ?? '';
|
||||||
|
|
||||||
const externalMediaProxy = config.mediaProxy ?
|
const externalMediaProxy = config.mediaProxy ?
|
||||||
config.mediaProxy.endsWith('/') ? config.mediaProxy.substring(0, config.mediaProxy.length - 1) : config.mediaProxy
|
config.mediaProxy.endsWith('/') ? config.mediaProxy.substring(0, config.mediaProxy.length - 1) : config.mediaProxy
|
||||||
: null;
|
: null;
|
||||||
|
@ -231,7 +235,7 @@ export function loadConfig(): Config {
|
||||||
apiUrl: `${scheme}://${host}/api`,
|
apiUrl: `${scheme}://${host}/api`,
|
||||||
authUrl: `${scheme}://${host}/auth`,
|
authUrl: `${scheme}://${host}/auth`,
|
||||||
driveUrl: `${scheme}://${host}/files`,
|
driveUrl: `${scheme}://${host}/files`,
|
||||||
db: config.db,
|
db: { ...config.db, db: dbDb, user: dbUser, pass: dbPass },
|
||||||
dbReplications: config.dbReplications,
|
dbReplications: config.dbReplications,
|
||||||
dbSlaves: config.dbSlaves,
|
dbSlaves: config.dbSlaves,
|
||||||
meilisearch: config.meilisearch,
|
meilisearch: config.meilisearch,
|
||||||
|
@ -259,7 +263,7 @@ export function loadConfig(): Config {
|
||||||
deliverJobMaxAttempts: config.deliverJobMaxAttempts,
|
deliverJobMaxAttempts: config.deliverJobMaxAttempts,
|
||||||
inboxJobMaxAttempts: config.inboxJobMaxAttempts,
|
inboxJobMaxAttempts: config.inboxJobMaxAttempts,
|
||||||
proxyRemoteFiles: config.proxyRemoteFiles,
|
proxyRemoteFiles: config.proxyRemoteFiles,
|
||||||
signToActivityPubGet: config.signToActivityPubGet,
|
signToActivityPubGet: config.signToActivityPubGet ?? true,
|
||||||
mediaProxy: externalMediaProxy ?? internalMediaProxy,
|
mediaProxy: externalMediaProxy ?? internalMediaProxy,
|
||||||
externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy,
|
externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy,
|
||||||
videoThumbnailGenerator: config.videoThumbnailGenerator ?
|
videoThumbnailGenerator: config.videoThumbnailGenerator ?
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// dummy
|
||||||
export const MAX_NOTE_TEXT_LENGTH = 3000;
|
export const MAX_NOTE_TEXT_LENGTH = 3000;
|
||||||
|
|
||||||
export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min
|
export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
|
} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
|
||||||
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
|
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
|
||||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||||
|
import { UserSearchService } from '@/core/UserSearchService.js';
|
||||||
import { AccountMoveService } from './AccountMoveService.js';
|
import { AccountMoveService } from './AccountMoveService.js';
|
||||||
import { AccountUpdateService } from './AccountUpdateService.js';
|
import { AccountUpdateService } from './AccountUpdateService.js';
|
||||||
import { AiService } from './AiService.js';
|
import { AiService } from './AiService.js';
|
||||||
|
@ -205,6 +206,7 @@ const $UserFollowingService: Provider = { provide: 'UserFollowingService', useEx
|
||||||
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
|
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
|
||||||
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
|
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
|
||||||
const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
|
const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
|
||||||
|
const $UserSearchService: Provider = { provide: 'UserSearchService', useExisting: UserSearchService };
|
||||||
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
|
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
|
||||||
const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
|
const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
|
||||||
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
|
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
|
||||||
|
@ -353,6 +355,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
UserKeypairService,
|
UserKeypairService,
|
||||||
UserListService,
|
UserListService,
|
||||||
UserMutingService,
|
UserMutingService,
|
||||||
|
UserSearchService,
|
||||||
UserSuspendService,
|
UserSuspendService,
|
||||||
UserAuthService,
|
UserAuthService,
|
||||||
VideoProcessingService,
|
VideoProcessingService,
|
||||||
|
@ -497,6 +500,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$UserKeypairService,
|
$UserKeypairService,
|
||||||
$UserListService,
|
$UserListService,
|
||||||
$UserMutingService,
|
$UserMutingService,
|
||||||
|
$UserSearchService,
|
||||||
$UserSuspendService,
|
$UserSuspendService,
|
||||||
$UserAuthService,
|
$UserAuthService,
|
||||||
$VideoProcessingService,
|
$VideoProcessingService,
|
||||||
|
@ -642,6 +646,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
UserKeypairService,
|
UserKeypairService,
|
||||||
UserListService,
|
UserListService,
|
||||||
UserMutingService,
|
UserMutingService,
|
||||||
|
UserSearchService,
|
||||||
UserSuspendService,
|
UserSuspendService,
|
||||||
UserAuthService,
|
UserAuthService,
|
||||||
VideoProcessingService,
|
VideoProcessingService,
|
||||||
|
@ -785,6 +790,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$UserKeypairService,
|
$UserKeypairService,
|
||||||
$UserListService,
|
$UserListService,
|
||||||
$UserMutingService,
|
$UserMutingService,
|
||||||
|
$UserSearchService,
|
||||||
$UserSuspendService,
|
$UserSuspendService,
|
||||||
$UserAuthService,
|
$UserAuthService,
|
||||||
$VideoProcessingService,
|
$VideoProcessingService,
|
||||||
|
|
|
@ -40,6 +40,7 @@ export class FederatedInstanceService implements OnApplicationShutdown {
|
||||||
firstRetrievedAt: new Date(parsed.firstRetrievedAt),
|
firstRetrievedAt: new Date(parsed.firstRetrievedAt),
|
||||||
latestRequestReceivedAt: parsed.latestRequestReceivedAt ? new Date(parsed.latestRequestReceivedAt) : null,
|
latestRequestReceivedAt: parsed.latestRequestReceivedAt ? new Date(parsed.latestRequestReceivedAt) : null,
|
||||||
infoUpdatedAt: parsed.infoUpdatedAt ? new Date(parsed.infoUpdatedAt) : null,
|
infoUpdatedAt: parsed.infoUpdatedAt ? new Date(parsed.infoUpdatedAt) : null,
|
||||||
|
notRespondingSince: parsed.notRespondingSince ? new Date(parsed.notRespondingSince) : null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,10 +14,12 @@ import { intersperse } from '@/misc/prelude/array.js';
|
||||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||||
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js';
|
import type { DefaultTreeAdapterMap } from 'parse5';
|
||||||
import type * as mfm from 'mfm-js';
|
import type * as mfm from 'mfm-js';
|
||||||
|
|
||||||
const treeAdapter = TreeAdapter.defaultTreeAdapter;
|
const treeAdapter = parse5.defaultTreeAdapter;
|
||||||
|
type Node = DefaultTreeAdapterMap['node'];
|
||||||
|
type ChildNode = DefaultTreeAdapterMap['childNode'];
|
||||||
|
|
||||||
const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
|
const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
|
||||||
const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
|
const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
|
||||||
|
@ -47,7 +49,7 @@ export class MfmService {
|
||||||
|
|
||||||
return text.trim();
|
return text.trim();
|
||||||
|
|
||||||
function getText(node: TreeAdapter.Node): string {
|
function getText(node: Node): string {
|
||||||
if (treeAdapter.isTextNode(node)) return node.value;
|
if (treeAdapter.isTextNode(node)) return node.value;
|
||||||
if (!treeAdapter.isElementNode(node)) return '';
|
if (!treeAdapter.isElementNode(node)) return '';
|
||||||
if (node.nodeName === 'br') return '\n';
|
if (node.nodeName === 'br') return '\n';
|
||||||
|
@ -59,7 +61,7 @@ export class MfmService {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendChildren(childNodes: TreeAdapter.ChildNode[]): void {
|
function appendChildren(childNodes: ChildNode[]): void {
|
||||||
if (childNodes) {
|
if (childNodes) {
|
||||||
for (const n of childNodes) {
|
for (const n of childNodes) {
|
||||||
analyze(n);
|
analyze(n);
|
||||||
|
@ -67,14 +69,16 @@ export class MfmService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function analyze(node: TreeAdapter.Node) {
|
function analyze(node: Node) {
|
||||||
if (treeAdapter.isTextNode(node)) {
|
if (treeAdapter.isTextNode(node)) {
|
||||||
text += node.value;
|
text += node.value;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip comment or document type node
|
// Skip comment or document type node
|
||||||
if (!treeAdapter.isElementNode(node)) return;
|
if (!treeAdapter.isElementNode(node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (node.nodeName) {
|
switch (node.nodeName) {
|
||||||
case 'br': {
|
case 'br': {
|
||||||
|
@ -82,8 +86,7 @@ export class MfmService {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'a':
|
case 'a': {
|
||||||
{
|
|
||||||
const txt = getText(node);
|
const txt = getText(node);
|
||||||
const rel = node.attrs.find(x => x.name === 'rel');
|
const rel = node.attrs.find(x => x.name === 'rel');
|
||||||
const href = node.attrs.find(x => x.name === 'href');
|
const href = node.attrs.find(x => x.name === 'href');
|
||||||
|
@ -91,7 +94,7 @@ export class MfmService {
|
||||||
// ハッシュタグ
|
// ハッシュタグ
|
||||||
if (normalizedHashtagNames && href && normalizedHashtagNames.has(normalizeForSearch(txt))) {
|
if (normalizedHashtagNames && href && normalizedHashtagNames.has(normalizeForSearch(txt))) {
|
||||||
text += txt;
|
text += txt;
|
||||||
// メンション
|
// メンション
|
||||||
} else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) {
|
} else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) {
|
||||||
const part = txt.split('@');
|
const part = txt.split('@');
|
||||||
|
|
||||||
|
@ -103,7 +106,7 @@ export class MfmService {
|
||||||
} else if (part.length === 3) {
|
} else if (part.length === 3) {
|
||||||
text += txt;
|
text += txt;
|
||||||
}
|
}
|
||||||
// その他
|
// その他
|
||||||
} else {
|
} else {
|
||||||
const generateLink = () => {
|
const generateLink = () => {
|
||||||
if (!href && !txt) {
|
if (!href && !txt) {
|
||||||
|
@ -131,8 +134,7 @@ export class MfmService {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'h1':
|
case 'h1': {
|
||||||
{
|
|
||||||
text += '【';
|
text += '【';
|
||||||
appendChildren(node.childNodes);
|
appendChildren(node.childNodes);
|
||||||
text += '】\n';
|
text += '】\n';
|
||||||
|
@ -140,16 +142,14 @@ export class MfmService {
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'b':
|
case 'b':
|
||||||
case 'strong':
|
case 'strong': {
|
||||||
{
|
|
||||||
text += '**';
|
text += '**';
|
||||||
appendChildren(node.childNodes);
|
appendChildren(node.childNodes);
|
||||||
text += '**';
|
text += '**';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'small':
|
case 'small': {
|
||||||
{
|
|
||||||
text += '<small>';
|
text += '<small>';
|
||||||
appendChildren(node.childNodes);
|
appendChildren(node.childNodes);
|
||||||
text += '</small>';
|
text += '</small>';
|
||||||
|
@ -157,8 +157,7 @@ export class MfmService {
|
||||||
}
|
}
|
||||||
|
|
||||||
case 's':
|
case 's':
|
||||||
case 'del':
|
case 'del': {
|
||||||
{
|
|
||||||
text += '~~';
|
text += '~~';
|
||||||
appendChildren(node.childNodes);
|
appendChildren(node.childNodes);
|
||||||
text += '~~';
|
text += '~~';
|
||||||
|
@ -166,8 +165,7 @@ export class MfmService {
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'i':
|
case 'i':
|
||||||
case 'em':
|
case 'em': {
|
||||||
{
|
|
||||||
text += '<i>';
|
text += '<i>';
|
||||||
appendChildren(node.childNodes);
|
appendChildren(node.childNodes);
|
||||||
text += '</i>';
|
text += '</i>';
|
||||||
|
@ -208,8 +206,7 @@ export class MfmService {
|
||||||
case 'h3':
|
case 'h3':
|
||||||
case 'h4':
|
case 'h4':
|
||||||
case 'h5':
|
case 'h5':
|
||||||
case 'h6':
|
case 'h6': {
|
||||||
{
|
|
||||||
text += '\n\n';
|
text += '\n\n';
|
||||||
appendChildren(node.childNodes);
|
appendChildren(node.childNodes);
|
||||||
break;
|
break;
|
||||||
|
@ -222,8 +219,7 @@ export class MfmService {
|
||||||
case 'article':
|
case 'article':
|
||||||
case 'li':
|
case 'li':
|
||||||
case 'dt':
|
case 'dt':
|
||||||
case 'dd':
|
case 'dd': {
|
||||||
{
|
|
||||||
text += '\n';
|
text += '\n';
|
||||||
appendChildren(node.childNodes);
|
appendChildren(node.childNodes);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -50,6 +50,7 @@ export type RolePolicies = {
|
||||||
canHideAds: boolean;
|
canHideAds: boolean;
|
||||||
driveCapacityMb: number;
|
driveCapacityMb: number;
|
||||||
alwaysMarkNsfw: boolean;
|
alwaysMarkNsfw: boolean;
|
||||||
|
canUpdateBioMedia: boolean;
|
||||||
pinLimit: number;
|
pinLimit: number;
|
||||||
antennaLimit: number;
|
antennaLimit: number;
|
||||||
wordMuteLimit: number;
|
wordMuteLimit: number;
|
||||||
|
@ -84,6 +85,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||||
canHideAds: false,
|
canHideAds: false,
|
||||||
driveCapacityMb: 100,
|
driveCapacityMb: 100,
|
||||||
alwaysMarkNsfw: false,
|
alwaysMarkNsfw: false,
|
||||||
|
canUpdateBioMedia: true,
|
||||||
pinLimit: 5,
|
pinLimit: 5,
|
||||||
antennaLimit: 5,
|
antennaLimit: 5,
|
||||||
wordMuteLimit: 200,
|
wordMuteLimit: 200,
|
||||||
|
@ -392,6 +394,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
|
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
|
||||||
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
|
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
|
||||||
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
|
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
|
||||||
|
canUpdateBioMedia: calc('canUpdateBioMedia', vs => vs.some(v => v === true)),
|
||||||
pinLimit: calc('pinLimit', vs => Math.max(...vs)),
|
pinLimit: calc('pinLimit', vs => Math.max(...vs)),
|
||||||
antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
|
antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
|
||||||
wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)),
|
wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)),
|
||||||
|
|
205
packages/backend/src/core/UserSearchService.ts
Normal file
205
packages/backend/src/core/UserSearchService.ts
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Brackets, SelectQueryBuilder } from 'typeorm';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { type FollowingsRepository, MiUser, type UsersRepository } from '@/models/_.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||||
|
import type { Config } from '@/config.js';
|
||||||
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
|
import { Packed } from '@/misc/json-schema.js';
|
||||||
|
|
||||||
|
function defaultActiveThreshold() {
|
||||||
|
return new Date(Date.now() - 1000 * 60 * 60 * 24 * 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserSearchService {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.config)
|
||||||
|
private config: Config,
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
@Inject(DI.followingsRepository)
|
||||||
|
private followingsRepository: FollowingsRepository,
|
||||||
|
private userEntityService: UserEntityService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ユーザ名とホスト名によるユーザ検索を行う.
|
||||||
|
*
|
||||||
|
* - 検索結果には優先順位がつけられており、以下の順序で検索が行われる.
|
||||||
|
* 1. フォローしているユーザのうち、一定期間以内(※)に更新されたユーザ
|
||||||
|
* 2. フォローしているユーザのうち、一定期間以内に更新されていないユーザ
|
||||||
|
* 3. フォローしていないユーザのうち、一定期間以内に更新されたユーザ
|
||||||
|
* 4. フォローしていないユーザのうち、一定期間以内に更新されていないユーザ
|
||||||
|
* - ログインしていない場合は、以下の順序で検索が行われる.
|
||||||
|
* 1. 一定期間以内に更新されたユーザ
|
||||||
|
* 2. 一定期間以内に更新されていないユーザ
|
||||||
|
* - それぞれの検索結果はユーザ名の昇順でソートされる.
|
||||||
|
* - 動作的には先に登場した検索結果の登場位置が優先される(条件的にユーザIDが重複することはないが).
|
||||||
|
* (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
|
@bindThis
|
||||||
public getNoteHtml(note: MiNote, apAppend?: string) {
|
public getNoteHtml(note: Pick<MiNote, 'text' | 'mentionedRemoteUsers'>, apAppend?: string) {
|
||||||
let noMisskeyContent = false;
|
let noMisskeyContent = false;
|
||||||
const srcMfm = (note.text ?? '') + (apAppend ?? '');
|
const srcMfm = (note.text ?? '') + (apAppend ?? '');
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ import { StatusError } from '@/misc/status-error.js';
|
||||||
import type { UtilityService } from '@/core/UtilityService.js';
|
import type { UtilityService } from '@/core/UtilityService.js';
|
||||||
import type { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import type { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||||
import type { AccountMoveService } from '@/core/AccountMoveService.js';
|
import type { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||||
|
@ -100,6 +101,8 @@ export class ApPersonService implements OnModuleInit {
|
||||||
|
|
||||||
@Inject(DI.followingsRepository)
|
@Inject(DI.followingsRepository)
|
||||||
private followingsRepository: FollowingsRepository,
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
|
private roleService: RoleService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,6 +241,11 @@ export class ApPersonService implements OnModuleInit {
|
||||||
return this.apImageService.resolveImage(user, img).catch(() => null);
|
return this.apImageService.resolveImage(user, img).catch(() => null);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
if (((avatar != null && avatar.id != null) || (banner != null && banner.id != null))
|
||||||
|
&& !(await this.roleService.getUserPolicies(user.id)).canUpdateBioMedia) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
we don't want to return nulls on errors! if the database fields
|
we don't want to return nulls on errors! if the database fields
|
||||||
are already null, nothing changes; if the database has old
|
are already null, nothing changes; if the database has old
|
||||||
|
|
|
@ -74,10 +74,10 @@ export class ApQuestionService {
|
||||||
|
|
||||||
//#region このサーバーに既に登録されているか
|
//#region このサーバーに既に登録されているか
|
||||||
const note = await this.notesRepository.findOneBy({ uri });
|
const note = await this.notesRepository.findOneBy({ uri });
|
||||||
if (note == null) throw new Error('Question is not registed');
|
if (note == null) throw new Error('Question is not registered');
|
||||||
|
|
||||||
const poll = await this.pollsRepository.findOneBy({ noteId: note.id });
|
const poll = await this.pollsRepository.findOneBy({ noteId: note.id });
|
||||||
if (poll == null) throw new Error('Question is not registed');
|
if (poll == null) throw new Error('Question is not registered');
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
// resolve new Question object
|
// resolve new Question object
|
||||||
|
|
|
@ -50,6 +50,22 @@ export class MetaEntityService {
|
||||||
}))
|
}))
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
||||||
|
// クライアントの手間を減らすためあらかじめJSONに変換しておく
|
||||||
|
let defaultLightTheme = null;
|
||||||
|
let defaultDarkTheme = null;
|
||||||
|
if (instance.defaultLightTheme) {
|
||||||
|
try {
|
||||||
|
defaultLightTheme = JSON.stringify(JSON5.parse(instance.defaultLightTheme));
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (instance.defaultDarkTheme) {
|
||||||
|
try {
|
||||||
|
defaultDarkTheme = JSON.stringify(JSON5.parse(instance.defaultDarkTheme));
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const packed: Packed<'MetaLite'> = {
|
const packed: Packed<'MetaLite'> = {
|
||||||
maintainerName: instance.maintainerName,
|
maintainerName: instance.maintainerName,
|
||||||
maintainerEmail: instance.maintainerEmail,
|
maintainerEmail: instance.maintainerEmail,
|
||||||
|
@ -96,9 +112,8 @@ export class MetaEntityService {
|
||||||
backgroundImageUrl: instance.backgroundImageUrl,
|
backgroundImageUrl: instance.backgroundImageUrl,
|
||||||
logoImageUrl: instance.logoImageUrl,
|
logoImageUrl: instance.logoImageUrl,
|
||||||
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
|
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
|
||||||
// クライアントの手間を減らすためあらかじめJSONに変換しておく
|
defaultLightTheme,
|
||||||
defaultLightTheme: instance.defaultLightTheme ? JSON.stringify(JSON5.parse(instance.defaultLightTheme)) : null,
|
defaultDarkTheme,
|
||||||
defaultDarkTheme: instance.defaultDarkTheme ? JSON.stringify(JSON5.parse(instance.defaultDarkTheme)) : null,
|
|
||||||
ads: ads.map(ad => ({
|
ads: ads.map(ad => ({
|
||||||
id: ad.id,
|
id: ad.id,
|
||||||
url: ad.url,
|
url: ad.url,
|
||||||
|
|
|
@ -500,11 +500,15 @@ export class UserEntityService implements OnModuleInit {
|
||||||
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
|
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
|
||||||
onlineStatus: this.getOnlineStatus(user),
|
onlineStatus: this.getOnlineStatus(user),
|
||||||
// パフォーマンス上の理由でローカルユーザーのみ
|
// パフォーマンス上の理由でローカルユーザーのみ
|
||||||
badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then(rs => rs.sort((a, b) => b.displayOrder - a.displayOrder).map(r => ({
|
badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then((rs) => rs
|
||||||
name: r.name,
|
.filter((r) => r.isPublic || iAmModerator)
|
||||||
iconUrl: r.iconUrl,
|
.sort((a, b) => b.displayOrder - a.displayOrder)
|
||||||
displayOrder: r.displayOrder,
|
.map((r) => ({
|
||||||
}))) : undefined,
|
name: r.name,
|
||||||
|
iconUrl: r.iconUrl,
|
||||||
|
displayOrder: r.displayOrder,
|
||||||
|
}))
|
||||||
|
) : undefined,
|
||||||
...(user.host == null ? { getPoints: user.getPoints } : {}),
|
...(user.host == null ? { getPoints: user.getPoints } : {}),
|
||||||
...(isDetailed ? {
|
...(isDetailed ? {
|
||||||
url: profile!.url,
|
url: profile!.url,
|
||||||
|
|
|
@ -4,6 +4,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function isUserRelated(note: any, userIds: Set<string>, ignoreAuthor = false): boolean {
|
export function isUserRelated(note: any, userIds: Set<string>, ignoreAuthor = false): boolean {
|
||||||
|
if (!note) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (userIds.has(note.userId) && !ignoreAuthor) {
|
if (userIds.has(note.userId) && !ignoreAuthor) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ export const packedDriveFileSchema = {
|
||||||
name: {
|
name: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
example: 'lenna.jpg',
|
example: '192.jpg',
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
|
|
@ -228,6 +228,10 @@ export const packedRolePoliciesSchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
canUpdateBioMedia: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
pinLimit: {
|
pinLimit: {
|
||||||
type: 'integer',
|
type: 'integer',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
|
|
@ -61,7 +61,7 @@ export const meta = {
|
||||||
name: {
|
name: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
example: 'lenna.jpg',
|
example: '192.jpg',
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
|
|
@ -25,7 +25,7 @@ import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||||
import { AccountUpdateService } from '@/core/AccountUpdateService.js';
|
import { AccountUpdateService } from '@/core/AccountUpdateService.js';
|
||||||
import { HashtagService } from '@/core/HashtagService.js';
|
import { HashtagService } from '@/core/HashtagService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RolePolicies, RoleService } from '@/core/RoleService.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
||||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||||
|
@ -257,8 +257,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
const profileUpdates = {} as Partial<MiUserProfile>;
|
const profileUpdates = {} as Partial<MiUserProfile>;
|
||||||
|
|
||||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
||||||
|
let policies: RolePolicies | null = null;
|
||||||
|
|
||||||
if (ps.name !== undefined) updates.name = ps.name;
|
if (ps.name !== undefined) {
|
||||||
|
if (ps.name === null) {
|
||||||
|
updates.name = null;
|
||||||
|
} else {
|
||||||
|
const trimmedName = ps.name.trim();
|
||||||
|
updates.name = trimmedName === '' ? null : trimmedName;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (ps.description !== undefined) profileUpdates.description = ps.description;
|
if (ps.description !== undefined) profileUpdates.description = ps.description;
|
||||||
if (ps.lang !== undefined) profileUpdates.lang = ps.lang;
|
if (ps.lang !== undefined) profileUpdates.lang = ps.lang;
|
||||||
if (ps.location !== undefined) profileUpdates.location = ps.location;
|
if (ps.location !== undefined) profileUpdates.location = ps.location;
|
||||||
|
@ -290,14 +298,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ps.mutedWords !== undefined) {
|
if (ps.mutedWords !== undefined) {
|
||||||
checkMuteWordCount(ps.mutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
|
policies ??= await this.roleService.getUserPolicies(user.id);
|
||||||
|
checkMuteWordCount(ps.mutedWords, policies.wordMuteLimit);
|
||||||
validateMuteWordRegex(ps.mutedWords);
|
validateMuteWordRegex(ps.mutedWords);
|
||||||
|
|
||||||
profileUpdates.mutedWords = ps.mutedWords;
|
profileUpdates.mutedWords = ps.mutedWords;
|
||||||
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
|
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
|
||||||
}
|
}
|
||||||
if (ps.hardMutedWords !== undefined) {
|
if (ps.hardMutedWords !== undefined) {
|
||||||
checkMuteWordCount(ps.hardMutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
|
policies ??= await this.roleService.getUserPolicies(user.id);
|
||||||
|
checkMuteWordCount(ps.hardMutedWords, policies.wordMuteLimit);
|
||||||
validateMuteWordRegex(ps.hardMutedWords);
|
validateMuteWordRegex(ps.hardMutedWords);
|
||||||
profileUpdates.hardMutedWords = ps.hardMutedWords;
|
profileUpdates.hardMutedWords = ps.hardMutedWords;
|
||||||
}
|
}
|
||||||
|
@ -321,13 +331,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
|
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
|
||||||
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
|
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
|
||||||
if (typeof ps.alwaysMarkNsfw === 'boolean') {
|
if (typeof ps.alwaysMarkNsfw === 'boolean') {
|
||||||
if ((await roleService.getUserPolicies(user.id)).alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole);
|
policies ??= await this.roleService.getUserPolicies(user.id);
|
||||||
|
if (policies.alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole);
|
||||||
profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
|
profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
|
||||||
}
|
}
|
||||||
if (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive;
|
if (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive;
|
||||||
if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
|
if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
|
||||||
|
|
||||||
if (ps.avatarId) {
|
if (ps.avatarId) {
|
||||||
|
policies ??= await this.roleService.getUserPolicies(user.id);
|
||||||
|
if (!policies.canUpdateBioMedia) throw new ApiError(meta.errors.restrictedByRole);
|
||||||
|
|
||||||
const avatar = await this.driveFilesRepository.findOneBy({ id: ps.avatarId });
|
const avatar = await this.driveFilesRepository.findOneBy({ id: ps.avatarId });
|
||||||
|
|
||||||
if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar);
|
if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar);
|
||||||
|
@ -343,6 +357,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ps.bannerId) {
|
if (ps.bannerId) {
|
||||||
|
policies ??= await this.roleService.getUserPolicies(user.id);
|
||||||
|
if (!policies.canUpdateBioMedia) throw new ApiError(meta.errors.restrictedByRole);
|
||||||
|
|
||||||
const banner = await this.driveFilesRepository.findOneBy({ id: ps.bannerId });
|
const banner = await this.driveFilesRepository.findOneBy({ id: ps.bannerId });
|
||||||
|
|
||||||
if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner);
|
if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner);
|
||||||
|
@ -358,14 +375,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ps.avatarDecorations) {
|
if (ps.avatarDecorations) {
|
||||||
|
policies ??= await this.roleService.getUserPolicies(user.id);
|
||||||
const decorations = await this.avatarDecorationService.getAll(true);
|
const decorations = await this.avatarDecorationService.getAll(true);
|
||||||
const [myRoles, myPolicies] = await Promise.all([this.roleService.getUserRoles(user.id), this.roleService.getUserPolicies(user.id)]);
|
const myRoles = await this.roleService.getUserRoles(user.id);
|
||||||
const allRoles = await this.roleService.getRoles();
|
const allRoles = await this.roleService.getRoles();
|
||||||
const decorationIds = decorations
|
const decorationIds = decorations
|
||||||
.filter(d => (d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id))))
|
.filter(d => (d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id))))
|
||||||
.map(d => d.id);
|
.map(d => d.id);
|
||||||
|
|
||||||
if (ps.avatarDecorations.length > myPolicies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole);
|
if (ps.avatarDecorations.length > policies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole);
|
||||||
updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({
|
updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({
|
||||||
id: d.id,
|
id: d.id,
|
||||||
angle: d.angle ?? 0,
|
angle: d.angle ?? 0,
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { DI } from '@/di-symbols.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -74,6 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
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
|
const iAmModerator = me ? await this.roleService.isModerator(me) : false; // Moderators can see reactions of all users
|
||||||
if (!iAmModerator) {
|
if (!iAmModerator) {
|
||||||
const user = await this.cacheService.findUserById(ps.userId);
|
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) {
|
if ((me == null || me.id !== ps.userId) && !profile.publicReactions) {
|
||||||
throw new ApiError(meta.errors.reactionsNotPublic);
|
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'),
|
const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'),
|
||||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||||
.andWhere('reaction.userId = :userId', { userId: ps.userId })
|
.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);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
|
|
||||||
const reactions = await query
|
const reactions = (await query
|
||||||
.limit(ps.limit)
|
.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 });
|
return await this.noteReactionEntityService.packMany(reactions, me, { withNote: true });
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,15 +3,9 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Brackets } from 'typeorm';
|
import { Injectable } from '@nestjs/common';
|
||||||
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 { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserSearchService } from '@/core/UserSearchService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
|
||||||
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['users'],
|
tags: ['users'],
|
||||||
|
@ -49,89 +43,16 @@ export const paramDef = {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
private userSearchService: UserSearchService,
|
||||||
private config: Config,
|
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
|
||||||
private usersRepository: UsersRepository,
|
|
||||||
|
|
||||||
@Inject(DI.followingsRepository)
|
|
||||||
private followingsRepository: FollowingsRepository,
|
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, (ps, me) => {
|
||||||
const setUsernameAndHostQuery = (query = this.usersRepository.createQueryBuilder('user')) => {
|
return this.userSearchService.search({
|
||||||
if (ps.username) {
|
username: ps.username,
|
||||||
query.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' });
|
host: ps.host,
|
||||||
}
|
}, {
|
||||||
|
limit: ps.limit,
|
||||||
if (ps.host) {
|
detail: ps.detail,
|
||||||
if (ps.host === this.config.hostname || ps.host === '.') {
|
}, me);
|
||||||
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' });
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ export class OpenApiServerService {
|
||||||
public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) {
|
public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) {
|
||||||
fastify.get('/api-doc', async (_request, reply) => {
|
fastify.get('/api-doc', async (_request, reply) => {
|
||||||
reply.header('Cache-Control', 'public, max-age=86400');
|
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) => {
|
fastify.get('/api.json', (_request, reply) => {
|
||||||
reply.header('Cache-Control', 'public, max-age=600');
|
reply.header('Cache-Control', 'public, max-age=600');
|
||||||
|
|
|
@ -15,7 +15,6 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) {
|
||||||
info: {
|
info: {
|
||||||
version: config.version,
|
version: config.version,
|
||||||
title: 'Misskey API',
|
title: 'Misskey API',
|
||||||
'x-logo': { url: '/static-assets/api-doc.png' },
|
|
||||||
},
|
},
|
||||||
|
|
||||||
externalDocs: {
|
externalDocs: {
|
||||||
|
|
|
@ -29,7 +29,8 @@
|
||||||
|
|
||||||
let forceError = localStorage.getItem('forceError');
|
let forceError = localStorage.getItem('forceError');
|
||||||
if (forceError != null) {
|
if (forceError != null) {
|
||||||
renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.')
|
renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region Detect language & fetch translations
|
//#region Detect language & fetch translations
|
||||||
|
@ -155,7 +156,12 @@
|
||||||
document.head.appendChild(css);
|
document.head.appendChild(css);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderError(code, details) {
|
async function renderError(code, details) {
|
||||||
|
// Cannot set property 'innerHTML' of null を回避
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
|
||||||
|
}
|
||||||
|
|
||||||
let errorsElement = document.getElementById('errors');
|
let errorsElement = document.getElementById('errors');
|
||||||
|
|
||||||
if (!errorsElement) {
|
if (!errorsElement) {
|
||||||
|
@ -314,6 +320,6 @@
|
||||||
#errorInfo {
|
#errorInfo {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
}
|
}
|
||||||
`)
|
}`)
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
parserOptions: {
|
|
||||||
tsconfigRootDir: __dirname,
|
|
||||||
project: ['./tsconfig.json'],
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
'../../shared/.eslintrc.js',
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
'import/order': ['warn', {
|
|
||||||
'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
|
|
||||||
'pathGroups': [
|
|
||||||
{
|
|
||||||
'pattern': '@/**',
|
|
||||||
'group': 'external',
|
|
||||||
'position': 'after'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}],
|
|
||||||
'no-restricted-globals': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
'name': '__dirname',
|
|
||||||
'message': 'Not in ESModule. Use `import.meta.url` instead.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': '__filename',
|
|
||||||
'message': 'Not in ESModule. Use `import.meta.url` instead.'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
};
|
|
43
packages/backend/test-server/eslint.config.js
Normal file
43
packages/backend/test-server/eslint.config.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import tsParser from '@typescript-eslint/parser';
|
||||||
|
import sharedConfig from '../../shared/eslint.config.js';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...sharedConfig,
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
|
sourceType: 'module',
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'import/order': ['warn', {
|
||||||
|
groups: [
|
||||||
|
'builtin',
|
||||||
|
'external',
|
||||||
|
'internal',
|
||||||
|
'parent',
|
||||||
|
'sibling',
|
||||||
|
'index',
|
||||||
|
'object',
|
||||||
|
'type',
|
||||||
|
],
|
||||||
|
pathGroups: [{
|
||||||
|
pattern: '@/**',
|
||||||
|
group: 'external',
|
||||||
|
position: 'after',
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
'no-restricted-globals': ['error', {
|
||||||
|
name: '__dirname',
|
||||||
|
message: 'Not in ESModule. Use `import.meta.url` instead.',
|
||||||
|
}, {
|
||||||
|
name: '__filename',
|
||||||
|
message: 'Not in ESModule. Use `import.meta.url` instead.',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
|
@ -1,11 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
parserOptions: {
|
|
||||||
tsconfigRootDir: __dirname,
|
|
||||||
project: ['./tsconfig.json'],
|
|
||||||
},
|
|
||||||
extends: ['../.eslintrc.cjs'],
|
|
||||||
env: {
|
|
||||||
node: true,
|
|
||||||
jest: true,
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -206,7 +206,7 @@ describe('2要素認証', () => {
|
||||||
username,
|
username,
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(usersShowResponse.status, 200);
|
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', {
|
const signinResponse = await api('signin', {
|
||||||
...signinParam(),
|
...signinParam(),
|
||||||
|
@ -248,7 +248,7 @@ describe('2要素認証', () => {
|
||||||
keyName,
|
keyName,
|
||||||
credentialId,
|
credentialId,
|
||||||
creationOptions: registerKeyResponse.body,
|
creationOptions: registerKeyResponse.body,
|
||||||
}) as any, alice);
|
} as any) as any, alice);
|
||||||
assert.strictEqual(keyDoneResponse.status, 200);
|
assert.strictEqual(keyDoneResponse.status, 200);
|
||||||
assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url'));
|
assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url'));
|
||||||
assert.strictEqual(keyDoneResponse.body.name, keyName);
|
assert.strictEqual(keyDoneResponse.body.name, keyName);
|
||||||
|
@ -257,22 +257,22 @@ describe('2要素認証', () => {
|
||||||
username,
|
username,
|
||||||
});
|
});
|
||||||
assert.strictEqual(usersShowResponse.status, 200);
|
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', {
|
const signinResponse = await api('signin', {
|
||||||
...signinParam(),
|
...signinParam(),
|
||||||
});
|
});
|
||||||
assert.strictEqual(signinResponse.status, 200);
|
assert.strictEqual(signinResponse.status, 200);
|
||||||
assert.strictEqual(signinResponse.body.i, undefined);
|
assert.strictEqual(signinResponse.body.i, undefined);
|
||||||
assert.notEqual(signinResponse.body.challenge, undefined);
|
assert.notEqual((signinResponse.body as unknown as { challenge: unknown | undefined }).challenge, undefined);
|
||||||
assert.notEqual(signinResponse.body.allowCredentials, undefined);
|
assert.notEqual((signinResponse.body as unknown as { allowCredentials: unknown | undefined }).allowCredentials, undefined);
|
||||||
assert.strictEqual(signinResponse.body.allowCredentials[0].id, credentialId.toString('base64url'));
|
assert.strictEqual((signinResponse.body as unknown as { allowCredentials: {id: string}[] }).allowCredentials[0].id, credentialId.toString('base64url'));
|
||||||
|
|
||||||
const signinResponse2 = await api('signin', signinWithSecurityKeyParam({
|
const signinResponse2 = await api('signin', signinWithSecurityKeyParam({
|
||||||
keyName,
|
keyName,
|
||||||
credentialId,
|
credentialId,
|
||||||
requestOptions: signinResponse.body,
|
requestOptions: signinResponse.body,
|
||||||
}));
|
} as any));
|
||||||
assert.strictEqual(signinResponse2.status, 200);
|
assert.strictEqual(signinResponse2.status, 200);
|
||||||
assert.notEqual(signinResponse2.body.i, undefined);
|
assert.notEqual(signinResponse2.body.i, undefined);
|
||||||
|
|
||||||
|
@ -307,7 +307,7 @@ describe('2要素認証', () => {
|
||||||
keyName,
|
keyName,
|
||||||
credentialId,
|
credentialId,
|
||||||
creationOptions: registerKeyResponse.body,
|
creationOptions: registerKeyResponse.body,
|
||||||
}) as any, alice);
|
} as any) as any, alice);
|
||||||
assert.strictEqual(keyDoneResponse.status, 200);
|
assert.strictEqual(keyDoneResponse.status, 200);
|
||||||
|
|
||||||
const passwordLessResponse = await api('i/2fa/password-less', {
|
const passwordLessResponse = await api('i/2fa/password-less', {
|
||||||
|
@ -319,7 +319,7 @@ describe('2要素認証', () => {
|
||||||
username,
|
username,
|
||||||
});
|
});
|
||||||
assert.strictEqual(usersShowResponse.status, 200);
|
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', {
|
const signinResponse = await api('signin', {
|
||||||
...signinParam(),
|
...signinParam(),
|
||||||
|
@ -333,7 +333,7 @@ describe('2要素認証', () => {
|
||||||
keyName,
|
keyName,
|
||||||
credentialId,
|
credentialId,
|
||||||
requestOptions: signinResponse.body,
|
requestOptions: signinResponse.body,
|
||||||
}),
|
} as any),
|
||||||
password: '',
|
password: '',
|
||||||
});
|
});
|
||||||
assert.strictEqual(signinResponse2.status, 200);
|
assert.strictEqual(signinResponse2.status, 200);
|
||||||
|
@ -370,7 +370,7 @@ describe('2要素認証', () => {
|
||||||
keyName,
|
keyName,
|
||||||
credentialId,
|
credentialId,
|
||||||
creationOptions: registerKeyResponse.body,
|
creationOptions: registerKeyResponse.body,
|
||||||
}) as any, alice);
|
} as any) as any, alice);
|
||||||
assert.strictEqual(keyDoneResponse.status, 200);
|
assert.strictEqual(keyDoneResponse.status, 200);
|
||||||
|
|
||||||
const renamedKey = 'other-key';
|
const renamedKey = 'other-key';
|
||||||
|
@ -383,6 +383,7 @@ describe('2要素認証', () => {
|
||||||
const iResponse = await api('i', {
|
const iResponse = await api('i', {
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(iResponse.status, 200);
|
assert.strictEqual(iResponse.status, 200);
|
||||||
|
assert.ok(iResponse.body.securityKeysList);
|
||||||
const securityKeys = iResponse.body.securityKeysList.filter((s: { id: string; }) => s.id === credentialId.toString('base64url'));
|
const securityKeys = iResponse.body.securityKeysList.filter((s: { id: string; }) => s.id === credentialId.toString('base64url'));
|
||||||
assert.strictEqual(securityKeys.length, 1);
|
assert.strictEqual(securityKeys.length, 1);
|
||||||
assert.strictEqual(securityKeys[0].name, renamedKey);
|
assert.strictEqual(securityKeys[0].name, renamedKey);
|
||||||
|
@ -419,13 +420,14 @@ describe('2要素認証', () => {
|
||||||
keyName,
|
keyName,
|
||||||
credentialId,
|
credentialId,
|
||||||
creationOptions: registerKeyResponse.body,
|
creationOptions: registerKeyResponse.body,
|
||||||
}) as any, alice);
|
} as any) as any, alice);
|
||||||
assert.strictEqual(keyDoneResponse.status, 200);
|
assert.strictEqual(keyDoneResponse.status, 200);
|
||||||
|
|
||||||
// テストの実行順によっては複数残ってるので全部消す
|
// テストの実行順によっては複数残ってるので全部消す
|
||||||
const iResponse = await api('i', {
|
const iResponse = await api('i', {
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(iResponse.status, 200);
|
assert.strictEqual(iResponse.status, 200);
|
||||||
|
assert.ok(iResponse.body.securityKeysList);
|
||||||
for (const key of iResponse.body.securityKeysList) {
|
for (const key of iResponse.body.securityKeysList) {
|
||||||
const removeKeyResponse = await api('i/2fa/remove-key', {
|
const removeKeyResponse = await api('i/2fa/remove-key', {
|
||||||
token: otpToken(registerResponse.body.secret),
|
token: otpToken(registerResponse.body.secret),
|
||||||
|
@ -439,7 +441,7 @@ describe('2要素認証', () => {
|
||||||
username,
|
username,
|
||||||
});
|
});
|
||||||
assert.strictEqual(usersShowResponse.status, 200);
|
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', {
|
const signinResponse = await api('signin', {
|
||||||
...signinParam(),
|
...signinParam(),
|
||||||
|
@ -470,7 +472,7 @@ describe('2要素認証', () => {
|
||||||
username,
|
username,
|
||||||
});
|
});
|
||||||
assert.strictEqual(usersShowResponse.status, 200);
|
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', {
|
const unregisterResponse = await api('i/2fa/unregister', {
|
||||||
token: otpToken(registerResponse.body.secret),
|
token: otpToken(registerResponse.body.secret),
|
||||||
|
|
|
@ -410,21 +410,21 @@ describe('API visibility', () => {
|
||||||
test('[HTL] public-post が 自分が見れる', async () => {
|
test('[HTL] public-post が 自分が見れる', async () => {
|
||||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||||
assert.strictEqual(res.status, 200);
|
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');
|
assert.strictEqual(notes[0].text, 'x');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[HTL] public-post が 非フォロワーから見れない', async () => {
|
test('[HTL] public-post が 非フォロワーから見れない', async () => {
|
||||||
const res = await api('notes/timeline', { limit: 100 }, other);
|
const res = await api('notes/timeline', { limit: 100 }, other);
|
||||||
assert.strictEqual(res.status, 200);
|
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);
|
assert.strictEqual(notes.length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[HTL] followers-post が フォロワーから見れる', async () => {
|
test('[HTL] followers-post が フォロワーから見れる', async () => {
|
||||||
const res = await api('notes/timeline', { limit: 100 }, follower);
|
const res = await api('notes/timeline', { limit: 100 }, follower);
|
||||||
assert.strictEqual(res.status, 200);
|
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');
|
assert.strictEqual(notes[0].text, 'x');
|
||||||
});
|
});
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@ -433,21 +433,21 @@ describe('API visibility', () => {
|
||||||
test('[replies] followers-reply が フォロワーから見れる', async () => {
|
test('[replies] followers-reply が フォロワーから見れる', async () => {
|
||||||
const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, follower);
|
const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, follower);
|
||||||
assert.strictEqual(res.status, 200);
|
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');
|
assert.strictEqual(notes[0].text, 'x');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => {
|
test('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => {
|
||||||
const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, other);
|
const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, other);
|
||||||
assert.strictEqual(res.status, 200);
|
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);
|
assert.strictEqual(notes.length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
|
test('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
|
||||||
const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, target);
|
const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, target);
|
||||||
assert.strictEqual(res.status, 200);
|
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');
|
assert.strictEqual(notes[0].text, 'x');
|
||||||
});
|
});
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@ -456,14 +456,14 @@ describe('API visibility', () => {
|
||||||
test('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
|
test('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
|
||||||
const res = await api('notes/mentions', { limit: 100 }, target);
|
const res = await api('notes/mentions', { limit: 100 }, target);
|
||||||
assert.strictEqual(res.status, 200);
|
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');
|
assert.strictEqual(notes[0].text, 'x');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async () => {
|
test('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async () => {
|
||||||
const res = await api('notes/mentions', { limit: 100 }, target);
|
const res = await api('notes/mentions', { limit: 100 }, target);
|
||||||
assert.strictEqual(res.status, 200);
|
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');
|
assert.strictEqual(notes[0].text, '@target x');
|
||||||
});
|
});
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
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';
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
describe('Block', () => {
|
describe('Block', () => {
|
||||||
|
@ -33,7 +33,7 @@ describe('Block', () => {
|
||||||
const res = await api('following/create', { userId: alice.id }, bob);
|
const res = await api('following/create', { userId: alice.id }, bob);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
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 () => {
|
test('ブロックされているユーザーにリアクションできない', async () => {
|
||||||
|
@ -42,7 +42,8 @@ describe('Block', () => {
|
||||||
const res = await api('notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob);
|
const res = await api('notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
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 () => {
|
test('ブロックされているユーザーに返信できない', async () => {
|
||||||
|
@ -51,7 +52,8 @@ describe('Block', () => {
|
||||||
const res = await api('notes/create', { replyId: note.id, text: 'yo' }, bob);
|
const res = await api('notes/create', { replyId: note.id, text: 'yo' }, bob);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
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 () => {
|
test('ブロックされているユーザーのノートをRenoteできない', async () => {
|
||||||
|
@ -60,7 +62,7 @@ describe('Block', () => {
|
||||||
const res = await api('notes/create', { renoteId: note.id, text: 'yo' }, bob);
|
const res = await api('notes/create', { renoteId: note.id, text: 'yo' }, bob);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
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: ユーザーリストに入れられないテスト
|
// TODO: ユーザーリストに入れられないテスト
|
||||||
|
|
|
@ -79,14 +79,14 @@ describe('クリップ', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteClip = async (parameters: Misskey.entities.ClipsDeleteRequest, request: Partial<ApiRequest<'clips/delete'>> = {}): Promise<void> => {
|
const deleteClip = async (parameters: Misskey.entities.ClipsDeleteRequest, request: Partial<ApiRequest<'clips/delete'>> = {}): Promise<void> => {
|
||||||
return await successfulApiCall({
|
await successfulApiCall({
|
||||||
endpoint: 'clips/delete',
|
endpoint: 'clips/delete',
|
||||||
parameters,
|
parameters,
|
||||||
user: alice,
|
user: alice,
|
||||||
...request,
|
...request,
|
||||||
}, {
|
}, {
|
||||||
status: 204,
|
status: 204,
|
||||||
}) as any as void;
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const show = async (parameters: Misskey.entities.ClipsShowRequest, request: Partial<ApiRequest<'clips/show'>> = {}): Promise<Misskey.entities.Clip> => {
|
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;
|
let aliceClip: Misskey.entities.Clip;
|
||||||
|
|
||||||
const favorite = async (parameters: Misskey.entities.ClipsFavoriteRequest, request: Partial<ApiRequest<'clips/favorite'>> = {}): Promise<void> => {
|
const favorite = async (parameters: Misskey.entities.ClipsFavoriteRequest, request: Partial<ApiRequest<'clips/favorite'>> = {}): Promise<void> => {
|
||||||
return successfulApiCall({
|
await successfulApiCall({
|
||||||
endpoint: 'clips/favorite',
|
endpoint: 'clips/favorite',
|
||||||
parameters,
|
parameters,
|
||||||
user: alice,
|
user: alice,
|
||||||
...request,
|
...request,
|
||||||
}, {
|
}, {
|
||||||
status: 204,
|
status: 204,
|
||||||
}) as any as void;
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const unfavorite = async (parameters: Misskey.entities.ClipsUnfavoriteRequest, request: Partial<ApiRequest<'clips/unfavorite'>> = {}): Promise<void> => {
|
const unfavorite = async (parameters: Misskey.entities.ClipsUnfavoriteRequest, request: Partial<ApiRequest<'clips/unfavorite'>> = {}): Promise<void> => {
|
||||||
return successfulApiCall({
|
await successfulApiCall({
|
||||||
endpoint: 'clips/unfavorite',
|
endpoint: 'clips/unfavorite',
|
||||||
parameters,
|
parameters,
|
||||||
user: alice,
|
user: alice,
|
||||||
...request,
|
...request,
|
||||||
}, {
|
}, {
|
||||||
status: 204,
|
status: 204,
|
||||||
}) as any as void;
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const myFavorites = async (request: Partial<ApiRequest<'clips/my-favorites'>> = {}): Promise<Misskey.entities.Clip[]> => {
|
const myFavorites = async (request: Partial<ApiRequest<'clips/my-favorites'>> = {}): Promise<Misskey.entities.Clip[]> => {
|
||||||
|
|
|
@ -23,7 +23,7 @@ describe('Drive', () => {
|
||||||
|
|
||||||
const marker = Math.random().toString();
|
const marker = Math.random().toString();
|
||||||
|
|
||||||
const url = 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg';
|
const url = 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg';
|
||||||
|
|
||||||
const catcher = makeStreamCatcher(
|
const catcher = makeStreamCatcher(
|
||||||
alice,
|
alice,
|
||||||
|
@ -41,14 +41,14 @@ describe('Drive', () => {
|
||||||
const file = await catcher;
|
const file = await catcher;
|
||||||
|
|
||||||
assert.strictEqual(res.status, 204);
|
assert.strictEqual(res.status, 204);
|
||||||
assert.strictEqual(file.name, 'Lenna.jpg');
|
assert.strictEqual(file.name, '192.jpg');
|
||||||
assert.strictEqual(file.type, 'image/jpeg');
|
assert.strictEqual(file.type, 'image/jpeg');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ローカルからアップロードできる', async () => {
|
test('ローカルからアップロードできる', async () => {
|
||||||
// APIレスポンスを直接使用するので utils.js uploadFile が通過することで成功とする
|
// APIレスポンスを直接使用するので utils.js uploadFile が通過することで成功とする
|
||||||
|
|
||||||
const res = await uploadFile(alice, { path: 'Lenna.jpg', name: 'テスト画像' });
|
const res = await uploadFile(alice, { path: '192.jpg', name: 'テスト画像' });
|
||||||
|
|
||||||
assert.strictEqual(res.body?.name, 'テスト画像.jpg');
|
assert.strictEqual(res.body?.name, 'テスト画像.jpg');
|
||||||
assert.strictEqual(res.body.type, 'image/jpeg');
|
assert.strictEqual(res.body.type, 'image/jpeg');
|
||||||
|
|
|
@ -10,7 +10,7 @@ import * as assert from 'assert';
|
||||||
// https://github.com/node-fetch/node-fetch/pull/1664
|
// https://github.com/node-fetch/node-fetch/pull/1664
|
||||||
import { Blob } from 'node-fetch';
|
import { Blob } from 'node-fetch';
|
||||||
import { MiUser } from '@/models/_.js';
|
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';
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
describe('Endpoints', () => {
|
describe('Endpoints', () => {
|
||||||
|
@ -117,12 +117,21 @@ describe('Endpoints', () => {
|
||||||
assert.strictEqual(res.body.birthday, myBirthday);
|
assert.strictEqual(res.body.birthday, myBirthday);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('名前を空白にできる', async () => {
|
test('名前を空白のみにした場合nullになる', async () => {
|
||||||
const res = await api('i/update', {
|
const res = await api('i/update', {
|
||||||
name: ' ',
|
name: ' ',
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(res.body.name, ' ');
|
assert.strictEqual(res.body.name, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('名前の前後に空白(ホワイトスペース)を入れてもトリムされる', async () => {
|
||||||
|
const res = await api('i/update', {
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#white_space
|
||||||
|
name: ' あ い う \u0009\u000b\u000c\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\ufeff',
|
||||||
|
}, alice);
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.strictEqual(res.body.name, 'あ い う');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('誕生日の設定を削除できる', async () => {
|
test('誕生日の設定を削除できる', async () => {
|
||||||
|
@ -155,7 +164,7 @@ describe('Endpoints', () => {
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
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 () => {
|
test('ユーザーが存在しなかったら怒る', async () => {
|
||||||
|
@ -276,7 +285,8 @@ describe('Endpoints', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
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 () => {
|
test('引用にリアクションできる', async () => {
|
||||||
|
@ -584,7 +594,7 @@ describe('Endpoints', () => {
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||||
assert.strictEqual(res.body!.name, 'Lenna.jpg');
|
assert.strictEqual(res.body!.name, '192.jpg');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ファイルに名前を付けられる', async () => {
|
test('ファイルに名前を付けられる', async () => {
|
||||||
|
@ -1054,7 +1064,7 @@ describe('Endpoints', () => {
|
||||||
userId: bob.id,
|
userId: bob.id,
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(res1.status, 204);
|
assert.strictEqual(res1.status, 204);
|
||||||
assert.strictEqual(res2.body?.memo, memo);
|
assert.strictEqual((res2.body as unknown as { memo: string })?.memo, memo);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('自分に関するメモを更新できる', async () => {
|
test('自分に関するメモを更新できる', async () => {
|
||||||
|
@ -1069,7 +1079,7 @@ describe('Endpoints', () => {
|
||||||
userId: alice.id,
|
userId: alice.id,
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(res1.status, 204);
|
assert.strictEqual(res1.status, 204);
|
||||||
assert.strictEqual(res2.body?.memo, memo);
|
assert.strictEqual((res2.body as unknown as { memo: string })?.memo, memo);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('メモを削除できる', async () => {
|
test('メモを削除できる', async () => {
|
||||||
|
@ -1090,7 +1100,7 @@ describe('Endpoints', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
// memoには常に文字列かnullが入っている(5cac151)
|
// memoには常に文字列かnullが入っている(5cac151)
|
||||||
assert.strictEqual(res.body.memo, null);
|
assert.strictEqual((res.body as unknown as { memo: string | null }).memo, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('メモは個人ごとに独立して保存される', async () => {
|
test('メモは個人ごとに独立して保存される', async () => {
|
||||||
|
@ -1117,8 +1127,8 @@ describe('Endpoints', () => {
|
||||||
}, carol),
|
}, carol),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
assert.strictEqual(resAlice.body.memo, memoAliceToBob);
|
assert.strictEqual((resAlice.body as unknown as { memo: string }).memo, memoAliceToBob);
|
||||||
assert.strictEqual(resCarol.body.memo, memoCarolToBob);
|
assert.strictEqual((resCarol.body as unknown as { memo: string }).memo, memoCarolToBob);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -61,14 +61,14 @@ describe('export-clips', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('basic export', async () => {
|
test('basic export', async () => {
|
||||||
let res = await api('clips/create', {
|
const res1 = await api('clips/create', {
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
description: 'bar',
|
description: 'bar',
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res1.status, 200);
|
||||||
|
|
||||||
res = await api('i/export-clips', {}, alice);
|
const res2 = await api('i/export-clips', {}, alice);
|
||||||
assert.strictEqual(res.status, 204);
|
assert.strictEqual(res2.status, 204);
|
||||||
|
|
||||||
const exported = await pollFirstDriveFile();
|
const exported = await pollFirstDriveFile();
|
||||||
assert.strictEqual(exported[0].name, 'foo');
|
assert.strictEqual(exported[0].name, 'foo');
|
||||||
|
@ -77,7 +77,7 @@ describe('export-clips', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('export with notes', async () => {
|
test('export with notes', async () => {
|
||||||
let res = await api('clips/create', {
|
const res = await api('clips/create', {
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
description: 'bar',
|
description: 'bar',
|
||||||
}, alice);
|
}, alice);
|
||||||
|
@ -96,15 +96,15 @@ describe('export-clips', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const note of [note1, note2]) {
|
for (const note of [note1, note2]) {
|
||||||
res = await api('clips/add-note', {
|
const res2 = await api('clips/add-note', {
|
||||||
clipId: clip.id,
|
clipId: clip.id,
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(res.status, 204);
|
assert.strictEqual(res2.status, 204);
|
||||||
}
|
}
|
||||||
|
|
||||||
res = await api('i/export-clips', {}, alice);
|
const res3 = await api('i/export-clips', {}, alice);
|
||||||
assert.strictEqual(res.status, 204);
|
assert.strictEqual(res3.status, 204);
|
||||||
|
|
||||||
const exported = await pollFirstDriveFile();
|
const exported = await pollFirstDriveFile();
|
||||||
assert.strictEqual(exported[0].name, 'foo');
|
assert.strictEqual(exported[0].name, 'foo');
|
||||||
|
@ -116,19 +116,19 @@ describe('export-clips', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('multiple clips', async () => {
|
test('multiple clips', async () => {
|
||||||
let res = await api('clips/create', {
|
const res1 = await api('clips/create', {
|
||||||
name: 'kawaii',
|
name: 'kawaii',
|
||||||
description: 'kawaii',
|
description: 'kawaii',
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res1.status, 200);
|
||||||
const clip1 = res.body;
|
const clip1 = res1.body;
|
||||||
|
|
||||||
res = await api('clips/create', {
|
const res2 = await api('clips/create', {
|
||||||
name: 'yuri',
|
name: 'yuri',
|
||||||
description: 'yuri',
|
description: 'yuri',
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res2.status, 200);
|
||||||
const clip2 = res.body;
|
const clip2 = res2.body;
|
||||||
|
|
||||||
const note1 = await post(alice, {
|
const note1 = await post(alice, {
|
||||||
text: 'baz1',
|
text: 'baz1',
|
||||||
|
@ -138,20 +138,26 @@ describe('export-clips', () => {
|
||||||
text: 'baz2',
|
text: 'baz2',
|
||||||
});
|
});
|
||||||
|
|
||||||
res = await api('clips/add-note', {
|
{
|
||||||
clipId: clip1.id,
|
const res = await api('clips/add-note', {
|
||||||
noteId: note1.id,
|
clipId: clip1.id,
|
||||||
}, alice);
|
noteId: note1.id,
|
||||||
assert.strictEqual(res.status, 204);
|
}, alice);
|
||||||
|
assert.strictEqual(res.status, 204);
|
||||||
|
}
|
||||||
|
|
||||||
res = await api('clips/add-note', {
|
{
|
||||||
clipId: clip2.id,
|
const res = await api('clips/add-note', {
|
||||||
noteId: note2.id,
|
clipId: clip2.id,
|
||||||
}, alice);
|
noteId: note2.id,
|
||||||
assert.strictEqual(res.status, 204);
|
}, 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();
|
const exported = await pollFirstDriveFile();
|
||||||
assert.strictEqual(exported[0].name, 'kawaii');
|
assert.strictEqual(exported[0].name, 'kawaii');
|
||||||
|
@ -163,7 +169,7 @@ describe('export-clips', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Clipping other user\'s note', async () => {
|
test('Clipping other user\'s note', async () => {
|
||||||
let res = await api('clips/create', {
|
const res = await api('clips/create', {
|
||||||
name: 'kawaii',
|
name: 'kawaii',
|
||||||
description: 'kawaii',
|
description: 'kawaii',
|
||||||
}, alice);
|
}, alice);
|
||||||
|
@ -175,14 +181,14 @@ describe('export-clips', () => {
|
||||||
visibility: 'followers',
|
visibility: 'followers',
|
||||||
});
|
});
|
||||||
|
|
||||||
res = await api('clips/add-note', {
|
const res2 = await api('clips/add-note', {
|
||||||
clipId: clip.id,
|
clipId: clip.id,
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(res.status, 204);
|
assert.strictEqual(res2.status, 204);
|
||||||
|
|
||||||
res = await api('i/export-clips', {}, alice);
|
const res3 = await api('i/export-clips', {}, alice);
|
||||||
assert.strictEqual(res.status, 204);
|
assert.strictEqual(res3.status, 204);
|
||||||
|
|
||||||
const exported = await pollFirstDriveFile();
|
const exported = await pollFirstDriveFile();
|
||||||
assert.strictEqual(exported[0].name, 'kawaii');
|
assert.strictEqual(exported[0].name, 'kawaii');
|
||||||
|
|
|
@ -7,19 +7,20 @@ import { INestApplicationContext } from '@nestjs/common';
|
||||||
|
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
|
import { setTimeout } from 'node:timers/promises';
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import { loadConfig } from '@/config.js';
|
import { loadConfig } from '@/config.js';
|
||||||
import { MiRepository, MiUser, UsersRepository, miRepository } from '@/models/_.js';
|
import { MiRepository, MiUser, UsersRepository, miRepository } from '@/models/_.js';
|
||||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||||
import { jobQueue } from '@/boot/common.js';
|
import { jobQueue } from '@/boot/common.js';
|
||||||
import { api, initTestDb, signup, sleep, successfulApiCall, uploadFile } from '../utils.js';
|
import { api, castAsError, initTestDb, signup, successfulApiCall, uploadFile } from '../utils.js';
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
describe('Account Move', () => {
|
describe('Account Move', () => {
|
||||||
let jq: INestApplicationContext;
|
let jq: INestApplicationContext;
|
||||||
let url: URL;
|
let url: URL;
|
||||||
|
|
||||||
let root: any;
|
let root: misskey.entities.SignupResponse;
|
||||||
let alice: misskey.entities.SignupResponse;
|
let alice: misskey.entities.SignupResponse;
|
||||||
let bob: misskey.entities.SignupResponse;
|
let bob: misskey.entities.SignupResponse;
|
||||||
let carol: misskey.entities.SignupResponse;
|
let carol: misskey.entities.SignupResponse;
|
||||||
|
@ -92,8 +93,8 @@ describe('Account Move', () => {
|
||||||
}, bob);
|
}, bob);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.code, 'NO_SUCH_USER');
|
assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_USER');
|
||||||
assert.strictEqual(res.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
assert.strictEqual(castAsError(res.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Unable to add duplicated aliases to alsoKnownAs', async () => {
|
test('Unable to add duplicated aliases to alsoKnownAs', async () => {
|
||||||
|
@ -102,8 +103,8 @@ describe('Account Move', () => {
|
||||||
}, bob);
|
}, bob);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.code, 'INVALID_PARAM');
|
assert.strictEqual(castAsError(res.body).error.code, 'INVALID_PARAM');
|
||||||
assert.strictEqual(res.body.error.id, '3d81ceae-475f-4600-b2a8-2bc116157532');
|
assert.strictEqual(castAsError(res.body).error.id, '3d81ceae-475f-4600-b2a8-2bc116157532');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Unable to add itself', async () => {
|
test('Unable to add itself', async () => {
|
||||||
|
@ -112,8 +113,8 @@ describe('Account Move', () => {
|
||||||
}, bob);
|
}, bob);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.code, 'FORBIDDEN_TO_SET_YOURSELF');
|
assert.strictEqual(castAsError(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.id, '25c90186-4ab0-49c8-9bba-a1fa6c202ba4');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Unable to add a nonexisting local account to alsoKnownAs', async () => {
|
test('Unable to add a nonexisting local account to alsoKnownAs', async () => {
|
||||||
|
@ -122,16 +123,16 @@ describe('Account Move', () => {
|
||||||
}, bob);
|
}, bob);
|
||||||
|
|
||||||
assert.strictEqual(res1.status, 400);
|
assert.strictEqual(res1.status, 400);
|
||||||
assert.strictEqual(res1.body.error.code, 'NO_SUCH_USER');
|
assert.strictEqual(castAsError(res1.body).error.code, 'NO_SUCH_USER');
|
||||||
assert.strictEqual(res1.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
assert.strictEqual(castAsError(res1.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
||||||
|
|
||||||
const res2 = await api('i/update', {
|
const res2 = await api('i/update', {
|
||||||
alsoKnownAs: ['@alice', 'nonexist'],
|
alsoKnownAs: ['@alice', 'nonexist'],
|
||||||
}, bob);
|
}, bob);
|
||||||
|
|
||||||
assert.strictEqual(res2.status, 400);
|
assert.strictEqual(res2.status, 400);
|
||||||
assert.strictEqual(res2.body.error.code, 'NO_SUCH_USER');
|
assert.strictEqual(castAsError(res2.body).error.code, 'NO_SUCH_USER');
|
||||||
assert.strictEqual(res2.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
assert.strictEqual(castAsError(res2.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Able to add two existing local account to alsoKnownAs', async () => {
|
test('Able to add two existing local account to alsoKnownAs', async () => {
|
||||||
|
@ -240,8 +241,8 @@ describe('Account Move', () => {
|
||||||
}, root);
|
}, root);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.code, 'NOT_ROOT_FORBIDDEN');
|
assert.strictEqual(castAsError(res.body).error.code, 'NOT_ROOT_FORBIDDEN');
|
||||||
assert.strictEqual(res.body.error.id, '4362e8dc-731f-4ad8-a694-be2a88922a24');
|
assert.strictEqual(castAsError(res.body).error.id, '4362e8dc-731f-4ad8-a694-be2a88922a24');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Unable to move to a nonexisting local account', async () => {
|
test('Unable to move to a nonexisting local account', async () => {
|
||||||
|
@ -250,8 +251,8 @@ describe('Account Move', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.code, 'NO_SUCH_USER');
|
assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_USER');
|
||||||
assert.strictEqual(res.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
assert.strictEqual(castAsError(res.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Unable to move if alsoKnownAs is invalid', async () => {
|
test('Unable to move if alsoKnownAs is invalid', async () => {
|
||||||
|
@ -260,8 +261,8 @@ describe('Account Move', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.code, 'DESTINATION_ACCOUNT_FORBIDS');
|
assert.strictEqual(castAsError(res.body).error.code, 'DESTINATION_ACCOUNT_FORBIDS');
|
||||||
assert.strictEqual(res.body.error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4');
|
assert.strictEqual(castAsError(res.body).error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Relationships have been properly migrated', async () => {
|
test('Relationships have been properly migrated', async () => {
|
||||||
|
@ -271,43 +272,51 @@ describe('Account Move', () => {
|
||||||
|
|
||||||
assert.strictEqual(move.status, 200);
|
assert.strictEqual(move.status, 200);
|
||||||
|
|
||||||
await sleep(1000 * 3); // wait for jobs to finish
|
await setTimeout(1000 * 3); // wait for jobs to finish
|
||||||
|
|
||||||
// Unfollow delayed?
|
// Unfollow delayed?
|
||||||
const aliceFollowings = await api('users/following', {
|
const aliceFollowings = await api('users/following', {
|
||||||
userId: alice.id,
|
userId: alice.id,
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(aliceFollowings.status, 200);
|
assert.strictEqual(aliceFollowings.status, 200);
|
||||||
|
assert.ok(aliceFollowings);
|
||||||
assert.strictEqual(aliceFollowings.body.length, 3);
|
assert.strictEqual(aliceFollowings.body.length, 3);
|
||||||
|
|
||||||
const carolFollowings = await api('users/following', {
|
const carolFollowings = await api('users/following', {
|
||||||
userId: carol.id,
|
userId: carol.id,
|
||||||
}, carol);
|
}, carol);
|
||||||
assert.strictEqual(carolFollowings.status, 200);
|
assert.strictEqual(carolFollowings.status, 200);
|
||||||
|
assert.ok(carolFollowings);
|
||||||
assert.strictEqual(carolFollowings.body.length, 2);
|
assert.strictEqual(carolFollowings.body.length, 2);
|
||||||
assert.strictEqual(carolFollowings.body[0].followeeId, bob.id);
|
assert.strictEqual(carolFollowings.body[0].followeeId, bob.id);
|
||||||
assert.strictEqual(carolFollowings.body[1].followeeId, alice.id);
|
assert.strictEqual(carolFollowings.body[1].followeeId, alice.id);
|
||||||
|
|
||||||
const blockings = await api('blocking/list', {}, dave);
|
const blockings = await api('blocking/list', {}, dave);
|
||||||
assert.strictEqual(blockings.status, 200);
|
assert.strictEqual(blockings.status, 200);
|
||||||
|
assert.ok(blockings);
|
||||||
assert.strictEqual(blockings.body.length, 2);
|
assert.strictEqual(blockings.body.length, 2);
|
||||||
assert.strictEqual(blockings.body[0].blockeeId, bob.id);
|
assert.strictEqual(blockings.body[0].blockeeId, bob.id);
|
||||||
assert.strictEqual(blockings.body[1].blockeeId, alice.id);
|
assert.strictEqual(blockings.body[1].blockeeId, alice.id);
|
||||||
|
|
||||||
const mutings = await api('mute/list', {}, dave);
|
const mutings = await api('mute/list', {}, dave);
|
||||||
assert.strictEqual(mutings.status, 200);
|
assert.strictEqual(mutings.status, 200);
|
||||||
|
assert.ok(mutings);
|
||||||
assert.strictEqual(mutings.body.length, 2);
|
assert.strictEqual(mutings.body.length, 2);
|
||||||
assert.strictEqual(mutings.body[0].muteeId, bob.id);
|
assert.strictEqual(mutings.body[0].muteeId, bob.id);
|
||||||
assert.strictEqual(mutings.body[1].muteeId, alice.id);
|
assert.strictEqual(mutings.body[1].muteeId, alice.id);
|
||||||
|
|
||||||
const rootLists = await api('users/lists/list', {}, root);
|
const rootLists = await api('users/lists/list', {}, root);
|
||||||
assert.strictEqual(rootLists.status, 200);
|
assert.strictEqual(rootLists.status, 200);
|
||||||
|
assert.ok(rootLists);
|
||||||
|
assert.ok(rootLists.body[0].userIds);
|
||||||
assert.strictEqual(rootLists.body[0].userIds.length, 2);
|
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 === bob.id));
|
||||||
assert.ok(rootLists.body[0].userIds.find((id: string) => id === alice.id));
|
assert.ok(rootLists.body[0].userIds.find((id: string) => id === alice.id));
|
||||||
|
|
||||||
const eveLists = await api('users/lists/list', {}, eve);
|
const eveLists = await api('users/lists/list', {}, eve);
|
||||||
assert.strictEqual(eveLists.status, 200);
|
assert.strictEqual(eveLists.status, 200);
|
||||||
|
assert.ok(eveLists);
|
||||||
|
assert.ok(eveLists.body[0].userIds);
|
||||||
assert.strictEqual(eveLists.body[0].userIds.length, 1);
|
assert.strictEqual(eveLists.body[0].userIds.length, 1);
|
||||||
assert.ok(eveLists.body[0].userIds.find((id: string) => id === bob.id));
|
assert.ok(eveLists.body[0].userIds.find((id: string) => id === bob.id));
|
||||||
});
|
});
|
||||||
|
@ -330,7 +339,7 @@ describe('Account Move', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Unfollowed after 10 sec (24 hours in production).', async () => {
|
test('Unfollowed after 10 sec (24 hours in production).', async () => {
|
||||||
await sleep(1000 * 8);
|
await setTimeout(1000 * 8);
|
||||||
|
|
||||||
const following = await api('users/following', {
|
const following = await api('users/following', {
|
||||||
userId: alice.id,
|
userId: alice.id,
|
||||||
|
@ -346,8 +355,8 @@ describe('Account Move', () => {
|
||||||
}, bob);
|
}, bob);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.code, 'DESTINATION_ACCOUNT_FORBIDS');
|
assert.strictEqual(castAsError(res.body).error.code, 'DESTINATION_ACCOUNT_FORBIDS');
|
||||||
assert.strictEqual(res.body.error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4');
|
assert.strictEqual(castAsError(res.body).error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Follow and follower counts are properly adjusted', async () => {
|
test('Follow and follower counts are properly adjusted', async () => {
|
||||||
|
@ -418,8 +427,9 @@ describe('Account Move', () => {
|
||||||
] as const)('Prohibit access after moving: %s', async (endpoint) => {
|
] as const)('Prohibit access after moving: %s', async (endpoint) => {
|
||||||
const res = await api(endpoint, {}, alice);
|
const res = await api(endpoint, {}, alice);
|
||||||
assert.strictEqual(res.status, 403);
|
assert.strictEqual(res.status, 403);
|
||||||
assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED');
|
assert.ok(res.body);
|
||||||
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');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Prohibit access after moving: /antennas/update', async () => {
|
test('Prohibit access after moving: /antennas/update', async () => {
|
||||||
|
@ -437,16 +447,19 @@ describe('Account Move', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 403);
|
assert.strictEqual(res.status, 403);
|
||||||
assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED');
|
assert.ok(res.body);
|
||||||
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');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Prohibit access after moving: /drive/files/create', async () => {
|
test('Prohibit access after moving: /drive/files/create', async () => {
|
||||||
|
// FIXME: 一旦逃げておく
|
||||||
const res = await uploadFile(alice);
|
const res = await uploadFile(alice);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 403);
|
assert.strictEqual(res.status, 403);
|
||||||
assert.strictEqual((res.body! as any as { error: misskey.api.APIError }).error.code, 'YOUR_ACCOUNT_MOVED');
|
assert.ok(res.body);
|
||||||
assert.strictEqual((res.body! as any as { error: misskey.api.APIError }).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');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Prohibit updating alsoKnownAs after moving', async () => {
|
test('Prohibit updating alsoKnownAs after moving', async () => {
|
||||||
|
@ -455,8 +468,8 @@ describe('Account Move', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 403);
|
assert.strictEqual(res.status, 403);
|
||||||
assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED');
|
assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED');
|
||||||
assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
|
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(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
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 => 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 === carolNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => {
|
test('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => {
|
||||||
|
@ -92,9 +92,9 @@ describe('Mute', () => {
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
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 => 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 === bobNote.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async () => {
|
test('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async () => {
|
||||||
|
@ -108,9 +108,9 @@ describe('Mute', () => {
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
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 => note.id === aliceNote.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
assert.strictEqual(res.body.some(note => 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 === carolNote.id), false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -124,8 +124,8 @@ describe('Mute', () => {
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
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 => 'userId' in notification && 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 === carol.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
|
test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
|
||||||
|
@ -138,8 +138,8 @@ describe('Mute', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
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 => 'userId' in notification && 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 === carol.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
|
test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
|
||||||
|
@ -152,8 +152,8 @@ describe('Mute', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
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 => 'userId' in notification && 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 === carol.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('通知にミュートしているユーザーからの引用リノートが含まれない', async () => {
|
test('通知にミュートしているユーザーからの引用リノートが含まれない', async () => {
|
||||||
|
@ -166,8 +166,8 @@ describe('Mute', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
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 => 'userId' in notification && 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 === carol.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('通知にミュートしているユーザーからのリノートが含まれない', async () => {
|
test('通知にミュートしているユーザーからのリノートが含まれない', async () => {
|
||||||
|
@ -180,8 +180,8 @@ describe('Mute', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
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 => 'userId' in notification && 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 === carol.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => {
|
test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => {
|
||||||
|
@ -193,8 +193,8 @@ describe('Mute', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
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 => 'userId' in notification && 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 === carol.id), false);
|
||||||
|
|
||||||
await api('following/delete', { userId: alice.id }, bob);
|
await api('following/delete', { userId: alice.id }, bob);
|
||||||
await api('following/delete', { userId: alice.id }, carol);
|
await api('following/delete', { userId: alice.id }, carol);
|
||||||
|
@ -210,8 +210,8 @@ describe('Mute', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
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 => 'userId' in notification && 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 === carol.id), false);
|
||||||
|
|
||||||
await api('following/delete', { userId: alice.id }, bob);
|
await api('following/delete', { userId: alice.id }, bob);
|
||||||
await api('following/delete', { userId: alice.id }, carol);
|
await api('following/delete', { userId: alice.id }, carol);
|
||||||
|
@ -228,8 +228,8 @@ describe('Mute', () => {
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
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 => 'userId' in notification && 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 === carol.id), false);
|
||||||
});
|
});
|
||||||
test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
|
test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
|
||||||
const aliceNote = await post(alice, { text: 'hi' });
|
const aliceNote = await post(alice, { text: 'hi' });
|
||||||
|
@ -241,8 +241,8 @@ describe('Mute', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
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 => 'userId' in notification && 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 === carol.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
|
test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
|
||||||
|
@ -255,8 +255,8 @@ describe('Mute', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
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 => 'userId' in notification && 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 === carol.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('通知にミュートしているユーザーからの引用リノートが含まれない', async () => {
|
test('通知にミュートしているユーザーからの引用リノートが含まれない', async () => {
|
||||||
|
@ -269,8 +269,8 @@ describe('Mute', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
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 => 'userId' in notification && 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 === carol.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('通知にミュートしているユーザーからのリノートが含まれない', async () => {
|
test('通知にミュートしているユーザーからのリノートが含まれない', async () => {
|
||||||
|
@ -283,8 +283,8 @@ describe('Mute', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
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 => 'userId' in notification && 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 === carol.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => {
|
test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => {
|
||||||
|
@ -296,8 +296,8 @@ describe('Mute', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
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 => 'userId' in notification && 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 === carol.id), false);
|
||||||
|
|
||||||
await api('following/delete', { userId: alice.id }, bob);
|
await api('following/delete', { userId: alice.id }, bob);
|
||||||
await api('following/delete', { userId: alice.id }, carol);
|
await api('following/delete', { userId: alice.id }, carol);
|
||||||
|
@ -313,8 +313,8 @@ describe('Mute', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
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 => 'userId' in notification && 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 === carol.id), false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,16 +3,18 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { Repository } from "typeorm";
|
||||||
|
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import { MiNote } from '@/models/Note.js';
|
import { MiNote } from '@/models/Note.js';
|
||||||
import { MAX_NOTE_TEXT_LENGTH } from '@/const.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';
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
describe('Note', () => {
|
describe('Note', () => {
|
||||||
let Notes: any;
|
let Notes: Repository<MiNote>;
|
||||||
|
|
||||||
let root: misskey.entities.SignupResponse;
|
let root: misskey.entities.SignupResponse;
|
||||||
let alice: misskey.entities.SignupResponse;
|
let alice: misskey.entities.SignupResponse;
|
||||||
|
@ -41,7 +43,7 @@ describe('Note', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ファイルを添付できる', async () => {
|
test('ファイルを添付できる', async () => {
|
||||||
const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
|
const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg');
|
||||||
|
|
||||||
const res = await api('notes/create', {
|
const res = await api('notes/create', {
|
||||||
fileIds: [file.id],
|
fileIds: [file.id],
|
||||||
|
@ -53,7 +55,7 @@ describe('Note', () => {
|
||||||
}, 1000 * 10);
|
}, 1000 * 10);
|
||||||
|
|
||||||
test('他人のファイルで怒られる', async () => {
|
test('他人のファイルで怒られる', async () => {
|
||||||
const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
|
const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg');
|
||||||
|
|
||||||
const res = await api('notes/create', {
|
const res = await api('notes/create', {
|
||||||
text: 'test',
|
text: 'test',
|
||||||
|
@ -61,8 +63,8 @@ describe('Note', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE');
|
assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_FILE');
|
||||||
assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
|
assert.strictEqual(castAsError(res.body).error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
|
||||||
}, 1000 * 10);
|
}, 1000 * 10);
|
||||||
|
|
||||||
test('存在しないファイルで怒られる', async () => {
|
test('存在しないファイルで怒られる', async () => {
|
||||||
|
@ -72,8 +74,8 @@ describe('Note', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE');
|
assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_FILE');
|
||||||
assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
|
assert.strictEqual(castAsError(res.body).error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('不正なファイルIDで怒られる', async () => {
|
test('不正なファイルIDで怒られる', async () => {
|
||||||
|
@ -81,8 +83,8 @@ describe('Note', () => {
|
||||||
fileIds: ['kyoppie'],
|
fileIds: ['kyoppie'],
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE');
|
assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_FILE');
|
||||||
assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
|
assert.strictEqual(castAsError(res.body).error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('返信できる', async () => {
|
test('返信できる', async () => {
|
||||||
|
@ -101,6 +103,7 @@ describe('Note', () => {
|
||||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||||
assert.strictEqual(res.body.createdNote.text, alicePost.text);
|
assert.strictEqual(res.body.createdNote.text, alicePost.text);
|
||||||
assert.strictEqual(res.body.createdNote.replyId, alicePost.replyId);
|
assert.strictEqual(res.body.createdNote.replyId, alicePost.replyId);
|
||||||
|
assert.ok(res.body.createdNote.reply);
|
||||||
assert.strictEqual(res.body.createdNote.reply.text, bobPost.text);
|
assert.strictEqual(res.body.createdNote.reply.text, bobPost.text);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -118,6 +121,7 @@ describe('Note', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||||
assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
|
assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
|
||||||
|
assert.ok(res.body.createdNote.renote);
|
||||||
assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
|
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(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||||
assert.strictEqual(res.body.createdNote.text, alicePost.text);
|
assert.strictEqual(res.body.createdNote.text, alicePost.text);
|
||||||
assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
|
assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
|
||||||
|
assert.ok(res.body.createdNote.renote);
|
||||||
assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
|
assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -218,7 +223,7 @@ describe('Note', () => {
|
||||||
}, bob);
|
}, bob);
|
||||||
|
|
||||||
assert.strictEqual(bobReply.status, 400);
|
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 () => {
|
test('visibility: specifiedなノートに対してvisibility: specifiedで返信できる', async () => {
|
||||||
|
@ -256,7 +261,7 @@ describe('Note', () => {
|
||||||
}, bob);
|
}, bob);
|
||||||
|
|
||||||
assert.strictEqual(bobReply.status, 400);
|
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 () => {
|
test('文字数ぎりぎりで怒られない', async () => {
|
||||||
|
@ -333,6 +338,7 @@ describe('Note', () => {
|
||||||
assert.strictEqual(res.body.createdNote.text, post.text);
|
assert.strictEqual(res.body.createdNote.text, post.text);
|
||||||
|
|
||||||
const noteDoc = await Notes.findOneBy({ id: res.body.createdNote.id });
|
const noteDoc = await Notes.findOneBy({ id: res.body.createdNote.id });
|
||||||
|
assert.ok(noteDoc);
|
||||||
assert.deepStrictEqual(noteDoc.mentions, [bob.id]);
|
assert.deepStrictEqual(noteDoc.mentions, [bob.id]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -345,6 +351,7 @@ describe('Note', () => {
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
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.length, 1);
|
||||||
assert.strictEqual(res.body.createdNote.files[0].id, file.body!.id);
|
assert.strictEqual(res.body.createdNote.files[0].id, file.body!.id);
|
||||||
});
|
});
|
||||||
|
@ -363,8 +370,9 @@ describe('Note', () => {
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
const myNote = res.body.find((note: { id: string; files: { id: string }[] }) => note.id === createdNote.body.createdNote.id);
|
const myNote = res.body.find(note => note.id === createdNote.body.createdNote.id);
|
||||||
assert.notEqual(myNote, null);
|
assert.ok(myNote);
|
||||||
|
assert.ok(myNote.files);
|
||||||
assert.strictEqual(myNote.files.length, 1);
|
assert.strictEqual(myNote.files.length, 1);
|
||||||
assert.strictEqual(myNote.files[0].id, file.body!.id);
|
assert.strictEqual(myNote.files[0].id, file.body!.id);
|
||||||
});
|
});
|
||||||
|
@ -389,7 +397,9 @@ describe('Note', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id);
|
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.length, 1);
|
||||||
assert.strictEqual(myNote.renote.files[0].id, file.body!.id);
|
assert.strictEqual(myNote.renote.files[0].id, file.body!.id);
|
||||||
});
|
});
|
||||||
|
@ -415,7 +425,9 @@ describe('Note', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
const myNote = res.body.find((note: { id: string }) => note.id === reply.body.createdNote.id);
|
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.length, 1);
|
||||||
assert.strictEqual(myNote.reply.files[0].id, file.body!.id);
|
assert.strictEqual(myNote.reply.files[0].id, file.body!.id);
|
||||||
});
|
});
|
||||||
|
@ -446,7 +458,10 @@ describe('Note', () => {
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id);
|
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.length, 1);
|
||||||
assert.strictEqual(myNote.renote.reply.files[0].id, file.body!.id);
|
assert.strictEqual(myNote.renote.reply.files[0].id, file.body!.id);
|
||||||
});
|
});
|
||||||
|
@ -474,7 +489,7 @@ describe('Note', () => {
|
||||||
priority: 0,
|
priority: 0,
|
||||||
value: true,
|
value: true,
|
||||||
},
|
},
|
||||||
} as any,
|
},
|
||||||
}, root);
|
}, root);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
|
@ -498,7 +513,7 @@ describe('Note', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(liftnsfw.status, 400);
|
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', {
|
const oldaddnsfw = await api('drive/files/update', {
|
||||||
fileId: file.body!.id,
|
fileId: file.body!.id,
|
||||||
|
@ -710,7 +725,7 @@ describe('Note', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(note1.status, 400);
|
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 () => {
|
test('禁止ワードを含む投稿はエラーになる (正規表現)', async () => {
|
||||||
|
@ -727,7 +742,7 @@ describe('Note', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(note2.status, 400);
|
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 () => {
|
test('禁止ワードを含む投稿はエラーになる (スペースアンド)', async () => {
|
||||||
|
@ -744,7 +759,7 @@ describe('Note', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(note2.status, 400);
|
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 () => {
|
test('禁止ワードを含んでるリモートノートもエラーになる', async () => {
|
||||||
|
@ -786,7 +801,7 @@ describe('Note', () => {
|
||||||
priority: 1,
|
priority: 1,
|
||||||
value: 0,
|
value: 0,
|
||||||
},
|
},
|
||||||
} as any,
|
},
|
||||||
}, root);
|
}, root);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
|
@ -807,7 +822,7 @@ describe('Note', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(note.status, 400);
|
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', {
|
await api('admin/roles/unassign', {
|
||||||
userId: alice.id,
|
userId: alice.id,
|
||||||
|
@ -840,7 +855,7 @@ describe('Note', () => {
|
||||||
priority: 1,
|
priority: 1,
|
||||||
value: 0,
|
value: 0,
|
||||||
},
|
},
|
||||||
} as any,
|
},
|
||||||
}, root);
|
}, root);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
|
@ -863,7 +878,7 @@ describe('Note', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(note.status, 400);
|
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', {
|
await api('admin/roles/unassign', {
|
||||||
userId: alice.id,
|
userId: alice.id,
|
||||||
|
@ -896,7 +911,7 @@ describe('Note', () => {
|
||||||
priority: 1,
|
priority: 1,
|
||||||
value: 1,
|
value: 1,
|
||||||
},
|
},
|
||||||
} as any,
|
},
|
||||||
}, root);
|
}, root);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
|
@ -951,6 +966,7 @@ describe('Note', () => {
|
||||||
|
|
||||||
assert.strictEqual(deleteOneRes.status, 204);
|
assert.strictEqual(deleteOneRes.status, 204);
|
||||||
let mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id });
|
let mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id });
|
||||||
|
assert.ok(mainNote);
|
||||||
assert.strictEqual(mainNote.repliesCount, 1);
|
assert.strictEqual(mainNote.repliesCount, 1);
|
||||||
|
|
||||||
const deleteTwoRes = await api('notes/delete', {
|
const deleteTwoRes = await api('notes/delete', {
|
||||||
|
@ -959,6 +975,7 @@ describe('Note', () => {
|
||||||
|
|
||||||
assert.strictEqual(deleteTwoRes.status, 204);
|
assert.strictEqual(deleteTwoRes.status, 204);
|
||||||
mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id });
|
mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id });
|
||||||
|
assert.ok(mainNote);
|
||||||
assert.strictEqual(mainNote.repliesCount, 0);
|
assert.strictEqual(mainNote.repliesCount, 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -980,7 +997,7 @@ describe('Note', () => {
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.code, 'UNAVAILABLE');
|
assert.strictEqual(castAsError(res.body).error.code, 'UNAVAILABLE');
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
@ -992,7 +1009,7 @@ describe('Note', () => {
|
||||||
const res = await api('notes/translate', { noteId: 'foo', targetLang: 'ja' }, alice);
|
const res = await api('notes/translate', { noteId: 'foo', targetLang: 'ja' }, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 400);
|
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 () => {
|
test('不可視なノートは翻訳できない', async () => {
|
||||||
|
@ -1000,7 +1017,7 @@ describe('Note', () => {
|
||||||
const bobTranslateAttempt = await api('notes/translate', { noteId: aliceNote.id, targetLang: 'ja' }, bob);
|
const bobTranslateAttempt = await api('notes/translate', { noteId: aliceNote.id, targetLang: 'ja' }, bob);
|
||||||
|
|
||||||
assert.strictEqual(bobTranslateAttempt.status, 400);
|
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 () => {
|
test('text: null なノートを翻訳すると空のレスポンスが返ってくる', async () => {
|
||||||
|
@ -1016,7 +1033,7 @@ describe('Note', () => {
|
||||||
|
|
||||||
// NOTE: デフォルトでは登録されていないので落ちる
|
// NOTE: デフォルトでは登録されていないので落ちる
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
assert.strictEqual(res.body.error.code, 'UNAVAILABLE');
|
assert.strictEqual(castAsError(res.body).error.code, 'UNAVAILABLE');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,8 @@
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import { api, post, signup, sleep, waitFire } from '../utils.js';
|
import { setTimeout } from 'node:timers/promises';
|
||||||
|
import { api, post, signup, waitFire } from '../utils.js';
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
describe('Renote Mute', () => {
|
describe('Renote Mute', () => {
|
||||||
|
@ -35,15 +36,15 @@ describe('Renote Mute', () => {
|
||||||
const carolNote = await post(carol, { text: 'hi' });
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
|
||||||
// redisに追加されるのを待つ
|
// redisに追加されるのを待つ
|
||||||
await sleep(100);
|
await setTimeout(100);
|
||||||
|
|
||||||
const res = await api('notes/local-timeline', {}, alice);
|
const res = await api('notes/local-timeline', {}, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
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 => note.id === bobNote.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolRenote.id), false);
|
assert.strictEqual(res.body.some(note => 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 === carolNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('タイムラインにリノートミュートしているユーザーの引用が含まれる', async () => {
|
test('タイムラインにリノートミュートしているユーザーの引用が含まれる', async () => {
|
||||||
|
@ -52,15 +53,15 @@ describe('Renote Mute', () => {
|
||||||
const carolNote = await post(carol, { text: 'hi' });
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
|
||||||
// redisに追加されるのを待つ
|
// redisに追加されるのを待つ
|
||||||
await sleep(100);
|
await setTimeout(100);
|
||||||
|
|
||||||
const res = await api('notes/local-timeline', {}, alice);
|
const res = await api('notes/local-timeline', {}, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
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 => note.id === bobNote.id), true);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolRenote.id), true);
|
assert.strictEqual(res.body.some(note => 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 === carolNote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// #12956
|
// #12956
|
||||||
|
@ -69,14 +70,14 @@ describe('Renote Mute', () => {
|
||||||
const bobRenote = await post(bob, { renoteId: carolNote.id });
|
const bobRenote = await post(bob, { renoteId: carolNote.id });
|
||||||
|
|
||||||
// redisに追加されるのを待つ
|
// redisに追加されるのを待つ
|
||||||
await sleep(100);
|
await setTimeout(100);
|
||||||
|
|
||||||
const res = await api('notes/local-timeline', {}, alice);
|
const res = await api('notes/local-timeline', {}, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
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 => 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 === bobRenote.id), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ストリームにリノートミュートしているユーザーのリノートが流れない', async () => {
|
test('ストリームにリノートミュートしているユーザーのリノートが流れない', async () => {
|
||||||
|
|
|
@ -33,9 +33,9 @@ describe('Note thread mute', () => {
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
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 => note.id === bobNote.id), false);
|
||||||
assert.strictEqual(res.body.some((note: any) => note.id === carolReply.id), false);
|
assert.strictEqual(res.body.some(note => 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 === carolReplyWithoutMention.id), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => {
|
test('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => {
|
||||||
|
@ -93,8 +93,8 @@ describe('Note thread mute', () => {
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
assert.strictEqual(Array.isArray(res.body), true);
|
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 => 'note' in notification && 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 === carolReplyWithoutMention.id), false);
|
||||||
|
|
||||||
// NOTE: bobの投稿はスレッドミュート前に行われたため通知に含まれていてもよい
|
// NOTE: bobの投稿はスレッドミュート前に行われたため通知に含まれていてもよい
|
||||||
});
|
});
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -17,8 +17,8 @@ describe('users/notes', () => {
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
alice = await signup({ username: 'alice' });
|
alice = await signup({ username: 'alice' });
|
||||||
const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
|
const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg');
|
||||||
const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.png');
|
const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.png');
|
||||||
jpgNote = await post(alice, {
|
jpgNote = await post(alice, {
|
||||||
fileIds: [jpg.id],
|
fileIds: [jpg.id],
|
||||||
});
|
});
|
||||||
|
|
|
@ -231,7 +231,7 @@ describe('ユーザー', () => {
|
||||||
rolePublic = await role(root, { isPublic: true, name: 'Public Role' });
|
rolePublic = await role(root, { isPublic: true, name: 'Public Role' });
|
||||||
await api('admin/roles/assign', { userId: userRolePublic.id, roleId: rolePublic.id }, root);
|
await api('admin/roles/assign', { userId: userRolePublic.id, roleId: rolePublic.id }, root);
|
||||||
userRoleBadge = await signup({ username: 'userRoleBadge' });
|
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);
|
await api('admin/roles/assign', { userId: userRoleBadge.id, roleId: roleBadge.id }, root);
|
||||||
userSilenced = await signup({ username: 'userSilenced' });
|
userSilenced = await signup({ username: 'userSilenced' });
|
||||||
await post(userSilenced, { text: 'test' });
|
await post(userSilenced, { text: 'test' });
|
||||||
|
@ -655,7 +655,16 @@ describe('ユーザー', () => {
|
||||||
iconUrl: roleBadge.iconUrl,
|
iconUrl: roleBadge.iconUrl,
|
||||||
displayOrder: roleBadge.displayOrder,
|
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 () => {
|
test('をID指定のリスト形式で取得することができる(空)', async () => {
|
||||||
const parameters = { userIds: [] };
|
const parameters = { userIds: [] };
|
||||||
|
|
22
packages/backend/test/eslint.config.js
Normal file
22
packages/backend/test/eslint.config.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import globals from 'globals';
|
||||||
|
import tsParser from '@typescript-eslint/parser';
|
||||||
|
import sharedConfig from '../../shared/eslint.config.js';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...sharedConfig,
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
...globals.jest,
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
|
sourceType: 'module',
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
BIN
packages/backend/test/resources/192.jpg
Normal file
BIN
packages/backend/test/resources/192.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 5 KiB |
BIN
packages/backend/test/resources/192.png
Normal file
BIN
packages/backend/test/resources/192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
Before Width: | Height: | Size: 25 KiB |
Binary file not shown.
Before Width: | Height: | Size: 463 KiB |
|
@ -23,10 +23,10 @@ describe('ApMfmService', () => {
|
||||||
|
|
||||||
describe('getNoteHtml', () => {
|
describe('getNoteHtml', () => {
|
||||||
test('Do not provide _misskey_content for simple text', () => {
|
test('Do not provide _misskey_content for simple text', () => {
|
||||||
const note: MiNote = {
|
const note = {
|
||||||
text: 'テキスト #タグ @mention 🍊 :emoji: https://example.com',
|
text: 'テキスト #タグ @mention 🍊 :emoji: https://example.com',
|
||||||
mentionedRemoteUsers: '[]',
|
mentionedRemoteUsers: '[]',
|
||||||
} as any;
|
};
|
||||||
|
|
||||||
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
|
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
|
||||||
|
|
||||||
|
@ -35,10 +35,10 @@ describe('ApMfmService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Provide _misskey_content for MFM', () => {
|
test('Provide _misskey_content for MFM', () => {
|
||||||
const note: MiNote = {
|
const note = {
|
||||||
text: '$[tada foo]',
|
text: '$[tada foo]',
|
||||||
mentionedRemoteUsers: '[]',
|
mentionedRemoteUsers: '[]',
|
||||||
} as any;
|
};
|
||||||
|
|
||||||
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
|
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { ModuleMocker } from 'jest-mock';
|
||||||
import { Test } from '@nestjs/testing';
|
import { Test } from '@nestjs/testing';
|
||||||
import { afterAll, beforeAll, describe, test } from '@jest/globals';
|
import { afterAll, beforeAll, describe, test } from '@jest/globals';
|
||||||
import { GlobalModule } from '@/GlobalModule.js';
|
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 { DI } from '@/di-symbols.js';
|
||||||
import { AiService } from '@/core/AiService.js';
|
import { AiService } from '@/core/AiService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
|
@ -28,6 +28,15 @@ const moduleMocker = new ModuleMocker(global);
|
||||||
describe('FileInfoService', () => {
|
describe('FileInfoService', () => {
|
||||||
let app: TestingModule;
|
let app: TestingModule;
|
||||||
let fileInfoService: FileInfoService;
|
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 () => {
|
beforeAll(async () => {
|
||||||
app = await Test.createTestingModule({
|
app = await Test.createTestingModule({
|
||||||
|
@ -63,11 +72,7 @@ describe('FileInfoService', () => {
|
||||||
|
|
||||||
test('Empty file', async () => {
|
test('Empty file', async () => {
|
||||||
const path = `${resources}/emptyfile`;
|
const path = `${resources}/emptyfile`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
assert.deepStrictEqual(info, {
|
assert.deepStrictEqual(info, {
|
||||||
size: 0,
|
size: 0,
|
||||||
md5: 'd41d8cd98f00b204e9800998ecf8427e',
|
md5: 'd41d8cd98f00b204e9800998ecf8427e',
|
||||||
|
@ -83,32 +88,24 @@ describe('FileInfoService', () => {
|
||||||
|
|
||||||
describe('IMAGE', () => {
|
describe('IMAGE', () => {
|
||||||
test('Generic JPEG', async () => {
|
test('Generic JPEG', async () => {
|
||||||
const path = `${resources}/Lenna.jpg`;
|
const path = `${resources}/192.jpg`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
assert.deepStrictEqual(info, {
|
assert.deepStrictEqual(info, {
|
||||||
size: 25360,
|
size: 5131,
|
||||||
md5: '091b3f259662aa31e2ffef4519951168',
|
md5: '8c9ed0677dd2b8f9f7472c3af247e5e3',
|
||||||
type: {
|
type: {
|
||||||
mime: 'image/jpeg',
|
mime: 'image/jpeg',
|
||||||
ext: 'jpg',
|
ext: 'jpg',
|
||||||
},
|
},
|
||||||
width: 512,
|
width: 192,
|
||||||
height: 512,
|
height: 192,
|
||||||
orientation: undefined,
|
orientation: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Generic APNG', async () => {
|
test('Generic APNG', async () => {
|
||||||
const path = `${resources}/anime.png`;
|
const path = `${resources}/anime.png`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
assert.deepStrictEqual(info, {
|
assert.deepStrictEqual(info, {
|
||||||
size: 1868,
|
size: 1868,
|
||||||
md5: '08189c607bea3b952704676bb3c979e0',
|
md5: '08189c607bea3b952704676bb3c979e0',
|
||||||
|
@ -124,11 +121,7 @@ describe('FileInfoService', () => {
|
||||||
|
|
||||||
test('Generic AGIF', async () => {
|
test('Generic AGIF', async () => {
|
||||||
const path = `${resources}/anime.gif`;
|
const path = `${resources}/anime.gif`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
assert.deepStrictEqual(info, {
|
assert.deepStrictEqual(info, {
|
||||||
size: 2248,
|
size: 2248,
|
||||||
md5: '32c47a11555675d9267aee1a86571e7e',
|
md5: '32c47a11555675d9267aee1a86571e7e',
|
||||||
|
@ -144,11 +137,7 @@ describe('FileInfoService', () => {
|
||||||
|
|
||||||
test('PNG with alpha', async () => {
|
test('PNG with alpha', async () => {
|
||||||
const path = `${resources}/with-alpha.png`;
|
const path = `${resources}/with-alpha.png`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
assert.deepStrictEqual(info, {
|
assert.deepStrictEqual(info, {
|
||||||
size: 3772,
|
size: 3772,
|
||||||
md5: 'f73535c3e1e27508885b69b10cf6e991',
|
md5: 'f73535c3e1e27508885b69b10cf6e991',
|
||||||
|
@ -164,11 +153,7 @@ describe('FileInfoService', () => {
|
||||||
|
|
||||||
test('Generic SVG', async () => {
|
test('Generic SVG', async () => {
|
||||||
const path = `${resources}/image.svg`;
|
const path = `${resources}/image.svg`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
assert.deepStrictEqual(info, {
|
assert.deepStrictEqual(info, {
|
||||||
size: 505,
|
size: 505,
|
||||||
md5: 'b6f52b4b021e7b92cdd04509c7267965',
|
md5: 'b6f52b4b021e7b92cdd04509c7267965',
|
||||||
|
@ -185,11 +170,7 @@ describe('FileInfoService', () => {
|
||||||
test('SVG with XML definition', async () => {
|
test('SVG with XML definition', async () => {
|
||||||
// https://github.com/misskey-dev/misskey/issues/4413
|
// https://github.com/misskey-dev/misskey/issues/4413
|
||||||
const path = `${resources}/with-xml-def.svg`;
|
const path = `${resources}/with-xml-def.svg`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
assert.deepStrictEqual(info, {
|
assert.deepStrictEqual(info, {
|
||||||
size: 544,
|
size: 544,
|
||||||
md5: '4b7a346cde9ccbeb267e812567e33397',
|
md5: '4b7a346cde9ccbeb267e812567e33397',
|
||||||
|
@ -205,11 +186,7 @@ describe('FileInfoService', () => {
|
||||||
|
|
||||||
test('Dimension limit', async () => {
|
test('Dimension limit', async () => {
|
||||||
const path = `${resources}/25000x25000.png`;
|
const path = `${resources}/25000x25000.png`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
assert.deepStrictEqual(info, {
|
assert.deepStrictEqual(info, {
|
||||||
size: 75933,
|
size: 75933,
|
||||||
md5: '268c5dde99e17cf8fe09f1ab3f97df56',
|
md5: '268c5dde99e17cf8fe09f1ab3f97df56',
|
||||||
|
@ -225,11 +202,7 @@ describe('FileInfoService', () => {
|
||||||
|
|
||||||
test('Rotate JPEG', async () => {
|
test('Rotate JPEG', async () => {
|
||||||
const path = `${resources}/rotate.jpg`;
|
const path = `${resources}/rotate.jpg`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
assert.deepStrictEqual(info, {
|
assert.deepStrictEqual(info, {
|
||||||
size: 12624,
|
size: 12624,
|
||||||
md5: '68d5b2d8d1d1acbbce99203e3ec3857e',
|
md5: '68d5b2d8d1d1acbbce99203e3ec3857e',
|
||||||
|
@ -247,11 +220,7 @@ describe('FileInfoService', () => {
|
||||||
describe('AUDIO', () => {
|
describe('AUDIO', () => {
|
||||||
test('MP3', async () => {
|
test('MP3', async () => {
|
||||||
const path = `${resources}/kick_gaba7.mp3`;
|
const path = `${resources}/kick_gaba7.mp3`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
delete info.width;
|
delete info.width;
|
||||||
delete info.height;
|
delete info.height;
|
||||||
delete info.orientation;
|
delete info.orientation;
|
||||||
|
@ -267,11 +236,7 @@ describe('FileInfoService', () => {
|
||||||
|
|
||||||
test('WAV', async () => {
|
test('WAV', async () => {
|
||||||
const path = `${resources}/kick_gaba7.wav`;
|
const path = `${resources}/kick_gaba7.wav`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
delete info.width;
|
delete info.width;
|
||||||
delete info.height;
|
delete info.height;
|
||||||
delete info.orientation;
|
delete info.orientation;
|
||||||
|
@ -287,11 +252,7 @@ describe('FileInfoService', () => {
|
||||||
|
|
||||||
test('AAC', async () => {
|
test('AAC', async () => {
|
||||||
const path = `${resources}/kick_gaba7.aac`;
|
const path = `${resources}/kick_gaba7.aac`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
delete info.width;
|
delete info.width;
|
||||||
delete info.height;
|
delete info.height;
|
||||||
delete info.orientation;
|
delete info.orientation;
|
||||||
|
@ -307,11 +268,7 @@ describe('FileInfoService', () => {
|
||||||
|
|
||||||
test('FLAC', async () => {
|
test('FLAC', async () => {
|
||||||
const path = `${resources}/kick_gaba7.flac`;
|
const path = `${resources}/kick_gaba7.flac`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
delete info.width;
|
delete info.width;
|
||||||
delete info.height;
|
delete info.height;
|
||||||
delete info.orientation;
|
delete info.orientation;
|
||||||
|
@ -327,11 +284,7 @@ describe('FileInfoService', () => {
|
||||||
|
|
||||||
test('MPEG-4 AUDIO (M4A)', async () => {
|
test('MPEG-4 AUDIO (M4A)', async () => {
|
||||||
const path = `${resources}/kick_gaba7.m4a`;
|
const path = `${resources}/kick_gaba7.m4a`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
delete info.width;
|
delete info.width;
|
||||||
delete info.height;
|
delete info.height;
|
||||||
delete info.orientation;
|
delete info.orientation;
|
||||||
|
@ -347,11 +300,7 @@ describe('FileInfoService', () => {
|
||||||
|
|
||||||
test('WEBM AUDIO', async () => {
|
test('WEBM AUDIO', async () => {
|
||||||
const path = `${resources}/kick_gaba7.webm`;
|
const path = `${resources}/kick_gaba7.webm`;
|
||||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||||
delete info.warnings;
|
|
||||||
delete info.blurhash;
|
|
||||||
delete info.sensitive;
|
|
||||||
delete info.porn;
|
|
||||||
delete info.width;
|
delete info.width;
|
||||||
delete info.height;
|
delete info.height;
|
||||||
delete info.orientation;
|
delete info.orientation;
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
|
import { setTimeout } from 'node:timers/promises';
|
||||||
import { jest } from '@jest/globals';
|
import { jest } from '@jest/globals';
|
||||||
import { ModuleMocker } from 'jest-mock';
|
import { ModuleMocker } from 'jest-mock';
|
||||||
import { Test } from '@nestjs/testing';
|
import { Test } from '@nestjs/testing';
|
||||||
|
@ -29,7 +30,6 @@ import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||||
import { NotificationService } from '@/core/NotificationService.js';
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { RoleCondFormulaValue } from '@/models/Role.js';
|
import { RoleCondFormulaValue } from '@/models/Role.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { sleep } from '../utils.js';
|
|
||||||
import type { TestingModule } from '@nestjs/testing';
|
import type { TestingModule } from '@nestjs/testing';
|
||||||
import type { MockFunctionMetadata } from 'jest-mock';
|
import type { MockFunctionMetadata } from 'jest-mock';
|
||||||
|
|
||||||
|
@ -278,7 +278,7 @@ describe('RoleService', () => {
|
||||||
|
|
||||||
// ストリーミング経由で反映されるまでちょっと待つ
|
// ストリーミング経由で反映されるまでちょっと待つ
|
||||||
clock.uninstall();
|
clock.uninstall();
|
||||||
await sleep(100);
|
await setTimeout(100);
|
||||||
|
|
||||||
const resultAfter25hAgain = await roleService.getUserPolicies(user.id);
|
const resultAfter25hAgain = await roleService.getUserPolicies(user.id);
|
||||||
expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true);
|
expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true);
|
||||||
|
@ -807,7 +807,7 @@ describe('RoleService', () => {
|
||||||
await roleService.assign(user.id, role.id);
|
await roleService.assign(user.id, role.id);
|
||||||
|
|
||||||
clock.uninstall();
|
clock.uninstall();
|
||||||
await sleep(100);
|
await setTimeout(100);
|
||||||
|
|
||||||
const assignments = await roleAssignmentsRepository.find({
|
const assignments = await roleAssignmentsRepository.find({
|
||||||
where: {
|
where: {
|
||||||
|
@ -835,7 +835,7 @@ describe('RoleService', () => {
|
||||||
await roleService.assign(user.id, role.id);
|
await roleService.assign(user.id, role.id);
|
||||||
|
|
||||||
clock.uninstall();
|
clock.uninstall();
|
||||||
await sleep(100);
|
await setTimeout(100);
|
||||||
|
|
||||||
const assignments = await roleAssignmentsRepository.find({
|
const assignments = await roleAssignmentsRepository.find({
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { setTimeout } from 'node:timers/promises';
|
||||||
import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals';
|
import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { MiUser } from '@/models/User.js';
|
import { MiUser } from '@/models/User.js';
|
||||||
|
@ -16,7 +17,7 @@ import { DI } from '@/di-symbols.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||||
import { randomString, sleep } from '../utils.js';
|
import { randomString } from '../utils.js';
|
||||||
|
|
||||||
describe('SystemWebhookService', () => {
|
describe('SystemWebhookService', () => {
|
||||||
let app: TestingModule;
|
let app: TestingModule;
|
||||||
|
@ -358,7 +359,7 @@ describe('SystemWebhookService', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// redisでの配信経由で更新されるのでちょっと待つ
|
// redisでの配信経由で更新されるのでちょっと待つ
|
||||||
await sleep(500);
|
await setTimeout(500);
|
||||||
|
|
||||||
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
|
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
|
||||||
expect(fetchedWebhooks).toEqual([webhook]);
|
expect(fetchedWebhooks).toEqual([webhook]);
|
||||||
|
@ -377,7 +378,7 @@ describe('SystemWebhookService', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// redisでの配信経由で更新されるのでちょっと待つ
|
// redisでの配信経由で更新されるのでちょっと待つ
|
||||||
await sleep(500);
|
await setTimeout(500);
|
||||||
|
|
||||||
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
|
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
|
||||||
expect(fetchedWebhooks).toEqual([]);
|
expect(fetchedWebhooks).toEqual([]);
|
||||||
|
@ -407,7 +408,7 @@ describe('SystemWebhookService', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// redisでの配信経由で更新されるのでちょっと待つ
|
// redisでの配信経由で更新されるのでちょっと待つ
|
||||||
await sleep(500);
|
await setTimeout(500);
|
||||||
|
|
||||||
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
|
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
|
||||||
expect(fetchedWebhooks).toEqual([webhook2]);
|
expect(fetchedWebhooks).toEqual([webhook2]);
|
||||||
|
@ -434,7 +435,7 @@ describe('SystemWebhookService', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// redisでの配信経由で更新されるのでちょっと待つ
|
// redisでの配信経由で更新されるのでちょっと待つ
|
||||||
await sleep(500);
|
await setTimeout(500);
|
||||||
|
|
||||||
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
|
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
|
||||||
expect(fetchedWebhooks.length).toEqual(0);
|
expect(fetchedWebhooks.length).toEqual(0);
|
||||||
|
@ -457,7 +458,7 @@ describe('SystemWebhookService', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// redisでの配信経由で更新されるのでちょっと待つ
|
// redisでの配信経由で更新されるのでちょっと待つ
|
||||||
await sleep(500);
|
await setTimeout(500);
|
||||||
|
|
||||||
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
|
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
|
||||||
expect(fetchedWebhooks).toEqual([webhook2]);
|
expect(fetchedWebhooks).toEqual([webhook2]);
|
||||||
|
@ -481,7 +482,7 @@ describe('SystemWebhookService', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// redisでの配信経由で更新されるのでちょっと待つ
|
// redisでの配信経由で更新されるのでちょっと待つ
|
||||||
await sleep(500);
|
await setTimeout(500);
|
||||||
|
|
||||||
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
|
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
|
||||||
expect(fetchedWebhooks.length).toEqual(0);
|
expect(fetchedWebhooks.length).toEqual(0);
|
||||||
|
@ -504,7 +505,7 @@ describe('SystemWebhookService', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// redisでの配信経由で更新されるのでちょっと待つ
|
// redisでの配信経由で更新されるのでちょっと待つ
|
||||||
await sleep(500);
|
await setTimeout(500);
|
||||||
|
|
||||||
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
|
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
|
||||||
expect(fetchedWebhooks.length).toEqual(0);
|
expect(fetchedWebhooks.length).toEqual(0);
|
||||||
|
|
265
packages/backend/test/unit/UserSearchService.ts
Normal file
265
packages/backend/test/unit/UserSearchService.ts
Normal file
|
@ -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 { loadConfig } from '../src/config.js';
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
import { type Response } from 'node-fetch';
|
import { type Response } from 'node-fetch';
|
||||||
|
import { ApiError } from "@/server/api/error.js";
|
||||||
|
|
||||||
export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.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 res = await api(endpoint, parameters, user);
|
||||||
const status = assertion.status ?? (res.body == null ? 204 : 200);
|
const status = assertion.status ?? (res.body == null ? 204 : 200);
|
||||||
assert.strictEqual(res.status, status, inspect(res.body, { depth: 5, colors: true }));
|
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,
|
status: number,
|
||||||
code: string,
|
code: string,
|
||||||
id: string
|
id: string
|
||||||
}): Promise<T> => {
|
}): Promise<void> => {
|
||||||
const { endpoint, parameters, user } = request;
|
const { endpoint, parameters, user } = request;
|
||||||
const { status, code, id } = assertion;
|
const { status, code, id } = assertion;
|
||||||
const res = await api(endpoint, parameters, user);
|
const res = await api(endpoint, parameters, user);
|
||||||
assert.strictEqual(res.status, status, inspect(res.body));
|
assert.strictEqual(res.status, status, inspect(res.body));
|
||||||
assert.strictEqual(res.body.error.code, code, inspect(res.body));
|
assert.ok(res.body);
|
||||||
assert.strictEqual(res.body.error.id, id, inspect(res.body));
|
assert.strictEqual(castAsError(res.body as any).error.code, code, inspect(res.body));
|
||||||
return 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,
|
status: number,
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
body: any
|
body: misskey.api.SwitchCaseResponseType<E, P>
|
||||||
}> => {
|
}> => {
|
||||||
const bodyAuth: Record<string, string> = {};
|
const bodyAuth: Record<string, string> = {};
|
||||||
const headers: 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'
|
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;
|
: null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: res.status,
|
status: res.status,
|
||||||
headers: res.headers,
|
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);
|
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][]) => {
|
export const createAppToken = async (user: UserToken, permissions: (typeof misskey.permissions)[number][]) => {
|
||||||
|
@ -297,7 +301,7 @@ export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadO
|
||||||
body: misskey.entities.DriveFile | null
|
body: misskey.entities.DriveFile | null
|
||||||
}> => {
|
}> => {
|
||||||
const absPath = path == null
|
const absPath = path == null
|
||||||
? new URL('resources/Lenna.jpg', import.meta.url)
|
? new URL('resources/192.jpg', import.meta.url)
|
||||||
: isAbsolute(path.toString())
|
: isAbsolute(path.toString())
|
||||||
? new URL(path)
|
? new URL(path)
|
||||||
: new URL(path, new URL('resources/', import.meta.url));
|
: new URL(path, new URL('resources/', import.meta.url));
|
||||||
|
@ -605,14 +609,6 @@ export async function initTestDb(justBorrow = false, initEntities?: any[]) {
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sleep(msec: number) {
|
|
||||||
return new Promise<void>(res => {
|
|
||||||
setTimeout(() => {
|
|
||||||
res();
|
|
||||||
}, msec);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function sendEnvUpdateRequest(params: { key: string, value?: string }) {
|
export async function sendEnvUpdateRequest(params: { key: string, value?: string }) {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`http://localhost:${port + 1000}/env`,
|
`http://localhost:${port + 1000}/env`,
|
||||||
|
@ -643,3 +639,9 @@ export async function sendEnvResetRequest() {
|
||||||
throw new Error('server env update failed.');
|
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 };
|
||||||
|
}
|
||||||
|
|
|
@ -1,82 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
env: {
|
|
||||||
'node': false,
|
|
||||||
},
|
|
||||||
parser: 'vue-eslint-parser',
|
|
||||||
parserOptions: {
|
|
||||||
'parser': '@typescript-eslint/parser',
|
|
||||||
tsconfigRootDir: __dirname,
|
|
||||||
project: ['./tsconfig.json'],
|
|
||||||
extraFileExtensions: ['.vue'],
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
'../shared/.eslintrc.js',
|
|
||||||
'plugin:vue/vue3-recommended',
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/no-empty-interface': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
'allowSingleExtends': true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
|
|
||||||
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
|
|
||||||
'id-denylist': ['error', 'window', 'e'],
|
|
||||||
'no-shadow': ['warn'],
|
|
||||||
'vue/attributes-order': ['error', {
|
|
||||||
'alphabetical': false,
|
|
||||||
}],
|
|
||||||
'vue/no-use-v-if-with-v-for': ['error', {
|
|
||||||
'allowUsingIterationVar': false,
|
|
||||||
}],
|
|
||||||
'vue/no-ref-as-operand': 'error',
|
|
||||||
'vue/no-multi-spaces': ['error', {
|
|
||||||
'ignoreProperties': false,
|
|
||||||
}],
|
|
||||||
'vue/no-v-html': 'warn',
|
|
||||||
'vue/order-in-components': 'error',
|
|
||||||
'vue/html-indent': ['warn', 'tab', {
|
|
||||||
'attribute': 1,
|
|
||||||
'baseIndent': 0,
|
|
||||||
'closeBracket': 0,
|
|
||||||
'alignAttributesVertically': true,
|
|
||||||
'ignores': [],
|
|
||||||
}],
|
|
||||||
'vue/html-closing-bracket-spacing': ['warn', {
|
|
||||||
'startTag': 'never',
|
|
||||||
'endTag': 'never',
|
|
||||||
'selfClosingTag': 'never',
|
|
||||||
}],
|
|
||||||
'vue/multi-word-component-names': 'warn',
|
|
||||||
'vue/require-v-for-key': 'warn',
|
|
||||||
'vue/no-unused-components': 'warn',
|
|
||||||
'vue/no-unused-vars': 'warn',
|
|
||||||
'vue/no-dupe-keys': 'warn',
|
|
||||||
'vue/valid-v-for': 'warn',
|
|
||||||
'vue/return-in-computed-property': 'warn',
|
|
||||||
'vue/no-setup-props-reactivity-loss': 'warn',
|
|
||||||
'vue/max-attributes-per-line': 'off',
|
|
||||||
'vue/html-self-closing': 'off',
|
|
||||||
'vue/singleline-html-element-content-newline': 'off',
|
|
||||||
'vue/v-on-event-hyphenation': ['error', 'never', { autofix: true }],
|
|
||||||
'vue/attribute-hyphenation': ['error', 'never'],
|
|
||||||
},
|
|
||||||
globals: {
|
|
||||||
// Node.js
|
|
||||||
'module': false,
|
|
||||||
'require': false,
|
|
||||||
'__dirname': false,
|
|
||||||
|
|
||||||
// Misskey
|
|
||||||
'_DEV_': false,
|
|
||||||
'_LANGS_': false,
|
|
||||||
'_VERSION_': false,
|
|
||||||
'_ENV_': false,
|
|
||||||
'_PERF_PREFIX_': false,
|
|
||||||
'_DATA_TRANSFER_DRIVE_FILE_': false,
|
|
||||||
'_DATA_TRANSFER_DRIVE_FOLDER_': false,
|
|
||||||
'_DATA_TRANSFER_DECK_COLUMN_': false,
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -47,7 +47,6 @@ await fs.readFile(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.map((path) => path.replace(/(?:(?<=\.stories)\.(?:impl|meta)|\.msw)(?=\.ts$)/g, ''))
|
.map((path) => path.replace(/(?:(?<=\.stories)\.(?:impl|meta)|\.msw)(?=\.ts$)/g, ''))
|
||||||
.map((path) => (path.startsWith('.') ? path : `./${path}`))
|
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
micromatch(Array.from(modules), [
|
micromatch(Array.from(modules), [
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { FORCE_REMOUNT } from '@storybook/core-events';
|
import { FORCE_RE_RENDER, FORCE_REMOUNT } from '@storybook/core-events';
|
||||||
import { addons } from '@storybook/preview-api';
|
import { addons } from '@storybook/preview-api';
|
||||||
import { type Preview, setup } from '@storybook/vue3';
|
import { type Preview, setup } from '@storybook/vue3';
|
||||||
import isChromatic from 'chromatic/isChromatic';
|
import isChromatic from 'chromatic/isChromatic';
|
||||||
|
@ -16,7 +16,7 @@ import '../src/style.scss';
|
||||||
|
|
||||||
const appInitialized = Symbol();
|
const appInitialized = Symbol();
|
||||||
|
|
||||||
let lastStory = null;
|
let lastStory: string | null = null;
|
||||||
let moduleInitialized = false;
|
let moduleInitialized = false;
|
||||||
let unobserve = () => {};
|
let unobserve = () => {};
|
||||||
let misskeyOS = null;
|
let misskeyOS = null;
|
||||||
|
@ -110,7 +110,7 @@ const preview = {
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
Promise.all([resetIndexedDBPromise, resetDefaultStorePromise]).then(() => {
|
Promise.all([resetIndexedDBPromise, resetDefaultStorePromise]).then(() => {
|
||||||
initLocalStorage();
|
initLocalStorage();
|
||||||
channel.emit(FORCE_REMOUNT, { storyId: context.id });
|
channel.emit(FORCE_RE_RENDER, { storyId: context.id });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const story = Story();
|
const story = Story();
|
||||||
|
|
95
packages/frontend/eslint.config.js
Normal file
95
packages/frontend/eslint.config.js
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
import globals from 'globals';
|
||||||
|
import tsParser from '@typescript-eslint/parser';
|
||||||
|
import parser from 'vue-eslint-parser';
|
||||||
|
import pluginVue from 'eslint-plugin-vue';
|
||||||
|
import pluginMisskey from '@misskey-dev/eslint-plugin';
|
||||||
|
import sharedConfig from '../shared/eslint.config.js';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...sharedConfig,
|
||||||
|
{
|
||||||
|
files: ['src/**/*.vue'],
|
||||||
|
...pluginMisskey.configs.typescript,
|
||||||
|
},
|
||||||
|
...pluginVue.configs['flat/recommended'],
|
||||||
|
{
|
||||||
|
files: ['src/**/*.{ts,vue}'],
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...Object.fromEntries(Object.entries(globals.node).map(([key]) => [key, 'off'])),
|
||||||
|
...globals.browser,
|
||||||
|
|
||||||
|
// Node.js
|
||||||
|
module: false,
|
||||||
|
require: false,
|
||||||
|
__dirname: false,
|
||||||
|
|
||||||
|
// Misskey
|
||||||
|
_DEV_: false,
|
||||||
|
_LANGS_: false,
|
||||||
|
_VERSION_: false,
|
||||||
|
_ENV_: false,
|
||||||
|
_PERF_PREFIX_: false,
|
||||||
|
_DATA_TRANSFER_DRIVE_FILE_: false,
|
||||||
|
_DATA_TRANSFER_DRIVE_FOLDER_: false,
|
||||||
|
_DATA_TRANSFER_DECK_COLUMN_: false,
|
||||||
|
},
|
||||||
|
parser,
|
||||||
|
parserOptions: {
|
||||||
|
extraFileExtensions: ['.vue'],
|
||||||
|
parser: tsParser,
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
|
sourceType: 'module',
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-empty-interface': ['error', {
|
||||||
|
allowSingleExtends: true,
|
||||||
|
}],
|
||||||
|
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
|
||||||
|
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
|
||||||
|
'id-denylist': ['error', 'window', 'e'],
|
||||||
|
'no-shadow': ['warn'],
|
||||||
|
'vue/attributes-order': ['error', {
|
||||||
|
alphabetical: false,
|
||||||
|
}],
|
||||||
|
'vue/no-use-v-if-with-v-for': ['error', {
|
||||||
|
allowUsingIterationVar: false,
|
||||||
|
}],
|
||||||
|
'vue/no-ref-as-operand': 'error',
|
||||||
|
'vue/no-multi-spaces': ['error', {
|
||||||
|
ignoreProperties: false,
|
||||||
|
}],
|
||||||
|
'vue/no-v-html': 'warn',
|
||||||
|
'vue/order-in-components': 'error',
|
||||||
|
'vue/html-indent': ['warn', 'tab', {
|
||||||
|
attribute: 1,
|
||||||
|
baseIndent: 0,
|
||||||
|
closeBracket: 0,
|
||||||
|
alignAttributesVertically: true,
|
||||||
|
ignores: [],
|
||||||
|
}],
|
||||||
|
'vue/html-closing-bracket-spacing': ['warn', {
|
||||||
|
startTag: 'never',
|
||||||
|
endTag: 'never',
|
||||||
|
selfClosingTag: 'never',
|
||||||
|
}],
|
||||||
|
'vue/multi-word-component-names': 'warn',
|
||||||
|
'vue/require-v-for-key': 'warn',
|
||||||
|
'vue/no-unused-components': 'warn',
|
||||||
|
'vue/no-unused-vars': 'warn',
|
||||||
|
'vue/no-dupe-keys': 'warn',
|
||||||
|
'vue/valid-v-for': 'warn',
|
||||||
|
'vue/return-in-computed-property': 'warn',
|
||||||
|
'vue/no-setup-props-reactivity-loss': 'warn',
|
||||||
|
'vue/max-attributes-per-line': 'off',
|
||||||
|
'vue/html-self-closing': 'off',
|
||||||
|
'vue/singleline-html-element-content-newline': 'off',
|
||||||
|
'vue/v-on-event-hyphenation': ['error', 'never', {
|
||||||
|
autofix: true,
|
||||||
|
}],
|
||||||
|
'vue/attribute-hyphenation': ['error', 'never'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
|
@ -23,24 +23,24 @@
|
||||||
"@meersagor/wavesurfer-vue": "^0.1.0",
|
"@meersagor/wavesurfer-vue": "^0.1.0",
|
||||||
"@misskey-dev/browser-image-resizer": "2024.1.0",
|
"@misskey-dev/browser-image-resizer": "2024.1.0",
|
||||||
"@rollup/plugin-json": "6.1.0",
|
"@rollup/plugin-json": "6.1.0",
|
||||||
"@rollup/plugin-replace": "5.0.5",
|
"@rollup/plugin-replace": "5.0.7",
|
||||||
"@rollup/pluginutils": "5.1.0",
|
"@rollup/pluginutils": "5.1.0",
|
||||||
"@syuilo/aiscript": "0.18.0",
|
"@syuilo/aiscript": "0.18.0",
|
||||||
"@tabler/icons-webfont": "3.3.0",
|
"@tabler/icons-webfont": "3.3.0",
|
||||||
"@twemoji/parser": "15.1.1",
|
"@twemoji/parser": "15.1.1",
|
||||||
"@vitejs/plugin-vue": "5.0.4",
|
"@vitejs/plugin-vue": "5.0.5",
|
||||||
"@vue/compiler-sfc": "3.4.26",
|
"@vue/compiler-sfc": "3.4.31",
|
||||||
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.9",
|
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.11",
|
||||||
"astring": "1.8.6",
|
"astring": "1.8.6",
|
||||||
"broadcast-channel": "7.0.0",
|
"broadcast-channel": "7.0.0",
|
||||||
"buraha": "0.0.1",
|
"buraha": "0.0.1",
|
||||||
"canvas-confetti": "1.9.3",
|
"canvas-confetti": "1.9.3",
|
||||||
"chart.js": "4.4.2",
|
"chart.js": "4.4.3",
|
||||||
"chartjs-adapter-date-fns": "3.0.0",
|
"chartjs-adapter-date-fns": "3.0.0",
|
||||||
"chartjs-chart-matrix": "2.0.1",
|
"chartjs-chart-matrix": "2.0.1",
|
||||||
"chartjs-plugin-gradient": "0.6.1",
|
"chartjs-plugin-gradient": "0.6.1",
|
||||||
"chartjs-plugin-zoom": "2.0.1",
|
"chartjs-plugin-zoom": "2.0.1",
|
||||||
"chromatic": "11.3.0",
|
"chromatic": "11.5.4",
|
||||||
"compare-versions": "6.1.0",
|
"compare-versions": "6.1.0",
|
||||||
"cropperjs": "2.0.0-beta.5",
|
"cropperjs": "2.0.0-beta.5",
|
||||||
"date-fns": "2.30.0",
|
"date-fns": "2.30.0",
|
||||||
|
@ -56,90 +56,88 @@
|
||||||
"misskey-bubble-game": "workspace:*",
|
"misskey-bubble-game": "workspace:*",
|
||||||
"misskey-js": "workspace:*",
|
"misskey-js": "workspace:*",
|
||||||
"misskey-reversi": "workspace:*",
|
"misskey-reversi": "workspace:*",
|
||||||
"photoswipe": "5.4.3",
|
"photoswipe": "5.4.4",
|
||||||
"punycode": "2.3.1",
|
"punycode": "2.3.1",
|
||||||
"rollup": "4.17.2",
|
"rollup": "4.18.0",
|
||||||
"sanitize-html": "2.13.0",
|
"sanitize-html": "2.13.0",
|
||||||
"sass": "1.76.0",
|
"sass": "1.77.6",
|
||||||
"shiki": "1.4.0",
|
"shiki": "1.10.0",
|
||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"textarea-caret": "3.1.0",
|
"textarea-caret": "3.1.0",
|
||||||
"three": "0.164.1",
|
"three": "0.165.0",
|
||||||
"throttle-debounce": "5.0.0",
|
"throttle-debounce": "5.0.2",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"tsc-alias": "1.8.8",
|
"tsc-alias": "1.8.10",
|
||||||
"tsconfig-paths": "4.2.0",
|
"tsconfig-paths": "4.2.0",
|
||||||
"typescript": "5.5.2",
|
"typescript": "5.5.3",
|
||||||
"uuid": "9.0.1",
|
"uuid": "10.0.0",
|
||||||
"v-code-diff": "1.11.0",
|
"v-code-diff": "1.12.0",
|
||||||
"vite": "5.2.11",
|
"vite": "5.3.2",
|
||||||
"vue": "3.4.26",
|
"vue": "3.4.31",
|
||||||
"vuedraggable": "next",
|
"vuedraggable": "next",
|
||||||
"wavesurfer.js": "^7.7.14"
|
"wavesurfer.js": "^7.7.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
|
||||||
"@misskey-dev/summaly": "5.1.0",
|
"@misskey-dev/summaly": "5.1.0",
|
||||||
"@storybook/addon-actions": "8.0.9",
|
"@storybook/addon-actions": "8.1.11",
|
||||||
"@storybook/addon-essentials": "8.0.9",
|
"@storybook/addon-essentials": "8.1.11",
|
||||||
"@storybook/addon-interactions": "8.0.9",
|
"@storybook/addon-interactions": "8.1.11",
|
||||||
"@storybook/addon-links": "8.0.9",
|
"@storybook/addon-links": "8.1.11",
|
||||||
"@storybook/addon-mdx-gfm": "8.0.9",
|
"@storybook/addon-mdx-gfm": "8.1.11",
|
||||||
"@storybook/addon-storysource": "8.0.9",
|
"@storybook/addon-storysource": "8.1.11",
|
||||||
"@storybook/blocks": "8.0.9",
|
"@storybook/blocks": "8.1.11",
|
||||||
"@storybook/components": "8.0.9",
|
"@storybook/components": "8.1.11",
|
||||||
"@storybook/core-events": "8.0.9",
|
"@storybook/core-events": "8.1.11",
|
||||||
"@storybook/manager-api": "8.0.9",
|
"@storybook/manager-api": "8.1.11",
|
||||||
"@storybook/preview-api": "8.0.9",
|
"@storybook/preview-api": "8.1.11",
|
||||||
"@storybook/react": "8.0.9",
|
"@storybook/react": "8.1.11",
|
||||||
"@storybook/react-vite": "8.0.9",
|
"@storybook/react-vite": "8.1.11",
|
||||||
"@storybook/test": "8.0.9",
|
"@storybook/test": "8.1.11",
|
||||||
"@storybook/theming": "8.0.9",
|
"@storybook/theming": "8.1.11",
|
||||||
"@storybook/types": "8.0.9",
|
"@storybook/types": "8.1.11",
|
||||||
"@storybook/vue3": "8.0.9",
|
"@storybook/vue3": "8.1.11",
|
||||||
"@storybook/vue3-vite": "8.0.9",
|
"@storybook/vue3-vite": "8.1.11",
|
||||||
"@testing-library/vue": "8.0.3",
|
"@testing-library/vue": "8.1.0",
|
||||||
"@types/escape-regexp": "0.0.3",
|
"@types/escape-regexp": "0.0.3",
|
||||||
"@types/estree": "1.0.5",
|
"@types/estree": "1.0.5",
|
||||||
"@types/matter-js": "0.19.6",
|
"@types/matter-js": "0.19.6",
|
||||||
"@types/micromatch": "4.0.7",
|
"@types/micromatch": "4.0.9",
|
||||||
"@types/node": "20.12.7",
|
"@types/node": "20.14.9",
|
||||||
"@types/punycode": "2.1.4",
|
"@types/punycode": "2.1.4",
|
||||||
"@types/sanitize-html": "2.11.0",
|
"@types/sanitize-html": "2.11.0",
|
||||||
"@types/seedrandom": "3.0.8",
|
"@types/seedrandom": "3.0.8",
|
||||||
"@types/throttle-debounce": "5.0.2",
|
"@types/throttle-debounce": "5.0.2",
|
||||||
"@types/tinycolor2": "1.4.6",
|
"@types/tinycolor2": "1.4.6",
|
||||||
"@types/uuid": "9.0.8",
|
"@types/uuid": "10.0.0",
|
||||||
"@types/ws": "8.5.10",
|
"@types/ws": "8.5.10",
|
||||||
"@typescript-eslint/eslint-plugin": "7.7.1",
|
"@typescript-eslint/eslint-plugin": "7.15.0",
|
||||||
"@typescript-eslint/parser": "7.7.1",
|
"@typescript-eslint/parser": "7.15.0",
|
||||||
"@vitest/coverage-v8": "0.34.6",
|
"@vitest/coverage-v8": "1.6.0",
|
||||||
"@vue/runtime-core": "3.4.26",
|
"@vue/runtime-core": "3.4.31",
|
||||||
"acorn": "8.11.3",
|
"acorn": "8.12.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "13.8.1",
|
"cypress": "13.13.0",
|
||||||
"eslint": "8.57.0",
|
|
||||||
"eslint-plugin-import": "2.29.1",
|
"eslint-plugin-import": "2.29.1",
|
||||||
"eslint-plugin-vue": "9.25.0",
|
"eslint-plugin-vue": "9.26.0",
|
||||||
"fast-glob": "3.3.2",
|
"fast-glob": "3.3.2",
|
||||||
"happy-dom": "10.0.3",
|
"happy-dom": "10.0.3",
|
||||||
"intersection-observer": "0.12.2",
|
"intersection-observer": "0.12.2",
|
||||||
"micromatch": "4.0.5",
|
"micromatch": "4.0.7",
|
||||||
"msw": "2.2.14",
|
"msw": "2.3.1",
|
||||||
"msw-storybook-addon": "2.0.1",
|
"msw-storybook-addon": "2.0.2",
|
||||||
"nodemon": "3.1.0",
|
"nodemon": "3.1.4",
|
||||||
"prettier": "3.2.5",
|
"prettier": "3.3.2",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"seedrandom": "3.0.5",
|
"seedrandom": "3.0.5",
|
||||||
"start-server-and-test": "2.0.3",
|
"start-server-and-test": "2.0.4",
|
||||||
"storybook": "8.0.9",
|
"storybook": "8.1.11",
|
||||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||||
"vite-plugin-turbosnap": "1.0.3",
|
"vite-plugin-turbosnap": "1.0.3",
|
||||||
"vitest": "0.34.6",
|
"vitest": "1.6.0",
|
||||||
"vitest-fetch-mock": "0.2.2",
|
"vitest-fetch-mock": "0.2.2",
|
||||||
"vue-component-type-helpers": "2.0.16",
|
"vue-component-type-helpers": "2.0.24",
|
||||||
"vue-eslint-parser": "9.4.2",
|
"vue-eslint-parser": "9.4.3",
|
||||||
"vue-tsc": "2.0.16"
|
"vue-tsc": "2.0.24"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -184,10 +184,12 @@ export async function refreshAccount() {
|
||||||
|
|
||||||
export async function login(token: Account['token'], redirect?: string) {
|
export async function login(token: Account['token'], redirect?: string) {
|
||||||
const showing = ref(true);
|
const showing = ref(true);
|
||||||
popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), {
|
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), {
|
||||||
success: false,
|
success: false,
|
||||||
showing: showing,
|
showing: showing,
|
||||||
}, {}, 'closed');
|
}, {
|
||||||
|
closed: () => dispose(),
|
||||||
|
});
|
||||||
if (_DEV_) console.log('logging as token ', token);
|
if (_DEV_) console.log('logging as token ', token);
|
||||||
const me = await fetchAccount(token, undefined, true)
|
const me = await fetchAccount(token, undefined, true)
|
||||||
.catch(reason => {
|
.catch(reason => {
|
||||||
|
@ -223,21 +225,23 @@ export async function openAccountMenu(opts: {
|
||||||
if (!$i) return;
|
if (!$i) return;
|
||||||
|
|
||||||
function showSigninDialog() {
|
function showSigninDialog() {
|
||||||
popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
|
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
|
||||||
done: res => {
|
done: res => {
|
||||||
addAccount(res.id, res.i);
|
addAccount(res.id, res.i);
|
||||||
success();
|
success();
|
||||||
},
|
},
|
||||||
}, 'closed');
|
closed: () => dispose(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAccount() {
|
function createAccount() {
|
||||||
popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
|
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
|
||||||
done: res => {
|
done: res => {
|
||||||
addAccount(res.id, res.i);
|
addAccount(res.id, res.i);
|
||||||
switchAccountWithToken(res.i);
|
switchAccountWithToken(res.i);
|
||||||
},
|
},
|
||||||
}, 'closed');
|
closed: () => dispose(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function switchAccount(account: Misskey.entities.UserDetailed) {
|
async function switchAccount(account: Misskey.entities.UserDetailed) {
|
||||||
|
|
|
@ -13,7 +13,6 @@ import * as sound from '@/scripts/sound.js';
|
||||||
import { $i, signout, updateAccount } from '@/account.js';
|
import { $i, signout, updateAccount } from '@/account.js';
|
||||||
import { instance } from '@/instance.js';
|
import { instance } from '@/instance.js';
|
||||||
import { ColdDeviceStorage, defaultStore } from '@/store.js';
|
import { ColdDeviceStorage, defaultStore } from '@/store.js';
|
||||||
import { makeHotkey } from '@/scripts/hotkey.js';
|
|
||||||
import { reactionPicker } from '@/scripts/reaction-picker.js';
|
import { reactionPicker } from '@/scripts/reaction-picker.js';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
|
import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
|
||||||
|
@ -21,6 +20,7 @@ import { initializeSw } from '@/scripts/initialize-sw.js';
|
||||||
import { deckStore } from '@/ui/deck/deck-store.js';
|
import { deckStore } from '@/ui/deck/deck-store.js';
|
||||||
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
||||||
import { mainRouter } from '@/router/main.js';
|
import { mainRouter } from '@/router/main.js';
|
||||||
|
import { type Keymap, makeHotkey } from '@/scripts/hotkey.js';
|
||||||
|
|
||||||
export async function mainBoot() {
|
export async function mainBoot() {
|
||||||
const { isClientUpdated } = await common(() => createApp(
|
const { isClientUpdated } = await common(() => createApp(
|
||||||
|
@ -36,7 +36,9 @@ export async function mainBoot() {
|
||||||
emojiPicker.init();
|
emojiPicker.init();
|
||||||
|
|
||||||
if (isClientUpdated && $i) {
|
if (isClientUpdated && $i) {
|
||||||
popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {}, 'closed');
|
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {
|
||||||
|
closed: () => dispose(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const stream = useStream();
|
const stream = useStream();
|
||||||
|
@ -68,14 +70,6 @@ export async function mainBoot() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const hotkeys = {
|
|
||||||
'd': (): void => {
|
|
||||||
defaultStore.set('darkMode', !defaultStore.state.darkMode);
|
|
||||||
},
|
|
||||||
's': (): void => {
|
|
||||||
mainRouter.push('/search');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
try {
|
try {
|
||||||
if (defaultStore.state.enableSeasonalScreenEffect) {
|
if (defaultStore.state.enableSeasonalScreenEffect) {
|
||||||
const month = new Date().getMonth() + 1;
|
const month = new Date().getMonth() + 1;
|
||||||
|
@ -104,27 +98,30 @@ export async function mainBoot() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($i) {
|
if ($i) {
|
||||||
// only add post shortcuts if logged in
|
|
||||||
hotkeys['p|n'] = post;
|
|
||||||
|
|
||||||
defaultStore.loaded.then(() => {
|
defaultStore.loaded.then(() => {
|
||||||
if (defaultStore.state.accountSetupWizard !== -1) {
|
if (defaultStore.state.accountSetupWizard !== -1) {
|
||||||
popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {}, 'closed');
|
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {
|
||||||
|
closed: () => dispose(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const announcement of ($i.unreadAnnouncements ?? []).filter(x => x.display === 'dialog')) {
|
for (const announcement of ($i.unreadAnnouncements ?? []).filter(x => x.display === 'dialog')) {
|
||||||
popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), {
|
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), {
|
||||||
announcement,
|
announcement,
|
||||||
}, {}, 'closed');
|
}, {
|
||||||
|
closed: () => dispose(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.on('announcementCreated', (ev) => {
|
stream.on('announcementCreated', (ev) => {
|
||||||
const announcement = ev.announcement;
|
const announcement = ev.announcement;
|
||||||
if (announcement.display === 'dialog') {
|
if (announcement.display === 'dialog') {
|
||||||
popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), {
|
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), {
|
||||||
announcement,
|
announcement,
|
||||||
}, {}, 'closed');
|
}, {
|
||||||
|
closed: () => dispose(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -248,13 +245,17 @@ export async function mainBoot() {
|
||||||
const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');
|
const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');
|
||||||
if (neverShowDonationInfo !== 'true' && (createdAt.getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) {
|
if (neverShowDonationInfo !== 'true' && (createdAt.getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) {
|
||||||
if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) {
|
if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) {
|
||||||
popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed');
|
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {
|
||||||
|
closed: () => dispose(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read');
|
const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read');
|
||||||
if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey') {
|
if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey') {
|
||||||
popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed');
|
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {
|
||||||
|
closed: () => dispose(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('Notification' in window) {
|
if ('Notification' in window) {
|
||||||
|
@ -323,7 +324,19 @@ export async function mainBoot() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// shortcut
|
// shortcut
|
||||||
document.addEventListener('keydown', makeHotkey(hotkeys));
|
const keymap = {
|
||||||
|
'p|n': () => {
|
||||||
|
if ($i == null) return;
|
||||||
|
post();
|
||||||
|
},
|
||||||
|
'd': () => {
|
||||||
|
defaultStore.set('darkMode', !defaultStore.state.darkMode);
|
||||||
|
},
|
||||||
|
's': () => {
|
||||||
|
mainRouter.push('/search');
|
||||||
|
},
|
||||||
|
} as const satisfies Keymap;
|
||||||
|
document.addEventListener('keydown', makeHotkey(keymap), { passive: false });
|
||||||
|
|
||||||
initializeSw();
|
initializeSw();
|
||||||
}
|
}
|
||||||
|
|
|
@ -153,7 +153,7 @@ onMounted(() => {
|
||||||
background: linear-gradient(0deg, #ffee20, #eb7018);
|
background: linear-gradient(0deg, #ffee20, #eb7018);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -173,7 +173,7 @@ onMounted(() => {
|
||||||
background: linear-gradient(0deg, #e1e1e1, #7c7c7c);
|
background: linear-gradient(0deg, #e1e1e1, #7c7c7c);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -423,7 +423,6 @@ function onMousedown(evt: MouseEvent): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
outline: solid 2px var(--focus);
|
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,14 +12,12 @@ import { expect, userEvent, within } from '@storybook/test';
|
||||||
import { channel } from '../../.storybook/fakes.js';
|
import { channel } from '../../.storybook/fakes.js';
|
||||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||||
import MkChannelFollowButton from './MkChannelFollowButton.vue';
|
import MkChannelFollowButton from './MkChannelFollowButton.vue';
|
||||||
import { semaphore } from '@/scripts/test-utils.js';
|
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
function sleep(ms: number) {
|
function sleep(ms: number) {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
const s = semaphore();
|
|
||||||
export const Default = {
|
export const Default = {
|
||||||
render(args) {
|
render(args) {
|
||||||
return {
|
return {
|
||||||
|
@ -46,17 +44,13 @@ export const Default = {
|
||||||
full: true,
|
full: true,
|
||||||
},
|
},
|
||||||
async play({ canvasElement }) {
|
async play({ canvasElement }) {
|
||||||
await s.acquire();
|
|
||||||
await sleep(1000);
|
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
const buttonElement = canvas.getByRole<HTMLButtonElement>('button');
|
const buttonElement = canvas.getByRole<HTMLButtonElement>('button');
|
||||||
await expect(buttonElement).toHaveTextContent(i18n.ts.follow);
|
await expect(buttonElement).toHaveTextContent(i18n.ts.follow);
|
||||||
await userEvent.click(buttonElement);
|
await userEvent.click(buttonElement);
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
await expect(buttonElement).toHaveTextContent(i18n.ts.unfollow);
|
await expect(buttonElement).toHaveTextContent(i18n.ts.unfollow);
|
||||||
await sleep(100);
|
|
||||||
await userEvent.click(buttonElement);
|
await userEvent.click(buttonElement);
|
||||||
s.release();
|
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
layout: 'centered',
|
layout: 'centered',
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<!--
|
<!--
|
||||||
SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License-Identifier: AGPL-3.0-only
|
SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -117,17 +118,7 @@ async function onClick() {
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
&:after {
|
outline-offset: 2px;
|
||||||
content: "";
|
|
||||||
pointer-events: none;
|
|
||||||
position: absolute;
|
|
||||||
top: -5px;
|
|
||||||
right: -5px;
|
|
||||||
bottom: -5px;
|
|
||||||
left: -5px;
|
|
||||||
border: 2px solid var(--focus);
|
|
||||||
border-radius: 32px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
|
@ -4,7 +4,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div style="position: relative;">
|
<div style="position: relative;">
|
||||||
<MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1" @click="updateLastReadedAt">
|
<MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" @click="updateLastReadedAt">
|
||||||
<div class="banner" :style="bannerStyle">
|
<div class="banner" :style="bannerStyle">
|
||||||
<div class="fade"></div>
|
<div class="fade"></div>
|
||||||
<div class="name"><i class="ti ti-device-tv"></i> {{ channel.name }}</div>
|
<div class="name"><i class="ti ti-device-tv"></i> {{ channel.name }}</div>
|
||||||
|
@ -79,6 +79,7 @@ const bannerStyle = computed(() => {
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.eftoefju {
|
.eftoefju {
|
||||||
display: block;
|
display: block;
|
||||||
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
@ -86,6 +87,22 @@ const bannerStyle = computed(() => {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: inherit;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: inset 0 0 0 2px var(--focus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> .banner {
|
> .banner {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
import { StoryObj } from '@storybook/vue3';
|
import { StoryObj } from '@storybook/vue3';
|
||||||
import { HttpResponse, http } from 'msw';
|
import { HttpResponse, http } from 'msw';
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
import { expect, within } from '@storybook/test';
|
import { expect, userEvent, within } from '@storybook/test';
|
||||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||||
import MkClickerGame from './MkClickerGame.vue';
|
import MkClickerGame from './MkClickerGame.vue';
|
||||||
|
|
||||||
|
@ -41,12 +41,10 @@ export const Default = {
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
const count = canvas.getByTestId('count');
|
const count = canvas.getByTestId('count');
|
||||||
// NOTE: flaky なので N/A も通しておく
|
await expect(count).toHaveTextContent('0');
|
||||||
await expect(count).toHaveTextContent(/^(0|N\/A)$/);
|
const buttonElement = canvas.getByRole<HTMLButtonElement>('button');
|
||||||
// FIXME: flaky
|
await userEvent.click(buttonElement);
|
||||||
// const buttonElement = canvas.getByRole<HTMLButtonElement>('button');
|
await expect(count).toHaveTextContent('1');
|
||||||
// await userEvent.click(buttonElement);
|
|
||||||
// await expect(count).toHaveTextContent('1');
|
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
layout: 'centered',
|
layout: 'centered',
|
||||||
|
|
|
@ -45,7 +45,9 @@ const prevCookies = ref(0);
|
||||||
function onClick(ev: MouseEvent) {
|
function onClick(ev: MouseEvent) {
|
||||||
const x = ev.clientX;
|
const x = ev.clientX;
|
||||||
const y = ev.clientY;
|
const y = ev.clientY;
|
||||||
os.popup(MkPlusOneEffect, { x, y }, {}, 'end');
|
const { dispose } = os.popup(MkPlusOneEffect, { x, y }, {
|
||||||
|
end: () => dispose(),
|
||||||
|
});
|
||||||
|
|
||||||
saveData.value!.cookies++;
|
saveData.value!.cookies++;
|
||||||
saveData.value!.totalCookies++;
|
saveData.value!.totalCookies++;
|
||||||
|
|
|
@ -39,6 +39,14 @@ const remaining = computed(() => {
|
||||||
.link {
|
.link {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
.root {
|
||||||
|
box-shadow: inset 0 0 0 2px var(--focus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
|
|
|
@ -11,7 +11,7 @@ SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-projectSPDX-License
|
||||||
:leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''"
|
:leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''"
|
||||||
>
|
>
|
||||||
<div ref="rootEl" :class="$style.root" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}">
|
<div ref="rootEl" :class="$style.root" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}">
|
||||||
<MkMenu :items="items" :align="'left'" @close="$emit('closed')"/>
|
<MkMenu :items="items" :align="'left'" @close="emit('closed')"/>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue