summaryrefslogtreecommitdiff
path: root/packages/backend
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-02-26 20:21:54 +0900
committerGitHub <noreply@github.com>2023-02-26 20:21:54 +0900
commit02c8fd9de51b6a8471ab9a89f23bcfeaecd7626c (patch)
tree018a46cad9a19cc8cdfcff91442b343a637b56f8 /packages/backend
parentMerge pull request #10058 from misskey-dev/develop (diff)
parentMerge branch 'develop' of https://github.com/misskey-dev/misskey into develop (diff)
downloadmisskey-02c8fd9de51b6a8471ab9a89f23bcfeaecd7626c.tar.gz
misskey-02c8fd9de51b6a8471ab9a89f23bcfeaecd7626c.tar.bz2
misskey-02c8fd9de51b6a8471ab9a89f23bcfeaecd7626c.zip
Merge pull request #10108 from misskey-dev/develop
* Add dialog to remove follower (#9718) * update PULL_REQUEST_TEMPLATE * 起動時にRedisの疎通確認を行う (#9832) * 起動時にRedisの疎通確認を行う * check:connectをstart内に移動 --------- Co-authored-by: tamaina <tamaina@hotmail.co.jp> * Pass `--detectOpenHandles` to Jest (#9895) Co-authored-by: tamaina <tamaina@hotmail.co.jp> * enhance(client): MkUrlPreviewの閉じるボタンを見やすく (#9913) Co-authored-by: tamaina <tamaina@hotmail.co.jp> * test(backend): restore ap-request tests (#9997) Co-authored-by: tamaina <tamaina@hotmail.co.jp> * fix/refaftor(client): MkTime.vueの変更 (#10061) * fix(client): MkTime.timeにstringでもDateでない値が入った場合、?を表示 * fix(client): MkTimeを改良 * numberを許容 * falsyな値もとる * 不明 * ありません * fix * fix(server): notes/createで、fileIdsと見つかったファイルの数が異なる場合はエラーにする (#9911) * fix(server): notes/createで、fileIdsと見つかったファイルの数が異なる場合はエラーにする * NO_SUCH_FILE * Update codecov.yml * Update apple-touch-icon.png * デプロイされているプレビュー環境がない場合はプレビュー環境を削除しないようにする (#10062) * デプロイされているプレビュー環境がない場合はDestroy preview environmentを実行しないようにする * CIがない場合の処理追加 * enhance(client): improve clip menu ux * 未知のユーザーが deleteActor されたら処理をスキップする (#10067) * fix(client): Android ChromeでPWAとしてインストールできない問題を修正 (#10069) * fix(client): Android ChromeでPWAとしてインストールできない問題を修正 * 順番関係ある? * Windows環境でswcを使うと正常にビルドができない問題の修正 (#10074) * Update @swc/core to v1.3.36 * Update CHANGELOG.md * Update CHANGELOG.md * バックグラウンドで一定時間経過したらページネーションのアイテム更新をしない (#10053) * :art: * feat: 2つの検索画面の統合 (#9949) (#10038) * feat: 検索画面の UI を統一 * fix: エラーの修正 * add: changelog --------- Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> * enhance(client): ノートメニューからユーザーメニューを開けるように Resolve #10019 * enhance(client): renoteした際の表示を改善 Resolve #10078 * Update CHANGELOG.md * enhance(client): tweak contextmenu position calculation * :art: * :art: * feat: in-channel featured note Resolve #9938 * refactor(frontend): fix eslint error (#10084) * Simplify search.vue (remove dead code) (#10088) * Simplify search.vue This is already handled by the code above it, no need to handle it twice * Remove unused imports * Update about-misskey.vue * test(server): add validation test of api:notes/create (#10090) * fix(server): notes/createのバリデーションが効いていない Fix #10079 Co-Authored-By: mei23 <m@m544.net> * anyOf内にバリデーションを書いても最初の一つしかチェックされない * :v: * wip * wip * :v: * RequiredProp * Revert "RequiredProp" This reverts commit 74693900119a590263106fa3adefd008d69ce80c. * add api:notes/create * fix lint * text * :v: * improve readability --------- Co-authored-by: mei23 <m@m544.net> Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> * New Crowdin updates (#10059) * New translations ja-JP.yml (Japanese, Kansai) * New translations ja-JP.yml (Romanian) * New translations ja-JP.yml (French) * New translations ja-JP.yml (Spanish) * New translations ja-JP.yml (Arabic) * New translations ja-JP.yml (Czech) * New translations ja-JP.yml (German) * New translations ja-JP.yml (Italian) * New translations ja-JP.yml (Korean) * New translations ja-JP.yml (Polish) * New translations ja-JP.yml (Russian) * New translations ja-JP.yml (Slovak) * New translations ja-JP.yml (Ukrainian) * New translations ja-JP.yml (Chinese Simplified) * New translations ja-JP.yml (Chinese Traditional) * New translations ja-JP.yml (English) * New translations ja-JP.yml (Vietnamese) * New translations ja-JP.yml (Indonesian) * New translations ja-JP.yml (Bengali) * New translations ja-JP.yml (Thai) * New translations ja-JP.yml (Japanese, Kansai) * New translations ja-JP.yml (Chinese Traditional) * New translations ja-JP.yml (Chinese Traditional) * New translations ja-JP.yml (German) * New translations ja-JP.yml (English) * New translations ja-JP.yml (German) * New translations ja-JP.yml (Ukrainian) * New translations ja-JP.yml (Chinese Simplified) * New translations ja-JP.yml (English) * New translations ja-JP.yml (Ukrainian) * New translations ja-JP.yml (Ukrainian) * New translations ja-JP.yml (Ukrainian) * New translations ja-JP.yml (Chinese Simplified) * New translations ja-JP.yml (Japanese, Kansai) * New translations ja-JP.yml (Thai) * New translations ja-JP.yml (Thai) * New translations ja-JP.yml (Thai) * New translations ja-JP.yml (Spanish) * New translations ja-JP.yml (Spanish) * enhance(client): improve user menu ux * enhance(client): photoswipe 表示時に戻る操作をしても前の画面に戻らないように (#10098) * enhance(client): photoswipe 表示時に戻る操作をしても前の画面に戻らないように * add: changelog --------- Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> * enhance(client): メニューの「もっと」からインスタンス情報を見れるように * [Fix] fixed an typo in error message (#10102) * Update codecov.yml * Update CHANGELOG.md * fix(server): エラーのスタックトレースは返さないように Fix #10064 * [chore]Editorconfig: ymlに加えてyamlファイルに対しても同じ規約を適用する (#10081) * Added yaml file in addition to yml file, in editorconfig * Applied editorconfig for pnpm-workspace.yaml --------- Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> * update deps * ホームタイムラインの読み込みでクエリタイムアウトになるのを修正する (#10106) * refactor * New translations ja-JP.yml (French) (#10103) * Update CHANGELOG.md * 13.8.0 --------- Co-authored-by: atsuchan <83960488+atsu1125@users.noreply.github.com> Co-authored-by: Masaya Suzuki <15100604+massongit@users.noreply.github.com> Co-authored-by: tamaina <tamaina@hotmail.co.jp> Co-authored-by: Kagami Sascha Rosylight <saschanaz@outlook.com> Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com> Co-authored-by: xianon <xianon@hotmail.co.jp> Co-authored-by: kabo2468 <28654659+kabo2468@users.noreply.github.com> Co-authored-by: YS <47836716+yszkst@users.noreply.github.com> Co-authored-by: Khsmty <me@khsmty.com> Co-authored-by: Soni L <EnderMoneyMod@gmail.com> Co-authored-by: mei23 <m@m544.net> Co-authored-by: daima3629 <52790780+daima3629@users.noreply.github.com> Co-authored-by: Windymelt <1113940+windymelt@users.noreply.github.com>
Diffstat (limited to '')
-rw-r--r--packages/backend/.eslintrc.cjs2
-rw-r--r--packages/backend/assets/apple-touch-icon.pngbin9828 -> 49532 bytes
-rw-r--r--packages/backend/check_connect.js10
-rw-r--r--packages/backend/jest.config.cjs3
-rw-r--r--packages/backend/package.json25
-rw-r--r--packages/backend/src/core/activitypub/ApInboxService.ts6
-rw-r--r--packages/backend/src/core/activitypub/ApRequestService.ts72
-rw-r--r--packages/backend/src/misc/schema.ts17
-rw-r--r--packages/backend/src/server/api/ApiCallService.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/admin/drive/show-file.ts18
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/add.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/show.ts18
-rw-r--r--packages/backend/src/server/api/endpoints/notes/create.test.ts263
-rw-r--r--packages/backend/src/server/api/endpoints/notes/create.ts110
-rw-r--r--packages/backend/src/server/api/endpoints/notes/featured.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/notes/search-by-tag.ts39
-rw-r--r--packages/backend/src/server/api/endpoints/notes/timeline.ts29
-rw-r--r--packages/backend/src/server/api/endpoints/pages/show.ts20
-rw-r--r--packages/backend/src/server/api/endpoints/users/followers.ts27
-rw-r--r--packages/backend/src/server/api/endpoints/users/following.ts27
-rw-r--r--packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts17
-rw-r--r--packages/backend/src/server/api/endpoints/users/show.ts40
-rw-r--r--packages/backend/src/server/web/manifest.json8
-rw-r--r--packages/backend/test/prelude/get-api-validator.ts11
-rw-r--r--packages/backend/test/resources/misskey.svg7
-rw-r--r--packages/backend/test/tsconfig.json6
-rw-r--r--packages/backend/test/unit/ap-request.ts (renamed from packages/backend/test/tests/ap-request.ts)11
-rw-r--r--packages/backend/tsconfig.json7
28 files changed, 524 insertions, 281 deletions
diff --git a/packages/backend/.eslintrc.cjs b/packages/backend/.eslintrc.cjs
index 5a06889dcd..f9fe4814e6 100644
--- a/packages/backend/.eslintrc.cjs
+++ b/packages/backend/.eslintrc.cjs
@@ -1,7 +1,7 @@
module.exports = {
parserOptions: {
tsconfigRootDir: __dirname,
- project: ['./tsconfig.json'],
+ project: ['./tsconfig.json', './test/tsconfig.json'],
},
extends: [
'../shared/.eslintrc.js',
diff --git a/packages/backend/assets/apple-touch-icon.png b/packages/backend/assets/apple-touch-icon.png
index 947c513bbb..06ad3f1bb4 100644
--- a/packages/backend/assets/apple-touch-icon.png
+++ b/packages/backend/assets/apple-touch-icon.png
Binary files differ
diff --git a/packages/backend/check_connect.js b/packages/backend/check_connect.js
new file mode 100644
index 0000000000..ed429c0254
--- /dev/null
+++ b/packages/backend/check_connect.js
@@ -0,0 +1,10 @@
+import { loadConfig } from './built/config.js';
+import { createRedisConnection } from './built/redis.js';
+
+const config = loadConfig();
+const redis = createRedisConnection(config);
+
+redis.on('connect', () => redis.disconnect());
+redis.on('error', (e) => {
+ throw e;
+});
diff --git a/packages/backend/jest.config.cjs b/packages/backend/jest.config.cjs
index 2f11f6a3e9..8a11ad848c 100644
--- a/packages/backend/jest.config.cjs
+++ b/packages/backend/jest.config.cjs
@@ -20,7 +20,7 @@ module.exports = {
// collectCoverage: false,
// An array of glob patterns indicating a set of files for which coverage information should be collected
- collectCoverageFrom: ['src/**/*.ts'],
+ collectCoverageFrom: ['src/**/*.ts', '!src/**/*.test.ts'],
// The directory where Jest should output its coverage files
coverageDirectory: "coverage",
@@ -159,6 +159,7 @@ module.exports = {
// The glob patterns Jest uses to detect test files
testMatch: [
"<rootDir>/test/unit/**/*.ts",
+ "<rootDir>/src/**/*.test.ts",
//"<rootDir>/test/e2e/**/*.ts"
],
diff --git a/packages/backend/package.json b/packages/backend/package.json
index df78219985..9fa1e68a46 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -7,6 +7,7 @@
"start": "node ./built/index.js",
"start:test": "NODE_ENV=test node ./built/index.js",
"migrate": "pnpm typeorm migration:run -d ormconfig.js",
+ "check:connect": "node ./check_connect.js",
"build": "swc src -d built -D",
"watch:swc": "swc src -d built -D -w",
"build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
@@ -14,8 +15,8 @@
"typecheck": "tsc --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 --runInBand",
- "jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --runInBand",
+ "jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --runInBand --detectOpenHandles",
+ "jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --runInBand --detectOpenHandles",
"jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache",
"test": "pnpm jest",
"test-and-coverage": "pnpm jest-and-coverage"
@@ -53,7 +54,7 @@
"@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.0.2",
"@swc/cli": "0.1.62",
- "@swc/core": "1.3.35",
+ "@swc/core": "1.3.36",
"accepts": "1.3.8",
"ajv": "8.12.0",
"archiver": "5.3.1",
@@ -79,7 +80,7 @@
"fluent-ffmpeg": "2.1.2",
"form-data": "4.0.0",
"got": "12.5.3",
- "happy-dom": "^8.7.0",
+ "happy-dom": "8.9.0",
"hpagent": "1.2.0",
"ioredis": "4.28.5",
"ip-cidr": "3.1.0",
@@ -87,7 +88,7 @@
"js-yaml": "4.1.0",
"jsdom": "21.1.0",
"json5": "2.2.3",
- "jsonld": "8.1.0",
+ "jsonld": "8.1.1",
"jsrsasign": "10.6.1",
"mfm-js": "0.23.3",
"mime-types": "2.1.35",
@@ -126,7 +127,7 @@
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"summaly": "github:misskey-dev/summaly",
- "systeminformation": "5.17.9",
+ "systeminformation": "5.17.10",
"tinycolor2": "1.6.0",
"tmp": "0.2.1",
"tsc-alias": "1.8.2",
@@ -155,7 +156,7 @@
"@types/color-convert": "2.0.0",
"@types/content-disposition": "0.5.5",
"@types/escape-regexp": "0.0.1",
- "@types/fluent-ffmpeg": "2.1.20",
+ "@types/fluent-ffmpeg": "2.1.21",
"@types/ioredis": "4.28.10",
"@types/jest": "29.4.0",
"@types/js-yaml": "4.0.5",
@@ -163,7 +164,7 @@
"@types/jsonld": "1.5.8",
"@types/jsrsasign": "10.5.5",
"@types/mime-types": "2.1.1",
- "@types/node": "18.14.0",
+ "@types/node": "18.14.1",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.7",
"@types/oauth": "0.9.1",
@@ -182,18 +183,18 @@
"@types/tinycolor2": "1.4.3",
"@types/tmp": "0.2.3",
"@types/unzipper": "0.10.5",
- "@types/uuid": "9.0.0",
+ "@types/uuid": "9.0.1",
"@types/vary": "1.1.0",
"@types/web-push": "3.3.2",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.52.0",
- "@typescript-eslint/parser": "5.52.0",
+ "@typescript-eslint/parser": "5.53.0",
"cross-env": "7.0.3",
- "eslint": "8.34.0",
+ "eslint": "8.35.0",
"eslint-plugin-import": "2.27.5",
"execa": "6.1.0",
"jest": "29.4.3",
"jest-mock": "29.4.3"
}
-} \ No newline at end of file
+}
diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts
index 21d2d16ed8..6d9569bce2 100644
--- a/packages/backend/src/core/activitypub/ApInboxService.ts
+++ b/packages/backend/src/core/activitypub/ApInboxService.ts
@@ -450,8 +450,10 @@ export class ApInboxService {
return `skip: delete actor ${actor.uri} !== ${uri}`;
}
- const user = await this.usersRepository.findOneByOrFail({ id: actor.id });
- if (user.isDeleted) {
+ const user = await this.usersRepository.findOneBy({ id: actor.id });
+ if (user == null) {
+ return 'skip: actor not found';
+ } else if (user.isDeleted) {
return 'skip: already deleted';
}
diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts
index bfd53dfabf..71fbc29476 100644
--- a/packages/backend/src/core/activitypub/ApRequestService.ts
+++ b/packages/backend/src/core/activitypub/ApRequestService.ts
@@ -28,31 +28,15 @@ type PrivateKey = {
keyId: string;
};
-@Injectable()
-export class ApRequestService {
- private logger: Logger;
-
- constructor(
- @Inject(DI.config)
- private config: Config,
-
- private userKeypairStoreService: UserKeypairStoreService,
- private httpRequestService: HttpRequestService,
- private loggerService: LoggerService,
- ) {
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- this.logger = this.loggerService?.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる
- }
-
- @bindThis
- private createSignedPost(args: { key: PrivateKey, url: string, body: string, additionalHeaders: Record<string, string> }): Signed {
+export class ApRequestCreator {
+ static createSignedPost(args: { key: PrivateKey, url: string, body: string, additionalHeaders: Record<string, string> }): Signed {
const u = new URL(args.url);
const digestHeader = `SHA-256=${crypto.createHash('sha256').update(args.body).digest('base64')}`;
const request: Request = {
url: u.href,
method: 'POST',
- headers: this.objectAssignWithLcKey({
+ headers: this.#objectAssignWithLcKey({
'Date': new Date().toUTCString(),
'Host': u.host,
'Content-Type': 'application/activity+json',
@@ -60,7 +44,7 @@ export class ApRequestService {
}, args.additionalHeaders),
};
- const result = this.signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']);
+ const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']);
return {
request,
@@ -70,21 +54,20 @@ export class ApRequestService {
};
}
- @bindThis
- private createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record<string, string> }): Signed {
+ static createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record<string, string> }): Signed {
const u = new URL(args.url);
const request: Request = {
url: u.href,
method: 'GET',
- headers: this.objectAssignWithLcKey({
+ headers: this.#objectAssignWithLcKey({
'Accept': 'application/activity+json, application/ld+json',
'Date': new Date().toUTCString(),
'Host': new URL(args.url).host,
}, args.additionalHeaders),
};
- const result = this.signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']);
+ const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']);
return {
request,
@@ -94,13 +77,12 @@ export class ApRequestService {
};
}
- @bindThis
- private signToRequest(request: Request, key: PrivateKey, includeHeaders: string[]): Signed {
- const signingString = this.genSigningString(request, includeHeaders);
+ static #signToRequest(request: Request, key: PrivateKey, includeHeaders: string[]): Signed {
+ const signingString = this.#genSigningString(request, includeHeaders);
const signature = crypto.sign('sha256', Buffer.from(signingString), key.privateKeyPem).toString('base64');
const signatureHeader = `keyId="${key.keyId}",algorithm="rsa-sha256",headers="${includeHeaders.join(' ')}",signature="${signature}"`;
- request.headers = this.objectAssignWithLcKey(request.headers, {
+ request.headers = this.#objectAssignWithLcKey(request.headers, {
Signature: signatureHeader,
});
// node-fetch will generate this for us. if we keep 'Host', it won't change with redirects!
@@ -114,9 +96,8 @@ export class ApRequestService {
};
}
- @bindThis
- private genSigningString(request: Request, includeHeaders: string[]): string {
- request.headers = this.lcObjectKey(request.headers);
+ static #genSigningString(request: Request, includeHeaders: string[]): string {
+ request.headers = this.#lcObjectKey(request.headers);
const results: string[] = [];
@@ -131,16 +112,31 @@ export class ApRequestService {
return results.join('\n');
}
- @bindThis
- private lcObjectKey(src: Record<string, string>): Record<string, string> {
+ static #lcObjectKey(src: Record<string, string>): Record<string, string> {
const dst: Record<string, string> = {};
for (const key of Object.keys(src).filter(x => x !== '__proto__' && typeof src[x] === 'string')) dst[key.toLowerCase()] = src[key];
return dst;
}
- @bindThis
- private objectAssignWithLcKey(a: Record<string, string>, b: Record<string, string>): Record<string, string> {
- return Object.assign(this.lcObjectKey(a), this.lcObjectKey(b));
+ static #objectAssignWithLcKey(a: Record<string, string>, b: Record<string, string>): Record<string, string> {
+ return Object.assign(this.#lcObjectKey(a), this.#lcObjectKey(b));
+ }
+}
+
+@Injectable()
+export class ApRequestService {
+ private logger: Logger;
+
+ constructor(
+ @Inject(DI.config)
+ private config: Config,
+
+ private userKeypairStoreService: UserKeypairStoreService,
+ private httpRequestService: HttpRequestService,
+ private loggerService: LoggerService,
+ ) {
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ this.logger = this.loggerService?.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる
}
@bindThis
@@ -149,7 +145,7 @@ export class ApRequestService {
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
- const req = this.createSignedPost({
+ const req = ApRequestCreator.createSignedPost({
key: {
privateKeyPem: keypair.privateKey,
keyId: `${this.config.url}/users/${user.id}#main-key`,
@@ -176,7 +172,7 @@ export class ApRequestService {
public async signedGet(url: string, user: { id: User['id'] }) {
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
- const req = this.createSignedGet({
+ const req = ApRequestCreator.createSignedGet({
key: {
privateKeyPem: keypair.privateKey,
keyId: `${this.config.url}/users/${user.id}#main-key`,
diff --git a/packages/backend/src/misc/schema.ts b/packages/backend/src/misc/schema.ts
index 7fc4a3e654..6a0802f8a4 100644
--- a/packages/backend/src/misc/schema.ts
+++ b/packages/backend/src/misc/schema.ts
@@ -116,10 +116,10 @@ export type Obj = Record<string, Schema>;
// https://github.com/misskey-dev/misskey/issues/8535
// To avoid excessive stack depth error,
// deceive TypeScript with UnionToIntersection (or more precisely, `infer` expression within it).
-export type ObjType<s extends Obj, RequiredProps extends keyof s> =
+export type ObjType<s extends Obj, RequiredProps extends ReadonlyArray<keyof s>> =
UnionToIntersection<
{ -readonly [R in RequiredPropertyNames<s>]-?: SchemaType<s[R]> } &
- { -readonly [R in RequiredProps]-?: SchemaType<s[R]> } &
+ { -readonly [R in RequiredProps[number]]-?: SchemaType<s[R]> } &
{ -readonly [P in keyof s]?: SchemaType<s[P]> }
>;
@@ -136,18 +136,19 @@ type PartialIntersection<T> = Partial<UnionToIntersection<T>>;
// https://github.com/misskey-dev/misskey/pull/8144#discussion_r785287552
// To get union, we use `Foo extends any ? Hoge<Foo> : never`
type UnionSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? SchemaType<X> : never;
-type UnionObjectSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? ObjectSchemaType<X> : never;
+//type UnionObjectSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? ObjectSchemaType<X> : never;
+type UnionObjType<s extends Obj, a extends readonly any[], X extends ReadonlyArray<keyof s> = a[number]> = X extends any ? ObjType<s, X> : never;
type ArrayUnion<T> = T extends any ? Array<T> : never;
type ObjectSchemaTypeDef<p extends Schema> =
p['ref'] extends keyof typeof refs ? Packed<p['ref']> :
p['properties'] extends NonNullable<Obj> ?
- p['anyOf'] extends ReadonlyArray<Schema> ?
- ObjType<p['properties'], NonNullable<p['required']>[number]> & UnionObjectSchemaType<p['anyOf']> & PartialIntersection<UnionObjectSchemaType<p['anyOf']>>
- :
- ObjType<p['properties'], NonNullable<p['required']>[number]>
+ p['anyOf'] extends ReadonlyArray<Schema> ? p['anyOf'][number]['required'] extends ReadonlyArray<keyof p['properties']> ?
+ UnionObjType<p['properties'], NonNullable<p['anyOf'][number]['required']>> & ObjType<p['properties'], NonNullable<p['required']>>
+ : never
+ : ObjType<p['properties'], NonNullable<p['required']>>
:
- p['anyOf'] extends ReadonlyArray<Schema> ? UnionObjectSchemaType<p['anyOf']> & PartialIntersection<UnionObjectSchemaType<p['anyOf']>> :
+ p['anyOf'] extends ReadonlyArray<Schema> ? never : // see CONTRIBUTING.md
p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> :
any
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index 347fa59d36..6d8540dd4f 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -2,6 +2,7 @@ import { pipeline } from 'node:stream';
import * as fs from 'node:fs';
import { promisify } from 'node:util';
import { Inject, Injectable } from '@nestjs/common';
+import { v4 as uuid } from 'uuid';
import { DI } from '@/di-symbols.js';
import { getIpHash } from '@/misc/get-ip-hash.js';
import type { LocalUser, User } from '@/models/entities/User.js';
@@ -320,6 +321,7 @@ export class ApiCallService implements OnApplicationShutdown {
if (err instanceof ApiError) {
throw err;
} else {
+ const errId = uuid();
this.logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, {
ep: ep.name,
ps: data,
@@ -327,14 +329,15 @@ export class ApiCallService implements OnApplicationShutdown {
message: err.message,
code: err.name,
stack: err.stack,
+ id: errId,
},
});
- console.error(err);
+ console.error(err, errId);
throw new ApiError(null, {
e: {
message: err.message,
code: err.name,
- stack: err.stack,
+ id: errId,
},
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
index 85b566aabe..1d27ac2137 100644
--- a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
+++ b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
@@ -138,19 +138,13 @@ export const meta = {
export const paramDef = {
type: 'object',
+ properties: {
+ fileId: { type: 'string', format: 'misskey:id' },
+ url: { type: 'string' },
+ },
anyOf: [
- {
- properties: {
- fileId: { type: 'string', format: 'misskey:id' },
- },
- required: ['fileId'],
- },
- {
- properties: {
- url: { type: 'string' },
- },
- required: ['url'],
- },
+ { required: ['fileId'] },
+ { required: ['url'] },
],
} as const;
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
index 8889f30269..04c58050ff 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
@@ -16,7 +16,7 @@ export const meta = {
errors: {
noSuchFile: {
message: 'No such file.',
- code: 'MO_SUCH_FILE',
+ code: 'NO_SUCH_FILE',
id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf',
},
},
diff --git a/packages/backend/src/server/api/endpoints/drive/files/show.ts b/packages/backend/src/server/api/endpoints/drive/files/show.ts
index e0a07a3640..271b33ef4b 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/show.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/show.ts
@@ -39,19 +39,13 @@ export const meta = {
export const paramDef = {
type: 'object',
+ properties: {
+ fileId: { type: 'string', format: 'misskey:id' },
+ url: { type: 'string' },
+ },
anyOf: [
- {
- properties: {
- fileId: { type: 'string', format: 'misskey:id' },
- },
- required: ['fileId'],
- },
- {
- properties: {
- url: { type: 'string' },
- },
- required: ['url'],
- },
+ { required: ['fileId'] },
+ { required: ['url'] },
],
} as const;
diff --git a/packages/backend/src/server/api/endpoints/notes/create.test.ts b/packages/backend/src/server/api/endpoints/notes/create.test.ts
new file mode 100644
index 0000000000..6bff7fc0c9
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/create.test.ts
@@ -0,0 +1,263 @@
+process.env.NODE_ENV = 'test';
+
+import { readFile } from 'node:fs/promises';
+import { fileURLToPath } from 'node:url';
+import { dirname } from 'node:path';
+import { describe, test, expect } from '@jest/globals';
+import { getValidator } from '../../../../../test/prelude/get-api-validator.js';
+import { paramDef } from './create.js';
+
+const _filename = fileURLToPath(import.meta.url);
+const _dirname = dirname(_filename);
+
+const VALID = true;
+const INVALID = false;
+
+describe('api:notes/create', () => {
+ describe('validation', () => {
+ const v = getValidator(paramDef);
+ const tooLong = readFile(_dirname + '/../../../../../test/resources/misskey.svg', 'utf-8');
+
+ test('reject empty', () => {
+ const valid = v({ });
+ expect(valid).toBe(INVALID);
+ });
+
+ describe('text', () => {
+ test('simple post', () => {
+ expect(v({ text: 'Hello, world!' }))
+ .toBe(VALID);
+ });
+
+ test('null post', () => {
+ expect(v({ text: null }))
+ .toBe(INVALID);
+ });
+
+ test('0 characters post', () => {
+ expect(v({ text: '' }))
+ .toBe(INVALID);
+ });
+
+ test('over 3000 characters post', async () => {
+ expect(v({ text: await tooLong }))
+ .toBe(INVALID);
+ });
+ });
+
+ describe('cw', () => {
+ test('simple cw', () => {
+ expect(v({ text: 'Hello, world!', cw: 'Hello, world!' }))
+ .toBe(VALID);
+ });
+
+ test('null cw', () => {
+ expect(v({ text: 'Body', cw: null }))
+ .toBe(VALID);
+ });
+
+ test('0 characters cw', () => {
+ expect(v({ text: 'Body', cw: '' }))
+ .toBe(VALID);
+ });
+
+ test('reject only cw', () => {
+ expect(v({ cw: 'Hello, world!' }))
+ .toBe(INVALID);
+ });
+
+ test('over 100 characters cw', async () => {
+ expect(v({ text: 'Body', cw: await tooLong }))
+ .toBe(INVALID);
+ });
+ });
+
+ describe('visibility', () => {
+ test('public', () => {
+ expect(v({ text: 'Hello, world!', visibility: 'public' }))
+ .toBe(VALID);
+ });
+
+ test('home', () => {
+ expect(v({ text: 'Hello, world!', visibility: 'home' }))
+ .toBe(VALID);
+ });
+
+ test('followers', () => {
+ expect(v({ text: 'Hello, world!', visibility: 'followers' }))
+ .toBe(VALID);
+ });
+
+ test('reject only visibility', () => {
+ expect(v({ visibility: 'public' }))
+ .toBe(INVALID);
+ });
+
+ test('reject invalid visibility', () => {
+ expect(v({ text: 'Hello, world!', visibility: 'invalid' }))
+ .toBe(INVALID);
+ });
+
+ test('reject null visibility', () => {
+ expect(v({ text: 'Hello, world!', visibility: null }))
+ .toBe(INVALID);
+ });
+
+ describe('visibility:specified', () => {
+ test('specified without visibleUserIds', () => {
+ expect(v({ text: 'Hello, world!', visibility: 'specified' }))
+ .toBe(VALID);
+ });
+
+ test('specified with empty visibleUserIds', () => {
+ expect(v({ text: 'Hello, world!', visibility: 'specified', visibleUserIds: [] }))
+ .toBe(VALID);
+ });
+
+ test('reject specified with non unique visibleUserIds', () => {
+ expect(v({ text: 'Hello, world!', visibility: 'specified', visibleUserIds: ['1', '1', '2'] }))
+ .toBe(INVALID);
+ });
+
+ test('reject specified with null visibleUserIds', () => {
+ expect(v({ text: 'Hello, world!', visibility: 'specified', visibleUserIds: null }))
+ .toBe(INVALID);
+ });
+ });
+ });
+
+ describe('fileIds', () => {
+ test('only fileIds', () => {
+ expect(v({ fileIds: ['1', '2', '3'] }))
+ .toBe(VALID);
+ });
+
+ test('text and fileIds', () => {
+ expect(v({ text: 'Hello, world!', fileIds: ['1', '2', '3'] }))
+ .toBe(VALID);
+ });
+
+ test('reject null fileIds', () => {
+ expect(v({ fileIds: null }))
+ .toBe(INVALID);
+ });
+
+ test('reject text and null fileIds (複合的なanyOfのバリデーションが正しく動作する)', () => {
+ expect(v({ text: 'Hello, world!', fileIds: null }))
+ .toBe(INVALID);
+ });
+
+ test('reject 0 files', () => {
+ expect(v({ fileIds: [] }))
+ .toBe(INVALID);
+ });
+
+ test('reject non unique', () => {
+ expect(v({ fileIds: ['1', '1', '2'] }))
+ .toBe(INVALID);
+ });
+
+ test('reject invalid id', () => {
+ expect(v({ fileIds: ['あ'] }))
+ .toBe(INVALID);
+ });
+
+ test('reject over 17 files', () => {
+ const valid = v({ text: 'Hello, world!', fileIds: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18'] });
+ expect(valid).toBe(INVALID);
+ });
+ });
+
+ describe('poll', () => {
+ test('note with poll', () => {
+ expect(v({ text: 'Hello, world!', poll: { choices: ['a', 'b', 'c'] } }))
+ .toBe(VALID);
+ });
+
+ test('null poll', () => {
+ expect(v({ text: 'Hello, world!', poll: null }))
+ .toBe(VALID);
+ });
+
+ test('allow only poll', () => {
+ expect(v({ poll: { choices: ['a', 'b', 'c'] } }))
+ .toBe(VALID);
+ });
+
+ test('poll with expiresAt', async () => {
+ expect(v({ poll: { choices: ['a', 'b', 'c'], expiresAt: 1 } }))
+ .toBe(VALID);
+ });
+
+ test('poll with expiredAfter', async () => {
+ expect(v({ poll: { choices: ['a', 'b', 'c'], expiredAfter: 1 } }))
+ .toBe(VALID);
+ });
+
+ test('reject poll without choices', () => {
+ expect(v({ poll: { } }))
+ .toBe(INVALID);
+ });
+
+ test('reject poll with empty choices', () => {
+ expect(v({ poll: { choices: [] } }))
+ .toBe(INVALID);
+ });
+
+ test('reject poll with null choices', () => {
+ expect(v({ poll: { choices: null } }))
+ .toBe(INVALID);
+ });
+
+ test('reject poll with 1 choice', () => {
+ expect(v({ poll: { choices: ['a'] } }))
+ .toBe(INVALID);
+ });
+
+ test('reject poll with too long choice', async () => {
+ expect(v({ poll: { choices: [await tooLong, '2'] } }))
+ .toBe(INVALID);
+ });
+
+ test('reject poll with too many choices', () => {
+ expect(v({ poll: { choices: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'] } }))
+ .toBe(INVALID);
+ });
+
+ test('reject poll with non unique choices', () => {
+ expect(v({ poll: { choices: ['a', 'a', 'b', 'c'] } }))
+ .toBe(INVALID);
+ });
+
+ test('reject poll with expiredAfter 0', async () => {
+ expect(v({ poll: { choices: ['a', 'b', 'c'], expiredAfter: 0 } }))
+ .toBe(INVALID);
+ });
+ });
+
+ describe('renote', () => {
+ test('just a renote', () => {
+ expect(v({ renoteId: '1' }))
+ .toBe(VALID);
+ });
+ test('just a quote', () => {
+ expect(v({ text: 'Hello, world!', renoteId: '1' }))
+ .toBe(VALID);
+ });
+ test('reject invalid renoteId', () => {
+ expect(v({ renoteId: 'あ' }))
+ .toBe(INVALID);
+ });
+ });
+
+ test('text, fileIds and poll', () => {
+ expect(v({ text: 'Hello, world!', fileIds: ['1', '2', '3'], poll: { choices: ['a', 'b', 'c'] } }))
+ .toBe(VALID);
+ });
+
+ test('text, invalid fileIds and invalid poll', () => {
+ expect(v({ text: 'Hello, world!', fileIds: ['あ'], poll: { choices: ['a'] } }))
+ .toBe(INVALID);
+ });
+ });
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index 593444968e..786ad103b0 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -79,6 +79,12 @@ export const meta = {
code: 'YOU_HAVE_BEEN_BLOCKED',
id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3',
},
+
+ noSuchFile: {
+ message: 'Some files are not found.',
+ code: 'NO_SUCH_FILE',
+ id: 'b6992544-63e7-67f0-fa7f-32444b1b5306',
+ },
},
} as const;
@@ -95,74 +101,56 @@ export const paramDef = {
noExtractHashtags: { type: 'boolean', default: false },
noExtractEmojis: { type: 'boolean', default: false },
replyId: { type: 'string', format: 'misskey:id', nullable: true },
+ renoteId: { type: 'string', format: 'misskey:id', nullable: true },
channelId: { type: 'string', format: 'misskey:id', nullable: true },
- },
- anyOf: [
- {
- // (re)note with text, files and poll are optional
- properties: {
- text: { type: 'string', minLength: 1, maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false },
- },
- required: ['text'],
+
+ // anyOf内にバリデーションを書いても最初の一つしかチェックされない
+ // See https://github.com/misskey-dev/misskey/pull/10082
+ text: {
+ type: 'string',
+ minLength: 1,
+ maxLength: MAX_NOTE_TEXT_LENGTH,
+ nullable: false
},
- {
- // (re)note with files, text and poll are optional
- properties: {
- fileIds: {
- type: 'array',
- uniqueItems: true,
- minItems: 1,
- maxItems: 16,
- items: { type: 'string', format: 'misskey:id' },
- },
- },
- required: ['fileIds'],
+ fileIds: {
+ type: 'array',
+ uniqueItems: true,
+ minItems: 1,
+ maxItems: 16,
+ items: { type: 'string', format: 'misskey:id' },
},
- {
- // (re)note with files, text and poll are optional
+ mediaIds: {
+ type: 'array',
+ uniqueItems: true,
+ minItems: 1,
+ maxItems: 16,
+ items: { type: 'string', format: 'misskey:id' },
+ },
+ poll: {
+ type: 'object',
+ nullable: true,
properties: {
- mediaIds: {
- deprecated: true,
- description: 'Use `fileIds` instead. If both are specified, this property is discarded.',
+ choices: {
type: 'array',
uniqueItems: true,
- minItems: 1,
- maxItems: 16,
- items: { type: 'string', format: 'misskey:id' },
- },
- },
- required: ['mediaIds'],
- },
- {
- // (re)note with poll, text and files are optional
- properties: {
- poll: {
- type: 'object',
- nullable: true,
- properties: {
- choices: {
- type: 'array',
- uniqueItems: true,
- minItems: 2,
- maxItems: 10,
- items: { type: 'string', minLength: 1, maxLength: 50 },
- },
- multiple: { type: 'boolean' },
- expiresAt: { type: 'integer', nullable: true },
- expiredAfter: { type: 'integer', nullable: true, minimum: 1 },
- },
- required: ['choices'],
+ minItems: 2,
+ maxItems: 10,
+ items: { type: 'string', minLength: 1, maxLength: 50 },
},
+ multiple: { type: 'boolean' },
+ expiresAt: { type: 'integer', nullable: true },
+ expiredAfter: { type: 'integer', nullable: true, minimum: 1 },
},
- required: ['poll'],
- },
- {
- // pure renote
- properties: {
- renoteId: { type: 'string', format: 'misskey:id', nullable: true },
- },
- required: ['renoteId'],
+ required: ['choices'],
},
+ },
+ // (re)note with text, files and poll are optional
+ anyOf: [
+ { required: ['text'] },
+ { required: ['renoteId'] },
+ { required: ['fileIds'] },
+ { required: ['mediaIds'] },
+ { required: ['poll'] },
],
} as const;
@@ -207,6 +195,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.orderBy('array_position(ARRAY[:...fileIds], "id"::text)')
.setParameters({ fileIds })
.getMany();
+
+ if (files.length !== fileIds.length) {
+ throw new ApiError(meta.errors.noSuchFile);
+ }
}
let renote: Note | null = null;
diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts
index 26f69373d1..cf939f6631 100644
--- a/packages/backend/src/server/api/endpoints/notes/featured.ts
+++ b/packages/backend/src/server/api/endpoints/notes/featured.ts
@@ -28,6 +28,7 @@ export const paramDef = {
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
offset: { type: 'integer', default: 0 },
+ channelId: { type: 'string', nullable: true, format: 'misskey:id' },
},
required: [],
} as const;
@@ -63,6 +64,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
+ if (ps.channelId) query.andWhere('note.channelId = :channelId', { channelId: ps.channelId });
+
if (me) this.queryService.generateMutedUserQuery(query, me);
if (me) this.queryService.generateBlockedUserQuery(query, me);
diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
index bcd793ac43..da1a4bcc46 100644
--- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
+++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
@@ -36,32 +36,25 @@ export const paramDef = {
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
- },
- anyOf: [
- {
- properties: {
- tag: { type: 'string', minLength: 1 },
- },
- required: ['tag'],
- },
- {
- properties: {
- query: {
- type: 'array',
- description: 'The outer arrays are chained with OR, the inner arrays are chained with AND.',
- items: {
- type: 'array',
- items: {
- type: 'string',
- minLength: 1,
- },
- minItems: 1,
- },
- minItems: 1,
+
+ tag: { type: 'string', minLength: 1 },
+ query: {
+ type: 'array',
+ description: 'The outer arrays are chained with OR, the inner arrays are chained with AND.',
+ items: {
+ type: 'array',
+ items: {
+ type: 'string',
+ minLength: 1,
},
+ minItems: 1,
},
- required: ['query'],
+ minItems: 1,
},
+ },
+ anyOf: [
+ { required: ['tag'] },
+ { required: ['query'] },
],
} as const;
diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts
index d1c35e36e2..e6de087c4a 100644
--- a/packages/backend/src/server/api/endpoints/notes/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts
@@ -58,25 +58,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private activeUsersChart: ActiveUsersChart,
) {
super(meta, paramDef, async (ps, me) => {
- const hasFollowing = (await this.followingsRepository.count({
- where: {
- followerId: me.id,
- },
- take: 1,
- })) !== 0;
-
- //#region Construct query
- const followingQuery = this.followingsRepository.createQueryBuilder('following')
+ const followees = await this.followingsRepository.createQueryBuilder('following')
.select('following.followeeId')
- .where('following.followerId = :followerId', { followerId: me.id });
+ .where('following.followerId = :followerId', { followerId: me.id })
+ .getMany();
+ //#region Construct query
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('note.createdAt > :minDate', { minDate: new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)) }) // 30日前まで
- .andWhere(new Brackets(qb => { qb
- .where('note.userId = :meId', { meId: me.id });
- if (hasFollowing) qb.orWhere(`note.userId IN (${ followingQuery.getQuery() })`);
- }))
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')
@@ -87,8 +77,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
- .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
- .setParameters(followingQuery.getParameters());
+ .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
+
+ if (followees.length > 0) {
+ const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)];
+
+ query.andWhere('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds });
+ } else {
+ query.andWhere('note.userId = :meId', { meId: me.id });
+ }
this.queryService.generateChannelQuery(query, me);
this.queryService.generateRepliesQuery(query, me);
diff --git a/packages/backend/src/server/api/endpoints/pages/show.ts b/packages/backend/src/server/api/endpoints/pages/show.ts
index 651252afbb..bf2b2a431e 100644
--- a/packages/backend/src/server/api/endpoints/pages/show.ts
+++ b/packages/backend/src/server/api/endpoints/pages/show.ts
@@ -29,20 +29,14 @@ export const meta = {
export const paramDef = {
type: 'object',
+ properties: {
+ pageId: { type: 'string', format: 'misskey:id' },
+ name: { type: 'string' },
+ username: { type: 'string' },
+ },
anyOf: [
- {
- properties: {
- pageId: { type: 'string', format: 'misskey:id' },
- },
- required: ['pageId'],
- },
- {
- properties: {
- name: { type: 'string' },
- username: { type: 'string' },
- },
- required: ['name', 'username'],
- },
+ { required: ['pageId'] },
+ { required: ['name', 'username'] },
],
} as const;
diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts
index 17ce920011..97f1310c36 100644
--- a/packages/backend/src/server/api/endpoints/users/followers.ts
+++ b/packages/backend/src/server/api/endpoints/users/followers.ts
@@ -46,25 +46,18 @@ export const paramDef = {
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+
+ userId: { type: 'string', format: 'misskey:id' },
+ username: { type: 'string' },
+ host: {
+ type: 'string',
+ nullable: true,
+ description: 'The local host is represented with `null`.',
+ },
},
anyOf: [
- {
- properties: {
- userId: { type: 'string', format: 'misskey:id' },
- },
- required: ['userId'],
- },
- {
- properties: {
- username: { type: 'string' },
- host: {
- type: 'string',
- nullable: true,
- description: 'The local host is represented with `null`.',
- },
- },
- required: ['username', 'host'],
- },
+ { required: ['userId'] },
+ { required: ['username', 'host'] },
],
} as const;
diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts
index 6dbda0d72f..d406594a2e 100644
--- a/packages/backend/src/server/api/endpoints/users/following.ts
+++ b/packages/backend/src/server/api/endpoints/users/following.ts
@@ -46,25 +46,18 @@ export const paramDef = {
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+
+ userId: { type: 'string', format: 'misskey:id' },
+ username: { type: 'string' },
+ host: {
+ type: 'string',
+ nullable: true,
+ description: 'The local host is represented with `null`.',
+ },
},
anyOf: [
- {
- properties: {
- userId: { type: 'string', format: 'misskey:id' },
- },
- required: ['userId'],
- },
- {
- properties: {
- username: { type: 'string' },
- host: {
- type: 'string',
- nullable: true,
- description: 'The local host is represented with `null`.',
- },
- },
- required: ['username', 'host'],
- },
+ { required: ['userId'] },
+ { required: ['username', 'host'] },
],
} as const;
diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
index 1cefcf2707..6c340d8fb2 100644
--- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
+++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
@@ -31,20 +31,13 @@ export const paramDef = {
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
detail: { type: 'boolean', default: true },
+
+ username: { type: 'string', nullable: true },
+ host: { type: 'string', nullable: true },
},
anyOf: [
- {
- properties: {
- username: { type: 'string', nullable: true },
- },
- required: ['username'],
- },
- {
- properties: {
- host: { type: 'string', nullable: true },
- },
- required: ['host'],
- },
+ { required: ['username'] },
+ { required: ['host'] },
],
} as const;
diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts
index 70258ef009..29f24b045a 100644
--- a/packages/backend/src/server/api/endpoints/users/show.ts
+++ b/packages/backend/src/server/api/endpoints/users/show.ts
@@ -54,32 +54,22 @@ export const meta = {
export const paramDef = {
type: 'object',
- anyOf: [
- {
- properties: {
- userId: { type: 'string', format: 'misskey:id' },
- },
- required: ['userId'],
- },
- {
- properties: {
- userIds: { type: 'array', uniqueItems: true, items: {
- type: 'string', format: 'misskey:id',
- } },
- },
- required: ['userIds'],
- },
- {
- properties: {
- username: { type: 'string' },
- host: {
- type: 'string',
- nullable: true,
- description: 'The local host is represented with `null`.',
- },
- },
- required: ['username'],
+ properties: {
+ userId: { type: 'string', format: 'misskey:id' },
+ userIds: { type: 'array', uniqueItems: true, items: {
+ type: 'string', format: 'misskey:id',
+ } },
+ username: { type: 'string' },
+ host: {
+ type: 'string',
+ nullable: true,
+ description: 'The local host is represented with `null`.',
},
+ },
+ anyOf: [
+ { required: ['userId'] },
+ { required: ['userIds'] },
+ { required: ['username'] },
],
} as const;
diff --git a/packages/backend/src/server/web/manifest.json b/packages/backend/src/server/web/manifest.json
index f8b77cbd93..41171d62a1 100644
--- a/packages/backend/src/server/web/manifest.json
+++ b/packages/backend/src/server/web/manifest.json
@@ -17,10 +17,18 @@
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
+ },
+ {
+ "src": "/static-assets/splash.png",
+ "sizes": "300x300",
+ "type": "image/png",
+ "purpose": "any"
}
],
"share_target": {
"action": "/share/",
+ "method": "GET",
+ "enctype": "application/x-www-form-urlencoded",
"params": {
"title": "title",
"text": "text",
diff --git a/packages/backend/test/prelude/get-api-validator.ts b/packages/backend/test/prelude/get-api-validator.ts
new file mode 100644
index 0000000000..1f4a2dbc95
--- /dev/null
+++ b/packages/backend/test/prelude/get-api-validator.ts
@@ -0,0 +1,11 @@
+import { Schema } from '@/misc/schema';
+import Ajv from 'ajv';
+
+export const getValidator = (paramDef: Schema) => {
+ const ajv = new Ajv({
+ useDefaults: true,
+ });
+ ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);
+
+ return ajv.compile(paramDef);
+}
diff --git a/packages/backend/test/resources/misskey.svg b/packages/backend/test/resources/misskey.svg
new file mode 100644
index 0000000000..3fcb2d3ecb
--- /dev/null
+++ b/packages/backend/test/resources/misskey.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="515px" height="136px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+ <g id="path2" transform="matrix(0.264585,0,0,0.264585,-50.0005,-50.0001)">
+ <path d="M256.418,188.976C248.558,188.944 240.758,190.308 233.379,193.013C220.308,197.613 209.533,205.888 201.091,217.802C193.02,229.329 188.977,242.195 188.977,256.409L188.977,508.89C188.977,527.332 195.52,543.29 208.576,556.732C222.032,569.803 237.99,576.331 256.418,576.331C275.259,576.331 291.204,569.803 304.274,556.747C317.73,543.291 324.441,527.332 324.441,508.89L324.441,462.983C324.584,453.04 334.824,455.655 340.01,462.983C349.695,479.767 370.251,494.19 394.193,494.105L394.193,494.119C418.119,494.019 438.005,482.196 448.375,462.983C452.304,458.354 463.377,450.455 464.52,462.983L464.52,508.89C464.52,527.332 471.047,543.29 484.104,556.732C497.574,569.803 513.511,576.331 531.953,576.331C550.78,576.331 566.739,569.803 579.809,556.747C593.265,543.291 599.977,527.332 599.977,508.89L599.977,256.409C599.977,242.195 595.752,229.329 587.309,217.802C579.224,205.874 568.653,197.613 555.597,193.013C547.912,190.314 540.228,188.976 532.543,188.976C511.788,188.976 494.301,197.046 480.073,213.188L411.636,293.281C410.107,294.438 405.006,303.247 394.178,303.247C383.379,303.247 378.868,294.439 377.325,293.296L308.297,213.188C294.47,197.046 277.173,188.976 256.418,188.976ZM682.904,188.983C666.763,188.983 652.926,194.748 641.404,206.271C630.261,217.413 624.691,231.054 624.691,247.196C624.691,263.338 630.261,277.174 641.404,288.697C652.926,299.839 666.763,305.41 682.904,305.41C699.046,305.41 712.88,299.839 724.412,288.697C735.935,277.174 741.693,263.338 741.693,247.196C741.693,231.054 735.935,217.413 724.412,206.271C712.88,194.748 699.046,188.983 682.904,188.983ZM1327.58,193.013C1311.45,193.013 1297.62,198.779 1286.1,210.302C1274.96,221.444 1269.38,235.08 1269.38,251.212L1269.38,519.72C1269.38,535.861 1274.96,549.697 1286.1,561.22C1297.62,572.362 1311.45,577.933 1327.58,577.933C1343.73,577.933 1357.55,572.362 1369.08,561.22C1380.6,549.697 1386.37,535.867 1386.37,519.734C1386.37,508.211 1387.9,502.453 1390.96,502.453C1392.51,502.453 1394.05,503.023 1395.58,504.166L1453.2,560.061C1464.72,571.203 1478.36,576.774 1494.11,576.774C1510.62,576.774 1524.45,571.009 1535.6,559.486C1546.74,547.572 1552.31,533.936 1552.31,518.575C1552.31,502.053 1546.36,488.029 1534.45,476.506C1508.32,450.765 1494.69,437.517 1493.54,436.755C1488.54,431.755 1488.73,427.53 1494.11,424.073L1495.27,423.497L1495.27,422.929L1531.57,399.875C1548.85,388.352 1557.5,372.026 1557.5,350.904C1557.5,339.381 1554.42,328.622 1548.28,318.623C1536.76,301.339 1520.43,292.691 1499.3,292.691C1487.78,292.691 1477.02,295.768 1467.04,301.911C1422.48,331.499 1399.42,346.678 1397.88,347.449C1395.2,349.363 1392.7,349.738 1390.4,348.586C1387.7,347.434 1386.35,344.939 1386.35,341.101L1386.35,251.212C1386.35,235.08 1380.59,221.444 1369.07,210.302C1357.55,198.779 1343.72,193.013 1327.58,193.013ZM1716.37,291.738C1676.42,291.738 1642.24,305.949 1613.81,334.376C1585.76,362.422 1571.74,396.227 1571.74,435.795C1571.74,475.745 1585.76,509.932 1613.81,538.359C1642.24,566.404 1676.42,580.428 1716.37,580.428C1755.94,580.428 1789.94,566.404 1818.37,538.359C1827.2,529.521 1831.62,518.773 1831.62,506.107C1831.62,493.423 1827.2,482.664 1818.37,473.827C1809.53,464.999 1798.77,460.584 1786.11,460.584C1773.42,460.584 1762.66,464.999 1753.83,473.827C1743.46,484.588 1730.97,489.963 1716.37,489.963C1701.4,489.963 1688.53,484.78 1677.77,474.41C1667.39,463.649 1662.2,450.775 1662.2,435.795C1662.2,421.206 1667.59,408.72 1678.35,398.34C1683.73,392.578 1690.26,388.74 1697.93,386.817C1699.87,386.436 1701.4,386.623 1702.55,387.385C1703.32,388.547 1702.93,389.702 1701.39,390.854L1689.87,402.953C1681.03,411.791 1676.61,422.359 1676.61,434.644C1676.61,447.319 1680.45,457.497 1688.13,465.182C1695.81,472.868 1706.57,476.705 1720.41,476.705C1730.01,476.705 1739.61,471.91 1749.21,462.311L1816.06,396.044C1824.9,387.197 1829.32,376.436 1829.32,363.77C1829.32,351.086 1824.9,340.332 1816.06,331.504C1789.17,304.992 1755.94,291.738 1716.37,291.738ZM877.977,292.668C841.947,292.194 813.839,301.679 793.662,321.133C775.996,338.036 767.168,359.358 767.168,385.089C767.549,417.363 780.035,441.565 804.624,457.697C811.918,462.687 820.941,466.72 831.693,469.796C837.083,471.72 846.111,474.02 858.777,476.705C869.919,479.391 882.023,481.886 895.088,484.191C897.774,484.962 898.924,486.312 898.543,488.236C898.543,490.541 897.58,491.691 895.657,491.691C890.667,492.072 886.059,492.266 881.831,492.266C850.328,488.81 829.001,485.927 817.859,483.622C814.793,482.851 811.535,482.463 808.078,482.463C796.165,482.463 785.787,486.884 776.949,495.721C768.502,504.178 764.282,514.551 764.282,526.836C764.282,536.825 767.352,545.854 773.494,553.92C780.027,561.986 788.486,567.169 798.866,569.473C831.13,576.778 860.317,580.428 886.429,580.428C922.16,580.428 950.013,570.825 969.992,551.617C987.277,535.094 995.925,513.775 995.925,487.653C995.925,455.388 983.626,431.187 959.037,415.045C945.972,406.598 927.915,400.45 904.869,396.612C891.042,393.927 879.518,391.427 870.3,389.112L870.3,389.127C867.605,388.356 866.067,386.818 865.686,384.513C865.686,382.59 867.224,381.44 870.3,381.059C873.757,380.678 877.415,380.678 881.262,381.059C913.146,384.135 934.652,386.823 945.794,389.127C948.861,389.889 951.931,390.271 955.007,390.271C967.301,390.271 977.674,386.051 986.121,377.604C994.959,368.767 999.379,358.393 999.379,346.49C999.379,336.109 996.109,326.894 989.576,318.837C983.043,310.761 974.79,305.566 964.81,303.261C938.298,297.5 911.788,294.042 885.285,292.89C882.813,292.77 880.379,292.7 877.977,292.668ZM1128.73,292.668C1092.7,292.194 1064.59,301.679 1044.42,321.133C1026.75,338.036 1017.92,359.358 1017.92,385.089C1018.3,417.363 1030.79,441.565 1055.38,457.697C1062.67,462.687 1071.7,466.72 1082.46,469.796C1087.84,471.72 1096.86,474.02 1109.54,476.705C1120.68,479.391 1132.79,481.886 1145.84,484.191C1148.53,484.962 1149.68,486.312 1149.3,488.236C1149.3,490.541 1148.34,491.691 1146.41,491.691C1141.42,492.072 1136.81,492.266 1132.59,492.266C1101.09,488.81 1079.77,485.927 1068.63,483.622C1065.55,482.851 1062.29,482.463 1058.83,482.463C1046.92,482.463 1036.55,486.884 1027.72,495.721C1019.26,504.178 1015.04,514.551 1015.04,526.836C1015.04,536.825 1018.11,545.854 1024.26,553.92C1030.79,561.986 1039.24,567.169 1049.62,569.473C1081.88,576.778 1111.08,580.428 1137.2,580.428C1172.92,580.428 1200.77,570.825 1220.75,551.617C1238.03,535.094 1246.68,513.775 1246.68,487.653C1246.68,455.388 1234.39,431.187 1209.81,415.045C1196.74,406.598 1178.68,400.45 1155.64,396.612C1141.81,393.927 1130.29,391.427 1121.07,389.112L1121.05,389.127C1118.37,388.356 1116.84,386.818 1116.45,384.513C1116.45,382.59 1117.99,381.44 1121.05,381.059C1124.52,380.678 1128.17,380.678 1132.01,381.059C1163.89,384.135 1185.41,386.823 1196.55,389.127C1199.62,389.889 1202.69,390.271 1205.76,390.271C1218.06,390.271 1228.43,386.051 1236.89,377.604C1245.72,368.767 1250.13,358.393 1250.13,346.49C1250.13,336.109 1246.87,326.894 1240.35,318.837C1233.81,310.761 1225.55,305.566 1215.56,303.261C1189.06,297.5 1162.55,294.042 1136.04,292.89C1133.57,292.77 1131.13,292.7 1128.73,292.668ZM1910.17,296.736C1894.04,296.736 1880.21,302.501 1868.69,314.024C1857.55,325.157 1851.98,338.793 1851.98,354.934L1851.98,435.028C1851.98,473.825 1865.8,507.05 1893.45,534.705C1921.12,562.36 1954.36,576.191 1993.15,576.191C2000.84,576.191 2007.95,575.614 2014.48,574.471C2018.32,573.699 2021,574.469 2022.53,576.774C2023.69,578.307 2023.3,580.42 2021.39,583.115C2016.39,590.029 2005.82,593.486 1989.69,593.486C1983.55,593.486 1975.68,591.949 1966.07,588.873C1956.47,585.797 1948.98,584.259 1943.6,584.259C1920.93,584.259 1904.99,594.638 1895.77,615.388C1892.32,622.302 1890.58,629.598 1890.58,637.283C1890.58,659.948 1900.77,675.892 1921.13,685.11C1941.49,694.709 1964.34,699.505 1989.69,699.505C2033.49,699.505 2068.25,686.639 2093.98,660.898C2120.11,635.166 2133.18,600.784 2133.18,557.758L2133.18,452.308C2133.94,446.157 2134.32,440.399 2134.32,435.028L2134.32,354.934C2134.32,338.802 2128.57,325.166 2117.04,314.024C2105.9,302.501 2092.27,296.736 2076.13,296.736C2059.99,296.736 2046.16,302.501 2034.63,314.024C2023.11,325.157 2017.35,338.793 2017.35,354.934L2017.35,435.028C2017.35,441.551 2015.04,447.309 2010.43,452.308C2005.83,456.918 2000.07,459.225 1993.15,459.225C1986.62,459.225 1980.86,456.918 1975.87,452.308C1971.26,447.309 1968.95,441.551 1968.95,435.028L1968.95,354.934C1968.95,338.802 1963.19,325.166 1951.67,314.024C1940.14,302.501 1926.3,296.736 1910.17,296.736ZM683.473,316.947C667.331,316.947 653.495,322.713 641.972,334.236C630.449,345.768 624.691,359.602 624.691,375.744L624.691,518.118C624.691,534.259 630.449,548.095 641.972,559.618C653.504,570.761 667.341,576.331 683.473,576.331C699.624,576.331 713.27,570.761 724.412,559.618C735.935,548.095 741.693,534.259 741.693,518.118L741.693,375.744C741.693,359.593 735.935,345.759 724.412,334.236C713.261,322.713 699.614,316.947 683.473,316.947Z" style="fill:white;fill-rule:nonzero;"/>
+ </g>
+</svg>
diff --git a/packages/backend/test/tsconfig.json b/packages/backend/test/tsconfig.json
index 5d91d0923a..8a024a678b 100644
--- a/packages/backend/test/tsconfig.json
+++ b/packages/backend/test/tsconfig.json
@@ -33,10 +33,12 @@
"lib": [
"esnext"
],
- "types": ["jest"]
+ "types": ["jest", "node"]
},
"compileOnSave": false,
"include": [
- "./**/*.ts"
+ "./**/*.ts",
+ "../src/**/*.test.ts",
+ "../src/@types/**/*.ts",
]
}
diff --git a/packages/backend/test/tests/ap-request.ts b/packages/backend/test/unit/ap-request.ts
index 8c586861ad..98f352e1c6 100644
--- a/packages/backend/test/tests/ap-request.ts
+++ b/packages/backend/test/unit/ap-request.ts
@@ -1,7 +1,8 @@
import * as assert from 'assert';
import httpSignature from '@peertube/http-signature';
-import { genRsaKeyPair } from '../../src/misc/gen-key-pair.js';
-import { createSignedPost, createSignedGet } from '../../src/activitypub/ap-request.js';
+
+import { genRsaKeyPair } from '@/misc/gen-key-pair.js';
+import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => {
return {
@@ -9,7 +10,7 @@ export const buildParsedSignature = (signingString: string, signature: string, a
params: {
keyId: 'KeyID', // dummy, not used for verify
algorithm: algorithm,
- headers: [ '(request-target)', 'date', 'host', 'digest' ], // dummy, not used for verify
+ headers: ['(request-target)', 'date', 'host', 'digest'], // dummy, not used for verify
signature: signature,
},
signingString: signingString,
@@ -29,7 +30,7 @@ describe('ap-request', () => {
'User-Agent': 'UA',
};
- const req = createSignedPost({ key, url, body, additionalHeaders: headers });
+ const req = ApRequestCreator.createSignedPost({ key, url, body, additionalHeaders: headers });
const parsed = buildParsedSignature(req.signingString, req.signature, 'rsa-sha256');
@@ -45,7 +46,7 @@ describe('ap-request', () => {
'User-Agent': 'UA',
};
- const req = createSignedGet({ key, url, additionalHeaders: headers });
+ const req = ApRequestCreator.createSignedGet({ key, url, additionalHeaders: headers });
const parsed = buildParsedSignature(req.signingString, req.signature, 'rsa-sha256');
diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json
index 6f335a2442..faadbcdfc6 100644
--- a/packages/backend/tsconfig.json
+++ b/packages/backend/tsconfig.json
@@ -26,9 +26,7 @@
"rootDir": "./src",
"baseUrl": "./",
"paths": {
- "@/*": [
- "./src/*"
- ]
+ "@/*": ["./src/*"]
},
"outDir": "./built",
"types": [
@@ -46,4 +44,7 @@
"include": [
"./src/**/*.ts"
],
+ "exclude": [
+ "./src/**/*.test.ts"
+ ]
}