Merge branch 'misskey-dev:develop' into develop

This commit is contained in:
老兄 2024-03-16 16:50:23 +08:00 committed by GitHub
commit 665dfb3ddb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
121 changed files with 2956 additions and 1471 deletions

View File

@ -38,7 +38,7 @@
# Option 3: If neither of the above applies to you.
# (In this case, the source code should be published
# on the Misskey interface. IT IS NOT ENOUGH TO
# DISCLOSE THE SOURCE CODE WEHN A USER REQUESTS IT BY
# DISCLOSE THE SOURCE CODE WHEN A USER REQUESTS IT BY
# E-MAIL OR OTHER MEANS. If you are not satisfied
# with this, it is recommended that you read the
# license again carefully. Anyway, enabling this

View File

@ -19,7 +19,6 @@
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint",
"Vue.volar",
"Vue.vscode-typescript-vue-plugin",
"Orta.vscode-jest",
"dbaeumer.vscode-eslint",
"mrmlnc.vscode-json5"

View File

@ -0,0 +1,75 @@
name: Check SPDX-License-Identifier
on:
push:
branches:
- master
- develop
pull_request:
jobs:
check-spdx-license-id:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.1.1
- name: Check
run: |
counter=0
search() {
local directory="$1"
find "$directory" -type f \
'(' \
-name "*.cjs" -and -not -name '*.config.cjs' -o \
-name "*.html" -o \
-name "*.js" -and -not -name '*.config.js' -o \
-name "*.mjs" -and -not -name '*.config.mjs' -o \
-name "*.scss" -o \
-name "*.ts" -and -not -name '*.config.ts' -o \
-name "*.vue" \
')' -and \
-not -name '*eslint*'
}
check() {
local file="$1"
if ! (
grep -q "SPDX-FileCopyrightText: syuilo and misskey-project" "$file" ||
grep -q "SPDX-License-Identifier: AGPL-3.0-only" "$file"
); then
echo "Missing: $file"
((counter++))
fi
}
directories=(
"cypress/e2e"
"packages/backend/migration"
"packages/backend/src"
"packages/backend/test"
"packages/frontend/.storybook"
"packages/frontend/@types"
"packages/frontend/lib"
"packages/frontend/public"
"packages/frontend/src"
"packages/frontend/test"
"packages/misskey-bubble-game/src"
"packages/misskey-reversi/src"
"packages/sw/src"
"scripts"
)
for directory in "${directories[@]}"; do
for file in $(search $directory); do
check "$file"
done
done
if [ $counter -gt 0 ]; then
echo "SPDX-License-Identifier is missing in $counter files."
exit 1
else
echo "SPDX-License-Identifier is certainly described in all target files!"
exit 0
fi

View File

@ -0,0 +1,40 @@
name: "Release Manager: sync changelog with PR"
on:
push:
branches:
- release/**
paths:
- 'CHANGELOG.md'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
permissions:
contents: write
issues: write
pull-requests: write
jobs:
edit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# headがrelease/かつopenのPRを1つ取得
- name: Get PR
run: |
echo "pr_number=$(gh pr list --limit 1 --head "${{ github.ref_name }}" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
id: get_pr
- name: Get target version
uses: misskey-dev/release-manager-actions/.github/actions/get-target-version@v1
id: v
# CHANGELOG.mdの内容を取得
- name: Get changelog
uses: misskey-dev/release-manager-actions/.github/actions/get-changelog@v1
with:
version: ${{ steps.v.outputs.target_version }}
id: changelog
# PRのnotesを更新
- name: Update PR
run: |
gh pr edit ${{ steps.get_pr.outputs.pr_number }} --body "${{ steps.changelog.outputs.changelog }}"

View File

@ -0,0 +1,122 @@
name: "Release Manager [Dispatch]"
on:
workflow_dispatch:
inputs:
## Specify the type of the next release.
#version_increment_type:
# type: choice
# description: 'VERSION INCREMENT TYPE'
# default: 'patch'
# required: false
# options:
# - 'major'
# - 'minor'
# - 'patch'
merge:
type: boolean
description: 'MERGE RELEASE BRANCH TO MAIN'
default: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
permissions:
contents: write
issues: write
pull-requests: write
jobs:
get-pr:
runs-on: ubuntu-latest
outputs:
pr_number: ${{ steps.get_pr.outputs.pr_number }}
steps:
- uses: actions/checkout@v4
# headがrelease/かつopenのPRを1つ取得
- name: Get PRs
run: |
echo "pr_number=$(gh pr list --limit 1 --search "head:release/ is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
id: get_pr
merge:
uses: misskey-dev/release-manager-actions/.github/workflows/merge.yml@v1
needs: get-pr
if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge == true }}
with:
pr_number: ${{ needs.get-pr.outputs.pr_number }}
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
# Text to prepend to the changelog
# The first line must be `## Unreleased`
changes_template: |
## Unreleased
### General
-
### Client
-
### Server
-
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
secrets:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
RULESET_EDIT_APP_ID: ${{ secrets.RULESET_EDIT_APP_ID }}
RULESET_EDIT_APP_PRIVATE_KEY: ${{ secrets.RULESET_EDIT_APP_PRIVATE_KEY }}
create-prerelease:
uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v1
needs: get-pr
if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge != true }}
with:
pr_number: ${{ needs.get-pr.outputs.pr_number }}
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
secrets:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
create-target:
uses: misskey-dev/release-manager-actions/.github/workflows/create-target.yml@v1
needs: get-pr
if: ${{ needs.get-pr.outputs.pr_number == '' }}
with:
# The script for version increment.
# process.env.CURRENT_VERSION: The current version.
#
# Misskey calender versioning (yyyy.MM.patch) example
version_increment_script: |
const now = new Date();
const year = now.toLocaleDateString('en-US', { year: 'numeric', timeZone: 'Asia/Tokyo' });
const month = now.toLocaleDateString('en-US', { month: 'numeric', timeZone: 'Asia/Tokyo' });
const [major, minor, _patch] = process.env.CURRENT_VERSION.split('.');
const patch = Number(_patch.split('-')[0]);
if (Number.isNaN(patch)) {
console.error('Invalid patch version', year, month, process.env.CURRENT_VERSION, major, minor, _patch);
throw new Error('Invalid patch version');
}
if (year !== major || month !== minor) {
return `${year}.${month}.0`;
} else {
return `${major}.${minor}.${patch + 1}`;
}
##Semver example
#version_increment_script: |
# const [major, minor, patch] = process.env.CURRENT_VERSION.split('.');
# if ("${{ inputs.version_increment_type }}" === "major") {
# return `${Number(major) + 1}.0.0`;
# } else if ("${{ inputs.version_increment_type }}" === "minor") {
# return `${major}.${Number(minor) + 1}.0`;
# } else {
# return `${major}.${minor}.${Number(patch) + 1}`;
# }
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
secrets:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
RULESET_EDIT_APP_ID: ${{ secrets.RULESET_EDIT_APP_ID }}
RULESET_EDIT_APP_PRIVATE_KEY: ${{ secrets.RULESET_EDIT_APP_PRIVATE_KEY }}

View File

@ -0,0 +1,38 @@
name: "Release Manager: release RC when ready for review"
on:
pull_request:
types: [ready_for_review]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
permissions:
contents: write
issues: write
pull-requests: write
jobs:
check:
runs-on: ubuntu-latest
outputs:
ref: ${{ steps.get_pr.outputs.ref }}
steps:
- uses: actions/checkout@v4
# PR情報を取得
- name: Get PR
run: |
pr_json=$(gh pr view ${{ github.event.pull_request.number }} --json isDraft,headRefName)
echo "ref=$(echo $pr_json | jq -r '.headRefName')" >> $GITHUB_OUTPUT
id: get_pr
release:
uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v1
needs: check
if: startsWith(needs.check.outputs.ref, 'release/')
with:
pr_number: ${{ github.event.pull_request.number }}
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
secrets:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}

View File

@ -3,9 +3,7 @@
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint",
"Vue.volar",
"Vue.vscode-typescript-vue-plugin",
"Orta.vscode-jest",
"dbaeumer.vscode-eslint",
"mrmlnc.vscode-json5"
]
}

View File

@ -7,7 +7,7 @@
"*.test.ts": "typescript"
},
"jest.jestCommandLine": "pnpm run jest",
"jest.autoRun": "off",
"jest.runMode": "on-demand",
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
},

View File

@ -1,16 +1,21 @@
<!--
## 202x.x.x (unreleased)
## Unreleased
### General
-
- Fix: Play作成時に設定した公開範囲が機能していない問題を修正
### Client
-
- Enhance: 自分のノートの添付ファイルから直接ファイルの詳細ページに飛べるように
- Enhance: 広告がMisskeyと同一ドメインの場合はRouterで遷移するように
- Enhance: リアクション・いいねの総数を表示するように
- Enhance: リアクション受け入れが「いいねのみ」の場合はリアクション絵文字一覧を表示しないように
- Enhance: 設定>プラグインのページからプラグインの簡易的なログやエラーを見られるように
- 実装の都合により、プラグインは1つエラーを起こした時に即時停止するようになりました
- Fix: 一部のページ内リンクが正しく動作しない問題を修正
- Fix: 周年の実績が閏年を考慮しない問題を修正
- Fix: ローカルURLのプレビューポップアップが左上に表示される
### Server
-
-->
- Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに
## 2024.3.1

View File

@ -316,6 +316,98 @@ export const handlers = [
Don't forget to re-run the `.storybook/generate.js` script after adding, editing, or removing the above files.
## Nest
### Nest Service Circular dependency / Nestでサービスの循環参照でエラーが起きた場合
#### forwardRef
まずは簡単に`forwardRef`を試してみる
```typescript
export class FooService {
constructor(
@Inject(forwardRef(() => BarService))
private barService: BarService
) {
}
}
```
#### OnModuleInit
できなければ`OnModuleInit`を使う
```typescript
import { Injectable, OnModuleInit } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { BarService } from '@/core/BarService';
@Injectable()
export class FooService implements OnModuleInit {
private barService: BarService // constructorから移動してくる
constructor(
private moduleRef: ModuleRef,
) {
}
async onModuleInit() {
this.barService = this.moduleRef.get(BarService.name);
}
public async niceMethod() {
return await this.barService.incredibleMethod({ hoge: 'fuga' });
}
}
```
##### Service Unit Test
テストで`onModuleInit`を呼び出す必要がある
```typescript
// import ...
describe('test', () => {
let app: TestingModule;
let fooService: FooService; // for test case
let barService: BarService; // for test case
beforeEach(async () => {
app = await Test.createTestingModule({
imports: ...,
providers: [
FooService,
{ // mockする (mockは必須ではないかもしれない)
provide: BarService,
useFactory: () => ({
incredibleMethod: jest.fn(),
}),
},
{ // Provideにする
provide: BarService.name,
useExisting: BarService,
},
],
})
.useMocker(...
.compile();
fooService = app.get<FooService>(FooService);
barService = app.get<BarService>(BarService) as jest.Mocked<BarService>;
// onModuleInitを実行する
await fooService.onModuleInit();
});
test('nice', () => {
await fooService.niceMethod();
expect(barService.incredibleMethod).toHaveBeenCalled();
expect(barService.incredibleMethod.mock.lastCall![0])
.toEqual({ hoge: 'fuga' });
});
})
```
## Notes
### Misskeyのドメイン固有の概念は`Mi`をprefixする

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
describe('Before setup instance', () => {
beforeEach(() => {
cy.resetState();

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
describe('Router transition', () => {
describe('Redirect', () => {
// サーバの初期化。ルートのテストに関しては各describeごとに1度だけ実行で十分だと思う使いまわした方が早い

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* flaky
describe('After user signed in', () => {
beforeEach(() => {

16
locales/index.d.ts vendored
View File

@ -1992,6 +1992,10 @@ export interface Locale extends ILocale {
*
*/
"showNoteActionsOnlyHover": string;
/**
*
*/
"showReactionsCount": string;
/**
*
*/
@ -6801,6 +6805,10 @@ export interface Locale extends ILocale {
*
*/
"viewSource": string;
/**
*
*/
"viewLog": string;
};
"_preferencesBackups": {
/**
@ -8627,6 +8635,10 @@ export interface Locale extends ILocale {
*
*/
"summary": string;
/**
* URLを知っている人は引き続きアクセスできます
*/
"visibilityDescription": string;
};
"_pages": {
/**
@ -8909,6 +8921,10 @@ export interface Locale extends ILocale {
* {n}
*/
"reactedBySomeUsers": ParameterizedString<"n">;
/**
* {n}
*/
"likedBySomeUsers": ParameterizedString<"n">;
/**
* {n}
*/

View File

@ -494,6 +494,7 @@ emojiStyle: "絵文字のスタイル"
native: "ネイティブ"
disableDrawer: "メニューをドロワーで表示しない"
showNoteActionsOnlyHover: "ノートのアクションをホバー時のみ表示する"
showReactionsCount: "ノートのリアクション数を表示する"
noHistory: "履歴はありません"
signinHistory: "ログイン履歴"
enableAdvancedMfm: "高度なMFMを有効にする"
@ -1771,6 +1772,7 @@ _plugin:
installWarn: "信頼できないプラグインはインストールしないでください。"
manage: "プラグインの管理"
viewSource: "ソースを表示"
viewLog: "ログを表示"
_preferencesBackups:
list: "作成したバックアップ"
@ -2278,6 +2280,7 @@ _play:
title: "タイトル"
script: "スクリプト"
summary: "説明"
visibilityDescription: "非公開に設定するとプロフィールに表示されなくなりますが、URLを知っている人は引き続きアクセスできます。"
_pages:
newPage: "ページの作成"
@ -2355,6 +2358,7 @@ _notification:
sendTestNotification: "テスト通知を送信する"
notificationWillBeDisplayedLikeThis: "通知はこのように表示されます"
reactedBySomeUsers: "{n}人がリアクションしました"
likedBySomeUsers: "{n}人がいいねしました"
renotedBySomeUsers: "{n}人がリノートしました"
followedBySomeUsers: "{n}人にフォローされました"
flushNotification: "通知の履歴をリセットする"

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { loadConfig } from './built/config.js'
import { genOpenapiSpec } from './built/server/api/openapi/gen-spec.js'
import { writeFileSync } from "node:fs";
@ -5,4 +10,4 @@ import { writeFileSync } from "node:fs";
const config = loadConfig();
const spec = genOpenapiSpec(config, true);
writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8');
writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8');

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class UserBlacklistAnntena1689325027964 {
name = 'UserBlacklistAnntena1689325027964'

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class FixRenoteMuting1690417561185 {
name = 'FixRenoteMuting1690417561185'

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class ChangeCacheRemoteFilesDefault1690417561186 {
name = 'ChangeCacheRemoteFilesDefault1690417561186'

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class Fix1690417561187 {
name = 'Fix1690417561187'

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class User2faBackupCodes1690569881926 {
name = 'User2faBackupCodes1690569881926'

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class RefineAnnouncement1691649257651 {
name = 'RefineAnnouncement1691649257651'

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class RefineAnnouncement21691657412740 {
name = 'RefineAnnouncement21691657412740'

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class VerifiedLinks1695260774117 {
name = 'VerifiedLinks1695260774117'

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class FollowingNotify1695288787870 {
name = 'FollowingNotify1695288787870'

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class ShortName1695440131671 {
name = 'ShortName1695440131671'

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class MutingNotificationTypes1695605508898 {
name = 'MutingNotificationTypes1695605508898'

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class NoteUpdatedAt1695901659683 {
name = 'NoteUpdatedAt1695901659683'

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class UserListMembership1696323464251 {
name = 'UserListMembership1696323464251'

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class Hibernation1696331570827 {
name = 'Hibernation1696331570827'

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class Clean1696332072038 {
name = 'Clean1696332072038'

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class HardMute1700383825690 {
name = 'HardMute1700383825690'

View File

@ -19,7 +19,7 @@
"watch": "node watch.mjs",
"restart": "pnpm build && pnpm start",
"dev": "nodemon -w src -e ts,js,mjs,cjs,json --exec \"cross-env NODE_ENV=development pnpm run restart\"",
"typecheck": "tsc --noEmit",
"typecheck": "tsc --noEmit && tsc -p test --noEmit",
"eslint": "eslint --quiet \"src/**/*.ts\"",
"lint": "pnpm typecheck && pnpm eslint",
"jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.unit.cjs",

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import Redis from 'ioredis';
import { DI } from '@/di-symbols.js';

View File

@ -51,21 +51,35 @@ export class FetchInstanceMetadataService {
}
@bindThis
public async tryLock(host: string): Promise<boolean> {
const mutex = await this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '1', 'GET');
return mutex !== '1';
// public for test
public async tryLock(host: string): Promise<string | null> {
// TODO: マイグレーションなのであとで消す (2024.3.1)
this.redisClient.del(`fetchInstanceMetadata:mutex:${host}`);
return await this.redisClient.set(
`fetchInstanceMetadata:mutex:v2:${host}`, '1',
'EX', 30, // 30秒したら自動でロック解除 https://github.com/misskey-dev/misskey/issues/13506#issuecomment-1975375395
'GET' // 古い値を返すなかったらnull
);
}
@bindThis
public unlock(host: string): Promise<'OK'> {
return this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '0');
// public for test
public unlock(host: string): Promise<number> {
return this.redisClient.del(`fetchInstanceMetadata:mutex:v2:${host}`);
}
@bindThis
public async fetchInstanceMetadata(instance: MiInstance, force = false): Promise<void> {
const host = instance.host;
// Acquire mutex to ensure no parallel runs
if (!await this.tryLock(host)) return;
// finallyでunlockされてしまうのでtry内でロックチェックをしない
// returnであってもfinallyは実行される
if (!force && await this.tryLock(host) === '1') {
// 1が返ってきていたらロックされているという意味なので、何もしない
return;
}
try {
if (!force) {
const _instance = await this.federatedInstanceService.fetch(host);

View File

@ -129,7 +129,7 @@ export class ApNoteService {
value,
object,
});
throw new Error('invalid note');
throw err;
}
const note = object as IPost;

View File

@ -248,7 +248,7 @@ export class DriveFileEntityService {
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
detail: true,
}) : null,
userId: opts.withUser ? file.userId : null,
userId: file.userId,
user: (opts.withUser && file.userId) ? this.userEntityService.pack(file.userId) : null,
});
}

View File

@ -333,6 +333,7 @@ export class NoteEntityService implements OnModuleInit {
visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined,
renoteCount: note.renoteCount,
repliesCount: note.repliesCount,
reactionCount: Object.values(note.reactions).reduce((a, b) => a + b, 0),
reactions: this.reactionService.convertLegacyReactions(note.reactions),
reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host),
reactionAndUserPairCache: opts.withReactionAndUserPairCache ? note.reactionAndUserPairCache : undefined,

View File

@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import _Ajv from 'ajv';
import { ModuleRef } from '@nestjs/core';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type { Packed } from '@/misc/json-schema.js';
@ -14,9 +15,30 @@ import type { Promiseable } from '@/misc/prelude/await-all.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js';
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/User.js';
import { MiNotification } from '@/models/Notification.js';
import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, UserNotePiningsRepository, UserProfilesRepository, AnnouncementReadsRepository, AnnouncementsRepository, MiUserProfile, RenoteMutingsRepository, UserMemoRepository } from '@/models/_.js';
import {
birthdaySchema,
descriptionSchema,
localUsernameSchema,
locationSchema,
nameSchema,
passwordSchema,
} from '@/models/User.js';
import type {
BlockingsRepository,
FollowingsRepository,
FollowRequestsRepository,
MiFollowing,
MiUserNotePining,
MiUserProfile,
MutingsRepository,
NoteUnreadsRepository,
RenoteMutingsRepository,
UserMemoRepository,
UserNotePiningsRepository,
UserProfilesRepository,
UserSecurityKeysRepository,
UsersRepository,
} from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
@ -46,11 +68,23 @@ function isRemoteUser(user: MiUser | { host: MiUser['host'] }): boolean {
return !isLocalUser(user);
}
export type UserRelation = {
id: MiUser['id']
following: MiFollowing | null,
isFollowing: boolean
isFollowed: boolean
hasPendingFollowRequestFromYou: boolean
hasPendingFollowRequestToYou: boolean
isBlocking: boolean
isBlocked: boolean
isMuted: boolean
isRenoteMuted: boolean
}
@Injectable()
export class UserEntityService implements OnModuleInit {
private apPersonService: ApPersonService;
private noteEntityService: NoteEntityService;
private driveFileEntityService: DriveFileEntityService;
private pageEntityService: PageEntityService;
private customEmojiService: CustomEmojiService;
private announcementService: AnnouncementService;
@ -89,9 +123,6 @@ export class UserEntityService implements OnModuleInit {
@Inject(DI.renoteMutingsRepository)
private renoteMutingsRepository: RenoteMutingsRepository,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
@Inject(DI.noteUnreadsRepository)
private noteUnreadsRepository: NoteUnreadsRepository,
@ -101,12 +132,6 @@ export class UserEntityService implements OnModuleInit {
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
@Inject(DI.announcementReadsRepository)
private announcementReadsRepository: AnnouncementReadsRepository,
@Inject(DI.announcementsRepository)
private announcementsRepository: AnnouncementsRepository,
@Inject(DI.userMemosRepository)
private userMemosRepository: UserMemoRepository,
) {
@ -115,7 +140,6 @@ export class UserEntityService implements OnModuleInit {
onModuleInit() {
this.apPersonService = this.moduleRef.get('ApPersonService');
this.noteEntityService = this.moduleRef.get('NoteEntityService');
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
this.pageEntityService = this.moduleRef.get('PageEntityService');
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
this.announcementService = this.moduleRef.get('AnnouncementService');
@ -138,7 +162,7 @@ export class UserEntityService implements OnModuleInit {
public isRemoteUser = isRemoteUser;
@bindThis
public async getRelation(me: MiUser['id'], target: MiUser['id']) {
public async getRelation(me: MiUser['id'], target: MiUser['id']): Promise<UserRelation> {
const [
following,
isFollowed,
@ -211,6 +235,59 @@ export class UserEntityService implements OnModuleInit {
};
}
@bindThis
public async getRelations(me: MiUser['id'], targets: MiUser['id'][]): Promise<Map<MiUser['id'], UserRelation>> {
const [
followers,
followees,
followersRequests,
followeesRequests,
blockers,
blockees,
muters,
renoteMuters,
] = await Promise.all([
this.followingsRepository.findBy({ followerId: me })
.then(f => new Map(f.map(it => [it.followeeId, it]))),
this.followingsRepository.findBy({ followeeId: me })
.then(it => it.map(it => it.followerId)),
this.followRequestsRepository.findBy({ followerId: me })
.then(it => it.map(it => it.followeeId)),
this.followRequestsRepository.findBy({ followeeId: me })
.then(it => it.map(it => it.followerId)),
this.blockingsRepository.findBy({ blockerId: me })
.then(it => it.map(it => it.blockeeId)),
this.blockingsRepository.findBy({ blockeeId: me })
.then(it => it.map(it => it.blockerId)),
this.mutingsRepository.findBy({ muterId: me })
.then(it => it.map(it => it.muteeId)),
this.renoteMutingsRepository.findBy({ muterId: me })
.then(it => it.map(it => it.muteeId)),
]);
return new Map(
targets.map(target => {
const following = followers.get(target) ?? null;
return [
target,
{
id: target,
following: following,
isFollowing: following != null,
isFollowed: followees.includes(target),
hasPendingFollowRequestFromYou: followersRequests.includes(target),
hasPendingFollowRequestToYou: followeesRequests.includes(target),
isBlocking: blockers.includes(target),
isBlocked: blockees.includes(target),
isMuted: muters.includes(target),
isRenoteMuted: renoteMuters.includes(target),
},
];
}),
);
}
@bindThis
public async getHasUnreadAntenna(userId: MiUser['id']): Promise<boolean> {
/*
@ -303,6 +380,9 @@ export class UserEntityService implements OnModuleInit {
schema?: S,
includeSecrets?: boolean,
userProfile?: MiUserProfile,
userRelations?: Map<MiUser['id'], UserRelation>,
userMemos?: Map<MiUser['id'], string | null>,
pinNotes?: Map<MiUser['id'], MiUserNotePining[]>,
},
): Promise<Packed<S>> {
const opts = Object.assign({
@ -317,13 +397,41 @@ export class UserEntityService implements OnModuleInit {
const isMe = meId === user.id;
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
const relation = meId && !isMe && isDetailed ? await this.getRelation(meId, user.id) : null;
const pins = isDetailed ? await this.userNotePiningsRepository.createQueryBuilder('pin')
.where('pin.userId = :userId', { userId: user.id })
.innerJoinAndSelect('pin.note', 'note')
.orderBy('pin.id', 'DESC')
.getMany() : [];
const profile = isDetailed ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null;
const profile = isDetailed
? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }))
: null;
let relation: UserRelation | null = null;
if (meId && !isMe && isDetailed) {
if (opts.userRelations) {
relation = opts.userRelations.get(user.id) ?? null;
} else {
relation = await this.getRelation(meId, user.id);
}
}
let memo: string | null = null;
if (isDetailed && meId) {
if (opts.userMemos) {
memo = opts.userMemos.get(user.id) ?? null;
} else {
memo = await this.userMemosRepository.findOneBy({ userId: meId, targetUserId: user.id })
.then(row => row?.memo ?? null);
}
}
let pins: MiUserNotePining[] = [];
if (isDetailed) {
if (opts.pinNotes) {
pins = opts.pinNotes.get(user.id) ?? [];
} else {
pins = await this.userNotePiningsRepository.createQueryBuilder('pin')
.where('pin.userId = :userId', { userId: user.id })
.innerJoinAndSelect('pin.note', 'note')
.orderBy('pin.id', 'DESC')
.getMany();
}
}
const followingCount = profile == null ? null :
(profile.followingVisibility === 'public') || isMe ? user.followingCount :
@ -416,9 +524,7 @@ export class UserEntityService implements OnModuleInit {
twoFactorEnabled: profile!.twoFactorEnabled,
usePasswordLessLogin: profile!.usePasswordLessLogin,
securityKeys: profile!.twoFactorEnabled
? this.userSecurityKeysRepository.countBy({
userId: user.id,
}).then(result => result >= 1)
? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
: false,
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
id: role.id,
@ -430,10 +536,7 @@ export class UserEntityService implements OnModuleInit {
isAdministrator: role.isAdministrator,
displayOrder: role.displayOrder,
}))),
memo: meId == null ? null : await this.userMemosRepository.findOneBy({
userId: meId,
targetUserId: user.id,
}).then(row => row?.memo ?? null),
memo: memo,
moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
} : {}),
@ -514,7 +617,7 @@ export class UserEntityService implements OnModuleInit {
return await awaitAll(packed);
}
public packMany<S extends 'MeDetailed' | 'UserDetailedNotMe' | 'UserDetailed' | 'UserLite' = 'UserLite'>(
public async packMany<S extends 'MeDetailed' | 'UserDetailedNotMe' | 'UserDetailed' | 'UserLite' = 'UserLite'>(
users: (MiUser['id'] | MiUser)[],
me?: { id: MiUser['id'] } | null | undefined,
options?: {
@ -522,6 +625,70 @@ export class UserEntityService implements OnModuleInit {
includeSecrets?: boolean,
},
): Promise<Packed<S>[]> {
return Promise.all(users.map(u => this.pack(u, me, options)));
// -- IDのみの要素を補完して完全なエンティティ一覧を作る
const _users = users.filter((user): user is MiUser => typeof user !== 'string');
if (_users.length !== users.length) {
_users.push(
...await this.usersRepository.findBy({
id: In(users.filter((user): user is string => typeof user === 'string')),
}),
);
}
const _userIds = _users.map(u => u.id);
// -- 特に前提条件のない値群を取得
const profilesMap = await this.userProfilesRepository.findBy({ userId: In(_userIds) })
.then(profiles => new Map(profiles.map(p => [p.userId, p])));
// -- 実行者の有無や指定スキーマの種別によって要否が異なる値群を取得
let userRelations: Map<MiUser['id'], UserRelation> = new Map();
let userMemos: Map<MiUser['id'], string | null> = new Map();
let pinNotes: Map<MiUser['id'], MiUserNotePining[]> = new Map();
if (options?.schema !== 'UserLite') {
const meId = me ? me.id : null;
if (meId) {
userMemos = await this.userMemosRepository.findBy({ userId: meId })
.then(memos => new Map(memos.map(memo => [memo.targetUserId, memo.memo])));
if (_userIds.length > 0) {
userRelations = await this.getRelations(meId, _userIds);
pinNotes = await this.userNotePiningsRepository.createQueryBuilder('pin')
.where('pin.userId IN (:...userIds)', { userIds: _userIds })
.innerJoinAndSelect('pin.note', 'note')
.getMany()
.then(pinsNotes => {
const map = new Map<MiUser['id'], MiUserNotePining[]>();
for (const note of pinsNotes) {
const notes = map.get(note.userId) ?? [];
notes.push(note);
map.set(note.userId, notes);
}
for (const [, notes] of map.entries()) {
// pack側ではDESCで取得しているので、それに合わせて降順に並び替えておく
notes.sort((a, b) => b.id.localeCompare(a.id));
}
return map;
});
}
}
}
return Promise.all(
_users.map(u => this.pack(
u,
me,
{
...options,
userProfile: profilesMap.get(u.id),
userRelations: userRelations,
userMemos: userMemos,
pinNotes: pinNotes,
},
)),
);
}
}

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { onRequestHookHandler } from 'fastify';
export const handleRequestRedirectToOmitSearch: onRequestHookHandler = (request, reply, done) => {

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { MiNote } from '@/models/Note.js';
export function isPureRenote(note: MiNote): note is MiNote & { renoteId: NonNullable<MiNote['renoteId']> } {

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export type FetchFunction<K, V> = (key: K) => Promise<V>;
type ResolveReject<V> = Parameters<ConstructorParameters<typeof Promise<V>>[0]>;

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { id } from './util/id.js';
import { MiUser } from './User.js';

View File

@ -223,6 +223,10 @@ export const packedNoteSchema = {
}],
},
},
reactionCount: {
type: 'number',
optional: false, nullable: false,
},
renoteCount: {
type: 'number',
optional: false, nullable: false,

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const packedSigninSchema = {
type: 'object',
properties: {

View File

@ -67,7 +67,7 @@ export const paramDef = {
withFile: { type: 'boolean' },
notify: { type: 'boolean' },
},
required: ['antennaId', 'name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'],
required: ['antennaId'],
} as const;
@Injectable()
@ -83,8 +83,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
throw new Error('either keywords or excludeKeywords is required.');
if (ps.keywords && ps.excludeKeywords) {
if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
throw new Error('either keywords or excludeKeywords is required.');
}
}
// Fetch the antenna
const antenna = await this.antennasRepository.findOneBy({
@ -98,7 +100,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
let userList;
if (ps.src === 'list' && ps.userListId) {
if ((ps.src === 'list' || antenna.src === 'list') && ps.userListId) {
userList = await this.userListsRepository.findOneBy({
id: ps.userListId,
userId: me.id,
@ -112,7 +114,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
await this.antennasRepository.update(antenna.id, {
name: ps.name,
src: ps.src,
userListId: userList ? userList.id : null,
userListId: ps.userListId !== undefined ? userList ? userList.id : null : undefined,
keywords: ps.keywords,
excludeKeywords: ps.excludeKeywords,
users: ps.users,

View File

@ -44,6 +44,7 @@ export const paramDef = {
permissions: { type: 'array', items: {
type: 'string',
} },
visibility: { type: 'string', enum: ['public', 'private'], default: 'public' },
},
required: ['title', 'summary', 'script', 'permissions'],
} as const;
@ -66,6 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
summary: ps.summary,
script: ps.script,
permissions: ps.permissions,
visibility: ps.visibility,
}).then(x => this.flashsRepository.findOneByOrFail(x.identifiers[0]));
return await this.flashEntityService.pack(flash);

View File

@ -132,11 +132,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private userEntityService: UserEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const ids = Array.isArray(ps.userId) ? ps.userId : [ps.userId];
const relations = await Promise.all(ids.map(id => this.userEntityService.getRelation(me.id, id)));
return Array.isArray(ps.userId) ? relations : relations[0];
return Array.isArray(ps.userId)
? await this.userEntityService.getRelations(me.id, ps.userId).then(it => [...it.values()])
: await this.userEntityService.getRelation(me.id, ps.userId).then(it => [it]);
});
}
}

View File

@ -86,8 +86,8 @@
//#endregion
//#region Script
function importAppScript() {
import(`/vite/${CLIENT_ENTRY}`)
async function importAppScript() {
await import(`/vite/${CLIENT_ENTRY}`)
.catch(async e => {
console.error(e);
renderError('APP_IMPORT', e);

View File

@ -187,7 +187,7 @@ describe('2要素認証', () => {
}, 1000 * 60 * 2);
test('が設定でき、OTPでログインできる。', async () => {
const registerResponse = await api('/i/2fa/register', {
const registerResponse = await api('i/2fa/register', {
password,
}, alice);
assert.strictEqual(registerResponse.status, 200);
@ -197,18 +197,18 @@ describe('2要素認証', () => {
assert.strictEqual(registerResponse.body.label, username);
assert.strictEqual(registerResponse.body.issuer, config.host);
const doneResponse = await api('/i/2fa/done', {
const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 200);
const usersShowResponse = await api('/users/show', {
const usersShowResponse = await api('users/show', {
username,
}, alice);
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true);
const signinResponse = await api('/signin', {
const signinResponse = await api('signin', {
...signinParam(),
token: otpToken(registerResponse.body.secret),
});
@ -216,24 +216,24 @@ describe('2要素認証', () => {
assert.notEqual(signinResponse.body.i, undefined);
// 後片付け
await api('/i/2fa/unregister', {
await api('i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、セキュリティキーでログインできる。', async () => {
const registerResponse = await api('/i/2fa/register', {
const registerResponse = await api('i/2fa/register', {
password,
}, alice);
assert.strictEqual(registerResponse.status, 200);
const doneResponse = await api('/i/2fa/done', {
const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 200);
const registerKeyResponse = await api('/i/2fa/register-key', {
const registerKeyResponse = await api('i/2fa/register-key', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
@ -243,23 +243,23 @@ describe('2要素認証', () => {
const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret),
keyName,
credentialId,
creationOptions: registerKeyResponse.body,
}), alice);
}) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200);
assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url'));
assert.strictEqual(keyDoneResponse.body.name, keyName);
const usersShowResponse = await api('/users/show', {
const usersShowResponse = await api('users/show', {
username,
});
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.securityKeys, true);
const signinResponse = await api('/signin', {
const signinResponse = await api('signin', {
...signinParam(),
});
assert.strictEqual(signinResponse.status, 200);
@ -268,7 +268,7 @@ describe('2要素認証', () => {
assert.notEqual(signinResponse.body.allowCredentials, undefined);
assert.strictEqual(signinResponse.body.allowCredentials[0].id, credentialId.toString('base64url'));
const signinResponse2 = await api('/signin', signinWithSecurityKeyParam({
const signinResponse2 = await api('signin', signinWithSecurityKeyParam({
keyName,
credentialId,
requestOptions: signinResponse.body,
@ -277,24 +277,24 @@ describe('2要素認証', () => {
assert.notEqual(signinResponse2.body.i, undefined);
// 後片付け
await api('/i/2fa/unregister', {
await api('i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、セキュリティキーでパスワードレスログインできる。', async () => {
const registerResponse = await api('/i/2fa/register', {
const registerResponse = await api('i/2fa/register', {
password,
}, alice);
assert.strictEqual(registerResponse.status, 200);
const doneResponse = await api('/i/2fa/done', {
const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 200);
const registerKeyResponse = await api('/i/2fa/register-key', {
const registerKeyResponse = await api('i/2fa/register-key', {
token: otpToken(registerResponse.body.secret),
password,
}, alice);
@ -302,33 +302,33 @@ describe('2要素認証', () => {
const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret),
keyName,
credentialId,
creationOptions: registerKeyResponse.body,
}), alice);
}) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200);
const passwordLessResponse = await api('/i/2fa/password-less', {
const passwordLessResponse = await api('i/2fa/password-less', {
value: true,
}, alice);
assert.strictEqual(passwordLessResponse.status, 204);
const usersShowResponse = await api('/users/show', {
const usersShowResponse = await api('users/show', {
username,
});
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.usePasswordLessLogin, true);
const signinResponse = await api('/signin', {
const signinResponse = await api('signin', {
...signinParam(),
password: '',
});
assert.strictEqual(signinResponse.status, 200);
assert.strictEqual(signinResponse.body.i, undefined);
const signinResponse2 = await api('/signin', {
const signinResponse2 = await api('signin', {
...signinWithSecurityKeyParam({
keyName,
credentialId,
@ -340,24 +340,24 @@ describe('2要素認証', () => {
assert.notEqual(signinResponse2.body.i, undefined);
// 後片付け
await api('/i/2fa/unregister', {
await api('i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、設定したセキュリティキーの名前を変更できる。', async () => {
const registerResponse = await api('/i/2fa/register', {
const registerResponse = await api('i/2fa/register', {
password,
}, alice);
assert.strictEqual(registerResponse.status, 200);
const doneResponse = await api('/i/2fa/done', {
const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 200);
const registerKeyResponse = await api('/i/2fa/register-key', {
const registerKeyResponse = await api('i/2fa/register-key', {
token: otpToken(registerResponse.body.secret),
password,
}, alice);
@ -365,22 +365,22 @@ describe('2要素認証', () => {
const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret),
keyName,
credentialId,
creationOptions: registerKeyResponse.body,
}), alice);
}) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200);
const renamedKey = 'other-key';
const updateKeyResponse = await api('/i/2fa/update-key', {
const updateKeyResponse = await api('i/2fa/update-key', {
name: renamedKey,
credentialId: credentialId.toString('base64url'),
}, alice);
assert.strictEqual(updateKeyResponse.status, 200);
const iResponse = await api('/i', {
const iResponse = await api('i', {
}, alice);
assert.strictEqual(iResponse.status, 200);
const securityKeys = iResponse.body.securityKeysList.filter((s: { id: string; }) => s.id === credentialId.toString('base64url'));
@ -389,24 +389,24 @@ describe('2要素認証', () => {
assert.notEqual(securityKeys[0].lastUsed, undefined);
// 後片付け
await api('/i/2fa/unregister', {
await api('i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、設定したセキュリティキーを削除できる。', async () => {
const registerResponse = await api('/i/2fa/register', {
const registerResponse = await api('i/2fa/register', {
password,
}, alice);
assert.strictEqual(registerResponse.status, 200);
const doneResponse = await api('/i/2fa/done', {
const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 200);
const registerKeyResponse = await api('/i/2fa/register-key', {
const registerKeyResponse = await api('i/2fa/register-key', {
token: otpToken(registerResponse.body.secret),
password,
}, alice);
@ -414,20 +414,20 @@ describe('2要素認証', () => {
const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret),
keyName,
credentialId,
creationOptions: registerKeyResponse.body,
}), alice);
}) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200);
// テストの実行順によっては複数残ってるので全部消す
const iResponse = await api('/i', {
const iResponse = await api('i', {
}, alice);
assert.strictEqual(iResponse.status, 200);
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),
password,
credentialId: key.id,
@ -435,13 +435,13 @@ describe('2要素認証', () => {
assert.strictEqual(removeKeyResponse.status, 200);
}
const usersShowResponse = await api('/users/show', {
const usersShowResponse = await api('users/show', {
username,
});
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.securityKeys, false);
const signinResponse = await api('/signin', {
const signinResponse = await api('signin', {
...signinParam(),
token: otpToken(registerResponse.body.secret),
});
@ -449,43 +449,43 @@ describe('2要素認証', () => {
assert.notEqual(signinResponse.body.i, undefined);
// 後片付け
await api('/i/2fa/unregister', {
await api('i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、設定解除できる。(パスワードのみでログインできる。)', async () => {
const registerResponse = await api('/i/2fa/register', {
const registerResponse = await api('i/2fa/register', {
password,
}, alice);
assert.strictEqual(registerResponse.status, 200);
const doneResponse = await api('/i/2fa/done', {
const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 200);
const usersShowResponse = await api('/users/show', {
const usersShowResponse = await api('users/show', {
username,
});
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true);
const unregisterResponse = await api('/i/2fa/unregister', {
const unregisterResponse = await api('i/2fa/unregister', {
token: otpToken(registerResponse.body.secret),
password,
}, alice);
assert.strictEqual(unregisterResponse.status, 204);
const signinResponse = await api('/signin', {
const signinResponse = await api('signin', {
...signinParam(),
});
assert.strictEqual(signinResponse.status, 200);
assert.notEqual(signinResponse.body.i, undefined);
// 後片付け
await api('/i/2fa/unregister', {
await api('i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);

View File

@ -7,7 +7,6 @@ process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import type { Packed } from '@/misc/json-schema.js';
import {
api,
failedApiCall,
@ -29,10 +28,7 @@ describe('アンテナ', () => {
// エンティティとしてのアンテナを主眼においたテストを記述する
// (Antennaを返すエンドポイント、Antennaエンティティを書き換えるエンドポイント、Antennaからートを取得するエンドポイントをテストする)
// BUG misskey-jsとjson-schemaが一致していない。
// - srcのenumにgroupが残っている
// - userGroupIdが残っている, isActiveがない
type Antenna = misskey.entities.Antenna | Packed<'Antenna'>;
type Antenna = misskey.entities.Antenna;
type User = misskey.entities.SignupResponse;
type Note = misskey.entities.Note;
@ -80,7 +76,7 @@ describe('アンテナ', () => {
aliceList = await userList(alice, {});
bob = await signup({ username: 'bob' });
aliceList = await userList(alice, {});
bobFile = (await uploadFile(bob)).body;
bobFile = (await uploadFile(bob)).body!;
bobList = await userList(bob);
carol = await signup({ username: 'carol' });
await api('users/lists/push', { listId: aliceList.id, userId: bob.id }, alice);
@ -129,9 +125,9 @@ describe('アンテナ', () => {
beforeEach(async () => {
// テスト間で影響し合わないように毎回全部消す。
for (const user of [alice, bob]) {
const list = await api('/antennas/list', {}, user);
const list = await api('antennas/list', {}, user);
for (const antenna of list.body) {
await api('/antennas/delete', { antennaId: antenna.id }, user);
await api('antennas/delete', { antennaId: antenna.id }, user);
}
}
});
@ -141,11 +137,11 @@ describe('アンテナ', () => {
test('が作成できること、キーが過不足なく入っていること。', async () => {
const response = await successfulApiCall({
endpoint: 'antennas/create',
parameters: { ...defaultParam },
parameters: defaultParam,
user: alice,
});
assert.match(response.id, /[0-9a-z]{10}/);
const expected = {
const expected: Antenna = {
id: response.id,
caseSensitive: false,
createdAt: new Date(response.createdAt).toISOString(),
@ -161,7 +157,7 @@ describe('アンテナ', () => {
withFile: false,
withReplies: false,
localOnly: false,
} as Antenna;
};
assert.deepStrictEqual(response, expected);
});
@ -202,27 +198,27 @@ describe('アンテナ', () => {
});
const antennaParamPattern = [
{ parameters: (): object => ({ name: 'x'.repeat(100) }) },
{ parameters: (): object => ({ name: 'x' }) },
{ parameters: (): object => ({ src: 'home' }) },
{ parameters: (): object => ({ src: 'all' }) },
{ parameters: (): object => ({ src: 'users' }) },
{ parameters: (): object => ({ src: 'list' }) },
{ parameters: (): object => ({ userListId: null }) },
{ parameters: (): object => ({ src: 'list', userListId: aliceList.id }) },
{ parameters: (): object => ({ keywords: [['x']] }) },
{ parameters: (): object => ({ keywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) },
{ parameters: (): object => ({ excludeKeywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) },
{ parameters: (): object => ({ users: [alice.username] }) },
{ parameters: (): object => ({ users: [alice.username, bob.username, carol.username] }) },
{ parameters: (): object => ({ caseSensitive: false }) },
{ parameters: (): object => ({ caseSensitive: true }) },
{ parameters: (): object => ({ withReplies: false }) },
{ parameters: (): object => ({ withReplies: true }) },
{ parameters: (): object => ({ withFile: false }) },
{ parameters: (): object => ({ withFile: true }) },
{ parameters: (): object => ({ notify: false }) },
{ parameters: (): object => ({ notify: true }) },
{ parameters: () => ({ name: 'x'.repeat(100) }) },
{ parameters: () => ({ name: 'x' }) },
{ parameters: () => ({ src: 'home' as const }) },
{ parameters: () => ({ src: 'all' as const }) },
{ parameters: () => ({ src: 'users' as const }) },
{ parameters: () => ({ src: 'list' as const }) },
{ parameters: () => ({ userListId: null }) },
{ parameters: () => ({ src: 'list' as const, userListId: aliceList.id }) },
{ parameters: () => ({ keywords: [['x']] }) },
{ parameters: () => ({ keywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) },
{ parameters: () => ({ excludeKeywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) },
{ parameters: () => ({ users: [alice.username] }) },
{ parameters: () => ({ users: [alice.username, bob.username, carol.username] }) },
{ parameters: () => ({ caseSensitive: false }) },
{ parameters: () => ({ caseSensitive: true }) },
{ parameters: () => ({ withReplies: false }) },
{ parameters: () => ({ withReplies: true }) },
{ parameters: () => ({ withFile: false }) },
{ parameters: () => ({ withFile: true }) },
{ parameters: () => ({ notify: false }) },
{ parameters: () => ({ notify: true }) },
];
test.each(antennaParamPattern)('を作成できること($#)', async ({ parameters }) => {
const response = await successfulApiCall({
@ -335,7 +331,7 @@ describe('アンテナ', () => {
test.each([
{
label: '全体から',
parameters: (): object => ({ src: 'all' }),
parameters: () => ({ src: 'all' }),
posts: [
{ note: (): Promise<Note> => post(alice, { text: `${keyword}` }), included: true },
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}` }), included: true },
@ -346,7 +342,7 @@ describe('アンテナ', () => {
{
// BUG e4144a1 以降home指定は壊れている(allと同じ)
label: 'ホーム指定はallと同じ',
parameters: (): object => ({ src: 'home' }),
parameters: () => ({ src: 'home' }),
posts: [
{ note: (): Promise<Note> => post(alice, { text: `${keyword}` }), included: true },
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}` }), included: true },
@ -357,7 +353,7 @@ describe('アンテナ', () => {
{
// https://github.com/misskey-dev/misskey/issues/9025
label: 'ただし、フォロワー限定投稿とDM投稿を含まない。フォロワーであっても。',
parameters: (): object => ({}),
parameters: () => ({}),
posts: [
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'public' }), included: true },
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'home' }), included: true },
@ -367,56 +363,56 @@ describe('アンテナ', () => {
},
{
label: 'ブロックしているユーザーのノートは含む',
parameters: (): object => ({}),
parameters: () => ({}),
posts: [
{ note: (): Promise<Note> => post(userBlockedByAlice, { text: `${keyword}` }), included: true },
],
},
{
label: 'ブロックされているユーザーのノートは含まない',
parameters: (): object => ({}),
parameters: () => ({}),
posts: [
{ note: (): Promise<Note> => post(userBlockingAlice, { text: `${keyword}` }) },
],
},
{
label: 'ミュートしているユーザーのノートは含まない',
parameters: (): object => ({}),
parameters: () => ({}),
posts: [
{ note: (): Promise<Note> => post(userMutedByAlice, { text: `${keyword}` }) },
],
},
{
label: 'ミュートされているユーザーのノートは含む',
parameters: (): object => ({}),
parameters: () => ({}),
posts: [
{ note: (): Promise<Note> => post(userMutingAlice, { text: `${keyword}` }), included: true },
],
},
{
label: '「見つけやすくする」がOFFのユーザーのートも含まれる',
parameters: (): object => ({}),
parameters: () => ({}),
posts: [
{ note: (): Promise<Note> => post(userNotExplorable, { text: `${keyword}` }), included: true },
],
},
{
label: '鍵付きユーザーのノートも含まれる',
parameters: (): object => ({}),
parameters: () => ({}),
posts: [
{ note: (): Promise<Note> => post(userLocking, { text: `${keyword}` }), included: true },
],
},
{
label: 'サイレンスのノートも含まれる',
parameters: (): object => ({}),
parameters: () => ({}),
posts: [
{ note: (): Promise<Note> => post(userSilenced, { text: `${keyword}` }), included: true },
],
},
{
label: '削除ユーザーのノートも含まれる',
parameters: (): object => ({}),
parameters: () => ({}),
posts: [
{ note: (): Promise<Note> => post(userDeletedBySelf, { text: `${keyword}` }), included: true },
{ note: (): Promise<Note> => post(userDeletedByAdmin, { text: `${keyword}` }), included: true },
@ -424,7 +420,7 @@ describe('アンテナ', () => {
},
{
label: 'ユーザー指定で',
parameters: (): object => ({ src: 'users', users: [`@${bob.username}`, `@${carol.username}`] }),
parameters: () => ({ src: 'users', users: [`@${bob.username}`, `@${carol.username}`] }),
posts: [
{ note: (): Promise<Note> => post(alice, { text: `test ${keyword}` }) },
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
@ -433,7 +429,7 @@ describe('アンテナ', () => {
},
{
label: 'リスト指定で',
parameters: (): object => ({ src: 'list', userListId: aliceList.id }),
parameters: () => ({ src: 'list', userListId: aliceList.id }),
posts: [
{ note: (): Promise<Note> => post(alice, { text: `test ${keyword}` }) },
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
@ -442,14 +438,14 @@ describe('アンテナ', () => {
},
{
label: 'CWにもマッチする',
parameters: (): object => ({ keywords: [[keyword]] }),
parameters: () => ({ keywords: [[keyword]] }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: 'test', cw: `cw ${keyword}` }), included: true },
],
},
{
label: 'キーワード1つ',
parameters: (): object => ({ keywords: [[keyword]] }),
parameters: () => ({ keywords: [[keyword]] }),
posts: [
{ note: (): Promise<Note> => post(alice, { text: 'test' }) },
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
@ -458,7 +454,7 @@ describe('アンテナ', () => {
},
{
label: 'キーワード3つ(AND)',
parameters: (): object => ({ keywords: [['A', 'B', 'C']] }),
parameters: () => ({ keywords: [['A', 'B', 'C']] }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: 'test A' }) },
{ note: (): Promise<Note> => post(bob, { text: 'test A B' }) },
@ -469,7 +465,7 @@ describe('アンテナ', () => {
},
{
label: 'キーワード3つ(OR)',
parameters: (): object => ({ keywords: [['A'], ['B'], ['C']] }),
parameters: () => ({ keywords: [['A'], ['B'], ['C']] }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: 'test' }) },
{ note: (): Promise<Note> => post(bob, { text: 'test A' }), included: true },
@ -482,7 +478,7 @@ describe('アンテナ', () => {
},
{
label: '除外ワード3つ(AND)',
parameters: (): object => ({ excludeKeywords: [['A', 'B', 'C']] }),
parameters: () => ({ excludeKeywords: [['A', 'B', 'C']] }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword} A` }), included: true },
@ -495,7 +491,7 @@ describe('アンテナ', () => {
},
{
label: '除外ワード3つ(OR)',
parameters: (): object => ({ excludeKeywords: [['A'], ['B'], ['C']] }),
parameters: () => ({ excludeKeywords: [['A'], ['B'], ['C']] }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword} A` }) },
@ -508,7 +504,7 @@ describe('アンテナ', () => {
},
{
label: 'キーワード1つ(大文字小文字区別する)',
parameters: (): object => ({ keywords: [['KEYWORD']], caseSensitive: true }),
parameters: () => ({ keywords: [['KEYWORD']], caseSensitive: true }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: 'keyword' }) },
{ note: (): Promise<Note> => post(bob, { text: 'kEyWoRd' }) },
@ -517,7 +513,7 @@ describe('アンテナ', () => {
},
{
label: 'キーワード1つ(大文字小文字区別しない)',
parameters: (): object => ({ keywords: [['KEYWORD']], caseSensitive: false }),
parameters: () => ({ keywords: [['KEYWORD']], caseSensitive: false }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: 'keyword' }), included: true },
{ note: (): Promise<Note> => post(bob, { text: 'kEyWoRd' }), included: true },
@ -526,7 +522,7 @@ describe('アンテナ', () => {
},
{
label: '除外ワード1つ(大文字小文字区別する)',
parameters: (): object => ({ excludeKeywords: [['KEYWORD']], caseSensitive: true }),
parameters: () => ({ excludeKeywords: [['KEYWORD']], caseSensitive: true }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `${keyword} keyword` }), included: true },
@ -536,7 +532,7 @@ describe('アンテナ', () => {
},
{
label: '除外ワード1つ(大文字小文字区別しない)',
parameters: (): object => ({ excludeKeywords: [['KEYWORD']], caseSensitive: false }),
parameters: () => ({ excludeKeywords: [['KEYWORD']], caseSensitive: false }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `${keyword} keyword` }) },
@ -546,7 +542,7 @@ describe('アンテナ', () => {
},
{
label: '添付ファイルを問わない',
parameters: (): object => ({ withFile: false }),
parameters: () => ({ withFile: false }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, fileIds: [bobFile.id] }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
@ -554,7 +550,7 @@ describe('アンテナ', () => {
},
{
label: '添付ファイル付きのみ',
parameters: (): object => ({ withFile: true }),
parameters: () => ({ withFile: true }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, fileIds: [bobFile.id] }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }) },
@ -562,7 +558,7 @@ describe('アンテナ', () => {
},
{
label: 'リプライ以外',
parameters: (): object => ({ withReplies: false }),
parameters: () => ({ withReplies: false }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, replyId: alicePost.id }) },
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
@ -570,7 +566,7 @@ describe('アンテナ', () => {
},
{
label: 'リプライも含む',
parameters: (): object => ({ withReplies: true }),
parameters: () => ({ withReplies: true }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, replyId: alicePost.id }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
@ -633,7 +629,7 @@ describe('アンテナ', () => {
endpoint: 'antennas/notes',
parameters: { antennaId: antenna.id, ...paginationParam },
user: alice,
}) as any as Note[];
});
}, offsetBy, 'desc');
});

View File

@ -6,7 +6,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { api, post, signup } from '../utils.js';
import { UserToken, api, post, signup } from '../utils.js';
import type * as misskey from 'misskey-js';
describe('API visibility', () => {
@ -24,38 +24,38 @@ describe('API visibility', () => {
let target2: misskey.entities.SignupResponse;
/** public-post */
let pub: any;
let pub: misskey.entities.Note;
/** home-post */
let home: any;
let home: misskey.entities.Note;
/** followers-post */
let fol: any;
let fol: misskey.entities.Note;
/** specified-post */
let spe: any;
let spe: misskey.entities.Note;
/** public-reply to target's post */
let pubR: any;
let pubR: misskey.entities.Note;
/** home-reply to target's post */
let homeR: any;
let homeR: misskey.entities.Note;
/** followers-reply to target's post */
let folR: any;
let folR: misskey.entities.Note;
/** specified-reply to target's post */
let speR: any;
let speR: misskey.entities.Note;
/** public-mention to target */
let pubM: any;
let pubM: misskey.entities.Note;
/** home-mention to target */
let homeM: any;
let homeM: misskey.entities.Note;
/** followers-mention to target */
let folM: any;
let folM: misskey.entities.Note;
/** specified-mention to target */
let speM: any;
let speM: misskey.entities.Note;
/** reply target post */
let tgt: any;
let tgt: misskey.entities.Note;
//#endregion
const show = async (noteId: any, by: any) => {
return await api('/notes/show', {
const show = async (noteId: misskey.entities.Note['id'], by?: UserToken) => {
return await api('notes/show', {
noteId,
}, by);
};
@ -70,7 +70,7 @@ describe('API visibility', () => {
target2 = await signup({ username: 'target2' });
// follow alice <= follower
await api('/following/create', { userId: alice.id }, follower);
await api('following/create', { userId: alice.id }, follower);
// normal posts
pub = await post(alice, { text: 'x', visibility: 'public' });
@ -111,7 +111,7 @@ describe('API visibility', () => {
});
test('[show] public-postを未認証が見れる', async () => {
const res = await show(pub.id, null);
const res = await show(pub.id);
assert.strictEqual(res.body.text, 'x');
});
@ -132,7 +132,7 @@ describe('API visibility', () => {
});
test('[show] home-postを未認証が見れる', async () => {
const res = await show(home.id, null);
const res = await show(home.id);
assert.strictEqual(res.body.text, 'x');
});
@ -153,7 +153,7 @@ describe('API visibility', () => {
});
test('[show] followers-postを未認証が見れない', async () => {
const res = await show(fol.id, null);
const res = await show(fol.id);
assert.strictEqual(res.body.isHidden, true);
});
@ -179,7 +179,7 @@ describe('API visibility', () => {
});
test('[show] specified-postを未認証が見れない', async () => {
const res = await show(spe.id, null);
const res = await show(spe.id);
assert.strictEqual(res.body.isHidden, true);
});
//#endregion
@ -207,7 +207,7 @@ describe('API visibility', () => {
});
test('[show] public-replyを未認証が見れる', async () => {
const res = await show(pubR.id, null);
const res = await show(pubR.id);
assert.strictEqual(res.body.text, 'x');
});
@ -233,7 +233,7 @@ describe('API visibility', () => {
});
test('[show] home-replyを未認証が見れる', async () => {
const res = await show(homeR.id, null);
const res = await show(homeR.id);
assert.strictEqual(res.body.text, 'x');
});
@ -259,7 +259,7 @@ describe('API visibility', () => {
});
test('[show] followers-replyを未認証が見れない', async () => {
const res = await show(folR.id, null);
const res = await show(folR.id);
assert.strictEqual(res.body.isHidden, true);
});
@ -290,7 +290,7 @@ describe('API visibility', () => {
});
test('[show] specified-replyを未認証が見れない', async () => {
const res = await show(speR.id, null);
const res = await show(speR.id);
assert.strictEqual(res.body.isHidden, true);
});
//#endregion
@ -318,7 +318,7 @@ describe('API visibility', () => {
});
test('[show] public-mentionを未認証が見れる', async () => {
const res = await show(pubM.id, null);
const res = await show(pubM.id);
assert.strictEqual(res.body.text, '@target x');
});
@ -344,7 +344,7 @@ describe('API visibility', () => {
});
test('[show] home-mentionを未認証が見れる', async () => {
const res = await show(homeM.id, null);
const res = await show(homeM.id);
assert.strictEqual(res.body.text, '@target x');
});
@ -370,7 +370,7 @@ describe('API visibility', () => {
});
test('[show] followers-mentionを未認証が見れない', async () => {
const res = await show(folM.id, null);
const res = await show(folM.id);
assert.strictEqual(res.body.isHidden, true);
});
@ -401,28 +401,28 @@ describe('API visibility', () => {
});
test('[show] specified-mentionを未認証が見れない', async () => {
const res = await show(speM.id, null);
const res = await show(speM.id);
assert.strictEqual(res.body.isHidden, true);
});
//#endregion
//#region HTL
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);
const notes = res.body.filter((n: any) => n.id === pub.id);
assert.strictEqual(notes[0].text, 'x');
});
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);
const notes = res.body.filter((n: any) => n.id === pub.id);
assert.strictEqual(notes.length, 0);
});
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);
const notes = res.body.filter((n: any) => n.id === fol.id);
assert.strictEqual(notes[0].text, 'x');
@ -431,21 +431,21 @@ describe('API visibility', () => {
//#region RTL
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);
const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes[0].text, 'x');
});
test('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => {
const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, other);
const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, other);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes.length, 0);
});
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);
const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes[0].text, 'x');
@ -454,14 +454,14 @@ describe('API visibility', () => {
//#region MTL
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);
const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes[0].text, 'x');
});
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);
const notes = res.body.filter((n: any) => n.id === folM.id);
assert.strictEqual(notes[0].text, '@target x');

View File

@ -23,32 +23,32 @@ import type * as misskey from 'misskey-js';
describe('API', () => {
let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse;
let carol: misskey.entities.SignupResponse;
beforeAll(async () => {
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' });
}, 1000 * 60 * 2);
describe('General validation', () => {
test('wrong type', async () => {
const res = await api('/test', {
const res = await api('test', {
required: true,
// @ts-expect-error string must be string
string: 42,
});
assert.strictEqual(res.status, 400);
});
test('missing require param', async () => {
const res = await api('/test', {
// @ts-expect-error required is required
const res = await api('test', {
string: 'a',
});
assert.strictEqual(res.status, 400);
});
test('invalid misskey:id (empty string)', async () => {
const res = await api('/test', {
const res = await api('test', {
required: true,
id: '',
});
@ -56,7 +56,7 @@ describe('API', () => {
});
test('valid misskey:id', async () => {
const res = await api('/test', {
const res = await api('test', {
required: true,
id: '8wvhjghbxu',
});
@ -64,7 +64,7 @@ describe('API', () => {
});
test('default value', async () => {
const res = await api('/test', {
const res = await api('test', {
required: true,
string: 'a',
});
@ -73,7 +73,7 @@ describe('API', () => {
});
test('can set null even if it has default value', async () => {
const res = await api('/test', {
const res = await api('test', {
required: true,
nullableDefault: null,
});
@ -82,7 +82,7 @@ describe('API', () => {
});
test('cannot set undefined if it has default value', async () => {
const res = await api('/test', {
const res = await api('test', {
required: true,
nullableDefault: undefined,
});
@ -99,14 +99,14 @@ describe('API', () => {
// aliceは管理者、APIを使える
await successfulApiCall({
endpoint: '/admin/get-index-stats',
endpoint: 'admin/get-index-stats',
parameters: {},
user: alice,
});
// bobは一般ユーザーだからダメ
await failedApiCall({
endpoint: '/admin/get-index-stats',
endpoint: 'admin/get-index-stats',
parameters: {},
user: bob,
}, {
@ -117,7 +117,7 @@ describe('API', () => {
// publicアクセスももちろんダメ
await failedApiCall({
endpoint: '/admin/get-index-stats',
endpoint: 'admin/get-index-stats',
parameters: {},
user: undefined,
}, {
@ -128,7 +128,7 @@ describe('API', () => {
// ごまがしもダメ
await failedApiCall({
endpoint: '/admin/get-index-stats',
endpoint: 'admin/get-index-stats',
parameters: {},
user: { token: 'tsukawasete' },
}, {
@ -138,13 +138,13 @@ describe('API', () => {
});
await successfulApiCall({
endpoint: '/admin/get-index-stats',
endpoint: 'admin/get-index-stats',
parameters: {},
user: { token: application2 },
});
await failedApiCall({
endpoint: '/admin/get-index-stats',
endpoint: 'admin/get-index-stats',
parameters: {},
user: { token: application },
}, {
@ -154,7 +154,7 @@ describe('API', () => {
});
await failedApiCall({
endpoint: '/admin/get-index-stats',
endpoint: 'admin/get-index-stats',
parameters: {},
user: { token: application3 },
}, {
@ -164,7 +164,7 @@ describe('API', () => {
});
await failedApiCall({
endpoint: '/admin/get-index-stats',
endpoint: 'admin/get-index-stats',
parameters: {},
user: { token: application4 },
}, {
@ -177,7 +177,7 @@ describe('API', () => {
describe('Authentication header', () => {
test('一般リクエスト', async () => {
await successfulApiCall({
endpoint: '/admin/get-index-stats',
endpoint: 'admin/get-index-stats',
parameters: {},
user: {
token: alice.token,
@ -211,7 +211,7 @@ describe('API', () => {
describe('tokenエラー応答でWWW-Authenticate headerを送る', () => {
describe('invalid_token', () => {
test('一般リクエスト', async () => {
const result = await api('/admin/get-index-stats', {}, {
const result = await api('admin/get-index-stats', {}, {
token: 'syuilo',
bearer: true,
});
@ -246,7 +246,7 @@ describe('API', () => {
describe('tokenがないとrealmだけおくる', () => {
test('一般リクエスト', async () => {
const result = await api('/admin/get-index-stats', {});
const result = await api('admin/get-index-stats', {});
assert.strictEqual(result.status, 401);
assert.strictEqual(result.headers.get('WWW-Authenticate'), 'Bearer realm="Misskey"');
});
@ -259,7 +259,8 @@ describe('API', () => {
});
test('invalid_request', async () => {
const result = await api('/notes/create', { text: true }, {
// @ts-expect-error text must be string
const result = await api('notes/create', { text: true }, {
token: alice.token,
bearer: true,
});

View File

@ -22,7 +22,7 @@ describe('Block', () => {
}, 1000 * 60 * 2);
test('Block作成', async () => {
const res = await api('/blocking/create', {
const res = await api('blocking/create', {
userId: bob.id,
}, alice);
@ -30,7 +30,7 @@ describe('Block', () => {
});
test('ブロックされているユーザーをフォローできない', async () => {
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.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0');
@ -39,7 +39,7 @@ describe('Block', () => {
test('ブロックされているユーザーにリアクションできない', async () => {
const note = await post(alice, { text: 'hello' });
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.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec');
@ -48,7 +48,7 @@ describe('Block', () => {
test('ブロックされているユーザーに返信できない', async () => {
const note = await post(alice, { text: 'hello' });
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.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
@ -57,7 +57,7 @@ describe('Block', () => {
test('ブロックされているユーザーのートをRenoteできない', async () => {
const note = await post(alice, { text: 'hello' });
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.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
@ -72,12 +72,13 @@ describe('Block', () => {
const bobNote = await post(bob, { text: 'hi' });
const carolNote = await post(carol, { text: 'hi' });
const res = await api('/notes/local-timeline', {}, bob);
const res = await api('notes/local-timeline', {}, bob);
const body = res.body as misskey.entities.Note[];
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
assert.strictEqual(body.some(note => note.id === aliceNote.id), false);
assert.strictEqual(body.some(note => note.id === bobNote.id), true);
assert.strictEqual(body.some(note => note.id === carolNote.id), true);
});
});

View File

@ -6,47 +6,34 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { JTDDataType } from 'ajv/dist/jtd';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import type { Packed } from '@/misc/json-schema.js';
import { paramDef as CreateParamDef } from '@/server/api/endpoints/clips/create.js';
import { paramDef as UpdateParamDef } from '@/server/api/endpoints/clips/update.js';
import { paramDef as DeleteParamDef } from '@/server/api/endpoints/clips/delete.js';
import { paramDef as ShowParamDef } from '@/server/api/endpoints/clips/show.js';
import { paramDef as FavoriteParamDef } from '@/server/api/endpoints/clips/favorite.js';
import { paramDef as UnfavoriteParamDef } from '@/server/api/endpoints/clips/unfavorite.js';
import { paramDef as AddNoteParamDef } from '@/server/api/endpoints/clips/add-note.js';
import { paramDef as RemoveNoteParamDef } from '@/server/api/endpoints/clips/remove-note.js';
import { paramDef as NotesParamDef } from '@/server/api/endpoints/clips/notes.js';
import { api, ApiRequest, failedApiCall, hiddenNote, post, signup, successfulApiCall } from '../utils.js';
import type * as Misskey from 'misskey-js';
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
describe('クリップ', () => {
type User = Packed<'User'>;
type Note = Packed<'Note'>;
type Clip = Packed<'Clip'>;
let alice: User;
let bob: User;
let aliceNote: Note;
let aliceHomeNote: Note;
let aliceFollowersNote: Note;
let aliceSpecifiedNote: Note;
let bobNote: Note;
let bobHomeNote: Note;
let bobFollowersNote: Note;
let bobSpecifiedNote: Note;
let alice: Misskey.entities.SignupResponse;
let bob: Misskey.entities.SignupResponse;
let aliceNote: Misskey.entities.Note;
let aliceHomeNote: Misskey.entities.Note;
let aliceFollowersNote: Misskey.entities.Note;
let aliceSpecifiedNote: Misskey.entities.Note;
let bobNote: Misskey.entities.Note;
let bobHomeNote: Misskey.entities.Note;
let bobFollowersNote: Misskey.entities.Note;
let bobSpecifiedNote: Misskey.entities.Note;
const compareBy = <T extends { id: string }, >(selector: (s: T) => string = (s: T): string => s.id) => (a: T, b: T): number => {
return selector(a).localeCompare(selector(b));
};
type CreateParam = JTDDataType<typeof CreateParamDef>;
const defaultCreate = (): Partial<CreateParam> => ({
const defaultCreate = (): Pick<Misskey.entities.ClipsCreateRequest, 'name'> => ({
name: 'test',
});
const create = async (parameters: Partial<CreateParam> = {}, request: Partial<ApiRequest> = {}): Promise<Clip> => {
const clip = await successfulApiCall<Clip>({
endpoint: '/clips/create',
const create = async (parameters: Partial<Misskey.entities.ClipsCreateRequest> = {}, request: Partial<ApiRequest<'clips/create'>> = {}): Promise<Misskey.entities.Clip> => {
const clip = await successfulApiCall({
endpoint: 'clips/create',
parameters: {
...defaultCreate(),
...parameters,
@ -64,17 +51,16 @@ describe('クリップ', () => {
return clip;
};
const createMany = async (parameters: Partial<CreateParam>, count = 10, user = alice): Promise<Clip[]> => {
const createMany = async (parameters: Partial<Misskey.entities.ClipsCreateRequest>, count = 10, user = alice): Promise<Misskey.entities.Clip[]> => {
return await Promise.all([...Array(count)].map((_, i) => create({
name: `test${i}`,
...parameters,
}, { user })));
};
type UpdateParam = JTDDataType<typeof UpdateParamDef>;
const update = async (parameters: Partial<UpdateParam>, request: Partial<ApiRequest> = {}): Promise<Clip> => {
const clip = await successfulApiCall<Clip>({
endpoint: '/clips/update',
const update = async (parameters: Optional<Misskey.entities.ClipsUpdateRequest, 'name'>, request: Partial<ApiRequest<'clips/update'>> = {}): Promise<Misskey.entities.Clip> => {
const clip = await successfulApiCall({
endpoint: 'clips/update',
parameters: {
name: 'updated',
...parameters,
@ -92,41 +78,39 @@ describe('クリップ', () => {
return clip;
};
type DeleteParam = JTDDataType<typeof DeleteParamDef>;
const deleteClip = async (parameters: DeleteParam, request: Partial<ApiRequest> = {}): Promise<void> => {
return await successfulApiCall<void>({
endpoint: '/clips/delete',
const deleteClip = async (parameters: Misskey.entities.ClipsDeleteRequest, request: Partial<ApiRequest<'clips/delete'>> = {}): Promise<void> => {
return await successfulApiCall({
endpoint: 'clips/delete',
parameters,
user: alice,
...request,
}, {
status: 204,
});
}) as any as void;
};
type ShowParam = JTDDataType<typeof ShowParamDef>;
const show = async (parameters: ShowParam, request: Partial<ApiRequest> = {}): Promise<Clip> => {
return await successfulApiCall<Clip>({
endpoint: '/clips/show',
const show = async (parameters: Misskey.entities.ClipsShowRequest, request: Partial<ApiRequest<'clips/show'>> = {}): Promise<Misskey.entities.Clip> => {
return await successfulApiCall({
endpoint: 'clips/show',
parameters,
user: alice,
...request,
});
};
const list = async (request: Partial<ApiRequest>): Promise<Clip[]> => {
return successfulApiCall<Clip[]>({
endpoint: '/clips/list',
const list = async (request: Partial<ApiRequest<'clips/list'>>): Promise<Misskey.entities.Clip[]> => {
return successfulApiCall({
endpoint: 'clips/list',
parameters: {},
user: alice,
...request,
});
};
const usersClips = async (request: Partial<ApiRequest>): Promise<Clip[]> => {
return await successfulApiCall<Clip[]>({
endpoint: '/users/clips',
parameters: {},
const usersClips = async (parameters: Misskey.entities.UsersClipsRequest, request: Partial<ApiRequest<'users/clips'>> = {}): Promise<Misskey.entities.Clip[]> => {
return await successfulApiCall({
endpoint: 'users/clips',
parameters,
user: alice,
...request,
});
@ -136,23 +120,22 @@ describe('クリップ', () => {
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
// FIXME: misskey-jsのNoteはoutdatedなので直接変換できない
aliceNote = await post(alice, { text: 'test' }) as any;
aliceHomeNote = await post(alice, { text: 'home only', visibility: 'home' }) as any;
aliceFollowersNote = await post(alice, { text: 'followers only', visibility: 'followers' }) as any;
aliceSpecifiedNote = await post(alice, { text: 'specified only', visibility: 'specified' }) as any;
bobNote = await post(bob, { text: 'test' }) as any;
bobHomeNote = await post(bob, { text: 'home only', visibility: 'home' }) as any;
bobFollowersNote = await post(bob, { text: 'followers only', visibility: 'followers' }) as any;
bobSpecifiedNote = await post(bob, { text: 'specified only', visibility: 'specified' }) as any;
aliceNote = await post(alice, { text: 'test' });
aliceHomeNote = await post(alice, { text: 'home only', visibility: 'home' });
aliceFollowersNote = await post(alice, { text: 'followers only', visibility: 'followers' });
aliceSpecifiedNote = await post(alice, { text: 'specified only', visibility: 'specified' });
bobNote = await post(bob, { text: 'test' });
bobHomeNote = await post(bob, { text: 'home only', visibility: 'home' });
bobFollowersNote = await post(bob, { text: 'followers only', visibility: 'followers' });
bobSpecifiedNote = await post(bob, { text: 'specified only', visibility: 'specified' });
}, 1000 * 60 * 2);
afterEach(async () => {
// テスト間で影響し合わないように毎回全部消す。
for (const user of [alice, bob]) {
const list = await api('/clips/list', { limit: 11 }, user);
const list = await api('clips/list', { limit: 11 }, user);
for (const clip of list.body) {
await api('/clips/delete', { clipId: clip.id }, user);
await api('clips/delete', { clipId: clip.id }, user);
}
}
});
@ -177,7 +160,7 @@ describe('クリップ', () => {
}
await failedApiCall({
endpoint: '/clips/create',
endpoint: 'clips/create',
parameters: defaultCreate(),
user: alice,
}, {
@ -204,7 +187,8 @@ describe('クリップ', () => {
{ label: 'descriptionが最大長+1', parameters: { description: 'a'.repeat(2049) } },
];
test.each(createClipDenyPattern)('の作成は$labelならできない', async ({ parameters }) => failedApiCall({
endpoint: '/clips/create',
endpoint: 'clips/create',
// @ts-expect-error invalid params
parameters: {
...defaultCreate(),
...parameters,
@ -246,15 +230,15 @@ describe('クリップ', () => {
code: 'NO_SUCH_CLIP',
id: 'b4d92d70-b216-46fa-9a3f-a8c811699257',
} },
{ label: '他人のクリップ', user: (): User => bob, assertion: {
{ label: '他人のクリップ', user: () => bob, assertion: {
code: 'NO_SUCH_CLIP',
id: 'b4d92d70-b216-46fa-9a3f-a8c811699257',
} },
...createClipDenyPattern as any,
])('の更新は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({
endpoint: '/clips/update',
endpoint: 'clips/update',
parameters: {
clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id,
clipId: (await create({}, { user: (user ?? (() => alice))() })).id,
name: 'updated',
...parameters,
},
@ -279,14 +263,15 @@ describe('クリップ', () => {
code: 'NO_SUCH_CLIP',
id: '70ca08ba-6865-4630-b6fb-8494759aa754',
} },
{ label: '他人のクリップ', user: (): User => bob, assertion: {
{ label: '他人のクリップ', user: () => bob, assertion: {
code: 'NO_SUCH_CLIP',
id: '70ca08ba-6865-4630-b6fb-8494759aa754',
} },
])('の削除は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({
endpoint: '/clips/delete',
endpoint: 'clips/delete',
parameters: {
clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id,
// @ts-expect-error clipId must not be null
clipId: (await create({}, { user: (user ?? (() => alice))() })).id,
...parameters,
},
user: alice,
@ -306,7 +291,7 @@ describe('クリップ', () => {
test('のID指定取得は他人のPrivateなクリップは取得できない', async () => {
const clip = await create({ isPublic: false }, { user: bob } );
failedApiCall({
endpoint: '/clips/show',
endpoint: 'clips/show',
parameters: { clipId: clip.id },
user: alice,
}, {
@ -323,7 +308,8 @@ describe('クリップ', () => {
id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20',
} },
])('のID指定取得は$labelならできない', async ({ parameters, assetion }) => failedApiCall({
endpoint: '/clips/show',
endpoint: 'clips/show',
// @ts-expect-error clipId must not be undefined
parameters: {
...parameters,
},
@ -356,27 +342,23 @@ describe('クリップ', () => {
test('の一覧が取得できる(空)', async () => {
const res = await usersClips({
parameters: {
userId: alice.id,
},
userId: alice.id,
});
assert.deepStrictEqual(res, []);
});
test.each([
{ label: '' },
{ label: '他人アカウントから', user: (): User => bob },
{ label: '他人アカウントから', user: () => bob },
])('の一覧が$label取得できる', async () => {
const clips = await createMany({ isPublic: true });
const res = await usersClips({
parameters: {
userId: alice.id,
},
userId: alice.id,
});
// 返ってくる配列には順序保障がないのでidでソートして厳密比較
assert.deepStrictEqual(
res.sort(compareBy<Clip>(s => s.id)),
res.sort(compareBy<Misskey.entities.Clip>(s => s.id)),
clips.sort(compareBy(s => s.id)));
// 認証状態で見たときだけisFavoritedが入っている
@ -386,17 +368,16 @@ describe('クリップ', () => {
});
test.each([
{ label: '未認証', user: (): undefined => undefined },
{ label: '未認証', user: () => undefined },
{ label: '存在しないユーザーのもの', parameters: { userId: 'xxxxxxx' } },
])('の一覧は$labelでも取得できる', async ({ parameters, user }) => {
const clips = await createMany({ isPublic: true });
const res = await usersClips({
parameters: {
userId: alice.id,
limit: clips.length,
...parameters,
},
user: (user ?? ((): User => alice))(),
userId: alice.id,
limit: clips.length,
...parameters,
}, {
user: (user ?? (() => alice))(),
});
// 未認証で見たときはisFavoritedは入らない
@ -409,10 +390,8 @@ describe('クリップ', () => {
await create({ isPublic: false });
const aliceClip = await create({ isPublic: true });
const res = await usersClips({
parameters: {
userId: alice.id,
limit: 2,
},
userId: alice.id,
limit: 2,
});
assert.deepStrictEqual(res, [aliceClip]);
});
@ -421,17 +400,15 @@ describe('クリップ', () => {
const clips = await createMany({ isPublic: true }, 7);
clips.sort(compareBy(s => s.id));
const res = await usersClips({
parameters: {
userId: alice.id,
sinceId: clips[1].id,
untilId: clips[5].id,
limit: 4,
},
userId: alice.id,
sinceId: clips[1].id,
untilId: clips[5].id,
limit: 4,
});
// Promise.allで返ってくる配列には順序保障がないのでidでソートして厳密比較
assert.deepStrictEqual(
res.sort(compareBy<Clip>(s => s.id)),
res.sort(compareBy<Misskey.entities.Clip>(s => s.id)),
[clips[2], clips[3], clips[4]], // sinceIdとuntilId自体は結果に含まれない
clips[1].id + ' ... ' + clips[3].id + ' with ' + clips.map(s => s.id) + ' vs. ' + res.map(s => s.id));
});
@ -441,8 +418,9 @@ describe('クリップ', () => {
{ label: 'limitゼロ', parameters: { limit: 0 } },
{ label: 'limit最大+1', parameters: { limit: 101 } },
])('の一覧は$labelだと取得できない', async ({ parameters }) => failedApiCall({
endpoint: '/users/clips',
endpoint: 'users/clips',
parameters: {
// @ts-expect-error userId must not be undefined
userId: alice.id,
...parameters,
},
@ -454,15 +432,15 @@ describe('クリップ', () => {
}));
test.each([
{ label: '作成', endpoint: '/clips/create' },
{ label: '更新', endpoint: '/clips/update' },
{ label: '削除', endpoint: '/clips/delete' },
{ label: '取得', endpoint: '/clips/list' },
{ label: 'お気に入り設定', endpoint: '/clips/favorite' },
{ label: 'お気に入り解除', endpoint: '/clips/unfavorite' },
{ label: 'お気に入り取得', endpoint: '/clips/my-favorites' },
{ label: 'ノート追加', endpoint: '/clips/add-note' },
{ label: 'ノート削除', endpoint: '/clips/remove-note' },
{ label: '作成', endpoint: 'clips/create' as const },
{ label: '更新', endpoint: 'clips/update' as const },
{ label: '削除', endpoint: 'clips/delete' as const },
{ label: '取得', endpoint: 'clips/list' as const },
{ label: 'お気に入り設定', endpoint: 'clips/favorite' as const },
{ label: 'お気に入り解除', endpoint: 'clips/unfavorite' as const },
{ label: 'お気に入り取得', endpoint: 'clips/my-favorites' as const },
{ label: 'ノート追加', endpoint: 'clips/add-note' as const },
{ label: 'ノート削除', endpoint: 'clips/remove-note' as const },
])('の$labelは未認証ではできない', async ({ endpoint }) => await failedApiCall({
endpoint: endpoint,
parameters: {},
@ -474,35 +452,33 @@ describe('クリップ', () => {
}));
describe('のお気に入り', () => {
let aliceClip: Clip;
let aliceClip: Misskey.entities.Clip;
type FavoriteParam = JTDDataType<typeof FavoriteParamDef>;
const favorite = async (parameters: FavoriteParam, request: Partial<ApiRequest> = {}): Promise<void> => {
return successfulApiCall<void>({
endpoint: '/clips/favorite',
const favorite = async (parameters: Misskey.entities.ClipsFavoriteRequest, request: Partial<ApiRequest<'clips/favorite'>> = {}): Promise<void> => {
return successfulApiCall({
endpoint: 'clips/favorite',
parameters,
user: alice,
...request,
}, {
status: 204,
});
}) as any as void;
};
type UnfavoriteParam = JTDDataType<typeof UnfavoriteParamDef>;
const unfavorite = async (parameters: UnfavoriteParam, request: Partial<ApiRequest> = {}): Promise<void> => {
return successfulApiCall<void>({
endpoint: '/clips/unfavorite',
const unfavorite = async (parameters: Misskey.entities.ClipsUnfavoriteRequest, request: Partial<ApiRequest<'clips/unfavorite'>> = {}): Promise<void> => {
return successfulApiCall({
endpoint: 'clips/unfavorite',
parameters,
user: alice,
...request,
}, {
status: 204,
});
}) as any as void;
};
const myFavorites = async (request: Partial<ApiRequest> = {}): Promise<Clip[]> => {
return successfulApiCall<Clip[]>({
endpoint: '/clips/my-favorites',
const myFavorites = async (request: Partial<ApiRequest<'clips/my-favorites'>> = {}): Promise<Misskey.entities.Clip[]> => {
return successfulApiCall({
endpoint: 'clips/my-favorites',
parameters: {},
user: alice,
...request,
@ -568,7 +544,7 @@ describe('クリップ', () => {
test('は同じクリップに対して二回設定できない。', async () => {
await favorite({ clipId: aliceClip.id });
await failedApiCall({
endpoint: '/clips/favorite',
endpoint: 'clips/favorite',
parameters: {
clipId: aliceClip.id,
},
@ -586,14 +562,15 @@ describe('クリップ', () => {
code: 'NO_SUCH_CLIP',
id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5',
} },
{ label: '他人のクリップ', user: (): User => bob, assertion: {
{ label: '他人のクリップ', user: () => bob, assertion: {
code: 'NO_SUCH_CLIP',
id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5',
} },
])('の設定は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({
endpoint: '/clips/favorite',
endpoint: 'clips/favorite',
parameters: {
clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id,
// @ts-expect-error clipId must not be null
clipId: (await create({}, { user: (user ?? (() => alice))() })).id,
...parameters,
},
user: alice,
@ -619,7 +596,7 @@ describe('クリップ', () => {
code: 'NO_SUCH_CLIP',
id: '2603966e-b865-426c-94a7-af4a01241dc1',
} },
{ label: '他人のクリップ', user: (): User => bob, assertion: {
{ label: '他人のクリップ', user: () => bob, assertion: {
code: 'NOT_FAVORITED',
id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187',
} },
@ -628,9 +605,10 @@ describe('クリップ', () => {
id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187',
} },
])('の設定解除は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({
endpoint: '/clips/unfavorite',
endpoint: 'clips/unfavorite',
parameters: {
clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id,
// @ts-expect-error clipId must not be null
clipId: (await create({}, { user: (user ?? (() => alice))() })).id,
...parameters,
},
user: alice,
@ -655,41 +633,38 @@ describe('クリップ', () => {
});
describe('に紐づくノート', () => {
let aliceClip: Clip;
let aliceClip: Misskey.entities.Clip;
const sampleNotes = (): Note[] => [
const sampleNotes = (): Misskey.entities.Note[] => [
aliceNote, aliceHomeNote, aliceFollowersNote, aliceSpecifiedNote,
bobNote, bobHomeNote, bobFollowersNote, bobSpecifiedNote,
];
type AddNoteParam = JTDDataType<typeof AddNoteParamDef>;
const addNote = async (parameters: AddNoteParam, request: Partial<ApiRequest> = {}): Promise<void> => {
return successfulApiCall<void>({
endpoint: '/clips/add-note',
const addNote = async (parameters: Misskey.entities.ClipsAddNoteRequest, request: Partial<ApiRequest<'clips/add-note'>> = {}): Promise<void> => {
return successfulApiCall({
endpoint: 'clips/add-note',
parameters,
user: alice,
...request,
}, {
status: 204,
});
}) as any as void;
};
type RemoveNoteParam = JTDDataType<typeof RemoveNoteParamDef>;
const removeNote = async (parameters: RemoveNoteParam, request: Partial<ApiRequest> = {}): Promise<void> => {
return successfulApiCall<void>({
endpoint: '/clips/remove-note',
const removeNote = async (parameters: Misskey.entities.ClipsRemoveNoteRequest, request: Partial<ApiRequest<'clips/remove-note'>> = {}): Promise<void> => {
return successfulApiCall({
endpoint: 'clips/remove-note',
parameters,
user: alice,
...request,
}, {
status: 204,
});
}) as any as void;
};
type NotesParam = JTDDataType<typeof NotesParamDef>;
const notes = async (parameters: Partial<NotesParam>, request: Partial<ApiRequest> = {}): Promise<Note[]> => {
return successfulApiCall<Note[]>({
endpoint: '/clips/notes',
const notes = async (parameters: Misskey.entities.ClipsNotesRequest, request: Partial<ApiRequest<'clips/notes'>> = {}): Promise<Misskey.entities.Note[]> => {
return successfulApiCall({
endpoint: 'clips/notes',
parameters,
user: alice,
...request,
@ -715,7 +690,7 @@ describe('クリップ', () => {
test('として同じノートを二回紐づけることはできない', async () => {
await addNote({ clipId: aliceClip.id, noteId: aliceNote.id });
await failedApiCall({
endpoint: '/clips/add-note',
endpoint: 'clips/add-note',
parameters: {
clipId: aliceClip.id,
noteId: aliceNote.id,
@ -733,11 +708,11 @@ describe('クリップ', () => {
const noteLimit = DEFAULT_POLICIES.noteEachClipsLimit + 1;
const noteList = await Promise.all([...Array(noteLimit)].map((_, i) => post(alice, {
text: `test ${i}`,
}) as unknown)) as Note[];
}) as unknown)) as Misskey.entities.Note[];
await Promise.all(noteList.map(s => addNote({ clipId: aliceClip.id, noteId: s.id })));
await failedApiCall({
endpoint: '/clips/add-note',
endpoint: 'clips/add-note',
parameters: {
clipId: aliceClip.id,
noteId: aliceNote.id,
@ -751,7 +726,7 @@ describe('クリップ', () => {
});
test('は他人のクリップへ追加できない。', async () => await failedApiCall({
endpoint: '/clips/add-note',
endpoint: 'clips/add-note',
parameters: {
clipId: aliceClip.id,
noteId: aliceNote.id,
@ -774,18 +749,20 @@ describe('クリップ', () => {
code: 'NO_SUCH_NOTE',
id: 'fc8c0b49-c7a3-4664-a0a6-b418d386bb8b',
} },
{ label: '他人のクリップ', user: (): object => bob, assetion: {
{ label: '他人のクリップ', user: () => bob, assetion: {
code: 'NO_SUCH_CLIP',
id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf',
} },
])('の追加は$labelだとできない', async ({ parameters, user, assetion }) => failedApiCall({
endpoint: '/clips/add-note',
endpoint: 'clips/add-note',
parameters: {
// @ts-expect-error clipId must not be undefined
clipId: aliceClip.id,
// @ts-expect-error noteId must not be undefined
noteId: aliceNote.id,
...parameters,
},
user: (user ?? ((): User => alice))(),
user: (user ?? (() => alice))(),
}, {
status: 400,
code: 'INVALID_PARAM',
@ -810,18 +787,20 @@ describe('クリップ', () => {
code: 'NO_SUCH_NOTE',
id: 'aff017de-190e-434b-893e-33a9ff5049d8', // add-noteと異なる
} },
{ label: '他人のクリップ', user: (): object => bob, assetion: {
{ label: '他人のクリップ', user: () => bob, assetion: {
code: 'NO_SUCH_CLIP',
id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52', // add-noteと異なる
} },
])('の削除は$labelだとできない', async ({ parameters, user, assetion }) => failedApiCall({
endpoint: '/clips/remove-note',
endpoint: 'clips/remove-note',
parameters: {
// @ts-expect-error clipId must not be undefined
clipId: aliceClip.id,
// @ts-expect-error noteId must not be undefined
noteId: aliceNote.id,
...parameters,
},
user: (user ?? ((): User => alice))(),
user: (user ?? (() => alice))(),
}, {
status: 400,
code: 'INVALID_PARAM',
@ -925,21 +904,22 @@ describe('クリップ', () => {
code: 'NO_SUCH_CLIP',
id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00',
} },
{ label: '他人のPrivateなクリップから', user: (): object => bob, assertion: {
{ label: '他人のPrivateなクリップから', user: () => bob, assertion: {
code: 'NO_SUCH_CLIP',
id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00',
} },
{ label: '未認証でPrivateなクリップから', user: (): undefined => undefined, assertion: {
{ label: '未認証でPrivateなクリップから', user: () => undefined, assertion: {
code: 'NO_SUCH_CLIP',
id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00',
} },
])('は$labelだと取得できない', async ({ parameters, user, assertion }) => failedApiCall({
endpoint: '/clips/notes',
endpoint: 'clips/notes',
parameters: {
// @ts-expect-error clipId must not be undefined
clipId: aliceClip.id,
...parameters,
},
user: (user ?? ((): User => alice))(),
user: (user ?? (() => alice))(),
}, {
status: 400,
code: 'INVALID_PARAM',

View File

@ -6,22 +6,14 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { MiNote } from '@/models/Note.js';
import { api, initTestDb, makeStreamCatcher, post, signup, uploadFile } from '../utils.js';
import { api, makeStreamCatcher, post, signup, uploadFile } from '../utils.js';
import type * as misskey from 'misskey-js';
import type{ Repository } from 'typeorm'
import type { Packed } from '@/misc/json-schema.js';
describe('Drive', () => {
let Notes: Repository<MiNote>;
let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse;
beforeAll(async () => {
const connection = await initTestDb(true);
Notes = connection.getRepository(MiNote);
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
}, 1000 * 60 * 2);
@ -31,13 +23,13 @@ describe('Drive', () => {
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/Lenna.jpg';
const catcher = makeStreamCatcher(
alice,
'main',
(msg) => msg.type === 'urlUploadFinished' && msg.body.marker === marker,
(msg) => msg.body.file as Packed<'DriveFile'>,
(msg) => msg.body.file,
10 * 1000);
const res = await api('drive/files/upload-from-url', {
@ -51,7 +43,7 @@ describe('Drive', () => {
assert.strictEqual(res.status, 204);
assert.strictEqual(file.name, 'Lenna.jpg');
assert.strictEqual(file.type, 'image/jpeg');
})
});
test('ローカルからアップロードできる', async () => {
// APIレスポンスを直接使用するので utils.js uploadFile が通過することで成功とする
@ -59,27 +51,27 @@ describe('Drive', () => {
const res = await uploadFile(alice, { path: 'Lenna.jpg', name: 'テスト画像' });
assert.strictEqual(res.body?.name, 'テスト画像.jpg');
assert.strictEqual(res.body?.type, 'image/jpeg');
})
assert.strictEqual(res.body.type, 'image/jpeg');
});
test('添付ノート一覧を取得できる', async () => {
const ids = (await Promise.all([uploadFile(alice), uploadFile(alice), uploadFile(alice)])).map(elm => elm.body!.id)
const ids = (await Promise.all([uploadFile(alice), uploadFile(alice), uploadFile(alice)])).map(elm => elm.body!.id);
const note0 = await post(alice, { fileIds: [ids[0]] });
const note1 = await post(alice, { fileIds: [ids[0], ids[1]] });
const attached0 = await api('drive/files/attached-notes', { fileId: ids[0] }, alice);
assert.strictEqual(attached0.body.length, 2);
assert.strictEqual(attached0.body[0].id, note1.id)
assert.strictEqual(attached0.body[1].id, note0.id)
assert.strictEqual(attached0.body[0].id, note1.id);
assert.strictEqual(attached0.body[1].id, note0.id);
const attached1 = await api('drive/files/attached-notes', { fileId: ids[1] }, alice);
assert.strictEqual(attached1.body.length, 1);
assert.strictEqual(attached1.body[0].id, note1.id)
assert.strictEqual(attached1.body[0].id, note1.id);
const attached2 = await api('drive/files/attached-notes', { fileId: ids[2] }, alice);
assert.strictEqual(attached2.body.length, 0)
})
assert.strictEqual(attached2.body.length, 0);
});
test('添付ノート一覧は他の人から見えない', async () => {
const file = await uploadFile(alice);
@ -89,7 +81,5 @@ describe('Drive', () => {
const res = await api('drive/files/attached-notes', { fileId: file.body!.id }, bob);
assert.strictEqual(res.status, 400);
assert.strictEqual('error' in res.body, true);
})
});
});

View File

@ -79,6 +79,7 @@ describe('Endpoints', () => {
test('クエリをインジェクションできない', async () => {
const res = await api('signin', {
username: 'test1',
// @ts-expect-error password must be string
password: {
$gt: '',
},
@ -103,7 +104,7 @@ describe('Endpoints', () => {
const myLocation = '七森中';
const myBirthday = '2000-09-07';
const res = await api('/i/update', {
const res = await api('i/update', {
name: myName,
location: myLocation,
birthday: myBirthday,
@ -117,7 +118,7 @@ describe('Endpoints', () => {
});
test('名前を空白にできる', async () => {
const res = await api('/i/update', {
const res = await api('i/update', {
name: ' ',
}, alice);
assert.strictEqual(res.status, 200);
@ -125,11 +126,11 @@ describe('Endpoints', () => {
});
test('誕生日の設定を削除できる', async () => {
await api('/i/update', {
await api('i/update', {
birthday: '2000-09-07',
}, alice);
const res = await api('/i/update', {
const res = await api('i/update', {
birthday: null,
}, alice);
@ -139,7 +140,7 @@ describe('Endpoints', () => {
});
test('不正な誕生日の形式で怒られる', async () => {
const res = await api('/i/update', {
const res = await api('i/update', {
birthday: '2000/09/07',
}, alice);
assert.strictEqual(res.status, 400);
@ -148,7 +149,7 @@ describe('Endpoints', () => {
describe('users/show', () => {
test('ユーザーが取得できる', async () => {
const res = await api('/users/show', {
const res = await api('users/show', {
userId: alice.id,
}, alice);
@ -158,14 +159,14 @@ describe('Endpoints', () => {
});
test('ユーザーが存在しなかったら怒る', async () => {
const res = await api('/users/show', {
const res = await api('users/show', {
userId: '000000000000000000000000',
});
assert.strictEqual(res.status, 404);
});
test('間違ったIDで怒られる', async () => {
const res = await api('/users/show', {
const res = await api('users/show', {
userId: 'kyoppie',
});
assert.strictEqual(res.status, 404);
@ -178,7 +179,7 @@ describe('Endpoints', () => {
text: 'test',
});
const res = await api('/notes/show', {
const res = await api('notes/show', {
noteId: myPost.id,
}, alice);
@ -189,14 +190,14 @@ describe('Endpoints', () => {
});
test('投稿が存在しなかったら怒る', async () => {
const res = await api('/notes/show', {
const res = await api('notes/show', {
noteId: '000000000000000000000000',
});
assert.strictEqual(res.status, 400);
});
test('間違ったIDで怒られる', async () => {
const res = await api('/notes/show', {
const res = await api('notes/show', {
noteId: 'kyoppie',
});
assert.strictEqual(res.status, 400);
@ -207,14 +208,14 @@ describe('Endpoints', () => {
test('リアクションできる', async () => {
const bobPost = await post(bob, { text: 'hi' });
const res = await api('/notes/reactions/create', {
const res = await api('notes/reactions/create', {
noteId: bobPost.id,
reaction: '🚀',
}, alice);
assert.strictEqual(res.status, 204);
const resNote = await api('/notes/show', {
const resNote = await api('notes/show', {
noteId: bobPost.id,
}, alice);
@ -225,7 +226,7 @@ describe('Endpoints', () => {
test('自分の投稿にもリアクションできる', async () => {
const myPost = await post(alice, { text: 'hi' });
const res = await api('/notes/reactions/create', {
const res = await api('notes/reactions/create', {
noteId: myPost.id,
reaction: '🚀',
}, alice);
@ -236,19 +237,19 @@ describe('Endpoints', () => {
test('二重にリアクションすると上書きされる', async () => {
const bobPost = await post(bob, { text: 'hi' });
await api('/notes/reactions/create', {
await api('notes/reactions/create', {
noteId: bobPost.id,
reaction: '🥰',
}, alice);
const res = await api('/notes/reactions/create', {
const res = await api('notes/reactions/create', {
noteId: bobPost.id,
reaction: '🚀',
}, alice);
assert.strictEqual(res.status, 204);
const resNote = await api('/notes/show', {
const resNote = await api('notes/show', {
noteId: bobPost.id,
}, alice);
@ -257,7 +258,7 @@ describe('Endpoints', () => {
});
test('存在しない投稿にはリアクションできない', async () => {
const res = await api('/notes/reactions/create', {
const res = await api('notes/reactions/create', {
noteId: '000000000000000000000000',
reaction: '🚀',
}, alice);
@ -266,13 +267,14 @@ describe('Endpoints', () => {
});
test('空のパラメータで怒られる', async () => {
const res = await api('/notes/reactions/create', {}, alice);
// @ts-expect-error param must not be empty
const res = await api('notes/reactions/create', {}, alice);
assert.strictEqual(res.status, 400);
});
test('間違ったIDで怒られる', async () => {
const res = await api('/notes/reactions/create', {
const res = await api('notes/reactions/create', {
noteId: 'kyoppie',
reaction: '🚀',
}, alice);
@ -283,7 +285,7 @@ describe('Endpoints', () => {
describe('following/create', () => {
test('フォローできる', async () => {
const res = await api('/following/create', {
const res = await api('following/create', {
userId: alice.id,
}, bob);
@ -301,7 +303,7 @@ describe('Endpoints', () => {
});
test('既にフォローしている場合は怒る', async () => {
const res = await api('/following/create', {
const res = await api('following/create', {
userId: alice.id,
}, bob);
@ -309,7 +311,7 @@ describe('Endpoints', () => {
});
test('存在しないユーザーはフォローできない', async () => {
const res = await api('/following/create', {
const res = await api('following/create', {
userId: '000000000000000000000000',
}, alice);
@ -317,7 +319,7 @@ describe('Endpoints', () => {
});
test('自分自身はフォローできない', async () => {
const res = await api('/following/create', {
const res = await api('following/create', {
userId: alice.id,
}, alice);
@ -325,13 +327,14 @@ describe('Endpoints', () => {
});
test('空のパラメータで怒られる', async () => {
const res = await api('/following/create', {}, alice);
// @ts-expect-error params must not be empty
const res = await api('following/create', {}, alice);
assert.strictEqual(res.status, 400);
});
test('間違ったIDで怒られる', async () => {
const res = await api('/following/create', {
const res = await api('following/create', {
userId: 'foo',
}, alice);
@ -341,11 +344,11 @@ describe('Endpoints', () => {
describe('following/delete', () => {
test('フォロー解除できる', async () => {
await api('/following/create', {
await api('following/create', {
userId: alice.id,
}, bob);
const res = await api('/following/delete', {
const res = await api('following/delete', {
userId: alice.id,
}, bob);
@ -363,7 +366,7 @@ describe('Endpoints', () => {
});
test('フォローしていない場合は怒る', async () => {
const res = await api('/following/delete', {
const res = await api('following/delete', {
userId: alice.id,
}, bob);
@ -371,7 +374,7 @@ describe('Endpoints', () => {
});
test('存在しないユーザーはフォロー解除できない', async () => {
const res = await api('/following/delete', {
const res = await api('following/delete', {
userId: '000000000000000000000000',
}, alice);
@ -379,7 +382,7 @@ describe('Endpoints', () => {
});
test('自分自身はフォロー解除できない', async () => {
const res = await api('/following/delete', {
const res = await api('following/delete', {
userId: alice.id,
}, alice);
@ -387,13 +390,14 @@ describe('Endpoints', () => {
});
test('空のパラメータで怒られる', async () => {
const res = await api('/following/delete', {}, alice);
// @ts-expect-error params must not be empty
const res = await api('following/delete', {}, alice);
assert.strictEqual(res.status, 400);
});
test('間違ったIDで怒られる', async () => {
const res = await api('/following/delete', {
const res = await api('following/delete', {
userId: 'kyoppie',
}, alice);
@ -403,20 +407,20 @@ describe('Endpoints', () => {
describe('channels/search', () => {
test('空白検索で一覧を取得できる', async () => {
await api('/channels/create', {
await api('channels/create', {
name: 'aaa',
description: 'bbb',
}, bob);
await api('/channels/create', {
await api('channels/create', {
name: 'ccc1',
description: 'ddd1',
}, bob);
await api('/channels/create', {
await api('channels/create', {
name: 'ccc2',
description: 'ddd2',
}, bob);
const res = await api('/channels/search', {
const res = await api('channels/search', {
query: '',
}, bob);
@ -425,7 +429,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.length, 3);
});
test('名前のみの検索で名前を検索できる', async () => {
const res = await api('/channels/search', {
const res = await api('channels/search', {
query: 'aaa',
type: 'nameOnly',
}, bob);
@ -436,7 +440,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body[0].name, 'aaa');
});
test('名前のみの検索で名前を複数検索できる', async () => {
const res = await api('/channels/search', {
const res = await api('channels/search', {
query: 'ccc',
type: 'nameOnly',
}, bob);
@ -446,7 +450,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.length, 2);
});
test('名前のみの検索で説明は検索できない', async () => {
const res = await api('/channels/search', {
const res = await api('channels/search', {
query: 'bbb',
type: 'nameOnly',
}, bob);
@ -456,7 +460,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.length, 0);
});
test('名前と説明の検索で名前を検索できる', async () => {
const res = await api('/channels/search', {
const res = await api('channels/search', {
query: 'ccc1',
}, bob);
@ -466,7 +470,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body[0].name, 'ccc1');
});
test('名前と説明での検索で説明を検索できる', async () => {
const res = await api('/channels/search', {
const res = await api('channels/search', {
query: 'ddd1',
}, bob);
@ -476,7 +480,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body[0].name, 'ccc1');
});
test('名前と説明の検索で名前を複数検索できる', async () => {
const res = await api('/channels/search', {
const res = await api('channels/search', {
query: 'ccc',
}, bob);
@ -485,7 +489,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.length, 2);
});
test('名前と説明での検索で説明を複数検索できる', async () => {
const res = await api('/channels/search', {
const res = await api('channels/search', {
query: 'ddd',
}, bob);
@ -506,7 +510,7 @@ describe('Endpoints', () => {
await uploadFile(alice, {
blob: new Blob([new Uint8Array(1024)]),
});
const res = await api('/drive', {}, alice);
const res = await api('drive', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
expect(res.body).toHaveProperty('usage', 1792);
@ -519,7 +523,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'Lenna.jpg');
assert.strictEqual(res.body!.name, 'Lenna.jpg');
});
test('ファイルに名前を付けられる', async () => {
@ -527,7 +531,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'Belmond.jpg');
assert.strictEqual(res.body!.name, 'Belmond.jpg');
});
test('ファイルに名前を付けられるが、拡張子は正しいものになる', async () => {
@ -535,11 +539,12 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'Belmond.png.jpg');
assert.strictEqual(res.body!.name, 'Belmond.png.jpg');
});
test('ファイル無しで怒られる', async () => {
const res = await api('/drive/files/create', {}, alice);
// @ts-expect-error params must not be empty
const res = await api('drive/files/create', {}, alice);
assert.strictEqual(res.status, 400);
});
@ -549,14 +554,14 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'image.svg');
assert.strictEqual(res.body.type, 'image/svg+xml');
assert.strictEqual(res.body!.name, 'image.svg');
assert.strictEqual(res.body!.type, 'image/svg+xml');
});
for (const type of ['webp', 'avif']) {
const mediaType = `image/${type}`;
const getWebpublicType = async (user: any, fileId: string): Promise<string> => {
const getWebpublicType = async (user: misskey.entities.SignupResponse, fileId: string): Promise<string> => {
// drive/files/create does not expose webpublicType directly, so get it by posting it
const res = await post(user, {
text: mediaType,
@ -573,10 +578,10 @@ describe('Endpoints', () => {
const res = await uploadFile(alice, { path });
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.name, path);
assert.strictEqual(res.body.type, mediaType);
assert.strictEqual(res.body!.name, path);
assert.strictEqual(res.body!.type, mediaType);
const webpublicType = await getWebpublicType(alice, res.body.id);
const webpublicType = await getWebpublicType(alice, res.body!.id);
assert.strictEqual(webpublicType, 'image/webp');
});
@ -584,10 +589,10 @@ describe('Endpoints', () => {
const path = `without-alpha.${type}`;
const res = await uploadFile(alice, { path });
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.name, path);
assert.strictEqual(res.body.type, mediaType);
assert.strictEqual(res.body!.name, path);
assert.strictEqual(res.body!.type, mediaType);
const webpublicType = await getWebpublicType(alice, res.body.id);
const webpublicType = await getWebpublicType(alice, res.body!.id);
assert.strictEqual(webpublicType, 'image/webp');
});
}
@ -598,8 +603,8 @@ describe('Endpoints', () => {
const file = (await uploadFile(alice)).body;
const newName = 'いちごパスタ.png';
const res = await api('/drive/files/update', {
fileId: file.id,
const res = await api('drive/files/update', {
fileId: file!.id,
name: newName,
}, alice);
@ -611,8 +616,8 @@ describe('Endpoints', () => {
test('他人のファイルは更新できない', async () => {
const file = (await uploadFile(alice)).body;
const res = await api('/drive/files/update', {
fileId: file.id,
const res = await api('drive/files/update', {
fileId: file!.id,
name: 'いちごパスタ.png',
}, bob);
@ -621,12 +626,12 @@ describe('Endpoints', () => {
test('親フォルダを更新できる', async () => {
const file = (await uploadFile(alice)).body;
const folder = (await api('/drive/folders/create', {
const folder = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
const res = await api('/drive/files/update', {
fileId: file.id,
const res = await api('drive/files/update', {
fileId: file!.id,
folderId: folder.id,
}, alice);
@ -638,17 +643,17 @@ describe('Endpoints', () => {
test('親フォルダを無しにできる', async () => {
const file = (await uploadFile(alice)).body;
const folder = (await api('/drive/folders/create', {
const folder = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
await api('/drive/files/update', {
fileId: file.id,
await api('drive/files/update', {
fileId: file!.id,
folderId: folder.id,
}, alice);
const res = await api('/drive/files/update', {
fileId: file.id,
const res = await api('drive/files/update', {
fileId: file!.id,
folderId: null,
}, alice);
@ -659,12 +664,12 @@ describe('Endpoints', () => {
test('他人のフォルダには入れられない', async () => {
const file = (await uploadFile(alice)).body;
const folder = (await api('/drive/folders/create', {
const folder = (await api('drive/folders/create', {
name: 'test',
}, bob)).body;
const res = await api('/drive/files/update', {
fileId: file.id,
const res = await api('drive/files/update', {
fileId: file!.id,
folderId: folder.id,
}, alice);
@ -674,8 +679,8 @@ describe('Endpoints', () => {
test('存在しないフォルダで怒られる', async () => {
const file = (await uploadFile(alice)).body;
const res = await api('/drive/files/update', {
fileId: file.id,
const res = await api('drive/files/update', {
fileId: file!.id,
folderId: '000000000000000000000000',
}, alice);
@ -685,8 +690,8 @@ describe('Endpoints', () => {
test('不正なフォルダIDで怒られる', async () => {
const file = (await uploadFile(alice)).body;
const res = await api('/drive/files/update', {
fileId: file.id,
const res = await api('drive/files/update', {
fileId: file!.id,
folderId: 'foo',
}, alice);
@ -694,7 +699,7 @@ describe('Endpoints', () => {
});
test('ファイルが存在しなかったら怒る', async () => {
const res = await api('/drive/files/update', {
const res = await api('drive/files/update', {
fileId: '000000000000000000000000',
name: 'いちごパスタ.png',
}, alice);
@ -706,8 +711,8 @@ describe('Endpoints', () => {
const file = (await uploadFile(alice)).body;
const newName = '';
const res = await api('/drive/files/update', {
fileId: file.id,
const res = await api('drive/files/update', {
fileId: file!.id,
name: newName,
}, alice);
@ -715,7 +720,7 @@ describe('Endpoints', () => {
});
test('間違ったIDで怒られる', async () => {
const res = await api('/drive/files/update', {
const res = await api('drive/files/update', {
fileId: 'kyoppie',
name: 'いちごパスタ.png',
}, alice);
@ -726,7 +731,7 @@ describe('Endpoints', () => {
describe('drive/folders/create', () => {
test('フォルダを作成できる', async () => {
const res = await api('/drive/folders/create', {
const res = await api('drive/folders/create', {
name: 'test',
}, alice);
@ -738,11 +743,11 @@ describe('Endpoints', () => {
describe('drive/folders/update', () => {
test('名前を更新できる', async () => {
const folder = (await api('/drive/folders/create', {
const folder = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
const res = await api('/drive/folders/update', {
const res = await api('drive/folders/update', {
folderId: folder.id,
name: 'new name',
}, alice);
@ -753,11 +758,11 @@ describe('Endpoints', () => {
});
test('他人のフォルダを更新できない', async () => {
const folder = (await api('/drive/folders/create', {
const folder = (await api('drive/folders/create', {
name: 'test',
}, bob)).body;
const res = await api('/drive/folders/update', {
const res = await api('drive/folders/update', {
folderId: folder.id,
name: 'new name',
}, alice);
@ -766,14 +771,14 @@ describe('Endpoints', () => {
});
test('親フォルダを更新できる', async () => {
const folder = (await api('/drive/folders/create', {
const folder = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
const parentFolder = (await api('/drive/folders/create', {
const parentFolder = (await api('drive/folders/create', {
name: 'parent',
}, alice)).body;
const res = await api('/drive/folders/update', {
const res = await api('drive/folders/update', {
folderId: folder.id,
parentId: parentFolder.id,
}, alice);
@ -784,18 +789,18 @@ describe('Endpoints', () => {
});
test('親フォルダを無しに更新できる', async () => {
const folder = (await api('/drive/folders/create', {
const folder = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
const parentFolder = (await api('/drive/folders/create', {
const parentFolder = (await api('drive/folders/create', {
name: 'parent',
}, alice)).body;
await api('/drive/folders/update', {
await api('drive/folders/update', {
folderId: folder.id,
parentId: parentFolder.id,
}, alice);
const res = await api('/drive/folders/update', {
const res = await api('drive/folders/update', {
folderId: folder.id,
parentId: null,
}, alice);
@ -806,14 +811,14 @@ describe('Endpoints', () => {
});
test('他人のフォルダを親フォルダに設定できない', async () => {
const folder = (await api('/drive/folders/create', {
const folder = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
const parentFolder = (await api('/drive/folders/create', {
const parentFolder = (await api('drive/folders/create', {
name: 'parent',
}, bob)).body;
const res = await api('/drive/folders/update', {
const res = await api('drive/folders/update', {
folderId: folder.id,
parentId: parentFolder.id,
}, alice);
@ -822,18 +827,18 @@ describe('Endpoints', () => {
});
test('フォルダが循環するような構造にできない', async () => {
const folder = (await api('/drive/folders/create', {
const folder = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
const parentFolder = (await api('/drive/folders/create', {
const parentFolder = (await api('drive/folders/create', {
name: 'parent',
}, alice)).body;
await api('/drive/folders/update', {
await api('drive/folders/update', {
folderId: parentFolder.id,
parentId: folder.id,
}, alice);
const res = await api('/drive/folders/update', {
const res = await api('drive/folders/update', {
folderId: folder.id,
parentId: parentFolder.id,
}, alice);
@ -842,25 +847,25 @@ describe('Endpoints', () => {
});
test('フォルダが循環するような構造にできない(再帰的)', async () => {
const folderA = (await api('/drive/folders/create', {
const folderA = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
const folderB = (await api('/drive/folders/create', {
const folderB = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
const folderC = (await api('/drive/folders/create', {
const folderC = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
await api('/drive/folders/update', {
await api('drive/folders/update', {
folderId: folderB.id,
parentId: folderA.id,
}, alice);
await api('/drive/folders/update', {
await api('drive/folders/update', {
folderId: folderC.id,
parentId: folderB.id,
}, alice);
const res = await api('/drive/folders/update', {
const res = await api('drive/folders/update', {
folderId: folderA.id,
parentId: folderC.id,
}, alice);
@ -869,11 +874,11 @@ describe('Endpoints', () => {
});
test('フォルダが循環するような構造にできない(自身)', async () => {
const folderA = (await api('/drive/folders/create', {
const folderA = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
const res = await api('/drive/folders/update', {
const res = await api('drive/folders/update', {
folderId: folderA.id,
parentId: folderA.id,
}, alice);
@ -882,11 +887,11 @@ describe('Endpoints', () => {
});
test('存在しない親フォルダを設定できない', async () => {
const folder = (await api('/drive/folders/create', {
const folder = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
const res = await api('/drive/folders/update', {
const res = await api('drive/folders/update', {
folderId: folder.id,
parentId: '000000000000000000000000',
}, alice);
@ -895,11 +900,11 @@ describe('Endpoints', () => {
});
test('不正な親フォルダIDで怒られる', async () => {
const folder = (await api('/drive/folders/create', {
const folder = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
const res = await api('/drive/folders/update', {
const res = await api('drive/folders/update', {
folderId: folder.id,
parentId: 'foo',
}, alice);
@ -908,7 +913,7 @@ describe('Endpoints', () => {
});
test('存在しないフォルダを更新できない', async () => {
const res = await api('/drive/folders/update', {
const res = await api('drive/folders/update', {
folderId: '000000000000000000000000',
}, alice);
@ -916,7 +921,7 @@ describe('Endpoints', () => {
});
test('不正なフォルダIDで怒られる', async () => {
const res = await api('/drive/folders/update', {
const res = await api('drive/folders/update', {
folderId: 'foo',
}, alice);
@ -937,7 +942,7 @@ describe('Endpoints', () => {
visibleUserIds: [alice.id],
});
const res = await api('/notes/replies', {
const res = await api('notes/replies', {
noteId: alicePost.id,
}, carol);
@ -949,7 +954,7 @@ describe('Endpoints', () => {
describe('notes/timeline', () => {
test('フォロワー限定投稿が含まれる', async () => {
await api('/following/create', {
await api('following/create', {
userId: carol.id,
}, dave);
@ -958,7 +963,7 @@ describe('Endpoints', () => {
visibility: 'followers',
});
const res = await api('/notes/timeline', {}, dave);
const res = await api('notes/timeline', {}, dave);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -979,12 +984,12 @@ describe('Endpoints', () => {
test('他者に関するメモを更新できる', async () => {
const memo = '10月まで低浮上とのこと。';
const res1 = await api('/users/update-memo', {
const res1 = await api('users/update-memo', {
memo,
userId: bob.id,
}, alice);
const res2 = await api('/users/show', {
const res2 = await api('users/show', {
userId: bob.id,
}, alice);
assert.strictEqual(res1.status, 204);
@ -994,12 +999,12 @@ describe('Endpoints', () => {
test('自分に関するメモを更新できる', async () => {
const memo = 'チケットを月末までに買う。';
const res1 = await api('/users/update-memo', {
const res1 = await api('users/update-memo', {
memo,
userId: alice.id,
}, alice);
const res2 = await api('/users/show', {
const res2 = await api('users/show', {
userId: alice.id,
}, alice);
assert.strictEqual(res1.status, 204);
@ -1009,17 +1014,17 @@ describe('Endpoints', () => {
test('メモを削除できる', async () => {
const memo = '10月まで低浮上とのこと。';
await api('/users/update-memo', {
await api('users/update-memo', {
memo,
userId: bob.id,
}, alice);
await api('/users/update-memo', {
await api('users/update-memo', {
memo: '',
userId: bob.id,
}, alice);
const res = await api('/users/show', {
const res = await api('users/show', {
userId: bob.id,
}, alice);
@ -1032,21 +1037,21 @@ describe('Endpoints', () => {
const memoCarolToBob = '例の件について今度問いただす。';
await Promise.all([
api('/users/update-memo', {
api('users/update-memo', {
memo: memoAliceToBob,
userId: bob.id,
}, alice),
api('/users/update-memo', {
api('users/update-memo', {
memo: memoCarolToBob,
userId: bob.id,
}, carol),
]);
const [resAlice, resCarol] = await Promise.all([
api('/users/show', {
api('users/show', {
userId: bob.id,
}, alice),
api('/users/show', {
api('users/show', {
userId: bob.id,
}, carol),
]);

View File

@ -18,7 +18,7 @@ describe('export-clips', () => {
// XXX: Any better way to get the result?
async function pollFirstDriveFile() {
while (true) {
const files = (await api('/drive/files', {}, alice)).body;
const files = (await api('drive/files', {}, alice)).body;
if (!files.length) {
await new Promise(r => setTimeout(r, 100));
continue;
@ -26,7 +26,7 @@ describe('export-clips', () => {
if (files.length > 1) {
throw new Error('Too many files?');
}
const file = (await api('/drive/files/show', { fileId: files[0].id }, alice)).body;
const file = (await api('drive/files/show', { fileId: files[0].id }, alice)).body;
const res = await fetch(new URL(new URL(file.url).pathname, `http://127.0.0.1:${port}`));
return await res.json();
}
@ -44,16 +44,16 @@ describe('export-clips', () => {
beforeEach(async () => {
// Clean all clips and files of alice
const clips = (await api('/clips/list', {}, alice)).body;
const clips = (await api('clips/list', {}, alice)).body;
for (const clip of clips) {
const res = await api('/clips/delete', { clipId: clip.id }, alice);
const res = await api('clips/delete', { clipId: clip.id }, alice);
if (res.status !== 204) {
throw new Error('Failed to delete clip');
}
}
const files = (await api('/drive/files', {}, alice)).body;
const files = (await api('drive/files', {}, alice)).body;
for (const file of files) {
const res = await api('/drive/files/delete', { fileId: file.id }, alice);
const res = await api('drive/files/delete', { fileId: file.id }, alice);
if (res.status !== 204) {
throw new Error('Failed to delete file');
}
@ -61,13 +61,13 @@ describe('export-clips', () => {
});
test('basic export', async () => {
let res = await api('/clips/create', {
let res = await api('clips/create', {
name: 'foo',
description: 'bar',
}, alice);
assert.strictEqual(res.status, 200);
res = await api('/i/export-clips', {}, alice);
res = await api('i/export-clips', {}, alice);
assert.strictEqual(res.status, 204);
const exported = await pollFirstDriveFile();
@ -77,7 +77,7 @@ describe('export-clips', () => {
});
test('export with notes', async () => {
let res = await api('/clips/create', {
let res = await api('clips/create', {
name: 'foo',
description: 'bar',
}, alice);
@ -96,14 +96,14 @@ describe('export-clips', () => {
});
for (const note of [note1, note2]) {
res = await api('/clips/add-note', {
res = await api('clips/add-note', {
clipId: clip.id,
noteId: note.id,
}, alice);
assert.strictEqual(res.status, 204);
}
res = await api('/i/export-clips', {}, alice);
res = await api('i/export-clips', {}, alice);
assert.strictEqual(res.status, 204);
const exported = await pollFirstDriveFile();
@ -116,14 +116,14 @@ describe('export-clips', () => {
});
test('multiple clips', async () => {
let res = await api('/clips/create', {
let res = await api('clips/create', {
name: 'kawaii',
description: 'kawaii',
}, alice);
assert.strictEqual(res.status, 200);
const clip1 = res.body;
res = await api('/clips/create', {
res = await api('clips/create', {
name: 'yuri',
description: 'yuri',
}, alice);
@ -138,19 +138,19 @@ describe('export-clips', () => {
text: 'baz2',
});
res = await api('/clips/add-note', {
res = await api('clips/add-note', {
clipId: clip1.id,
noteId: note1.id,
}, alice);
assert.strictEqual(res.status, 204);
res = await api('/clips/add-note', {
res = await api('clips/add-note', {
clipId: clip2.id,
noteId: note2.id,
}, alice);
assert.strictEqual(res.status, 204);
res = await api('/i/export-clips', {}, alice);
res = await api('i/export-clips', {}, alice);
assert.strictEqual(res.status, 204);
const exported = await pollFirstDriveFile();
@ -163,7 +163,7 @@ describe('export-clips', () => {
});
test('Clipping other user\'s note', async () => {
let res = await api('/clips/create', {
let res = await api('clips/create', {
name: 'kawaii',
description: 'kawaii',
}, alice);
@ -175,13 +175,13 @@ describe('export-clips', () => {
visibility: 'followers',
});
res = await api('/clips/add-note', {
res = await api('clips/add-note', {
clipId: clip.id,
noteId: note.id,
}, alice);
assert.strictEqual(res.status, 204);
res = await api('/i/export-clips', {}, alice);
res = await api('i/export-clips', {}, alice);
assert.strictEqual(res.status, 204);
const exported = await pollFirstDriveFile();

View File

@ -23,13 +23,13 @@ const JSON_UTF8 = 'application/json; charset=utf-8';
describe('Webリソース', () => {
let alice: misskey.entities.SignupResponse;
let aliceUploadedFile: any;
let alicesPost: any;
let alicePage: any;
let alicePlay: any;
let aliceClip: any;
let aliceGalleryPost: any;
let aliceChannel: any;
let aliceUploadedFile: misskey.entities.DriveFile | null;
let alicesPost: misskey.entities.Note;
let alicePage: misskey.entities.Page;
let alicePlay: misskey.entities.Flash;
let aliceClip: misskey.entities.Clip;
let aliceGalleryPost: misskey.entities.GalleryPost;
let aliceChannel: misskey.entities.Channel;
let bob: misskey.entities.SignupResponse;
@ -77,7 +77,7 @@ describe('Webリソース', () => {
beforeAll(async () => {
alice = await signup({ username: 'alice' });
aliceUploadedFile = await uploadFile(alice);
aliceUploadedFile = (await uploadFile(alice)).body;
alicesPost = await post(alice, {
text: 'test',
});
@ -85,7 +85,7 @@ describe('Webリソース', () => {
alicePlay = await play(alice, {});
aliceClip = await clip(alice, {});
aliceGalleryPost = await galleryPost(alice, {
fileIds: [aliceUploadedFile.body.id],
fileIds: [aliceUploadedFile!.id],
});
aliceChannel = await channel(alice, {});

View File

@ -19,15 +19,15 @@ describe('FF visibility', () => {
}, 1000 * 60 * 2);
test('followingVisibility, followersVisibility がともに public なユーザーのフォロー/フォロワーを誰でも見れる', async () => {
await api('/i/update', {
await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'public',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
@ -39,36 +39,36 @@ describe('FF visibility', () => {
test('followingVisibility が public であれば followersVisibility の設定に関わらずユーザーのフォローを誰でも見れる', async () => {
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'public',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'followers',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'private',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 200);
@ -78,36 +78,36 @@ describe('FF visibility', () => {
test('followersVisibility が public であれば followingVisibility の設定に関わらずユーザーのフォロワーを誰でも見れる', async () => {
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'public',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'public',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'public',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 200);
@ -116,15 +116,15 @@ describe('FF visibility', () => {
});
test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを自分で見れる', async () => {
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, alice);
@ -136,36 +136,36 @@ describe('FF visibility', () => {
test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => {
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'public',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'private',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(followingRes.status, 200);
@ -175,36 +175,36 @@ describe('FF visibility', () => {
test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => {
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'followers',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, alice);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, alice);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'followers',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, alice);
assert.strictEqual(followersRes.status, 200);
@ -213,15 +213,15 @@ describe('FF visibility', () => {
});
test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => {
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
@ -231,34 +231,34 @@ describe('FF visibility', () => {
test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず非フォロワーが見れない', async () => {
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'public',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 400);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 400);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'private',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 400);
@ -267,34 +267,34 @@ describe('FF visibility', () => {
test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず非フォロワーが見れない', async () => {
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'followers',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 400);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 400);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'followers',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 400);
@ -302,19 +302,19 @@ describe('FF visibility', () => {
});
test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => {
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
await api('/following/create', {
await api('following/create', {
userId: alice.id,
}, bob);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
@ -326,45 +326,45 @@ describe('FF visibility', () => {
test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらずフォロワーが見れる', async () => {
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'public',
}, alice);
await api('/following/create', {
await api('following/create', {
userId: alice.id,
}, bob);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
await api('/following/create', {
await api('following/create', {
userId: alice.id,
}, bob);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'private',
}, alice);
await api('/following/create', {
await api('following/create', {
userId: alice.id,
}, bob);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 200);
@ -374,45 +374,45 @@ describe('FF visibility', () => {
test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらずフォロワーが見れる', async () => {
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'followers',
}, alice);
await api('/following/create', {
await api('following/create', {
userId: alice.id,
}, bob);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
await api('/following/create', {
await api('following/create', {
userId: alice.id,
}, bob);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'followers',
}, alice);
await api('/following/create', {
await api('following/create', {
userId: alice.id,
}, bob);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 200);
@ -421,15 +421,15 @@ describe('FF visibility', () => {
});
test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを自分で見れる', async () => {
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'private',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, alice);
@ -441,36 +441,36 @@ describe('FF visibility', () => {
test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => {
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'public',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'followers',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'private',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(followingRes.status, 200);
@ -480,36 +480,36 @@ describe('FF visibility', () => {
test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => {
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'private',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, alice);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'private',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, alice);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'private',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, alice);
assert.strictEqual(followersRes.status, 200);
@ -518,15 +518,15 @@ describe('FF visibility', () => {
});
test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを他人が見れない', async () => {
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'private',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
@ -536,34 +536,34 @@ describe('FF visibility', () => {
test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず他人が見れない', async () => {
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'public',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 400);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'followers',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 400);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'private',
}, alice);
const followingRes = await api('/users/following', {
const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 400);
@ -572,34 +572,34 @@ describe('FF visibility', () => {
test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず他人が見れない', async () => {
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'private',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 400);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'private',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 400);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'private',
}, alice);
const followersRes = await api('/users/followers', {
const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 400);
@ -609,7 +609,7 @@ describe('FF visibility', () => {
describe('AP', () => {
test('followingVisibility が public 以外ならばAPからはフォローを取得できない', async () => {
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'public',
}, alice);
@ -617,7 +617,7 @@ describe('FF visibility', () => {
assert.strictEqual(followingRes.status, 200);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'followers',
}, alice);
@ -625,7 +625,7 @@ describe('FF visibility', () => {
assert.strictEqual(followingRes.status, 403);
}
{
await api('/i/update', {
await api('i/update', {
followingVisibility: 'private',
}, alice);
@ -636,7 +636,7 @@ describe('FF visibility', () => {
test('followersVisibility が public 以外ならばAPからはフォロワーを取得できない', async () => {
{
await api('/i/update', {
await api('i/update', {
followersVisibility: 'public',
}, alice);
@ -644,7 +644,7 @@ describe('FF visibility', () => {
assert.strictEqual(followersRes.status, 200);
}
{
await api('/i/update', {
await api('i/update', {
followersVisibility: 'followers',
}, alice);
@ -652,7 +652,7 @@ describe('FF visibility', () => {
assert.strictEqual(followersRes.status, 403);
}
{
await api('/i/update', {
await api('i/update', {
followersVisibility: 'private',
}, alice);

View File

@ -55,7 +55,7 @@ describe('Account Move', () => {
}, 1000 * 10);
test('Able to create an alias', async () => {
const res = await api('/i/update', {
const res = await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`],
}, bob);
@ -67,7 +67,7 @@ describe('Account Move', () => {
});
test('Able to create a local alias without hostname', async () => {
await api('/i/update', {
await api('i/update', {
alsoKnownAs: ['@alice'],
}, bob);
@ -77,7 +77,7 @@ describe('Account Move', () => {
});
test('Able to create a local alias without @', async () => {
await api('/i/update', {
await api('i/update', {
alsoKnownAs: ['alice'],
}, bob);
@ -87,7 +87,7 @@ describe('Account Move', () => {
});
test('Able to set remote user (but may fail)', async () => {
const res = await api('/i/update', {
const res = await api('i/update', {
alsoKnownAs: ['@syuilo@example.com'],
}, bob);
@ -97,7 +97,7 @@ describe('Account Move', () => {
});
test('Unable to add duplicated aliases to alsoKnownAs', async () => {
const res = await api('/i/update', {
const res = await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`, `@alice@${url.hostname}`],
}, bob);
@ -107,7 +107,7 @@ describe('Account Move', () => {
});
test('Unable to add itself', async () => {
const res = await api('/i/update', {
const res = await api('i/update', {
alsoKnownAs: [`@bob@${url.hostname}`],
}, bob);
@ -117,7 +117,7 @@ describe('Account Move', () => {
});
test('Unable to add a nonexisting local account to alsoKnownAs', async () => {
const res1 = await api('/i/update', {
const res1 = await api('i/update', {
alsoKnownAs: [`@nonexist@${url.hostname}`],
}, bob);
@ -125,7 +125,7 @@ describe('Account Move', () => {
assert.strictEqual(res1.body.error.code, 'NO_SUCH_USER');
assert.strictEqual(res1.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
const res2 = await api('/i/update', {
const res2 = await api('i/update', {
alsoKnownAs: ['@alice', 'nonexist'],
}, bob);
@ -135,7 +135,7 @@ describe('Account Move', () => {
});
test('Able to add two existing local account to alsoKnownAs', async () => {
await api('/i/update', {
await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`, `@carol@${url.hostname}`],
}, bob);
@ -146,10 +146,10 @@ describe('Account Move', () => {
});
test('Able to properly overwrite alsoKnownAs', async () => {
await api('/i/update', {
await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`],
}, bob);
await api('/i/update', {
await api('i/update', {
alsoKnownAs: [`@carol@${url.hostname}`, `@dave@${url.hostname}`],
}, bob);
@ -164,27 +164,27 @@ describe('Account Move', () => {
let antennaId = '';
beforeAll(async () => {
await api('/i/update', {
await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`],
}, root);
const listRoot = await api('/users/lists/create', {
const listRoot = await api('users/lists/create', {
name: secureRndstr(8),
}, root);
await api('/users/lists/push', {
await api('users/lists/push', {
listId: listRoot.body.id,
userId: alice.id,
}, root);
await api('/following/create', {
await api('following/create', {
userId: root.id,
}, alice);
await api('/following/create', {
await api('following/create', {
userId: eve.id,
}, alice);
const antenna = await api('/antennas/create', {
const antenna = await api('antennas/create', {
name: secureRndstr(8),
src: 'home',
keywords: [secureRndstr(8)],
keywords: [[secureRndstr(8)]],
excludeKeywords: [],
users: [],
caseSensitive: false,
@ -195,48 +195,48 @@ describe('Account Move', () => {
}, alice);
antennaId = antenna.body.id;
await api('/i/update', {
await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`],
}, bob);
await api('/following/create', {
await api('following/create', {
userId: alice.id,
}, carol);
await api('/mute/create', {
await api('mute/create', {
userId: alice.id,
}, dave);
await api('/blocking/create', {
await api('blocking/create', {
userId: alice.id,
}, dave);
await api('/following/create', {
await api('following/create', {
userId: eve.id,
}, dave);
await api('/following/create', {
await api('following/create', {
userId: dave.id,
}, eve);
const listEve = await api('/users/lists/create', {
const listEve = await api('users/lists/create', {
name: secureRndstr(8),
}, eve);
await api('/users/lists/push', {
await api('users/lists/push', {
listId: listEve.body.id,
userId: bob.id,
}, eve);
await api('/i/update', {
await api('i/update', {
isLocked: true,
}, frank);
await api('/following/create', {
await api('following/create', {
userId: frank.id,
}, alice);
await api('/following/requests/accept', {
await api('following/requests/accept', {
userId: alice.id,
}, frank);
}, 1000 * 10);
test('Prohibit the root account from moving', async () => {
const res = await api('/i/move', {
const res = await api('i/move', {
moveToAccount: `@bob@${url.hostname}`,
}, root);
@ -246,7 +246,7 @@ describe('Account Move', () => {
});
test('Unable to move to a nonexisting local account', async () => {
const res = await api('/i/move', {
const res = await api('i/move', {
moveToAccount: `@nonexist@${url.hostname}`,
}, alice);
@ -256,7 +256,7 @@ describe('Account Move', () => {
});
test('Unable to move if alsoKnownAs is invalid', async () => {
const res = await api('/i/move', {
const res = await api('i/move', {
moveToAccount: `@carol@${url.hostname}`,
}, alice);
@ -266,7 +266,7 @@ describe('Account Move', () => {
});
test('Relationships have been properly migrated', async () => {
const move = await api('/i/move', {
const move = await api('i/move', {
moveToAccount: `@bob@${url.hostname}`,
}, alice);
@ -275,13 +275,13 @@ describe('Account Move', () => {
await sleep(1000 * 3); // wait for jobs to finish
// Unfollow delayed?
const aliceFollowings = await api('/users/following', {
const aliceFollowings = await api('users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(aliceFollowings.status, 200);
assert.strictEqual(aliceFollowings.body.length, 3);
const carolFollowings = await api('/users/following', {
const carolFollowings = await api('users/following', {
userId: carol.id,
}, carol);
assert.strictEqual(carolFollowings.status, 200);
@ -289,25 +289,25 @@ describe('Account Move', () => {
assert.strictEqual(carolFollowings.body[0].followeeId, bob.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.body.length, 2);
assert.strictEqual(blockings.body[0].blockeeId, bob.id);
assert.strictEqual(blockings.body[1].blockeeId, alice.id);
const mutings = await api('/mute/list', {}, dave);
const mutings = await api('mute/list', {}, dave);
assert.strictEqual(mutings.status, 200);
assert.strictEqual(mutings.body.length, 2);
assert.strictEqual(mutings.body[0].muteeId, bob.id);
assert.strictEqual(mutings.body[1].muteeId, alice.id);
const rootLists = await api('/users/lists/list', {}, root);
const rootLists = await api('users/lists/list', {}, root);
assert.strictEqual(rootLists.status, 200);
assert.strictEqual(rootLists.body[0].userIds.length, 2);
assert.ok(rootLists.body[0].userIds.find((id: string) => id === bob.id));
assert.ok(rootLists.body[0].userIds.find((id: string) => id === alice.id));
const eveLists = await api('/users/lists/list', {}, eve);
const eveLists = await api('users/lists/list', {}, eve);
assert.strictEqual(eveLists.status, 200);
assert.strictEqual(eveLists.body[0].userIds.length, 1);
assert.ok(eveLists.body[0].userIds.find((id: string) => id === bob.id));
@ -315,13 +315,13 @@ describe('Account Move', () => {
test('A locked account automatically accept the follow request if it had already accepted the old account.', async () => {
await successfulApiCall({
endpoint: '/following/create',
endpoint: 'following/create',
parameters: {
userId: frank.id,
},
user: bob,
});
const followers = await api('/users/followers', {
const followers = await api('users/followers', {
userId: frank.id,
}, frank);
@ -333,7 +333,7 @@ describe('Account Move', () => {
test('Unfollowed after 10 sec (24 hours in production).', async () => {
await sleep(1000 * 8);
const following = await api('/users/following', {
const following = await api('users/following', {
userId: alice.id,
}, alice);
@ -342,7 +342,7 @@ describe('Account Move', () => {
});
test('Unable to move if the destination account has already moved.', async () => {
const res = await api('/i/move', {
const res = await api('i/move', {
moveToAccount: `@alice@${url.hostname}`,
}, bob);
@ -352,7 +352,7 @@ describe('Account Move', () => {
});
test('Follow and follower counts are properly adjusted', async () => {
await api('/following/create', {
await api('following/create', {
userId: alice.id,
}, eve);
const newAlice = await Users.findOneByOrFail({ id: alice.id });
@ -365,7 +365,7 @@ describe('Account Move', () => {
assert.strictEqual(newEve.followingCount, 1);
assert.strictEqual(newEve.followersCount, 1);
await api('/following/delete', {
await api('following/delete', {
userId: alice.id,
}, eve);
newEve = await Users.findOneByOrFail({ id: eve.id });
@ -374,49 +374,49 @@ describe('Account Move', () => {
});
test.each([
'/antennas/create',
'/channels/create',
'/channels/favorite',
'/channels/follow',
'/channels/unfavorite',
'/channels/unfollow',
'/clips/add-note',
'/clips/create',
'/clips/favorite',
'/clips/remove-note',
'/clips/unfavorite',
'/clips/update',
'/drive/files/upload-from-url',
'/flash/create',
'/flash/like',
'/flash/unlike',
'/flash/update',
'/following/create',
'/gallery/posts/create',
'/gallery/posts/like',
'/gallery/posts/unlike',
'/gallery/posts/update',
'/i/claim-achievement',
'/i/move',
'/i/import-blocking',
'/i/import-following',
'/i/import-muting',
'/i/import-user-lists',
'/i/pin',
'/mute/create',
'/notes/create',
'/notes/favorites/create',
'/notes/polls/vote',
'/notes/reactions/create',
'/pages/create',
'/pages/like',
'/pages/unlike',
'/pages/update',
'/renote-mute/create',
'/users/lists/create',
'/users/lists/pull',
'/users/lists/push',
])('Prohibit access after moving: %s', async (endpoint) => {
'antennas/create',
'channels/create',
'channels/favorite',
'channels/follow',
'channels/unfavorite',
'channels/unfollow',
'clips/add-note',
'clips/create',
'clips/favorite',
'clips/remove-note',
'clips/unfavorite',
'clips/update',
'drive/files/upload-from-url',
'flash/create',
'flash/like',
'flash/unlike',
'flash/update',
'following/create',
'gallery/posts/create',
'gallery/posts/like',
'gallery/posts/unlike',
'gallery/posts/update',
'i/claim-achievement',
'i/move',
'i/import-blocking',
'i/import-following',
'i/import-muting',
'i/import-user-lists',
'i/pin',
'mute/create',
'notes/create',
'notes/favorites/create',
'notes/polls/vote',
'notes/reactions/create',
'pages/create',
'pages/like',
'pages/unlike',
'pages/update',
'renote-mute/create',
'users/lists/create',
'users/lists/pull',
'users/lists/push',
] as const)('Prohibit access after moving: %s', async (endpoint) => {
const res = await api(endpoint, {}, alice);
assert.strictEqual(res.status, 403);
assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED');
@ -424,11 +424,11 @@ describe('Account Move', () => {
});
test('Prohibit access after moving: /antennas/update', async () => {
const res = await api('/antennas/update', {
const res = await api('antennas/update', {
antennaId,
name: secureRndstr(8),
src: 'users',
keywords: [secureRndstr(8)],
keywords: [[secureRndstr(8)]],
excludeKeywords: [],
users: [eve.id],
caseSensitive: false,
@ -447,12 +447,12 @@ describe('Account Move', () => {
const res = await uploadFile(alice);
assert.strictEqual(res.status, 403);
assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED');
assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
assert.strictEqual((res.body! as any as { error: misskey.api.APIError }).error.code, 'YOUR_ACCOUNT_MOVED');
assert.strictEqual((res.body! as any as { error: misskey.api.APIError }).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
});
test('Prohibit updating alsoKnownAs after moving', async () => {
const res = await api('/i/update', {
const res = await api('i/update', {
alsoKnownAs: [`@eve@${url.hostname}`],
}, alice);

View File

@ -19,21 +19,31 @@ describe('Mute', () => {
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' });
// Mute: alice ==> carol
await api('mute/create', {
userId: carol.id,
}, alice);
}, 1000 * 60 * 2);
test('ミュート作成', async () => {
const res = await api('/mute/create', {
userId: carol.id,
const res = await api('mute/create', {
userId: bob.id,
}, alice);
assert.strictEqual(res.status, 204);
// 単体でも走らせられるように副作用消す
await api('mute/delete', {
userId: bob.id,
}, alice);
});
test('「自分宛ての投稿」にミュートしているユーザーの投稿が含まれない', async () => {
const bobNote = await post(bob, { text: '@alice hi' });
const carolNote = await post(carol, { text: '@alice hi' });
const res = await api('/notes/mentions', {}, alice);
const res = await api('notes/mentions', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -43,11 +53,11 @@ describe('Mute', () => {
test('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => {
// 状態リセット
await api('/i/read-all-unread-notes', {}, alice);
await api('i/read-all-unread-notes', {}, alice);
await post(carol, { text: '@alice hi' });
const res = await api('/i', {}, alice);
const res = await api('i', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.hasUnreadMentions, false);
@ -55,7 +65,7 @@ describe('Mute', () => {
test('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', async () => {
// 状態リセット
await api('/i/read-all-unread-notes', {}, alice);
await api('i/read-all-unread-notes', {}, alice);
const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadMention');
@ -64,8 +74,8 @@ describe('Mute', () => {
test('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', async () => {
// 状態リセット
await api('/i/read-all-unread-notes', {}, alice);
await api('/notifications/mark-all-as-read', {}, alice);
await api('i/read-all-unread-notes', {}, alice);
await api('notifications/mark-all-as-read', {}, alice);
const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadNotification');
@ -78,7 +88,7 @@ describe('Mute', () => {
const bobNote = await post(bob, { text: 'hi' });
const carolNote = await post(carol, { text: 'hi' });
const res = await api('/notes/local-timeline', {}, alice);
const res = await api('notes/local-timeline', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -94,7 +104,7 @@ describe('Mute', () => {
renoteId: carolNote.id,
});
const res = await api('/notes/local-timeline', {}, alice);
const res = await api('notes/local-timeline', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -110,7 +120,7 @@ describe('Mute', () => {
await react(bob, aliceNote, 'like');
await react(carol, aliceNote, 'like');
const res = await api('/i/notifications', {}, alice);
const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -123,7 +133,7 @@ describe('Mute', () => {
await post(bob, { text: '@alice hi', replyId: aliceNote.id });
await post(carol, { text: '@alice hi', replyId: aliceNote.id });
const res = await api('/i/notifications', {}, alice);
const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -137,7 +147,7 @@ describe('Mute', () => {
await post(bob, { text: '@alice hi' });
await post(carol, { text: '@alice hi' });
const res = await api('/i/notifications', {}, alice);
const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -151,7 +161,7 @@ describe('Mute', () => {
await post(bob, { text: 'hi', renoteId: aliceNote.id });
await post(carol, { text: 'hi', renoteId: aliceNote.id });
const res = await api('/i/notifications', {}, alice);
const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -165,7 +175,7 @@ describe('Mute', () => {
await post(bob, { renoteId: aliceNote.id });
await post(carol, { renoteId: aliceNote.id });
const res = await api('/i/notifications', {}, alice);
const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -175,30 +185,36 @@ describe('Mute', () => {
});
test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => {
await api('/i/follow', { userId: alice.id }, bob);
await api('/i/follow', { userId: alice.id }, carol);
await api('following/create', { userId: alice.id }, bob);
await api('following/create', { userId: alice.id }, carol);
const res = await api('/i/notifications', {}, alice);
const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
await api('following/delete', { userId: alice.id }, bob);
await api('following/delete', { userId: alice.id }, carol);
});
test('通知にミュートしているユーザーからのフォローリクエストが含まれない', async () => {
await api('/i/update/', { isLocked: true }, alice);
await api('/following/create', { userId: alice.id }, bob);
await api('/following/create', { userId: alice.id }, carol);
await api('i/update', { isLocked: true }, alice);
await api('following/create', { userId: alice.id }, bob);
await api('following/create', { userId: alice.id }, carol);
const res = await api('/i/notifications', {}, alice);
const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
await api('following/delete', { userId: alice.id }, bob);
await api('following/delete', { userId: alice.id }, carol);
});
});
@ -208,7 +224,7 @@ describe('Mute', () => {
await react(bob, aliceNote, 'like');
await react(carol, aliceNote, 'like');
const res = await api('/i/notifications-grouped', {}, alice);
const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -220,7 +236,7 @@ describe('Mute', () => {
await post(bob, { text: '@alice hi', replyId: aliceNote.id });
await post(carol, { text: '@alice hi', replyId: aliceNote.id });
const res = await api('/i/notifications-grouped', {}, alice);
const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -234,7 +250,7 @@ describe('Mute', () => {
await post(bob, { text: '@alice hi' });
await post(carol, { text: '@alice hi' });
const res = await api('/i/notifications-grouped', {}, alice);
const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -248,7 +264,7 @@ describe('Mute', () => {
await post(bob, { text: 'hi', renoteId: aliceNote.id });
await post(carol, { text: 'hi', renoteId: aliceNote.id });
const res = await api('/i/notifications-grouped', {}, alice);
const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -262,7 +278,7 @@ describe('Mute', () => {
await post(bob, { renoteId: aliceNote.id });
await post(carol, { renoteId: aliceNote.id });
const res = await api('/i/notifications-grouped', {}, alice);
const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -272,24 +288,27 @@ describe('Mute', () => {
});
test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => {
await api('/i/follow', { userId: alice.id }, bob);
await api('/i/follow', { userId: alice.id }, carol);
await api('following/create', { userId: alice.id }, bob);
await api('following/create', { userId: alice.id }, carol);
const res = await api('/i/notifications-grouped', {}, alice);
const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
await api('following/delete', { userId: alice.id }, bob);
await api('following/delete', { userId: alice.id }, carol);
});
test('通知にミュートしているユーザーからのフォローリクエストが含まれない', async () => {
await api('/i/update/', { isLocked: true }, alice);
await api('/following/create', { userId: alice.id }, bob);
await api('/following/create', { userId: alice.id }, carol);
await api('i/update', { isLocked: true }, alice);
await api('following/create', { userId: alice.id }, bob);
await api('following/create', { userId: alice.id }, carol);
const res = await api('/i/notifications-grouped', {}, alice);
const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);

View File

@ -31,7 +31,7 @@ describe('Note', () => {
text: 'test',
};
const res = await api('/notes/create', post, alice);
const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@ -41,7 +41,7 @@ describe('Note', () => {
test('ファイルを添付できる', async () => {
const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
const res = await api('/notes/create', {
const res = await api('notes/create', {
fileIds: [file.id],
}, alice);
@ -53,7 +53,7 @@ describe('Note', () => {
test('他人のファイルで怒られる', async () => {
const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
const res = await api('/notes/create', {
const res = await api('notes/create', {
text: 'test',
fileIds: [file.id],
}, alice);
@ -64,7 +64,7 @@ describe('Note', () => {
}, 1000 * 10);
test('存在しないファイルで怒られる', async () => {
const res = await api('/notes/create', {
const res = await api('notes/create', {
text: 'test',
fileIds: ['000000000000000000000000'],
}, alice);
@ -75,7 +75,7 @@ describe('Note', () => {
});
test('不正なファイルIDで怒られる', async () => {
const res = await api('/notes/create', {
const res = await api('notes/create', {
fileIds: ['kyoppie'],
}, alice);
assert.strictEqual(res.status, 400);
@ -93,7 +93,7 @@ describe('Note', () => {
replyId: bobPost.id,
};
const res = await api('/notes/create', alicePost, alice);
const res = await api('notes/create', alicePost, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@ -111,7 +111,7 @@ describe('Note', () => {
renoteId: bobPost.id,
};
const res = await api('/notes/create', alicePost, alice);
const res = await api('notes/create', alicePost, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@ -129,7 +129,7 @@ describe('Note', () => {
renoteId: bobPost.id,
};
const res = await api('/notes/create', alicePost, alice);
const res = await api('notes/create', alicePost, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@ -142,7 +142,7 @@ describe('Note', () => {
const bobPost = await post(bob, {
text: 'test',
});
const res = await api('/notes/create', {
const res = await api('notes/create', {
text: ' ',
renoteId: bobPost.id,
}, alice);
@ -152,7 +152,7 @@ describe('Note', () => {
});
test('visibility: followersでrenoteできる', async () => {
const createRes = await api('/notes/create', {
const createRes = await api('notes/create', {
text: 'test',
visibility: 'followers',
}, alice);
@ -160,7 +160,7 @@ describe('Note', () => {
assert.strictEqual(createRes.status, 200);
const renoteId = createRes.body.createdNote.id;
const renoteRes = await api('/notes/create', {
const renoteRes = await api('notes/create', {
visibility: 'followers',
renoteId,
}, alice);
@ -169,7 +169,7 @@ describe('Note', () => {
assert.strictEqual(renoteRes.body.createdNote.renoteId, renoteId);
assert.strictEqual(renoteRes.body.createdNote.visibility, 'followers');
const deleteRes = await api('/notes/delete', {
const deleteRes = await api('notes/delete', {
noteId: renoteRes.body.createdNote.id,
}, alice);
@ -177,11 +177,11 @@ describe('Note', () => {
});
test('visibility: followersなートに対してフォロワーはリプライできる', async () => {
await api('/following/create', {
await api('following/create', {
userId: alice.id,
}, bob);
const aliceNote = await api('/notes/create', {
const aliceNote = await api('notes/create', {
text: 'direct note to bob',
visibility: 'followers',
}, alice);
@ -189,7 +189,7 @@ describe('Note', () => {
assert.strictEqual(aliceNote.status, 200);
const replyId = aliceNote.body.createdNote.id;
const bobReply = await api('/notes/create', {
const bobReply = await api('notes/create', {
text: 'reply to alice note',
replyId,
}, bob);
@ -197,20 +197,20 @@ describe('Note', () => {
assert.strictEqual(bobReply.status, 200);
assert.strictEqual(bobReply.body.createdNote.replyId, replyId);
await api('/following/delete', {
await api('following/delete', {
userId: alice.id,
}, bob);
});
test('visibility: followersなートに対してフォロワーでないユーザーがリプライしようとすると怒られる', async () => {
const aliceNote = await api('/notes/create', {
const aliceNote = await api('notes/create', {
text: 'direct note to bob',
visibility: 'followers',
}, alice);
assert.strictEqual(aliceNote.status, 200);
const bobReply = await api('/notes/create', {
const bobReply = await api('notes/create', {
text: 'reply to alice note',
replyId: aliceNote.body.createdNote.id,
}, bob);
@ -220,7 +220,7 @@ describe('Note', () => {
});
test('visibility: specifiedなートに対してvisibility: specifiedで返信できる', async () => {
const aliceNote = await api('/notes/create', {
const aliceNote = await api('notes/create', {
text: 'direct note to bob',
visibility: 'specified',
visibleUserIds: [bob.id],
@ -228,7 +228,7 @@ describe('Note', () => {
assert.strictEqual(aliceNote.status, 200);
const bobReply = await api('/notes/create', {
const bobReply = await api('notes/create', {
text: 'reply to alice note',
replyId: aliceNote.body.createdNote.id,
visibility: 'specified',
@ -239,7 +239,7 @@ describe('Note', () => {
});
test('visibility: specifiedなートに対してvisibility: follwersで返信しようとすると怒られる', async () => {
const aliceNote = await api('/notes/create', {
const aliceNote = await api('notes/create', {
text: 'direct note to bob',
visibility: 'specified',
visibleUserIds: [bob.id],
@ -247,7 +247,7 @@ describe('Note', () => {
assert.strictEqual(aliceNote.status, 200);
const bobReply = await api('/notes/create', {
const bobReply = await api('notes/create', {
text: 'reply to alice note with visibility: followers',
replyId: aliceNote.body.createdNote.id,
visibility: 'followers',
@ -261,7 +261,7 @@ describe('Note', () => {
const post = {
text: '!'.repeat(MAX_NOTE_TEXT_LENGTH), // 3000文字
};
const res = await api('/notes/create', post, alice);
const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 200);
});
@ -269,7 +269,7 @@ describe('Note', () => {
const post = {
text: '!'.repeat(MAX_NOTE_TEXT_LENGTH + 1), // 3001文字
};
const res = await api('/notes/create', post, alice);
const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 400);
});
@ -278,7 +278,7 @@ describe('Note', () => {
text: 'test',
replyId: '000000000000000000000000',
};
const res = await api('/notes/create', post, alice);
const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 400);
});
@ -286,7 +286,7 @@ describe('Note', () => {
const post = {
renoteId: '000000000000000000000000',
};
const res = await api('/notes/create', post, alice);
const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 400);
});
@ -295,7 +295,7 @@ describe('Note', () => {
text: 'test',
replyId: 'foo',
};
const res = await api('/notes/create', post, alice);
const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 400);
});
@ -303,7 +303,7 @@ describe('Note', () => {
const post = {
renoteId: 'foo',
};
const res = await api('/notes/create', post, alice);
const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 400);
});
@ -312,7 +312,7 @@ describe('Note', () => {
text: '@ghost yo',
};
const res = await api('/notes/create', post, alice);
const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@ -324,7 +324,7 @@ describe('Note', () => {
text: '@bob @bob @bob yo',
};
const res = await api('/notes/create', post, alice);
const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@ -337,25 +337,25 @@ describe('Note', () => {
describe('添付ファイル情報', () => {
test('ファイルを添付した場合、投稿成功時にファイル情報入りのレスポンスが帰ってくる', async () => {
const file = await uploadFile(alice);
const res = await api('/notes/create', {
fileIds: [file.body.id],
const res = await api('notes/create', {
fileIds: [file.body!.id],
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
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);
});
test('ファイルを添付した場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => {
const file = await uploadFile(alice);
const createdNote = await api('/notes/create', {
fileIds: [file.body.id],
const createdNote = await api('notes/create', {
fileIds: [file.body!.id],
}, alice);
assert.strictEqual(createdNote.status, 200);
const res = await api('/notes', {
const res = await api('notes', {
withFiles: true,
}, alice);
@ -364,23 +364,23 @@ describe('Note', () => {
const myNote = res.body.find((note: { id: string; files: { id: string }[] }) => note.id === createdNote.body.createdNote.id);
assert.notEqual(myNote, null);
assert.strictEqual(myNote.files.length, 1);
assert.strictEqual(myNote.files[0].id, file.body.id);
assert.strictEqual(myNote.files[0].id, file.body!.id);
});
test('ファイルが添付されたノートをリノートした場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => {
const file = await uploadFile(alice);
const createdNote = await api('/notes/create', {
fileIds: [file.body.id],
const createdNote = await api('notes/create', {
fileIds: [file.body!.id],
}, alice);
assert.strictEqual(createdNote.status, 200);
const renoted = await api('/notes/create', {
const renoted = await api('notes/create', {
renoteId: createdNote.body.createdNote.id,
}, alice);
assert.strictEqual(renoted.status, 200);
const res = await api('/notes', {
const res = await api('notes', {
renote: true,
}, alice);
@ -389,24 +389,24 @@ describe('Note', () => {
const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id);
assert.notEqual(myNote, null);
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);
});
test('ファイルが添付されたノートに返信した場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => {
const file = await uploadFile(alice);
const createdNote = await api('/notes/create', {
fileIds: [file.body.id],
const createdNote = await api('notes/create', {
fileIds: [file.body!.id],
}, alice);
assert.strictEqual(createdNote.status, 200);
const reply = await api('/notes/create', {
const reply = await api('notes/create', {
replyId: createdNote.body.createdNote.id,
text: 'this is reply',
}, alice);
assert.strictEqual(reply.status, 200);
const res = await api('/notes', {
const res = await api('notes', {
reply: true,
}, alice);
@ -415,29 +415,29 @@ describe('Note', () => {
const myNote = res.body.find((note: { id: string }) => note.id === reply.body.createdNote.id);
assert.notEqual(myNote, null);
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);
});
test('ファイルが添付されたノートへの返信をリノートした場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => {
const file = await uploadFile(alice);
const createdNote = await api('/notes/create', {
fileIds: [file.body.id],
const createdNote = await api('notes/create', {
fileIds: [file.body!.id],
}, alice);
assert.strictEqual(createdNote.status, 200);
const reply = await api('/notes/create', {
const reply = await api('notes/create', {
replyId: createdNote.body.createdNote.id,
text: 'this is reply',
}, alice);
assert.strictEqual(reply.status, 200);
const renoted = await api('/notes/create', {
const renoted = await api('notes/create', {
renoteId: reply.body.createdNote.id,
}, alice);
assert.strictEqual(renoted.status, 200);
const res = await api('/notes', {
const res = await api('notes', {
renote: true,
}, alice);
@ -446,7 +446,7 @@ describe('Note', () => {
const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id);
assert.notEqual(myNote, null);
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);
});
test('NSFWが強制されている場合変更できない', async () => {
@ -472,7 +472,7 @@ describe('Note', () => {
priority: 0,
value: true,
},
},
} as any,
}, alice);
assert.strictEqual(res.status, 200);
@ -483,15 +483,15 @@ describe('Note', () => {
}, alice);
assert.strictEqual(assign.status, 204);
assert.strictEqual(file.body.isSensitive, false);
assert.strictEqual(file.body!.isSensitive, false);
const nsfwfile = await uploadFile(alice);
assert.strictEqual(nsfwfile.status, 200);
assert.strictEqual(nsfwfile.body.isSensitive, true);
assert.strictEqual(nsfwfile.body!.isSensitive, true);
const liftnsfw = await api('drive/files/update', {
fileId: nsfwfile.body.id,
fileId: nsfwfile.body!.id,
isSensitive: false,
}, alice);
@ -499,7 +499,7 @@ describe('Note', () => {
assert.strictEqual(liftnsfw.body.error.code, 'RESTRICTED_BY_ROLE');
const oldaddnsfw = await api('drive/files/update', {
fileId: file.body.id,
fileId: file.body!.id,
isSensitive: true,
}, alice);
@ -518,7 +518,7 @@ describe('Note', () => {
describe('notes/create', () => {
test('投票を添付できる', async () => {
const res = await api('/notes/create', {
const res = await api('notes/create', {
text: 'test',
poll: {
choices: ['foo', 'bar'],
@ -531,14 +531,15 @@ describe('Note', () => {
});
test('投票の選択肢が無くて怒られる', async () => {
const res = await api('/notes/create', {
const res = await api('notes/create', {
// @ts-expect-error poll must not be empty
poll: {},
}, alice);
assert.strictEqual(res.status, 400);
});
test('投票の選択肢が無くて怒られる (空の配列)', async () => {
const res = await api('/notes/create', {
const res = await api('notes/create', {
poll: {
choices: [],
},
@ -547,7 +548,7 @@ describe('Note', () => {
});
test('投票の選択肢が1つで怒られる', async () => {
const res = await api('/notes/create', {
const res = await api('notes/create', {
poll: {
choices: ['Strawberry Pasta'],
},
@ -556,14 +557,14 @@ describe('Note', () => {
});
test('投票できる', async () => {
const { body } = await api('/notes/create', {
const { body } = await api('notes/create', {
text: 'test',
poll: {
choices: ['sakura', 'izumi', 'ako'],
},
}, alice);
const res = await api('/notes/polls/vote', {
const res = await api('notes/polls/vote', {
noteId: body.createdNote.id,
choice: 1,
}, alice);
@ -572,19 +573,19 @@ describe('Note', () => {
});
test('複数投票できない', async () => {
const { body } = await api('/notes/create', {
const { body } = await api('notes/create', {
text: 'test',
poll: {
choices: ['sakura', 'izumi', 'ako'],
},
}, alice);
await api('/notes/polls/vote', {
await api('notes/polls/vote', {
noteId: body.createdNote.id,
choice: 0,
}, alice);
const res = await api('/notes/polls/vote', {
const res = await api('notes/polls/vote', {
noteId: body.createdNote.id,
choice: 2,
}, alice);
@ -593,7 +594,7 @@ describe('Note', () => {
});
test('許可されている場合は複数投票できる', async () => {
const { body } = await api('/notes/create', {
const { body } = await api('notes/create', {
text: 'test',
poll: {
choices: ['sakura', 'izumi', 'ako'],
@ -601,17 +602,17 @@ describe('Note', () => {
},
}, alice);
await api('/notes/polls/vote', {
await api('notes/polls/vote', {
noteId: body.createdNote.id,
choice: 0,
}, alice);
await api('/notes/polls/vote', {
await api('notes/polls/vote', {
noteId: body.createdNote.id,
choice: 1,
}, alice);
const res = await api('/notes/polls/vote', {
const res = await api('notes/polls/vote', {
noteId: body.createdNote.id,
choice: 2,
}, alice);
@ -620,7 +621,7 @@ describe('Note', () => {
});
test('締め切られている場合は投票できない', async () => {
const { body } = await api('/notes/create', {
const { body } = await api('notes/create', {
text: 'test',
poll: {
choices: ['sakura', 'izumi', 'ako'],
@ -630,7 +631,7 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2));
const res = await api('/notes/polls/vote', {
const res = await api('notes/polls/vote', {
noteId: body.createdNote.id,
choice: 1,
}, alice);
@ -649,7 +650,7 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2));
const note1 = await api('/notes/create', {
const note1 = await api('notes/create', {
text: 'hogetesthuge',
}, alice);
@ -666,7 +667,7 @@ describe('Note', () => {
assert.strictEqual(sensitive.status, 204);
const note2 = await api('/notes/create', {
const note2 = await api('notes/create', {
text: 'hogetesthuge',
}, alice);
@ -683,7 +684,7 @@ describe('Note', () => {
assert.strictEqual(sensitive.status, 204);
const note2 = await api('/notes/create', {
const note2 = await api('notes/create', {
text: 'hogeTesthuge',
}, alice);
@ -702,7 +703,7 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2));
const note1 = await api('/notes/create', {
const note1 = await api('notes/create', {
text: 'hogetesthuge',
}, alice);
@ -719,7 +720,7 @@ describe('Note', () => {
assert.strictEqual(prohibited.status, 204);
const note2 = await api('/notes/create', {
const note2 = await api('notes/create', {
text: 'hogetesthuge',
}, alice);
@ -736,7 +737,7 @@ describe('Note', () => {
assert.strictEqual(prohibited.status, 204);
const note2 = await api('/notes/create', {
const note2 = await api('notes/create', {
text: 'hogeTesthuge',
}, alice);
@ -755,7 +756,7 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2));
const note1 = await api('/notes/create', {
const note1 = await api('notes/create', {
text: 'hogetesthuge',
}, tom);
@ -783,7 +784,7 @@ describe('Note', () => {
priority: 1,
value: 0,
},
},
} as any,
}, alice);
assert.strictEqual(res.status, 200);
@ -799,7 +800,7 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2));
const note = await api('/notes/create', {
const note = await api('notes/create', {
text: '@bob potentially annoying text',
}, alice);
@ -837,7 +838,7 @@ describe('Note', () => {
priority: 1,
value: 0,
},
},
} as any,
}, alice);
assert.strictEqual(res.status, 200);
@ -853,10 +854,10 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2));
const note = await api('/notes/create', {
const note = await api('notes/create', {
text: 'potentially annoying text',
visibility: 'specified',
visibleUserIds: [ bob.id ],
visibleUserIds: [bob.id],
}, alice);
assert.strictEqual(note.status, 400);
@ -893,7 +894,7 @@ describe('Note', () => {
priority: 1,
value: 1,
},
},
} as any,
}, alice);
assert.strictEqual(res.status, 200);
@ -909,10 +910,10 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2));
const note = await api('/notes/create', {
const note = await api('notes/create', {
text: '@bob potentially annoying text',
visibility: 'specified',
visibleUserIds: [ bob.id ],
visibleUserIds: [bob.id],
}, alice);
assert.strictEqual(note.status, 200);

View File

@ -22,7 +22,7 @@ describe('Renote Mute', () => {
}, 1000 * 60 * 2);
test('ミュート作成', async () => {
const res = await api('/renote-mute/create', {
const res = await api('renote-mute/create', {
userId: carol.id,
}, alice);
@ -37,7 +37,7 @@ describe('Renote Mute', () => {
// redisに追加されるのを待つ
await sleep(100);
const res = await api('/notes/local-timeline', {}, alice);
const res = await api('notes/local-timeline', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -54,7 +54,7 @@ describe('Renote Mute', () => {
// redisに追加されるのを待つ
await sleep(100);
const res = await api('/notes/local-timeline', {}, alice);
const res = await api('notes/local-timeline', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);

View File

@ -601,7 +601,7 @@ describe('Streaming', () => {
// #10443
test('ミュートしているサーバのートがリストTLに流れない', async () => {
await api('/i/update', {
await api('i/update', {
mutedInstances: ['example.com'],
}, chitose);
@ -618,7 +618,7 @@ describe('Streaming', () => {
// #10443
test('ミュートしているサーバのートに対するリプライがリストTLに流れない', async () => {
await api('/i/update', {
await api('i/update', {
mutedInstances: ['example.com'],
}, chitose);
@ -635,7 +635,7 @@ describe('Streaming', () => {
// #10443
test('ミュートしているサーバのートに対するリートがリストTLに流れない', async () => {
await api('/i/update', {
await api('i/update', {
mutedInstances: ['example.com'],
}, chitose);

View File

@ -24,12 +24,12 @@ describe('Note thread mute', () => {
const bobNote = await post(bob, { text: '@alice @carol root note' });
const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });
await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
await api('notes/thread-muting/create', { noteId: bobNote.id }, alice);
const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' });
const res = await api('/notes/mentions', {}, alice);
const res = await api('notes/mentions', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@ -40,15 +40,15 @@ describe('Note thread mute', () => {
test('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => {
// 状態リセット
await api('/i/read-all-unread-notes', {}, alice);
await api('i/read-all-unread-notes', {}, alice);
const bobNote = await post(bob, { text: '@alice @carol root note' });
await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
await api('notes/thread-muting/create', { noteId: bobNote.id }, alice);
const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
const res = await api('/i', {}, alice);
const res = await api('i', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.hasUnreadMentions, false);
@ -56,11 +56,11 @@ describe('Note thread mute', () => {
test('ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise<void>(async done => {
// 状態リセット
await api('/i/read-all-unread-notes', {}, alice);
await api('i/read-all-unread-notes', {}, alice);
const bobNote = await post(bob, { text: '@alice @carol root note' });
await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
await api('notes/thread-muting/create', { noteId: bobNote.id }, alice);
let fired = false;
@ -84,12 +84,12 @@ describe('Note thread mute', () => {
const bobNote = await post(bob, { text: '@alice @carol root note' });
const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });
await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
await api('notes/thread-muting/create', { noteId: bobNote.id }, alice);
const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' });
const res = await api('/i/notifications', {}, alice);
const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);

File diff suppressed because it is too large Load Diff

View File

@ -11,9 +11,9 @@ import type * as misskey from 'misskey-js';
describe('users/notes', () => {
let alice: misskey.entities.SignupResponse;
let jpgNote: any;
let pngNote: any;
let jpgPngNote: any;
let jpgNote: misskey.entities.Note;
let pngNote: misskey.entities.Note;
let jpgPngNote: misskey.entities.Note;
beforeAll(async () => {
alice = await signup({ username: 'alice' });
@ -31,7 +31,7 @@ describe('users/notes', () => {
}, 1000 * 60 * 2);
test('withFiles', async () => {
const res = await api('/users/notes', {
const res = await api('users/notes', {
userId: alice.id,
withFiles: true,
}, alice);

View File

@ -8,7 +8,7 @@ process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { inspect } from 'node:util';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import { api, page, post, role, signup, successfulApiCall, uploadFile } from '../utils.js';
import { api, post, role, signup, successfulApiCall, uploadFile } from '../utils.js';
import type * as misskey from 'misskey-js';
describe('ユーザー', () => {
@ -24,31 +24,12 @@ describe('ユーザー', () => {
}, {});
};
// BUG misskey-jsとjson-schemaと実際に返ってくるデータが全部違う
type UserLite = misskey.entities.UserLite & {
badgeRoles: any[],
};
type UserDetailedNotMe = UserLite &
misskey.entities.UserDetailed & {
roles: any[],
};
type MeDetailed = UserDetailedNotMe &
misskey.entities.MeDetailed & {
achievements: object[],
loggedInDays: number,
policies: object,
};
type User = MeDetailed & { token: string };
const show = async (id: string, me = root): Promise<MeDetailed | UserDetailedNotMe> => {
return successfulApiCall({ endpoint: 'users/show', parameters: { userId: id }, user: me }) as any;
const show = async (id: string, me = root): Promise<misskey.entities.UserDetailed> => {
return successfulApiCall({ endpoint: 'users/show', parameters: { userId: id }, user: me });
};
// UserLiteのキーが過不足なく入っている
const userLite = (user: User): Partial<UserLite> => {
const userLite = (user: misskey.entities.UserLite): Partial<misskey.entities.UserLite> => {
return stripUndefined({
id: user.id,
name: user.name,
@ -71,7 +52,7 @@ describe('ユーザー', () => {
};
// UserDetailedNotMeのキーが過不足なく入っている
const userDetailedNotMe = (user: User): Partial<UserDetailedNotMe> => {
const userDetailedNotMe = (user: misskey.entities.SignupResponse): Partial<misskey.entities.UserDetailedNotMe> => {
return stripUndefined({
...userLite(user),
url: user.url,
@ -111,7 +92,7 @@ describe('ユーザー', () => {
};
// Relations関連のキーが過不足なく入っている
const userDetailedNotMeWithRelations = (user: User): Partial<UserDetailedNotMe> => {
const userDetailedNotMeWithRelations = (user: misskey.entities.SignupResponse): Partial<misskey.entities.UserDetailedNotMe> => {
return stripUndefined({
...userDetailedNotMe(user),
isFollowing: user.isFollowing ?? false,
@ -128,7 +109,7 @@ describe('ユーザー', () => {
};
// MeDetailedのキーが過不足なく入っている
const meDetailed = (user: User, security = false): Partial<MeDetailed> => {
const meDetailed = (user: misskey.entities.SignupResponse, security = false): Partial<misskey.entities.MeDetailed> => {
return stripUndefined({
...userDetailedNotMe(user),
avatarId: user.avatarId,
@ -159,6 +140,7 @@ describe('ユーザー', () => {
mutedWords: user.mutedWords,
hardMutedWords: user.hardMutedWords,
mutedInstances: user.mutedInstances,
// @ts-expect-error 後方互換性
mutingNotificationTypes: user.mutingNotificationTypes,
notificationRecieveConfig: user.notificationRecieveConfig,
emailNotificationTypes: user.emailNotificationTypes,
@ -173,61 +155,53 @@ describe('ユーザー', () => {
});
};
let root: User;
let alice: User;
let root: misskey.entities.SignupResponse;
let alice: misskey.entities.SignupResponse;
let aliceNote: misskey.entities.Note;
let alicePage: misskey.entities.Page;
let aliceList: misskey.entities.UserList;
let bob: User;
let bobNote: misskey.entities.Note;
let bob: misskey.entities.SignupResponse;
let carol: User;
let dave: User;
let ellen: User;
let frank: User;
// NOTE: これがないと落ちるbob の updatedAt が null になってしまうため?)
let bobNote: misskey.entities.Note; // eslint-disable-line @typescript-eslint/no-unused-vars
let usersReplying: User[];
let carol: misskey.entities.SignupResponse;
let userNoNote: User;
let userNotExplorable: User;
let userLocking: User;
let userAdmin: User;
let roleAdmin: any;
let userModerator: User;
let roleModerator: any;
let userRolePublic: User;
let rolePublic: any;
let userRoleBadge: User;
let roleBadge: any;
let userSilenced: User;
let roleSilenced: any;
let userSuspended: User;
let userDeletedBySelf: User;
let userDeletedByAdmin: User;
let userFollowingAlice: User;
let userFollowedByAlice: User;
let userBlockingAlice: User;
let userBlockedByAlice: User;
let userMutingAlice: User;
let userMutedByAlice: User;
let userRnMutingAlice: User;
let userRnMutedByAlice: User;
let userFollowRequesting: User;
let userFollowRequested: User;
let usersReplying: misskey.entities.SignupResponse[];
let userNoNote: misskey.entities.SignupResponse;
let userNotExplorable: misskey.entities.SignupResponse;
let userLocking: misskey.entities.SignupResponse;
let userAdmin: misskey.entities.SignupResponse;
let roleAdmin: misskey.entities.Role;
let userModerator: misskey.entities.SignupResponse;
let roleModerator: misskey.entities.Role;
let userRolePublic: misskey.entities.SignupResponse;
let rolePublic: misskey.entities.Role;
let userRoleBadge: misskey.entities.SignupResponse;
let roleBadge: misskey.entities.Role;
let userSilenced: misskey.entities.SignupResponse;
let roleSilenced: misskey.entities.Role;
let userSuspended: misskey.entities.SignupResponse;
let userDeletedBySelf: misskey.entities.SignupResponse;
let userDeletedByAdmin: misskey.entities.SignupResponse;
let userFollowingAlice: misskey.entities.SignupResponse;
let userFollowedByAlice: misskey.entities.SignupResponse;
let userBlockingAlice: misskey.entities.SignupResponse;
let userBlockedByAlice: misskey.entities.SignupResponse;
let userMutingAlice: misskey.entities.SignupResponse;
let userMutedByAlice: misskey.entities.SignupResponse;
let userRnMutingAlice: misskey.entities.SignupResponse;
let userRnMutedByAlice: misskey.entities.SignupResponse;
let userFollowRequesting: misskey.entities.SignupResponse;
let userFollowRequested: misskey.entities.SignupResponse;
beforeAll(async () => {
root = await signup({ username: 'root' });
alice = await signup({ username: 'alice' });
aliceNote = await post(alice, { text: 'test' }) as any;
alicePage = await page(alice);
aliceList = (await api('users/list/create', { name: 'aliceList' }, alice)).body;
aliceNote = await post(alice, { text: 'test' });
bob = await signup({ username: 'bob' });
bobNote = await post(bob, { text: 'test' }) as any;
bobNote = await post(bob, { text: 'test' });
carol = await signup({ username: 'carol' });
dave = await signup({ username: 'dave' });
ellen = await signup({ username: 'ellen' });
frank = await signup({ username: 'frank' });
// @alice -> @replyingへのリプライ。Promise.allで一気に作るとtimeoutしてしまうのでreduceで一つ一つawaitする
usersReplying = await [...Array(10)].map((_, i) => i).reduce(async (acc, i) => {
@ -238,7 +212,7 @@ describe('ユーザー', () => {
}
return (await acc).concat(u);
}, Promise.resolve([] as User[]));
}, Promise.resolve([] as misskey.entities.SignupResponse[]));
userNoNote = await signup({ username: 'userNoNote' });
userNotExplorable = await signup({ username: 'userNotExplorable' });
@ -306,7 +280,7 @@ describe('ユーザー', () => {
beforeEach(async () => {
alice = {
...alice,
...await successfulApiCall({ endpoint: 'i', parameters: {}, user: alice }) as any,
...await successfulApiCall({ endpoint: 'i', parameters: {}, user: alice }),
};
aliceNote = await successfulApiCall({ endpoint: 'notes/show', parameters: { noteId: aliceNote.id }, user: alice });
});
@ -319,7 +293,7 @@ describe('ユーザー', () => {
endpoint: 'signup',
parameters: { username: 'zoe', password: 'password' },
user: undefined,
}) as unknown as User; // BUG MeDetailedに足りないキーがある
}) as unknown as misskey.entities.SignupResponse; // BUG MeDetailedに足りないキーがある
// signupの時はtokenが含まれる特別なMeDetailedが返ってくる
assert.match(response.token, /[a-zA-Z0-9]{16}/);
@ -329,7 +303,7 @@ describe('ユーザー', () => {
assert.strictEqual(response.name, null);
assert.strictEqual(response.username, 'zoe');
assert.strictEqual(response.host, null);
assert.match(response.avatarUrl, /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/);
response.avatarUrl && assert.match(response.avatarUrl, /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/);
assert.strictEqual(response.avatarBlurhash, null);
assert.deepStrictEqual(response.avatarDecorations, []);
assert.strictEqual(response.isBot, false);
@ -401,6 +375,7 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response.unreadAnnouncements, []);
assert.deepStrictEqual(response.mutedWords, []);
assert.deepStrictEqual(response.mutedInstances, []);
// @ts-expect-error 後方互換のため
assert.deepStrictEqual(response.mutingNotificationTypes, []);
assert.deepStrictEqual(response.notificationRecieveConfig, {});
assert.deepStrictEqual(response.emailNotificationTypes, ['follow', 'receiveFollowRequest']);
@ -430,66 +405,66 @@ describe('ユーザー', () => {
//#region 自分の情報の更新(i/update)
test.each([
{ parameters: (): object => ({ name: null }) },
{ parameters: (): object => ({ name: 'x'.repeat(50) }) },
{ parameters: (): object => ({ name: 'x' }) },
{ parameters: (): object => ({ name: 'My name' }) },
{ parameters: (): object => ({ description: null }) },
{ parameters: (): object => ({ description: 'x'.repeat(1500) }) },
{ parameters: (): object => ({ description: 'x' }) },
{ parameters: (): object => ({ description: 'My description' }) },
{ parameters: (): object => ({ location: null }) },
{ parameters: (): object => ({ location: 'x'.repeat(50) }) },
{ parameters: (): object => ({ location: 'x' }) },
{ parameters: (): object => ({ location: 'My location' }) },
{ parameters: (): object => ({ birthday: '0000-00-00' }) },
{ parameters: (): object => ({ birthday: '9999-99-99' }) },
{ parameters: (): object => ({ lang: 'en-US' }) },
{ parameters: (): object => ({ fields: [] }) },
{ parameters: (): object => ({ fields: [{ name: 'x', value: 'x' }] }) },
{ parameters: (): object => ({ fields: [{ name: 'x'.repeat(3000), value: 'x'.repeat(3000) }] }) }, // BUG? fieldには制限がない
{ parameters: (): object => ({ fields: Array(16).fill({ name: 'x', value: 'y' }) }) },
{ parameters: (): object => ({ isLocked: true }) },
{ parameters: (): object => ({ isLocked: false }) },
{ parameters: (): object => ({ isExplorable: false }) },
{ parameters: (): object => ({ isExplorable: true }) },
{ parameters: (): object => ({ hideOnlineStatus: true }) },
{ parameters: (): object => ({ hideOnlineStatus: false }) },
{ parameters: (): object => ({ publicReactions: false }) },
{ parameters: (): object => ({ publicReactions: true }) },
{ parameters: (): object => ({ autoAcceptFollowed: true }) },
{ parameters: (): object => ({ autoAcceptFollowed: false }) },
{ parameters: (): object => ({ noCrawle: true }) },
{ parameters: (): object => ({ noCrawle: false }) },
{ parameters: (): object => ({ preventAiLearning: false }) },
{ parameters: (): object => ({ preventAiLearning: true }) },
{ parameters: (): object => ({ isBot: true }) },
{ parameters: (): object => ({ isBot: false }) },
{ parameters: (): object => ({ isCat: true }) },
{ parameters: (): object => ({ isCat: false }) },
{ parameters: (): object => ({ injectFeaturedNote: true }) },
{ parameters: (): object => ({ injectFeaturedNote: false }) },
{ parameters: (): object => ({ receiveAnnouncementEmail: true }) },
{ parameters: (): object => ({ receiveAnnouncementEmail: false }) },
{ parameters: (): object => ({ alwaysMarkNsfw: true }) },
{ parameters: (): object => ({ alwaysMarkNsfw: false }) },
{ parameters: (): object => ({ autoSensitive: true }) },
{ parameters: (): object => ({ autoSensitive: false }) },
{ parameters: (): object => ({ followingVisibility: 'private' }) },
{ parameters: (): object => ({ followingVisibility: 'followers' }) },
{ parameters: (): object => ({ followingVisibility: 'public' }) },
{ parameters: (): object => ({ followersVisibility: 'private' }) },
{ parameters: (): object => ({ followersVisibility: 'followers' }) },
{ parameters: (): object => ({ followersVisibility: 'public' }) },
{ parameters: (): object => ({ mutedWords: Array(19).fill(['xxxxx']) }) },
{ parameters: (): object => ({ mutedWords: [['x'.repeat(194)]] }) },
{ parameters: (): object => ({ mutedWords: [] }) },
{ parameters: (): object => ({ mutedInstances: ['xxxx.xxxxx'] }) },
{ parameters: (): object => ({ mutedInstances: [] }) },
{ parameters: (): object => ({ notificationRecieveConfig: { mention: { type: 'following' } } }) },
{ parameters: (): object => ({ notificationRecieveConfig: {} }) },
{ parameters: (): object => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) },
{ parameters: (): object => ({ emailNotificationTypes: [] }) },
{ parameters: () => ({ name: null }) },
{ parameters: () => ({ name: 'x'.repeat(50) }) },
{ parameters: () => ({ name: 'x' }) },
{ parameters: () => ({ name: 'My name' }) },
{ parameters: () => ({ description: null }) },
{ parameters: () => ({ description: 'x'.repeat(1500) }) },
{ parameters: () => ({ description: 'x' }) },
{ parameters: () => ({ description: 'My description' }) },
{ parameters: () => ({ location: null }) },
{ parameters: () => ({ location: 'x'.repeat(50) }) },
{ parameters: () => ({ location: 'x' }) },
{ parameters: () => ({ location: 'My location' }) },
{ parameters: () => ({ birthday: '0000-00-00' }) },
{ parameters: () => ({ birthday: '9999-99-99' }) },
{ parameters: () => ({ lang: 'en-US' as const }) },
{ parameters: () => ({ fields: [] }) },
{ parameters: () => ({ fields: [{ name: 'x', value: 'x' }] }) },
{ parameters: () => ({ fields: [{ name: 'x'.repeat(3000), value: 'x'.repeat(3000) }] }) }, // BUG? fieldには制限がない
{ parameters: () => ({ fields: Array(16).fill({ name: 'x', value: 'y' }) }) },
{ parameters: () => ({ isLocked: true }) },
{ parameters: () => ({ isLocked: false }) },
{ parameters: () => ({ isExplorable: false }) },
{ parameters: () => ({ isExplorable: true }) },
{ parameters: () => ({ hideOnlineStatus: true }) },
{ parameters: () => ({ hideOnlineStatus: false }) },
{ parameters: () => ({ publicReactions: false }) },
{ parameters: () => ({ publicReactions: true }) },
{ parameters: () => ({ autoAcceptFollowed: true }) },
{ parameters: () => ({ autoAcceptFollowed: false }) },
{ parameters: () => ({ noCrawle: true }) },
{ parameters: () => ({ noCrawle: false }) },
{ parameters: () => ({ preventAiLearning: false }) },
{ parameters: () => ({ preventAiLearning: true }) },
{ parameters: () => ({ isBot: true }) },
{ parameters: () => ({ isBot: false }) },
{ parameters: () => ({ isCat: true }) },
{ parameters: () => ({ isCat: false }) },
{ parameters: () => ({ injectFeaturedNote: true }) },
{ parameters: () => ({ injectFeaturedNote: false }) },
{ parameters: () => ({ receiveAnnouncementEmail: true }) },
{ parameters: () => ({ receiveAnnouncementEmail: false }) },
{ parameters: () => ({ alwaysMarkNsfw: true }) },
{ parameters: () => ({ alwaysMarkNsfw: false }) },
{ parameters: () => ({ autoSensitive: true }) },
{ parameters: () => ({ autoSensitive: false }) },
{ parameters: () => ({ followingVisibility: 'private' as const }) },
{ parameters: () => ({ followingVisibility: 'followers' as const }) },
{ parameters: () => ({ followingVisibility: 'public' as const }) },
{ parameters: () => ({ followersVisibility: 'private' as const }) },
{ parameters: () => ({ followersVisibility: 'followers' as const }) },
{ parameters: () => ({ followersVisibility: 'public' as const }) },
{ parameters: () => ({ mutedWords: Array(19).fill(['xxxxx']) }) },
{ parameters: () => ({ mutedWords: [['x'.repeat(194)]] }) },
{ parameters: () => ({ mutedWords: [] }) },
{ parameters: () => ({ mutedInstances: ['xxxx.xxxxx'] }) },
{ parameters: () => ({ mutedInstances: [] }) },
{ parameters: () => ({ notificationRecieveConfig: { mention: { type: 'following' } } }) },
{ parameters: () => ({ notificationRecieveConfig: {} }) },
{ parameters: () => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) },
{ parameters: () => ({ emailNotificationTypes: [] }) },
] as const)('を書き換えることができる($#)', async ({ parameters }) => {
const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters(), user: alice });
const expected = { ...meDetailed(alice, true), ...parameters() };
@ -498,13 +473,13 @@ describe('ユーザー', () => {
test('を書き換えることができる(Avatar)', async () => {
const aliceFile = (await uploadFile(alice)).body;
const parameters = { avatarId: aliceFile.id };
const parameters = { avatarId: aliceFile!.id };
const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice });
assert.match(response.avatarUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/);
assert.match(response.avatarBlurhash ?? '.', /[ -~]{54}/);
const expected = {
...meDetailed(alice, true),
avatarId: aliceFile.id,
avatarId: aliceFile!.id,
avatarBlurhash: response.avatarBlurhash,
avatarUrl: response.avatarUrl,
};
@ -523,13 +498,13 @@ describe('ユーザー', () => {
test('を書き換えることができる(Banner)', async () => {
const aliceFile = (await uploadFile(alice)).body;
const parameters = { bannerId: aliceFile.id };
const parameters = { bannerId: aliceFile!.id };
const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice });
assert.match(response.bannerUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/);
assert.match(response.bannerBlurhash ?? '.', /[ -~]{54}/);
const expected = {
...meDetailed(alice, true),
bannerId: aliceFile.id,
bannerId: aliceFile!.id,
bannerBlurhash: response.bannerBlurhash,
bannerUrl: response.bannerUrl,
};
@ -579,13 +554,13 @@ describe('ユーザー', () => {
//#region ユーザー(users)
test.each([
{ label: 'ID昇順', parameters: { limit: 5 }, selector: (u: UserLite): string => u.id },
{ label: 'フォロワー昇順', parameters: { sort: '+follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) },
{ label: 'フォロワー降順', parameters: { sort: '-follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) },
{ label: '登録日時昇順', parameters: { sort: '+createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt },
{ label: '登録日時降順', parameters: { sort: '-createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt },
{ label: '投稿日時昇順', parameters: { sort: '+updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) },
{ label: '投稿日時降順', parameters: { sort: '-updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) },
{ label: 'ID昇順', parameters: { limit: 5 }, selector: (u: misskey.entities.UserLite): string => u.id },
{ label: 'フォロワー昇順', parameters: { sort: '+follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) },
{ label: 'フォロワー降順', parameters: { sort: '-follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) },
{ label: '登録日時昇順', parameters: { sort: '+createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt },
{ label: '登録日時降順', parameters: { sort: '-createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt },
{ label: '投稿日時昇順', parameters: { sort: '+updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) },
{ label: '投稿日時降順', parameters: { sort: '-updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) },
] as const)('をリスト形式で取得することができる($label', async ({ parameters, selector }) => {
const response = await successfulApiCall({ endpoint: 'users', parameters, user: alice });
@ -598,15 +573,15 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response, expected);
});
test.each([
{ label: '「見つけやすくする」がOFFのユーザーが含まれない', user: (): User => userNotExplorable, excluded: true },
{ label: 'ミュートユーザーが含まれない', user: (): User => userMutedByAlice, excluded: true },
{ label: 'ブロックされているユーザーが含まれない', user: (): User => userBlockedByAlice, excluded: true },
{ label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice, excluded: true },
{ label: '承認制ユーザーが含まれる', user: (): User => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced },
{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin },
{ label: '「見つけやすくする」がOFFのユーザーが含まれない', user: () => userNotExplorable, excluded: true },
{ label: 'ミュートユーザーが含まれない', user: () => userMutedByAlice, excluded: true },
{ label: 'ブロックされているユーザーが含まれない', user: () => userBlockedByAlice, excluded: true },
{ label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice, excluded: true },
{ label: '承認制ユーザーが含まれる', user: () => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
{ label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
] as const)('をリスト形式で取得することができ、結果に$label', async ({ user, excluded }) => {
const parameters = { limit: 100 };
const response = await successfulApiCall({ endpoint: 'users', parameters, user: alice });
@ -620,39 +595,44 @@ describe('ユーザー', () => {
//#region ユーザー情報(users/show)
test.each([
{ label: 'ID指定で自分自身を', parameters: (): object => ({ userId: alice.id }), user: (): User => alice, type: meDetailed },
{ label: 'ID指定で他人を', parameters: (): object => ({ userId: alice.id }), user: (): User => bob, type: userDetailedNotMeWithRelations },
{ label: 'ID指定かつ未認証', parameters: (): object => ({ userId: alice.id }), user: undefined, type: userDetailedNotMe },
{ label: '@指定で自分自身を', parameters: (): object => ({ username: alice.username }), user: (): User => alice, type: meDetailed },
{ label: '@指定で他人を', parameters: (): object => ({ username: alice.username }), user: (): User => bob, type: userDetailedNotMeWithRelations },
{ label: '@指定かつ未認証', parameters: (): object => ({ username: alice.username }), user: undefined, type: userDetailedNotMe },
{ label: 'ID指定で自分自身を', parameters: () => ({ userId: alice.id }), user: () => alice, type: meDetailed },
{ label: 'ID指定で他人を', parameters: () => ({ userId: alice.id }), user: () => bob, type: userDetailedNotMeWithRelations },
{ label: 'ID指定かつ未認証', parameters: () => ({ userId: alice.id }), user: undefined, type: userDetailedNotMe },
{ label: '@指定で自分自身を', parameters: () => ({ username: alice.username }), user: () => alice, type: meDetailed },
{ label: '@指定で他人を', parameters: () => ({ username: alice.username }), user: () => bob, type: userDetailedNotMeWithRelations },
{ label: '@指定かつ未認証', parameters: () => ({ username: alice.username }), user: undefined, type: userDetailedNotMe },
] as const)('を取得することができる($label', async ({ parameters, user, type }) => {
const response = await successfulApiCall({ endpoint: 'users/show', parameters: parameters(), user: user?.() });
const expected = type(alice);
assert.deepStrictEqual(response, expected);
});
test.each([
{ label: 'Administratorになっている', user: (): User => userAdmin, me: (): User => userAdmin, selector: (user: User): unknown => user.isAdmin },
{ label: '自分以外から見たときはAdministratorか判定できない', user: (): User => userAdmin, selector: (user: User): unknown => user.isAdmin, expected: (): undefined => undefined },
{ label: 'Moderatorになっている', user: (): User => userModerator, me: (): User => userModerator, selector: (user: User): unknown => user.isModerator },
{ label: '自分以外から見たときはModeratorか判定できない', user: (): User => userModerator, selector: (user: User): unknown => user.isModerator, expected: (): undefined => undefined },
{ label: 'サイレンスになっている', user: (): User => userSilenced, selector: (user: User): unknown => user.isSilenced },
//{ label: 'サスペンドになっている', user: (): User => userSuspended, selector: (user: User): unknown => user.isSuspended },
{ label: '削除済みになっている', user: (): User => userDeletedBySelf, me: (): User => userDeletedBySelf, selector: (user: User): unknown => user.isDeleted },
{ label: '自分以外から見たときは削除済みか判定できない', user: (): User => userDeletedBySelf, selector: (user: User): unknown => user.isDeleted, expected: (): undefined => undefined },
{ label: '削除済み(byAdmin)になっている', user: (): User => userDeletedByAdmin, me: (): User => userDeletedByAdmin, selector: (user: User): unknown => user.isDeleted },
{ label: '自分以外から見たときは削除済み(byAdmin)か判定できない', user: (): User => userDeletedByAdmin, selector: (user: User): unknown => user.isDeleted, expected: (): undefined => undefined },
{ label: 'フォロー中になっている', user: (): User => userFollowedByAlice, selector: (user: User): unknown => user.isFollowing },
{ label: 'フォローされている', user: (): User => userFollowingAlice, selector: (user: User): unknown => user.isFollowed },
{ label: 'ブロック中になっている', user: (): User => userBlockedByAlice, selector: (user: User): unknown => user.isBlocking },
{ label: 'ブロックされている', user: (): User => userBlockingAlice, selector: (user: User): unknown => user.isBlocked },
{ label: 'ミュート中になっている', user: (): User => userMutedByAlice, selector: (user: User): unknown => user.isMuted },
{ label: 'リノートミュート中になっている', user: (): User => userRnMutedByAlice, selector: (user: User): unknown => user.isRenoteMuted },
{ label: 'フォローリクエスト中になっている', user: (): User => userFollowRequested, me: (): User => userFollowRequesting, selector: (user: User): unknown => user.hasPendingFollowRequestFromYou },
{ label: 'フォローリクエストされている', user: (): User => userFollowRequesting, me: (): User => userFollowRequested, selector: (user: User): unknown => user.hasPendingFollowRequestToYou },
{ label: 'Administratorになっている', user: () => userAdmin, me: () => userAdmin, selector: (user: misskey.entities.MeDetailed) => user.isAdmin },
// @ts-expect-error UserDetailedNotMe doesn't include isAdmin
{ label: '自分以外から見たときはAdministratorか判定できない', user: () => userAdmin, selector: (user: misskey.entities.UserDetailedNotMe) => user.isAdmin, expected: () => undefined },
{ label: 'Moderatorになっている', user: () => userModerator, me: () => userModerator, selector: (user: misskey.entities.MeDetailed) => user.isModerator },
// @ts-expect-error UserDetailedNotMe doesn't include isModerator
{ label: '自分以外から見たときはModeratorか判定できない', user: () => userModerator, selector: (user: misskey.entities.UserDetailedNotMe) => user.isModerator, expected: () => undefined },
{ label: 'サイレンスになっている', user: () => userSilenced, selector: (user: misskey.entities.UserDetailed) => user.isSilenced },
// FIXME: 落ちる
//{ label: 'サスペンドになっている', user: () => userSuspended, selector: (user: misskey.entities.UserDetailed) => user.isSuspended },
{ label: '削除済みになっている', user: () => userDeletedBySelf, me: () => userDeletedBySelf, selector: (user: misskey.entities.MeDetailed) => user.isDeleted },
// @ts-expect-error UserDetailedNotMe doesn't include isDeleted
{ label: '自分以外から見たときは削除済みか判定できない', user: () => userDeletedBySelf, selector: (user: misskey.entities.UserDetailedNotMe) => user.isDeleted, expected: () => undefined },
{ label: '削除済み(byAdmin)になっている', user: () => userDeletedByAdmin, me: () => userDeletedByAdmin, selector: (user: misskey.entities.MeDetailed) => user.isDeleted },
// @ts-expect-error UserDetailedNotMe doesn't include isDeleted
{ label: '自分以外から見たときは削除済み(byAdmin)か判定できない', user: () => userDeletedByAdmin, selector: (user: misskey.entities.UserDetailedNotMe) => user.isDeleted, expected: () => undefined },
{ label: 'フォロー中になっている', user: () => userFollowedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isFollowing },
{ label: 'フォローされている', user: () => userFollowingAlice, selector: (user: misskey.entities.UserDetailed) => user.isFollowed },
{ label: 'ブロック中になっている', user: () => userBlockedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isBlocking },
{ label: 'ブロックされている', user: () => userBlockingAlice, selector: (user: misskey.entities.UserDetailed) => user.isBlocked },
{ label: 'ミュート中になっている', user: () => userMutedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isMuted },
{ label: 'リノートミュート中になっている', user: () => userRnMutedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isRenoteMuted },
{ label: 'フォローリクエスト中になっている', user: () => userFollowRequested, me: () => userFollowRequesting, selector: (user: misskey.entities.UserDetailed) => user.hasPendingFollowRequestFromYou },
{ label: 'フォローリクエストされている', user: () => userFollowRequesting, me: () => userFollowRequested, selector: (user: misskey.entities.UserDetailed) => user.hasPendingFollowRequestToYou },
] as const)('を取得することができ、$labelこと', async ({ user, me, selector, expected }) => {
const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: user().id }, user: me?.() ?? alice });
assert.strictEqual(selector(response), (expected ?? ((): true => true))());
assert.strictEqual(selector(response as any), (expected ?? ((): true => true))());
});
test('を取得することができ、Publicなロールがセットされていること', async () => {
const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: userRolePublic.id }, user: alice });
@ -694,17 +674,18 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response, expected);
});
test.each([
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice },
{ label: '承認制ユーザーが含まれる', user: (): User => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced },
{ label: 'サスペンドユーザーが(モデレーターが見るときは)含まれる', user: (): User => userSuspended, me: (): User => root },
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice },
{ label: '承認制ユーザーが含まれる', user: () => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
{ label: 'サスペンドユーザーが(モデレーターが見るときは)含まれる', user: () => userSuspended, me: () => root },
// BUG サスペンドユーザーを一般ユーザーから見るとrootユーザーが返ってくる
//{ label: 'サスペンドユーザーが(一般ユーザーが見るときは)含まれない', user: (): User => userSuspended, me: (): User => bob, excluded: true },
{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin },
//{ label: 'サスペンドユーザーが(一般ユーザーが見るときは)含まれない', user: () => userSuspended, me: () => bob, excluded: true },
{ label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
// @ts-expect-error excluded は上でコメントアウトされているので
] as const)('をID指定のリスト形式で取得することができ、結果に$label', async ({ user, me, excluded }) => {
const parameters = { userIds: [user().id] };
const response = await successfulApiCall({ endpoint: 'users/show', parameters, user: me?.() ?? alice });
@ -729,15 +710,15 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response, expected);
});
test.each([
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice },
{ label: '承認制ユーザーが含まれる', user: (): User => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced },
{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin },
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice },
{ label: '承認制ユーザーが含まれる', user: () => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
{ label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
] as const)('を検索することができ、結果に$labelが含まれる', async ({ user, excluded }) => {
const parameters = { query: user().username, limit: 1 };
const response = await successfulApiCall({ endpoint: 'users/search', parameters, user: alice });
@ -751,30 +732,30 @@ describe('ユーザー', () => {
//#region ID指定検索(users/search-by-username-and-host)
test.each([
{ label: '自分', parameters: { username: 'alice' }, user: (): User[] => [alice] },
{ label: '自分かつusernameが大文字', parameters: { username: 'ALICE' }, user: (): User[] => [alice] },
{ label: 'ローカルのフォロイーでノートなし', parameters: { username: 'userFollowedByAlice' }, user: (): User[] => [userFollowedByAlice] },
{ label: 'ローカルでノートなしは検索に載らない', parameters: { username: 'userNoNote' }, user: (): User[] => [] },
{ label: 'ローカルの他人1', parameters: { username: 'bob' }, user: (): User[] => [bob] },
{ label: 'ローカルの他人2', parameters: { username: 'bob', host: null }, user: (): User[] => [bob] },
{ label: 'ローカルの他人3', parameters: { username: 'bob', host: '.' }, user: (): User[] => [bob] },
{ label: 'ローカル', parameters: { host: null, limit: 1 }, user: (): User[] => [userFollowedByAlice] },
{ label: 'ローカル', parameters: { host: '.', limit: 1 }, user: (): User[] => [userFollowedByAlice] },
{ label: '自分', parameters: { username: 'alice' }, user: () => [alice] },
{ label: '自分かつusernameが大文字', parameters: { username: 'ALICE' }, user: () => [alice] },
{ label: 'ローカルのフォロイーでノートなし', parameters: { username: 'userFollowedByAlice' }, user: () => [userFollowedByAlice] },
{ label: 'ローカルでノートなしは検索に載らない', parameters: { username: 'userNoNote' }, user: () => [] },
{ label: 'ローカルの他人1', parameters: { username: 'bob' }, user: () => [bob] },
{ label: 'ローカルの他人2', parameters: { username: 'bob', host: null }, user: () => [bob] },
{ label: 'ローカルの他人3', parameters: { username: 'bob', host: '.' }, user: () => [bob] },
{ label: 'ローカル', parameters: { host: null, limit: 1 }, user: () => [userFollowedByAlice] },
{ label: 'ローカル', parameters: { host: '.', limit: 1 }, user: () => [userFollowedByAlice] },
])('をID&ホスト指定で検索できる($label)', async ({ parameters, user }) => {
const response = await successfulApiCall({ endpoint: 'users/search-by-username-and-host', parameters, user: alice });
const expected = await Promise.all(user().map(u => show(u.id, alice)));
assert.deepStrictEqual(response, expected);
});
test.each([
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice },
{ label: '承認制ユーザーが含まれる', user: (): User => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced },
{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin },
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice },
{ label: '承認制ユーザーが含まれる', user: () => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
{ label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
] as const)('をID&ホスト指定で検索でき、結果に$label', async ({ user, excluded }) => {
const parameters = { username: user().username };
const response = await successfulApiCall({ endpoint: 'users/search-by-username-and-host', parameters, user: alice });
@ -796,15 +777,15 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response, expected);
});
test.each([
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれない', user: (): User => userBlockingAlice, excluded: true },
{ label: '承認制ユーザーが含まれる', user: (): User => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced },
//{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin },
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれない', user: () => userBlockingAlice, excluded: true },
{ label: '承認制ユーザーが含まれる', user: () => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
//{ label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
] as const)('がよくリプライをするユーザーのリストを取得でき、結果に$label', async ({ user, excluded }) => {
const replyTo = (await successfulApiCall({ endpoint: 'users/notes', parameters: { userId: user().id }, user: undefined }))[0];
await post(alice, { text: `@${user().username} test`, replyId: replyTo.id });
@ -818,12 +799,12 @@ describe('ユーザー', () => {
//#region ハッシュタグ(hashtags/users)
test.each([
{ label: 'フォロワー昇順', sort: { sort: '+follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) },
{ label: 'フォロワー降順', sort: { sort: '-follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) },
{ label: '登録日時昇順', sort: { sort: '+createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt },
{ label: '登録日時降順', sort: { sort: '-createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt },
{ label: '投稿日時昇順', sort: { sort: '+updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) },
{ label: '投稿日時降順', sort: { sort: '-updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) },
{ label: 'フォロワー昇順', sort: { sort: '+follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) },
{ label: 'フォロワー降順', sort: { sort: '-follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) },
{ label: '登録日時昇順', sort: { sort: '+createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt },
{ label: '登録日時降順', sort: { sort: '-createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt },
{ label: '投稿日時昇順', sort: { sort: '+updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) },
{ label: '投稿日時降順', sort: { sort: '-updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) },
] as const)('をハッシュタグ指定で取得することができる($label)', async ({ sort, selector }) => {
const hashtag = 'test_hashtag';
await successfulApiCall({ endpoint: 'i/update', parameters: { description: `#${hashtag}` }, user: alice });
@ -837,15 +818,15 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response, expected);
});
test.each([
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice },
{ label: '承認制ユーザーが含まれる', user: (): User => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced },
{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin },
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice },
{ label: '承認制ユーザーが含まれる', user: () => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
{ label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
] as const)('をハッシュタグ指定で取得することができ、結果に$label', async ({ user, excluded }) => {
const hashtag = `user_test${user().username}`;
if (user() !== userSuspended) {

7
packages/backend/test/global.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type FIXME = any;

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { initTestDb, sendEnvResetRequest } from './utils.js';
beforeAll(async () => {

View File

@ -4,10 +4,10 @@
*/
import Ajv from 'ajv';
import { Schema } from '@/misc/schema';
import { Schema } from '@/misc/json-schema.js';
export const getValidator = (paramDef: Schema) => {
const ajv = new Ajv({
const ajv = new Ajv.default({
useDefaults: true,
});
ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);

View File

@ -5,7 +5,7 @@
"noImplicitAny": true,
"noImplicitReturns": true,
"noUnusedParameters": false,
"noUnusedLocals": true,
"noUnusedLocals": false,
"noFallthroughCasesInSwitch": true,
"declaration": false,
"sourceMap": true,
@ -18,6 +18,7 @@
"strict": true,
"strictNullChecks": true,
"strictPropertyInitialization": false,
"skipLibCheck": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"resolveJsonModule": true,

View File

@ -51,7 +51,7 @@ describe('AnnouncementService', () => {
function createAnnouncement(data: Partial<MiAnnouncement & { createdAt: Date }> = {}) {
return announcementsRepository.insert({
id: genAidx(data.createdAt ?? new Date()),
id: genAidx(data.createdAt?.getTime() ?? Date.now()),
updatedAt: null,
title: 'Title',
text: 'Text',

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as assert from 'assert';
import { Test } from '@nestjs/testing';

View File

@ -19,8 +19,8 @@ import { DI } from '@/di-symbols.js';
import type { TestingModule } from '@nestjs/testing';
function mockRedis() {
const hash = {};
const set = jest.fn((key, value) => {
const hash = {} as any;
const set = jest.fn((key: string, value) => {
const ret = hash[key];
hash[key] = value;
return ret;
@ -56,12 +56,13 @@ describe('FetchInstanceMetadataService', () => {
} else if (token === DI.redis) {
return mockRedis;
}
return null;
})
.compile();
app.enableShutdownHooks();
fetchInstanceMetadataService = app.get<FetchInstanceMetadataService>(FetchInstanceMetadataService);
fetchInstanceMetadataService = app.get<FetchInstanceMetadataService>(FetchInstanceMetadataService) as jest.Mocked<FetchInstanceMetadataService>;
federatedInstanceService = app.get<FederatedInstanceService>(FederatedInstanceService) as jest.Mocked<FederatedInstanceService>;
redisClient = app.get<Redis>(DI.redis) as jest.Mocked<Redis>;
httpRequestService = app.get<HttpRequestService>(HttpRequestService) as jest.Mocked<HttpRequestService>;
@ -74,11 +75,12 @@ describe('FetchInstanceMetadataService', () => {
test('Lock and update', async () => {
redisClient.set = mockRedis();
const now = Date.now();
federatedInstanceService.fetch.mockReturnValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } });
federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); });
const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' });
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
expect(tryLockSpy).toHaveBeenCalledTimes(1);
expect(unlockSpy).toHaveBeenCalledTimes(1);
expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1);
@ -88,11 +90,12 @@ describe('FetchInstanceMetadataService', () => {
test('Lock and don\'t update', async () => {
redisClient.set = mockRedis();
const now = Date.now();
federatedInstanceService.fetch.mockReturnValue({ infoUpdatedAt: { getTime: () => now } });
federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); });
const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' });
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
expect(tryLockSpy).toHaveBeenCalledTimes(1);
expect(unlockSpy).toHaveBeenCalledTimes(1);
expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1);
@ -101,15 +104,33 @@ describe('FetchInstanceMetadataService', () => {
test('Do nothing when lock not acquired', async () => {
redisClient.set = mockRedis();
federatedInstanceService.fetch.mockReturnValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } });
const now = Date.now();
federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); });
await fetchInstanceMetadataService.tryLock('example.com');
const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
await fetchInstanceMetadataService.tryLock('example.com');
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' });
expect(tryLockSpy).toHaveBeenCalledTimes(2);
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
expect(tryLockSpy).toHaveBeenCalledTimes(1);
expect(unlockSpy).toHaveBeenCalledTimes(0);
expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0);
expect(httpRequestService.getJson).toHaveBeenCalledTimes(0);
});
test('Do when lock not acquired but forced', async () => {
redisClient.set = mockRedis();
const now = Date.now();
federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); });
await fetchInstanceMetadataService.tryLock('example.com');
const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any, true);
expect(tryLockSpy).toHaveBeenCalledTimes(0);
expect(unlockSpy).toHaveBeenCalledTimes(1);
expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0);
expect(httpRequestService.getJson).toHaveBeenCalled();
});
});

View File

@ -90,7 +90,8 @@ describe('RelayService', () => {
expect(queueService.deliver).toHaveBeenCalled();
expect(queueService.deliver.mock.lastCall![1]?.type).toBe('Undo');
expect(queueService.deliver.mock.lastCall![1]?.object.type).toBe('Follow');
expect(typeof queueService.deliver.mock.lastCall![1]?.object).toBe('object');
expect((queueService.deliver.mock.lastCall![1]?.object as any).type).toBe('Follow');
expect(queueService.deliver.mock.lastCall![2]).toBe('https://example.com');
//expect(queueService.deliver.mock.lastCall![0].username).toBe('relay.actor');

View File

@ -228,11 +228,14 @@ describe('RoleService', () => {
},
target: 'conditional',
condFormula: {
id: '232a4221-9816-49a6-a967-ae0fac52ec5e',
type: 'and',
values: [{
id: '2a37ef43-2d93-4c4d-87f6-f2fdb7d9b530',
type: 'followersMoreThanOrEq',
value: 10,
}, {
id: '1bd67839-b126-4f92-bad0-4e285dab453b',
type: 'createdMoreThan',
sec: 60 * 60 * 24 * 7,
}],

View File

@ -0,0 +1,528 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Test, TestingModule } from '@nestjs/testing';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { GlobalModule } from '@/GlobalModule.js';
import { CoreModule } from '@/core/CoreModule.js';
import type { MiUser } from '@/models/User.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import { genAidx } from '@/misc/id/aidx.js';
import {
BlockingsRepository,
FollowingsRepository, FollowRequestsRepository,
MiUserProfile, MutingsRepository, RenoteMutingsRepository,
UserMemoRepository,
UserProfilesRepository,
UsersRepository,
} from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { PageEntityService } from '@/core/entities/PageEntityService.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { AnnouncementService } from '@/core/AnnouncementService.js';
import { RoleService } from '@/core/RoleService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { IdService } from '@/core/IdService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { MetaService } from '@/core/MetaService.js';
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
import { CacheService } from '@/core/CacheService.js';
import { ApResolverService } from '@/core/activitypub/ApResolverService.js';
import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
import { ApImageService } from '@/core/activitypub/models/ApImageService.js';
import { ApMfmService } from '@/core/activitypub/ApMfmService.js';
import { MfmService } from '@/core/MfmService.js';
import { HashtagService } from '@/core/HashtagService.js';
import UsersChart from '@/core/chart/charts/users.js';
import { ChartLoggerService } from '@/core/chart/ChartLoggerService.js';
import InstanceChart from '@/core/chart/charts/instance.js';
import { ApLoggerService } from '@/core/activitypub/ApLoggerService.js';
import { AccountMoveService } from '@/core/AccountMoveService.js';
import { ReactionService } from '@/core/ReactionService.js';
import { NotificationService } from '@/core/NotificationService.js';
process.env.NODE_ENV = 'test';
describe('UserEntityService', () => {
describe('pack/packMany', () => {
let app: TestingModule;
let service: UserEntityService;
let usersRepository: UsersRepository;
let userProfileRepository: UserProfilesRepository;
let userMemosRepository: UserMemoRepository;
let followingRepository: FollowingsRepository;
let followingRequestRepository: FollowRequestsRepository;
let blockingRepository: BlockingsRepository;
let mutingRepository: MutingsRepository;
let renoteMutingsRepository: RenoteMutingsRepository;
async function createUser(userData: Partial<MiUser> = {}, profileData: Partial<MiUserProfile> = {}) {
const un = secureRndstr(16);
const user = await usersRepository
.insert({
...userData,
id: genAidx(Date.now()),
username: un,
usernameLower: un,
})
.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
await userProfileRepository.insert({
...profileData,
userId: user.id,
});
return user;
}
async function memo(writer: MiUser, target: MiUser, memo: string) {
await userMemosRepository.insert({
id: genAidx(Date.now()),
userId: writer.id,
targetUserId: target.id,
memo,
});
}
async function follow(follower: MiUser, followee: MiUser) {
await followingRepository.insert({
id: genAidx(Date.now()),
followerId: follower.id,
followeeId: followee.id,
});
}
async function requestFollow(requester: MiUser, requestee: MiUser) {
await followingRequestRepository.insert({
id: genAidx(Date.now()),
followerId: requester.id,
followeeId: requestee.id,
});
}
async function block(blocker: MiUser, blockee: MiUser) {
await blockingRepository.insert({
id: genAidx(Date.now()),
blockerId: blocker.id,
blockeeId: blockee.id,
});
}
async function mute(mutant: MiUser, mutee: MiUser) {
await mutingRepository.insert({
id: genAidx(Date.now()),
muterId: mutant.id,
muteeId: mutee.id,
});
}
async function muteRenote(mutant: MiUser, mutee: MiUser) {
await renoteMutingsRepository.insert({
id: genAidx(Date.now()),
muterId: mutant.id,
muteeId: mutee.id,
});
}
function randomIntRange(weight = 10) {
return [...Array(Math.floor(Math.random() * weight))].map((it, idx) => idx);
}
beforeAll(async () => {
const services = [
UserEntityService,
ApPersonService,
NoteEntityService,
PageEntityService,
CustomEmojiService,
AnnouncementService,
RoleService,
FederatedInstanceService,
IdService,
AvatarDecorationService,
UtilityService,
EmojiEntityService,
ModerationLogService,
GlobalEventService,
DriveFileEntityService,
MetaService,
FetchInstanceMetadataService,
CacheService,
ApResolverService,
ApNoteService,
ApImageService,
ApMfmService,
MfmService,
HashtagService,
UsersChart,
ChartLoggerService,
InstanceChart,
ApLoggerService,
AccountMoveService,
ReactionService,
NotificationService,
];
app = await Test.createTestingModule({
imports: [GlobalModule, CoreModule],
providers: [
...services,
...services.map(x => ({ provide: x.name, useExisting: x })),
],
}).compile();
await app.init();
app.enableShutdownHooks();
service = app.get<UserEntityService>(UserEntityService);
usersRepository = app.get<UsersRepository>(DI.usersRepository);
userProfileRepository = app.get<UserProfilesRepository>(DI.userProfilesRepository);
userMemosRepository = app.get<UserMemoRepository>(DI.userMemosRepository);
followingRepository = app.get<FollowingsRepository>(DI.followingsRepository);
followingRequestRepository = app.get<FollowRequestsRepository>(DI.followRequestsRepository);
blockingRepository = app.get<BlockingsRepository>(DI.blockingsRepository);
mutingRepository = app.get<MutingsRepository>(DI.mutingsRepository);
renoteMutingsRepository = app.get<RenoteMutingsRepository>(DI.renoteMutingsRepository);
});
afterAll(async () => {
await app.close();
});
test('UserLite', async() => {
const me = await createUser();
const who = await createUser();
await memo(me, who, 'memo');
const actual = await service.pack(who, me, { schema: 'UserLite' }) as any;
// no detail
expect(actual.memo).toBeUndefined();
// no detail and me
expect(actual.birthday).toBeUndefined();
// no detail and me
expect(actual.achievements).toBeUndefined();
});
test('UserDetailedNotMe', async() => {
const me = await createUser();
const who = await createUser({}, { birthday: '2000-01-01' });
await memo(me, who, 'memo');
const actual = await service.pack(who, me, { schema: 'UserDetailedNotMe' }) as any;
// is detail
expect(actual.memo).toBe('memo');
// is detail
expect(actual.birthday).toBe('2000-01-01');
// no detail and me
expect(actual.achievements).toBeUndefined();
});
test('MeDetailed', async() => {
const achievements = [{ name: 'achievement', unlockedAt: new Date().getTime() }];
const me = await createUser({}, {
birthday: '2000-01-01',
achievements: achievements,
});
await memo(me, me, 'memo');
const actual = await service.pack(me, me, { schema: 'MeDetailed' }) as any;
// is detail
expect(actual.memo).toBe('memo');
// is detail
expect(actual.birthday).toBe('2000-01-01');
// is detail and me
expect(actual.achievements).toEqual(achievements);
});
describe('packManyによるpreloadがある時、preloadが無い時とpackの結果が同じになるか見たい', () => {
test('no-preload', async() => {
const me = await createUser();
// meがフォローしてる人たち
const followeeMe = await Promise.all(randomIntRange().map(() => createUser()));
for (const who of followeeMe) {
await follow(me, who);
const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
expect(actual.isFollowing).toBe(true);
expect(actual.isFollowed).toBe(false);
expect(actual.hasPendingFollowRequestFromYou).toBe(false);
expect(actual.hasPendingFollowRequestToYou).toBe(false);
expect(actual.isBlocking).toBe(false);
expect(actual.isBlocked).toBe(false);
expect(actual.isMuted).toBe(false);
expect(actual.isRenoteMuted).toBe(false);
}
// meをフォローしてる人たち
const followerMe = await Promise.all(randomIntRange().map(() => createUser()));
for (const who of followerMe) {
await follow(who, me);
const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
expect(actual.isFollowing).toBe(false);
expect(actual.isFollowed).toBe(true);
expect(actual.hasPendingFollowRequestFromYou).toBe(false);
expect(actual.hasPendingFollowRequestToYou).toBe(false);
expect(actual.isBlocking).toBe(false);
expect(actual.isBlocked).toBe(false);
expect(actual.isMuted).toBe(false);
expect(actual.isRenoteMuted).toBe(false);
}
// meがフォローリクエストを送った人たち
const requestsFromYou = await Promise.all(randomIntRange().map(() => createUser()));
for (const who of requestsFromYou) {
await requestFollow(me, who);
const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
expect(actual.isFollowing).toBe(false);
expect(actual.isFollowed).toBe(false);
expect(actual.hasPendingFollowRequestFromYou).toBe(true);
expect(actual.hasPendingFollowRequestToYou).toBe(false);
expect(actual.isBlocking).toBe(false);
expect(actual.isBlocked).toBe(false);
expect(actual.isMuted).toBe(false);
expect(actual.isRenoteMuted).toBe(false);
}
// meにフォローリクエストを送った人たち
const requestsToYou = await Promise.all(randomIntRange().map(() => createUser()));
for (const who of requestsToYou) {
await requestFollow(who, me);
const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
expect(actual.isFollowing).toBe(false);
expect(actual.isFollowed).toBe(false);
expect(actual.hasPendingFollowRequestFromYou).toBe(false);
expect(actual.hasPendingFollowRequestToYou).toBe(true);
expect(actual.isBlocking).toBe(false);
expect(actual.isBlocked).toBe(false);
expect(actual.isMuted).toBe(false);
expect(actual.isRenoteMuted).toBe(false);
}
// meがブロックしてる人たち
const blockingYou = await Promise.all(randomIntRange().map(() => createUser()));
for (const who of blockingYou) {
await block(me, who);
const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
expect(actual.isFollowing).toBe(false);
expect(actual.isFollowed).toBe(false);
expect(actual.hasPendingFollowRequestFromYou).toBe(false);
expect(actual.hasPendingFollowRequestToYou).toBe(false);
expect(actual.isBlocking).toBe(true);
expect(actual.isBlocked).toBe(false);
expect(actual.isMuted).toBe(false);
expect(actual.isRenoteMuted).toBe(false);
}
// meをブロックしてる人たち
const blockingMe = await Promise.all(randomIntRange().map(() => createUser()));
for (const who of blockingMe) {
await block(who, me);
const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
expect(actual.isFollowing).toBe(false);
expect(actual.isFollowed).toBe(false);
expect(actual.hasPendingFollowRequestFromYou).toBe(false);
expect(actual.hasPendingFollowRequestToYou).toBe(false);
expect(actual.isBlocking).toBe(false);
expect(actual.isBlocked).toBe(true);
expect(actual.isMuted).toBe(false);
expect(actual.isRenoteMuted).toBe(false);
}
// meがミュートしてる人たち
const muters = await Promise.all(randomIntRange().map(() => createUser()));
for (const who of muters) {
await mute(me, who);
const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
expect(actual.isFollowing).toBe(false);
expect(actual.isFollowed).toBe(false);
expect(actual.hasPendingFollowRequestFromYou).toBe(false);
expect(actual.hasPendingFollowRequestToYou).toBe(false);
expect(actual.isBlocking).toBe(false);
expect(actual.isBlocked).toBe(false);
expect(actual.isMuted).toBe(true);
expect(actual.isRenoteMuted).toBe(false);
}
// meがリートミュートしてる人たち
const renoteMuters = await Promise.all(randomIntRange().map(() => createUser()));
for (const who of renoteMuters) {
await muteRenote(me, who);
const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
expect(actual.isFollowing).toBe(false);
expect(actual.isFollowed).toBe(false);
expect(actual.hasPendingFollowRequestFromYou).toBe(false);
expect(actual.hasPendingFollowRequestToYou).toBe(false);
expect(actual.isBlocking).toBe(false);
expect(actual.isBlocked).toBe(false);
expect(actual.isMuted).toBe(false);
expect(actual.isRenoteMuted).toBe(true);
}
});
test('preload', async() => {
const me = await createUser();
{
// meがフォローしてる人たち
const followeeMe = await Promise.all(randomIntRange().map(() => createUser()));
for (const who of followeeMe) {
await follow(me, who);
}
const actualList = await service.packMany(followeeMe, me, { schema: 'UserDetailed' }) as any;
for (const actual of actualList) {
expect(actual.isFollowing).toBe(true);
expect(actual.isFollowed).toBe(false);
expect(actual.hasPendingFollowRequestFromYou).toBe(false);
expect(actual.hasPendingFollowRequestToYou).toBe(false);
expect(actual.isBlocking).toBe(false);
expect(actual.isBlocked).toBe(false);
expect(actual.isMuted).toBe(false);
expect(actual.isRenoteMuted).toBe(false);
}
}
{
// meをフォローしてる人たち
const followerMe = await Promise.all(randomIntRange().map(() => createUser()));
for (const who of followerMe) {
await follow(who, me);
}
const actualList = await service.packMany(followerMe, me, { schema: 'UserDetailed' }) as any;
for (const actual of actualList) {
expect(actual.isFollowing).toBe(false);
expect(actual.isFollowed).toBe(true);
expect(actual.hasPendingFollowRequestFromYou).toBe(false);
expect(actual.hasPendingFollowRequestToYou).toBe(false);
expect(actual.isBlocking).toBe(false);
expect(actual.isBlocked).toBe(false);
expect(actual.isMuted).toBe(false);
expect(actual.isRenoteMuted).toBe(false);
}
}
{
// meがフォローリクエストを送った人たち
const requestsFromYou = await Promise.all(randomIntRange().map(() => createUser()));
for (const who of requestsFromYou) {
await requestFollow(me, who);
}
const actualList = await service.packMany(requestsFromYou, me, { schema: 'UserDetailed' }) as any;
for (const actual of actualList) {
expect(actual.isFollowing).toBe(false);
expect(actual.isFollowed).toBe(false);
expect(actual.hasPendingFollowRequestFromYou).toBe(true);
expect(actual.hasPendingFollowRequestToYou).toBe(false);
expect(actual.isBlocking).toBe(false);
expect(actual.isBlocked).toBe(false);
expect(actual.isMuted).toBe(false);
expect(actual.isRenoteMuted).toBe(false);
}
}
{
// meにフォローリクエストを送った人たち
const requestsToYou = await Promise.all(randomIntRange().map(() => createUser()));
for (const who of requestsToYou) {
await requestFollow(who, me);
}
const actualList = await service.packMany(requestsToYou, me, { schema: 'UserDetailed' }) as any;
for (const actual of actualList) {
expect(actual.isFollowing).toBe(false);
expect(actual.isFollowed).toBe(false);
expect(actual.hasPendingFollowRequestFromYou).toBe(false);
expect(actual.hasPendingFollowRequestToYou).toBe(true);
expect(actual.isBlocking).toBe(false);
expect(actual.isBlocked).toBe(false);
expect(actual.isMuted).toBe(false);
expect(actual.isRenoteMuted).toBe(false);
}
}
{
// meがブロックしてる人たち
const blockingYou = await Promise.all(randomIntRange().map(() => createUser()));
for (const who of blockingYou) {
await block(me, who);
}
const actualList = await service.packMany(blockingYou, me, { schema: 'UserDetailed' }) as any;
for (const actual of actualList) {
expect(actual.isFollowing).toBe(false);
expect(actual.isFollowed).toBe(false);
expect(actual.hasPendingFollowRequestFromYou).toBe(false);
expect(actual.hasPendingFollowRequestToYou).toBe(false);
expect(actual.isBlocking).toBe(true);
expect(actual.isBlocked).toBe(false);
expect(actual.isMuted).toBe(false);
expect(actual.isRenoteMuted).toBe(false);
}
}
{
// meをブロックしてる人たち
const blockingMe = await Promise.all(randomIntRange().map(() => createUser()));
for (const who of blockingMe) {
await block(who, me);
}
const actualList = await service.packMany(blockingMe, me, { schema: 'UserDetailed' }) as any;
for (const actual of actualList) {
expect(actual.isFollowing).toBe(false);
expect(actual.isFollowed).toBe(false);
expect(actual.hasPendingFollowRequestFromYou).toBe(false);
expect(actual.hasPendingFollowRequestToYou).toBe(false);
expect(actual.isBlocking).toBe(false);
expect(actual.isBlocked).toBe(true);
expect(actual.isMuted).toBe(false);
expect(actual.isRenoteMuted).toBe(false);
}
}
{
// meがミュートしてる人たち
const muters = await Promise.all(randomIntRange().map(() => createUser()));
for (const who of muters) {
await mute(me, who);
}
const actualList = await service.packMany(muters, me, { schema: 'UserDetailed' }) as any;
for (const actual of actualList) {
expect(actual.isFollowing).toBe(false);
expect(actual.isFollowed).toBe(false);
expect(actual.hasPendingFollowRequestFromYou).toBe(false);
expect(actual.hasPendingFollowRequestToYou).toBe(false);
expect(actual.isBlocking).toBe(false);
expect(actual.isBlocked).toBe(false);
expect(actual.isMuted).toBe(true);
expect(actual.isRenoteMuted).toBe(false);
}
}
{
// meがリートミュートしてる人たち
const renoteMuters = await Promise.all(randomIntRange().map(() => createUser()));
for (const who of renoteMuters) {
await muteRenote(me, who);
}
const actualList = await service.packMany(renoteMuters, me, { schema: 'UserDetailed' }) as any;
for (const actual of actualList) {
expect(actual.isFollowing).toBe(false);
expect(actual.isFollowed).toBe(false);
expect(actual.hasPendingFollowRequestFromYou).toBe(false);
expect(actual.hasPendingFollowRequestToYou).toBe(false);
expect(actual.isBlocking).toBe(false);
expect(actual.isBlocked).toBe(false);
expect(actual.isMuted).toBe(false);
expect(actual.isRenoteMuted).toBe(true);
}
}
});
});
});
});

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { DebounceLoader } from '@/misc/loader.js';
class Mock {

View File

@ -9,11 +9,10 @@ import { basename, isAbsolute } from 'node:path';
import { randomUUID } from 'node:crypto';
import { inspect } from 'node:util';
import WebSocket, { ClientOptions } from 'ws';
import fetch, { File, RequestInit } from 'node-fetch';
import fetch, { File, RequestInit, type Headers } from 'node-fetch';
import { DataSource } from 'typeorm';
import { JSDOM } from 'jsdom';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import { Packed } from '@/misc/json-schema.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import { entities } from '../src/postgres.js';
import { loadConfig } from '../src/config.js';
@ -21,7 +20,7 @@ import type * as misskey from 'misskey-js';
export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js';
interface UserToken {
export interface UserToken {
token: string;
bearer?: boolean;
}
@ -35,20 +34,15 @@ export const cookie = (me: UserToken): string => {
return `token=${me.token};`;
};
export const api = async (endpoint: string, params: any, me?: UserToken) => {
const normalized = endpoint.replace(/^\//, '');
return await request(`api/${normalized}`, params, me);
};
export type ApiRequest = {
endpoint: string,
parameters: object,
export type ApiRequest<E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req'] = misskey.Endpoints[E]['req']> = {
endpoint: E,
parameters: P,
user: UserToken | undefined,
};
export const successfulApiCall = async <T, >(request: ApiRequest, assertion: {
export const successfulApiCall = async <E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req']>(request: ApiRequest<E, P>, assertion: {
status?: number,
} = {}): Promise<T> => {
} = {}): Promise<misskey.api.SwitchCaseResponseType<E, P>> => {
const { endpoint, parameters, user } = request;
const res = await api(endpoint, parameters, user);
const status = assertion.status ?? (res.body == null ? 204 : 200);
@ -56,7 +50,7 @@ export const successfulApiCall = async <T, >(request: ApiRequest, assertion: {
return res.body;
};
export const failedApiCall = async <T, >(request: ApiRequest, assertion: {
export const failedApiCall = async <T, E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req']>(request: ApiRequest<E, P>, assertion: {
status: number,
code: string,
id: string
@ -70,7 +64,7 @@ export const failedApiCall = async <T, >(request: ApiRequest, assertion: {
return res.body;
};
const request = async (path: string, params: any, me?: UserToken): Promise<{
export const api = async <E extends keyof misskey.Endpoints>(path: E, params: misskey.Endpoints[E]['req'], me?: UserToken): Promise<{
status: number,
headers: Headers,
body: any
@ -86,7 +80,7 @@ const request = async (path: string, params: any, me?: UserToken): Promise<{
bodyAuth.i = me.token;
}
const res = await relativeFetch(path, {
const res = await relativeFetch(`api/${path}`, {
method: 'POST',
headers,
body: JSON.stringify(Object.assign(bodyAuth, params)),
@ -141,7 +135,7 @@ export const signup = async (params?: Partial<misskey.Endpoints['signup']['req']
return res.body;
};
export const post = async (user: UserToken, params?: misskey.Endpoints['notes/create']['req']): Promise<misskey.entities.Note> => {
export const post = async (user: UserToken, params: misskey.Endpoints['notes/create']['req']): Promise<misskey.entities.Note> => {
const q = params;
const res = await api('notes/create', q, user);
@ -159,8 +153,8 @@ export const createAppToken = async (user: UserToken, permissions: (typeof missk
};
// 非公開ートをAPI越しに見たときのート NoteEntityService.ts
export const hiddenNote = (note: any): any => {
const temp = {
export const hiddenNote = (note: misskey.entities.Note): misskey.entities.Note => {
const temp: misskey.entities.Note = {
...note,
fileIds: [],
files: [],
@ -173,21 +167,22 @@ export const hiddenNote = (note: any): any => {
return temp;
};
export const react = async (user: UserToken, note: any, reaction: string): Promise<any> => {
export const react = async (user: UserToken, note: misskey.entities.Note, reaction: string): Promise<void> => {
await api('notes/reactions/create', {
noteId: note.id,
reaction: reaction,
}, user);
};
export const userList = async (user: UserToken, userList: any = {}): Promise<any> => {
export const userList = async (user: UserToken, userList: Partial<misskey.entities.UserList> = {}): Promise<misskey.entities.UserList> => {
const res = await api('users/lists/create', {
name: 'test',
...userList,
}, user);
return res.body;
};
export const page = async (user: UserToken, page: any = {}): Promise<any> => {
export const page = async (user: UserToken, page: Partial<misskey.entities.Page> = {}): Promise<misskey.entities.Page> => {
const res = await api('pages/create', {
alignCenter: false,
content: [
@ -198,7 +193,7 @@ export const page = async (user: UserToken, page: any = {}): Promise<any> => {
},
],
eyeCatchingImageId: null,
font: 'sans-serif',
font: 'sans-serif' as any,
hideTitleWhenPinned: false,
name: '1678594845072',
script: '',
@ -210,7 +205,7 @@ export const page = async (user: UserToken, page: any = {}): Promise<any> => {
return res.body;
};
export const play = async (user: UserToken, play: any = {}): Promise<any> => {
export const play = async (user: UserToken, play: Partial<misskey.entities.Flash> = {}): Promise<misskey.entities.Flash> => {
const res = await api('flash/create', {
permissions: [],
script: 'test',
@ -221,7 +216,7 @@ export const play = async (user: UserToken, play: any = {}): Promise<any> => {
return res.body;
};
export const clip = async (user: UserToken, clip: any = {}): Promise<any> => {
export const clip = async (user: UserToken, clip: Partial<misskey.entities.Clip> = {}): Promise<misskey.entities.Clip> => {
const res = await api('clips/create', {
description: null,
isPublic: true,
@ -231,18 +226,18 @@ export const clip = async (user: UserToken, clip: any = {}): Promise<any> => {
return res.body;
};
export const galleryPost = async (user: UserToken, channel: any = {}): Promise<any> => {
export const galleryPost = async (user: UserToken, galleryPost: Partial<misskey.entities.GalleryPost> = {}): Promise<misskey.entities.GalleryPost> => {
const res = await api('gallery/posts/create', {
description: null,
fileIds: [],
isSensitive: false,
title: 'test',
...channel,
...galleryPost,
}, user);
return res.body;
};
export const channel = async (user: UserToken, channel: any = {}): Promise<any> => {
export const channel = async (user: UserToken, channel: Partial<misskey.entities.Channel> = {}): Promise<misskey.entities.Channel> => {
const res = await api('channels/create', {
bannerId: null,
description: null,
@ -252,7 +247,7 @@ export const channel = async (user: UserToken, channel: any = {}): Promise<any>
return res.body;
};
export const role = async (user: UserToken, role: any = {}, policies: any = {}): Promise<any> => {
export const role = async (user: UserToken, role: Partial<misskey.entities.Role> = {}, policies: any = {}): Promise<misskey.entities.Role> => {
const res = await api('admin/roles/create', {
asBadge: false,
canEditMembersByModerator: false,
@ -260,7 +255,7 @@ export const role = async (user: UserToken, role: any = {}, policies: any = {}):
condFormula: {
id: 'ebef1684-672d-49b6-ad82-1b3ec3784f85',
type: 'isRemote',
},
} as any,
description: '',
displayOrder: 0,
iconUrl: null,
@ -298,7 +293,7 @@ interface UploadOptions {
export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadOptions = {}): Promise<{
status: number,
headers: Headers,
body: misskey.Endpoints['drive/files/create']['res'] | null
body: misskey.entities.DriveFile | null
}> => {
const absPath = path == null
? new URL('resources/Lenna.jpg', import.meta.url)
@ -335,14 +330,14 @@ export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadO
};
};
export const uploadUrl = async (user: UserToken, url: string): Promise<Packed<'DriveFile'>> => {
export const uploadUrl = async (user: UserToken, url: string): Promise<misskey.entities.DriveFile> => {
const marker = Math.random().toString();
const catcher = makeStreamCatcher(
user,
'main',
(msg) => msg.type === 'urlUploadFinished' && msg.body.marker === marker,
(msg) => msg.body.file as Packed<'DriveFile'>,
(msg) => msg.body.file,
60 * 1000,
);

View File

@ -1,3 +1,8 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true" as="image" type="image/png" crossorigin="anonymous">
<link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true" as="image" type="image/jpeg" crossorigin="anonymous">
<link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@2.44.0/tabler-icons.min.css">

View File

@ -187,14 +187,26 @@ export async function mainBoot() {
if ($i.followersCount >= 500) claimAchievement('followers500');
if ($i.followersCount >= 1000) claimAchievement('followers1000');
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365) {
claimAchievement('passedSinceAccountCreated1');
}
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 2) {
claimAchievement('passedSinceAccountCreated2');
}
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 3) {
const createdAt = new Date($i.createdAt);
const createdAtThreeYearsLater = new Date($i.createdAt);
createdAtThreeYearsLater.setFullYear(createdAtThreeYearsLater.getFullYear() + 3);
if (now >= createdAtThreeYearsLater) {
claimAchievement('passedSinceAccountCreated3');
claimAchievement('passedSinceAccountCreated2');
claimAchievement('passedSinceAccountCreated1');
} else {
const createdAtTwoYearsLater = new Date($i.createdAt);
createdAtTwoYearsLater.setFullYear(createdAtTwoYearsLater.getFullYear() + 2);
if (now >= createdAtTwoYearsLater) {
claimAchievement('passedSinceAccountCreated2');
claimAchievement('passedSinceAccountCreated1');
} else {
const createdAtOneYearLater = new Date($i.createdAt);
createdAtOneYearLater.setFullYear(createdAtOneYearLater.getFullYear() + 1);
if (now >= createdAtOneYearLater) {
claimAchievement('passedSinceAccountCreated1');
}
}
}
if (claimedAchievements.length >= 30) {
@ -229,7 +241,7 @@ export async function mainBoot() {
const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt');
const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');
if (neverShowDonationInfo !== 'true' && (new Date($i.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)))) {
popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed');
}

View File

@ -29,13 +29,13 @@ const self = props.url.startsWith(local);
const attr = self ? 'to' : 'href';
const target = self ? null : '_blank';
const el = ref<HTMLElement>();
const el = ref<HTMLElement | { $el: HTMLElement }>();
useTooltip(el, (showing) => {
os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
showing,
url: props.url,
source: el.value,
source: el.value instanceof HTMLElement ? el.value : el.value?.$el,
}, {}, 'closed');
});
</script>

View File

@ -66,7 +66,7 @@ import * as os from '@/os.js';
import bytes from '@/filters/bytes.js';
import { hms } from '@/filters/hms.js';
import MkMediaRange from '@/components/MkMediaRange.vue';
import { iAmModerator } from '@/account.js';
import { $i, iAmModerator } from '@/account.js';
const props = defineProps<{
audio: Misskey.entities.DriveFile;
@ -96,8 +96,6 @@ function showMenu(ev: MouseEvent) {
if (iAmModerator) {
menu.push({
type: 'divider',
}, {
text: props.audio.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
icon: props.audio.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation',
danger: true,
@ -105,6 +103,17 @@ function showMenu(ev: MouseEvent) {
});
}
if ($i?.id === props.audio.userId) {
menu.push({
type: 'divider',
}, {
type: 'link' as const,
text: i18n.ts._fileViewer.title,
icon: 'ti ti-info-circle',
to: `/my/drive/file/${props.audio.id}`,
});
}
menuShowing.value = true;
os.popupMenu(menu, ev.currentTarget ?? ev.target, {
align: 'right',

View File

@ -59,7 +59,7 @@ import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { iAmModerator } from '@/account.js';
import { $i, iAmModerator } from '@/account.js';
const props = withDefaults(defineProps<{
image: Misskey.entities.DriveFile;
@ -114,6 +114,13 @@ function showMenu(ev: MouseEvent) {
action: () => {
os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true });
},
}] : []), ...($i?.id === props.image.userId ? [{
type: 'divider' as const,
}, {
type: 'link' as const,
text: i18n.ts._fileViewer.title,
icon: 'ti ti-info-circle',
to: `/my/drive/file/${props.image.id}`,
}] : [])], ev.currentTarget ?? ev.target);
}

View File

@ -94,7 +94,7 @@ import * as os from '@/os.js';
import { isFullscreenNotSupported } from '@/scripts/device-kind.js';
import hasAudio from '@/scripts/media-has-audio.js';
import MkMediaRange from '@/components/MkMediaRange.vue';
import { iAmModerator } from '@/account.js';
import { $i, iAmModerator } from '@/account.js';
const props = defineProps<{
video: Misskey.entities.DriveFile;
@ -122,8 +122,6 @@ function showMenu(ev: MouseEvent) {
if (iAmModerator) {
menu.push({
type: 'divider',
}, {
text: props.video.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
icon: props.video.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation',
danger: true,
@ -131,6 +129,17 @@ function showMenu(ev: MouseEvent) {
});
}
if ($i?.id === props.video.userId) {
menu.push({
type: 'divider',
}, {
type: 'link' as const,
text: i18n.ts._fileViewer.title,
icon: 'ti ti-info-circle',
to: `/my/drive/file/${props.video.id}`,
});
}
menuShowing.value = true;
os.popupMenu(menu, ev.currentTarget ?? ev.target, {
align: 'right',

View File

@ -93,7 +93,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
</div>
<MkReactionsViewer :note="appearNote" :maxNumber="16" @mockUpdateMyReaction="emitUpdReaction">
<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" :note="appearNote" :maxNumber="16" @mockUpdateMyReaction="emitUpdReaction">
<template #more>
<div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div>
</template>
@ -101,7 +101,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<footer :class="$style.footer">
<button :class="$style.footerButton" class="_button" @click="reply()">
<i class="ti ti-arrow-back-up"></i>
<p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ appearNote.repliesCount }}</p>
<p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.repliesCount) }}</p>
</button>
<button
v-if="canRenote"
@ -111,17 +111,17 @@ SPDX-License-Identifier: AGPL-3.0-only
@mousedown="renote()"
>
<i class="ti ti-repeat"></i>
<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ appearNote.renoteCount }}</p>
<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p>
</button>
<button v-else :class="$style.footerButton" class="_button" disabled>
<i class="ti ti-ban"></i>
</button>
<button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.footerButton" class="_button" @mousedown="react()">
<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
<i v-else class="ti ti-plus"></i>
</button>
<button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" @click="undoReact(appearNote)">
<i class="ti ti-minus"></i>
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
</button>
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()">
<i class="ti ti-paperclip"></i>
@ -175,6 +175,7 @@ import { pleaseLogin } from '@/scripts/please-login.js';
import { focusPrev, focusNext } from '@/scripts/focus.js';
import { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js';
import number from '@/filters/number.js';
import * as os from '@/os.js';
import * as sound from '@/scripts/sound.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
@ -420,6 +421,14 @@ function undoReact(targetNote: Misskey.entities.Note): void {
});
}
function toggleReact() {
if (appearNote.value.myReaction == null) {
react();
} else {
undoReact(appearNote.value);
}
}
function onContextmenu(ev: MouseEvent): void {
if (props.mock) {
return;

View File

@ -106,10 +106,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkTime :time="appearNote.createdAt" mode="detail" colored/>
</MkA>
</div>
<MkReactionsViewer ref="reactionsViewer" :note="appearNote"/>
<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" :note="appearNote"/>
<button class="_button" :class="$style.noteFooterButton" @click="reply()">
<i class="ti ti-arrow-back-up"></i>
<p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ appearNote.repliesCount }}</p>
<p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.repliesCount) }}</p>
</button>
<button
v-if="canRenote"
@ -119,17 +119,17 @@ SPDX-License-Identifier: AGPL-3.0-only
@mousedown="renote()"
>
<i class="ti ti-repeat"></i>
<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ appearNote.renoteCount }}</p>
<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
</button>
<button v-else class="_button" :class="$style.noteFooterButton" disabled>
<i class="ti ti-ban"></i>
</button>
<button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.noteFooterButton" class="_button" @mousedown="react()">
<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
<i v-else class="ti ti-plus"></i>
</button>
<button v-if="appearNote.myReaction != null" ref="reactButton" class="_button" :class="[$style.noteFooterButton, $style.reacted]" @click="undoReact(appearNote)">
<i class="ti ti-minus"></i>
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
</button>
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()">
<i class="ti ti-paperclip"></i>
@ -209,6 +209,7 @@ import { pleaseLogin } from '@/scripts/please-login.js';
import { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js';
import { notePage } from '@/filters/note.js';
import number from '@/filters/number.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import * as sound from '@/scripts/sound.js';
@ -401,14 +402,22 @@ function react(viaKeyboard = false): void {
}
}
function undoReact(note): void {
const oldReaction = note.myReaction;
function undoReact(targetNote: Misskey.entities.Note): void {
const oldReaction = targetNote.myReaction;
if (!oldReaction) return;
misskeyApi('notes/reactions/delete', {
noteId: note.id,
noteId: targetNote.id,
});
}
function toggleReact() {
if (appearNote.value.myReaction == null) {
react();
} else {
undoReact(appearNote.value);
}
}
function onContextmenu(ev: MouseEvent): void {
const isLink = (el: HTMLElement): boolean => {
if (el.tagName === 'A') return true;

View File

@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.head">
<MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && notification.note" :class="$style.icon" :user="notification.note.user" link preview/>
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ti ti-heart" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
@ -57,6 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
<span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: notification.reactions.length }) }}</span>
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: notification.reactions.length }) }}</span>
<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
<span v-else-if="notification.type === 'app'">{{ notification.header }}</span>
@ -201,6 +203,7 @@ const rejectFollowRequest = () => {
}
.icon_reactionGroup,
.icon_reactionGroupHeart,
.icon_renoteGroup {
display: grid;
align-items: center;
@ -213,11 +216,15 @@ const rejectFollowRequest = () => {
}
.icon_reactionGroup {
background: #e99a0b;
background: var(--eventReaction);
}
.icon_reactionGroupHeart {
background: var(--eventReactionHeart);
}
.icon_renoteGroup {
background: #36d298;
background: var(--eventRenote);
}
.icon_app {
@ -246,49 +253,49 @@ const rejectFollowRequest = () => {
.t_follow, .t_followRequestAccepted, .t_receiveFollowRequest {
padding: 3px;
background: #36aed2;
background: var(--eventFollow);
pointer-events: none;
}
.t_renote {
padding: 3px;
background: #36d298;
background: var(--eventRenote);
pointer-events: none;
}
.t_quote {
padding: 3px;
background: #36d298;
background: var(--eventRenote);
pointer-events: none;
}
.t_reply {
padding: 3px;
background: #007aff;
background: var(--eventReply);
pointer-events: none;
}
.t_mention {
padding: 3px;
background: #88a6b7;
background: var(--eventOther);
pointer-events: none;
}
.t_pollEnded {
padding: 3px;
background: #88a6b7;
background: var(--eventOther);
pointer-events: none;
}
.t_achievementEarned {
padding: 3px;
background: #cb9a11;
background: var(--eventAchievement);
pointer-events: none;
}
.t_roleAssigned {
padding: 3px;
background: #88a6b7;
background: var(--eventOther);
pointer-events: none;
}

View File

@ -63,6 +63,7 @@ const exampleNote = reactive<Misskey.entities.Note>({
reactionAcceptance: null,
renoteCount: 0,
repliesCount: 1,
reactionCount: 0,
reactions: {},
reactionEmojis: {},
fileIds: [],

View File

@ -68,6 +68,7 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
reactionAcceptance: null,
renoteCount: 0,
repliesCount: 1,
reactionCount: 0,
reactions: {},
reactionEmojis: {},
fileIds: [],

View File

@ -58,6 +58,7 @@ const exampleNote = reactive<Misskey.entities.Note>({
reactionAcceptance: null,
renoteCount: 0,
repliesCount: 1,
reactionCount: 0,
reactions: {},
reactionEmojis: {},
fileIds: ['0000000002'],

View File

@ -1,3 +1,8 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<render/>
</template>

View File

@ -4,13 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<a :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu">
<a ref="el" :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu">
<slot></slot>
</a>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, shallowRef } from 'vue';
import * as os from '@/os.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { url } from '@/config.js';
@ -26,6 +26,10 @@ const props = withDefaults(defineProps<{
behavior: null,
});
const el = shallowRef<HTMLElement>();
defineExpose({ $el: el });
const router = useRouter();
const active = computed(() => {

View File

@ -14,10 +14,20 @@ SPDX-License-Identifier: AGPL-3.0-only
[$style.form_vertical]: chosen.place === 'vertical',
}]"
>
<a :href="chosen.url" target="_blank" :class="$style.link">
<component
:is="self ? 'MkA' : 'a'"
:class="$style.link"
v-bind="self ? {
to: chosen.url.substring(local.length),
} : {
href: chosen.url,
rel: 'nofollow noopener',
target: '_blank',
}"
>
<img :src="chosen.imageUrl" :class="$style.img">
<button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ti ti-info-circle"></i></button>
</a>
</component>
</div>
<div v-else :class="$style.menu">
<div :class="$style.menuContainer">
@ -32,10 +42,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { ref, computed } from 'vue';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { host } from '@/config.js';
import { url as local, host } from '@/config.js';
import MkButton from '@/components/MkButton.vue';
import { defaultStore } from '@/store.js';
import * as os from '@/os.js';
@ -96,6 +106,9 @@ const choseAd = (): Ad | null => {
};
const chosen = ref(choseAd());
const self = computed(() => chosen.value?.url.startsWith(local));
const shouldHide = ref(!defaultStore.state.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null));
function reduceFrequency(): void {

View File

@ -49,7 +49,7 @@ if (props.showUrlPreview) {
os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
showing,
url: props.url,
source: el.value,
source: el.value instanceof HTMLElement ? el.value : el.value?.$el,
}, {}, 'closed');
});
}

View File

@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export default (v, fractionDigits = 0) => {
if (v == null) return 'N/A';
if (v === 0) return '0';

View File

@ -373,7 +373,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
this.currentRoute.value = res.route;
this.currentKey = res.route.globalCacheKey ?? key ?? path;
if (emitChange) {
if (emitChange && res.route.path !== '/:(*)') {
this.emit('change', {
beforePath,
path,
@ -408,13 +408,17 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
if (cancel) return;
}
const res = this.navigate(path, null);
this.emit('push', {
beforePath,
path: res._parsedRoute.fullPath,
route: res.route,
props: res.props,
key: this.currentKey,
});
if (res.route.path === '/:(*)') {
location.href = path;
} else {
this.emit('push', {
beforePath,
path: res._parsedRoute.fullPath,
route: res.route,
props: res.props,
key: this.currentKey,
});
}
}
public replace(path: string, key?: string | null) {

View File

@ -324,6 +324,7 @@ const patrons = [
'てば',
'たっくん',
'SHO SEKIGUCHI',
'塩キャベツ',
];
const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure'));

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