diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 37c3bede9..9e40ac1c8 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -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" diff --git a/.vscode/extensions.json b/.vscode/extensions.json index baca8db24..3cdf81e33 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -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" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index e2a82b1ff..0ceec23ac 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,7 +7,7 @@ "*.test.ts": "typescript" }, "jest.jestCommandLine": "pnpm run jest", - "jest.autoRun": "off", + "jest.runMode": "on-demand", "editor.codeActionsOnSave": { "source.fixAll": "explicit" }, diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fc561d9a..0719132d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,17 @@ ### Client - Enhance: 自分のノートの添付ファイルから直接ファイルの詳細ページに飛べるように +- Enhance: 広告がMisskeyと同一ドメインの場合はRouterで遷移するように - Enhance: リアクション・いいねの総数を表示するように - Enhance: リアクション受け入れが「いいねのみ」の場合はリアクション絵文字一覧を表示しないように +- Enhance: 設定>プラグインのページからプラグインの簡易的なログやエラーを見られるように + - 実装の都合により、プラグインは1つエラーを起こした時に即時停止するようになりました - Fix: 一部のページ内リンクが正しく動作しない問題を修正 +- Fix: 周年の実績が閏年を考慮しない問題を修正 +- Fix: ローカルURLのプレビューポップアップが左上に表示される ### Server -- +- Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに ## 2024.3.1 diff --git a/cypress/e2e/basic.cy.js b/cypress/e2e/basic.cy.js index 604241d13..d2525e0a7 100644 --- a/cypress/e2e/basic.cy.js +++ b/cypress/e2e/basic.cy.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + describe('Before setup instance', () => { beforeEach(() => { cy.resetState(); diff --git a/cypress/e2e/router.cy.js b/cypress/e2e/router.cy.js index 6de27be5f..8d8fb3af3 100644 --- a/cypress/e2e/router.cy.js +++ b/cypress/e2e/router.cy.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + describe('Router transition', () => { describe('Redirect', () => { // サーバの初期化。ルートのテストに関しては各describeごとに1度だけ実行で十分だと思う(使いまわした方が早い) diff --git a/cypress/e2e/widgets.cy.js b/cypress/e2e/widgets.cy.js index df6ec8357..847801a69 100644 --- a/cypress/e2e/widgets.cy.js +++ b/cypress/e2e/widgets.cy.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + /* flaky describe('After user signed in', () => { beforeEach(() => { diff --git a/locales/index.d.ts b/locales/index.d.ts index f97061437..6d61ea99e 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -6973,6 +6973,10 @@ export interface Locale extends ILocale { * ソースを表示 */ "viewSource": string; + /** + * ログを表示 + */ + "viewLog": string; }; "_preferencesBackups": { /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index d5969082d..4cc38b72f 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1817,6 +1817,7 @@ _plugin: installWarn: "信頼できないプラグインはインストールしないでください。" manage: "プラグインの管理" viewSource: "ソースを表示" + viewLog: "ログを表示" _preferencesBackups: list: "作成したバックアップ" diff --git a/packages/backend/generate_api_json.js b/packages/backend/generate_api_json.js index 4079b3bb0..602ced1d7 100644 --- a/packages/backend/generate_api_json.js +++ b/packages/backend/generate_api_json.js @@ -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'); \ No newline at end of file +writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8'); diff --git a/packages/backend/migration/1689325027964-UserBlacklistAnntena.js b/packages/backend/migration/1689325027964-UserBlacklistAnntena.js index ce246b20f..2dc777449 100644 --- a/packages/backend/migration/1689325027964-UserBlacklistAnntena.js +++ b/packages/backend/migration/1689325027964-UserBlacklistAnntena.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class UserBlacklistAnntena1689325027964 { name = 'UserBlacklistAnntena1689325027964' diff --git a/packages/backend/migration/1690417561185-fix-renote-muting.js b/packages/backend/migration/1690417561185-fix-renote-muting.js index 14150b036..d9604ca26 100644 --- a/packages/backend/migration/1690417561185-fix-renote-muting.js +++ b/packages/backend/migration/1690417561185-fix-renote-muting.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class FixRenoteMuting1690417561185 { name = 'FixRenoteMuting1690417561185' diff --git a/packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js b/packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js index 7eda5debe..9bccdb3bb 100644 --- a/packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js +++ b/packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class ChangeCacheRemoteFilesDefault1690417561186 { name = 'ChangeCacheRemoteFilesDefault1690417561186' diff --git a/packages/backend/migration/1690417561187-Fix.js b/packages/backend/migration/1690417561187-Fix.js index e780e66d7..7f1d62d68 100644 --- a/packages/backend/migration/1690417561187-Fix.js +++ b/packages/backend/migration/1690417561187-Fix.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class Fix1690417561187 { name = 'Fix1690417561187' diff --git a/packages/backend/migration/1691649257651-refine-announcement.js b/packages/backend/migration/1691649257651-refine-announcement.js index 16fe987b0..5bdb17592 100644 --- a/packages/backend/migration/1691649257651-refine-announcement.js +++ b/packages/backend/migration/1691649257651-refine-announcement.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class RefineAnnouncement1691649257651 { name = 'RefineAnnouncement1691649257651' diff --git a/packages/backend/migration/1691657412740-refine-announcement-2.js b/packages/backend/migration/1691657412740-refine-announcement-2.js index 8791f99f4..67edf1965 100644 --- a/packages/backend/migration/1691657412740-refine-announcement-2.js +++ b/packages/backend/migration/1691657412740-refine-announcement-2.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class RefineAnnouncement21691657412740 { name = 'RefineAnnouncement21691657412740' diff --git a/packages/backend/migration/1695260774117-verified-links.js b/packages/backend/migration/1695260774117-verified-links.js index 18e0571d8..64c8a9ad8 100644 --- a/packages/backend/migration/1695260774117-verified-links.js +++ b/packages/backend/migration/1695260774117-verified-links.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class VerifiedLinks1695260774117 { name = 'VerifiedLinks1695260774117' diff --git a/packages/backend/migration/1695288787870-following-notify.js b/packages/backend/migration/1695288787870-following-notify.js index e7e2194b1..b3f78d5f2 100644 --- a/packages/backend/migration/1695288787870-following-notify.js +++ b/packages/backend/migration/1695288787870-following-notify.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class FollowingNotify1695288787870 { name = 'FollowingNotify1695288787870' diff --git a/packages/backend/migration/1695440131671-short-name.js b/packages/backend/migration/1695440131671-short-name.js index 2c37297fc..fdc256caf 100644 --- a/packages/backend/migration/1695440131671-short-name.js +++ b/packages/backend/migration/1695440131671-short-name.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class ShortName1695440131671 { name = 'ShortName1695440131671' diff --git a/packages/backend/migration/1695605508898-mutingNotificationTypes.js b/packages/backend/migration/1695605508898-mutingNotificationTypes.js index 8c0e52a2f..67d424314 100644 --- a/packages/backend/migration/1695605508898-mutingNotificationTypes.js +++ b/packages/backend/migration/1695605508898-mutingNotificationTypes.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class MutingNotificationTypes1695605508898 { name = 'MutingNotificationTypes1695605508898' diff --git a/packages/backend/migration/1695901659683-note-updated-at.js b/packages/backend/migration/1695901659683-note-updated-at.js index d8a151a1f..e828fb1a6 100644 --- a/packages/backend/migration/1695901659683-note-updated-at.js +++ b/packages/backend/migration/1695901659683-note-updated-at.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class NoteUpdatedAt1695901659683 { name = 'NoteUpdatedAt1695901659683' diff --git a/packages/backend/migration/1696323464251-user-list-membership.js b/packages/backend/migration/1696323464251-user-list-membership.js index 7534040c4..dc1d438dd 100644 --- a/packages/backend/migration/1696323464251-user-list-membership.js +++ b/packages/backend/migration/1696323464251-user-list-membership.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class UserListMembership1696323464251 { name = 'UserListMembership1696323464251' diff --git a/packages/backend/migration/1696331570827-hibernation.js b/packages/backend/migration/1696331570827-hibernation.js index 119d35913..1487ece77 100644 --- a/packages/backend/migration/1696331570827-hibernation.js +++ b/packages/backend/migration/1696331570827-hibernation.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class Hibernation1696331570827 { name = 'Hibernation1696331570827' diff --git a/packages/backend/migration/1696332072038-clean.js b/packages/backend/migration/1696332072038-clean.js index 97dba655f..92a6810d6 100644 --- a/packages/backend/migration/1696332072038-clean.js +++ b/packages/backend/migration/1696332072038-clean.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class Clean1696332072038 { name = 'Clean1696332072038' diff --git a/packages/backend/migration/1700383825690-hard-mute.js b/packages/backend/migration/1700383825690-hard-mute.js index f920a61b1..809308022 100644 --- a/packages/backend/migration/1700383825690-hard-mute.js +++ b/packages/backend/migration/1700383825690-hard-mute.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class HardMute1700383825690 { name = 'HardMute1700383825690' diff --git a/packages/backend/package.json b/packages/backend/package.json index e16d2bbbe..f55fe21fb 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -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", diff --git a/packages/backend/src/core/ChannelFollowingService.ts b/packages/backend/src/core/ChannelFollowingService.ts index 75843b977..12251595e 100644 --- a/packages/backend/src/core/ChannelFollowingService.ts +++ b/packages/backend/src/core/ChannelFollowingService.ts @@ -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'; diff --git a/packages/backend/src/core/WebAuthnService.ts b/packages/backend/src/core/WebAuthnService.ts index 62ecefecf..06ff5f416 100644 --- a/packages/backend/src/core/WebAuthnService.ts +++ b/packages/backend/src/core/WebAuthnService.ts @@ -12,7 +12,6 @@ import { verifyRegistrationResponse, } from '@simplewebauthn/server'; import { AttestationFormat, isoCBOR } from '@simplewebauthn/server/helpers'; -import { tinyCbor } from '@simplewebauthn/server/esm/deps.js'; import { DI } from '@/di-symbols.js'; import type { UserSecurityKeysRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; @@ -199,7 +198,7 @@ export class WebAuthnService { if (cert[0] === 0x04) { // 前の実装ではいつも 0x04 で始まっていた const halfLength = (cert.length - 1) / 2; - const cborMap = new Map(); + const cborMap = new Map(); cborMap.set(1, 2); // kty, EC2 cborMap.set(3, -7); // alg, ES256 cborMap.set(-1, 1); // crv, P256 diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index c61c15dcc..4da85be74 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -135,7 +135,7 @@ export class ApNoteService { object, error: err }); - throw new Error('invalid note'); + throw err; } const note = object as IPost; diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 3673646d1..225824f93 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -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'; @@ -47,11 +69,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; @@ -90,9 +124,6 @@ export class UserEntityService implements OnModuleInit { @Inject(DI.renoteMutingsRepository) private renoteMutingsRepository: RenoteMutingsRepository, - @Inject(DI.driveFilesRepository) - private driveFilesRepository: DriveFilesRepository, - @Inject(DI.noteUnreadsRepository) private noteUnreadsRepository: NoteUnreadsRepository, @@ -102,12 +133,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, ) { @@ -116,7 +141,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'); @@ -139,7 +163,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 { const [ following, isFollowed, @@ -212,6 +236,59 @@ export class UserEntityService implements OnModuleInit { }; } + @bindThis + public async getRelations(me: MiUser['id'], targets: MiUser['id'][]): Promise> { + 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 { /* @@ -304,6 +381,9 @@ export class UserEntityService implements OnModuleInit { schema?: S, includeSecrets?: boolean, userProfile?: MiUserProfile, + userRelations?: Map, + userMemos?: Map, + pinNotes?: Map, }, ): Promise> { const opts = Object.assign({ @@ -319,13 +399,41 @@ export class UserEntityService implements OnModuleInit { const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false; if (user.isSuspended && !iAmModerator) throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `User ${user.id} has been suspended.`); - 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 || iAmModerator) @@ -434,10 +540,7 @@ export class UserEntityService implements OnModuleInit { 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, } : {}), @@ -525,7 +628,59 @@ export class UserEntityService implements OnModuleInit { includeSecrets?: boolean, }, ): Promise[]> { - return (await Promise.allSettled(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 = new Map(); + let userMemos: Map = new Map(); + let pinNotes: Map = 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(); + 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 (await Promise.allSettled(_users.map(u => this.pack(u, me, { ...options, userProfile: profilesMap.get(u.id), userRelations: userRelations, userMemos: userMemos, pinNotes: pinNotes })))) .filter(result => result.status === 'fulfilled') .map(result => (result as PromiseFulfilledResult>).value); } diff --git a/packages/backend/src/misc/fastify-hook-handlers.ts b/packages/backend/src/misc/fastify-hook-handlers.ts index 49a48f6a6..3e1c099e0 100644 --- a/packages/backend/src/misc/fastify-hook-handlers.ts +++ b/packages/backend/src/misc/fastify-hook-handlers.ts @@ -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) => { diff --git a/packages/backend/src/misc/is-pure-renote.ts b/packages/backend/src/misc/is-pure-renote.ts index 994d98152..f9c2243a0 100644 --- a/packages/backend/src/misc/is-pure-renote.ts +++ b/packages/backend/src/misc/is-pure-renote.ts @@ -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 } { diff --git a/packages/backend/src/misc/loader.ts b/packages/backend/src/misc/loader.ts index 25f7b54d3..7f29b9db1 100644 --- a/packages/backend/src/misc/loader.ts +++ b/packages/backend/src/misc/loader.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + export type FetchFunction = (key: K) => Promise; type ResolveReject = Parameters>[0]>; diff --git a/packages/backend/src/models/ReversiGame.ts b/packages/backend/src/models/ReversiGame.ts index 59e0c8723..b1ba213f2 100644 --- a/packages/backend/src/models/ReversiGame.ts +++ b/packages/backend/src/models/ReversiGame.ts @@ -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'; diff --git a/packages/backend/src/models/json-schema/signin.ts b/packages/backend/src/models/json-schema/signin.ts index d27d2490c..45732a742 100644 --- a/packages/backend/src/models/json-schema/signin.ts +++ b/packages/backend/src/models/json-schema/signin.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + export const packedSigninSchema = { type: 'object', properties: { diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts index 50bdcdecc..6e154430f 100644 --- a/packages/backend/src/server/api/endpoints/antennas/update.ts +++ b/packages/backend/src/server/api/endpoints/antennas/update.ts @@ -68,7 +68,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() @@ -84,8 +84,10 @@ export default class extends Endpoint { // 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({ @@ -99,7 +101,7 @@ export default class extends Endpoint { // 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, @@ -113,7 +115,7 @@ export default class extends Endpoint { // 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, diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts index 6a5b2262f..1d75437b8 100644 --- a/packages/backend/src/server/api/endpoints/users/relation.ts +++ b/packages/backend/src/server/api/endpoints/users/relation.ts @@ -132,11 +132,9 @@ export default class extends Endpoint { // 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]); }); } } diff --git a/packages/backend/test/e2e/2fa.ts b/packages/backend/test/e2e/2fa.ts index 13c56b88a..bea57f36c 100644 --- a/packages/backend/test/e2e/2fa.ts +++ b/packages/backend/test/e2e/2fa.ts @@ -248,7 +248,7 @@ describe('2要素認証', () => { keyName, credentialId, creationOptions: registerKeyResponse.body, - }) as any, alice); + }) as FIXME, alice); assert.strictEqual(keyDoneResponse.status, 200); assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url')); assert.strictEqual(keyDoneResponse.body.name, keyName); @@ -307,7 +307,7 @@ describe('2要素認証', () => { keyName, credentialId, creationOptions: registerKeyResponse.body, - }) as any, alice); + }) as FIXME, alice); assert.strictEqual(keyDoneResponse.status, 200); const passwordLessResponse = await api('i/2fa/password-less', { @@ -370,7 +370,7 @@ describe('2要素認証', () => { keyName, credentialId, creationOptions: registerKeyResponse.body, - }) as any, alice); + }) as FIXME, alice); assert.strictEqual(keyDoneResponse.status, 200); const renamedKey = 'other-key'; @@ -419,7 +419,7 @@ describe('2要素認証', () => { keyName, credentialId, creationOptions: registerKeyResponse.body, - }) as any, alice); + }) as FIXME, alice); assert.strictEqual(keyDoneResponse.status, 200); // テストの実行順によっては複数残ってるので全部消す diff --git a/packages/backend/test/e2e/clips.ts b/packages/backend/test/e2e/clips.ts index ba6f9d6a6..91d8bfa5d 100644 --- a/packages/backend/test/e2e/clips.ts +++ b/packages/backend/test/e2e/clips.ts @@ -86,7 +86,7 @@ describe('クリップ', () => { ...request, }, { status: 204, - }) as any as void; + }) as FIXME as void; }; const show = async (parameters: Misskey.entities.ClipsShowRequest, request: Partial> = {}): Promise => { @@ -234,7 +234,7 @@ describe('クリップ', () => { code: 'NO_SUCH_CLIP', id: 'b4d92d70-b216-46fa-9a3f-a8c811699257', } }, - ...createClipDenyPattern as any, + ...createClipDenyPattern as FIXME, ])('の更新は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ endpoint: 'clips/update', parameters: { @@ -462,7 +462,7 @@ describe('クリップ', () => { ...request, }, { status: 204, - }) as any as void; + }) as FIXME as void; }; const unfavorite = async (parameters: Misskey.entities.ClipsUnfavoriteRequest, request: Partial> = {}): Promise => { @@ -473,7 +473,7 @@ describe('クリップ', () => { ...request, }, { status: 204, - }) as any as void; + }) as FIXME as void; }; const myFavorites = async (request: Partial> = {}): Promise => { @@ -648,7 +648,7 @@ describe('クリップ', () => { ...request, }, { status: 204, - }) as any as void; + }) as FIXME as void; }; const removeNote = async (parameters: Misskey.entities.ClipsRemoveNoteRequest, request: Partial> = {}): Promise => { @@ -659,7 +659,7 @@ describe('クリップ', () => { ...request, }, { status: 204, - }) as any as void; + }) as FIXME as void; }; const notes = async (parameters: Misskey.entities.ClipsNotesRequest, request: Partial> = {}): Promise => { diff --git a/packages/backend/test/e2e/move.ts b/packages/backend/test/e2e/move.ts index 4e5306da9..a9b0fbea2 100644 --- a/packages/backend/test/e2e/move.ts +++ b/packages/backend/test/e2e/move.ts @@ -447,8 +447,8 @@ describe('Account Move', () => { const res = await uploadFile(alice); assert.strictEqual(res.status, 403); - assert.strictEqual((res.body! as any as { error: misskey.api.APIError }).error.code, 'YOUR_ACCOUNT_MOVED'); - assert.strictEqual((res.body! as any as { error: misskey.api.APIError }).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); + assert.strictEqual((res.body! as FIXME as { error: misskey.api.APIError }).error.code, 'YOUR_ACCOUNT_MOVED'); + assert.strictEqual((res.body! as FIXME as { error: misskey.api.APIError }).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); }); test('Prohibit updating alsoKnownAs after moving', async () => { diff --git a/packages/backend/test/e2e/nodeinfo.ts b/packages/backend/test/e2e/nodeinfo.ts index 28b96fe8c..e0d252545 100644 --- a/packages/backend/test/e2e/nodeinfo.ts +++ b/packages/backend/test/e2e/nodeinfo.ts @@ -14,7 +14,7 @@ describe('nodeinfo', () => { assert.ok(res.ok); assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); - const nodeInfo = await res.json() as any; + const nodeInfo = await res.json() as FIXME; assert.strictEqual(nodeInfo.software.name, 'misskey'); }); @@ -23,7 +23,7 @@ describe('nodeinfo', () => { assert.ok(res.ok); assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); - const nodeInfo = await res.json() as any; + const nodeInfo = await res.json() as FIXME; assert.strictEqual(nodeInfo.software.name, 'misskey'); }); }); diff --git a/packages/backend/test/e2e/note.ts b/packages/backend/test/e2e/note.ts index 9b97dcce3..c027eebf3 100644 --- a/packages/backend/test/e2e/note.ts +++ b/packages/backend/test/e2e/note.ts @@ -474,7 +474,7 @@ describe('Note', () => { priority: 0, value: true, }, - }, + } as FIXME, }, alice); assert.strictEqual(res.status, 200); @@ -787,7 +787,7 @@ describe('Note', () => { priority: 1, value: 0, }, - }, + } as FIXME, }, alice); assert.strictEqual(res.status, 200); @@ -841,7 +841,7 @@ describe('Note', () => { priority: 1, value: 0, }, - }, + } as FIXME, }, alice); assert.strictEqual(res.status, 200); @@ -897,7 +897,7 @@ describe('Note', () => { priority: 1, value: 1, }, - }, + } as FIXME, }, alice); assert.strictEqual(res.status, 200); diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts index c63829207..87a8378b7 100644 --- a/packages/backend/test/e2e/timelines.ts +++ b/packages/backend/test/e2e/timelines.ts @@ -890,17 +890,35 @@ describe('Timelines', () => { const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); + await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice); await sleep(1000); const aliceNote = await post(alice, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); await waitForPushToTl(); - const res = await api('notes/user-list-timeline', { listId: list.id, withReplies: false }, alice); + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); }); + test.concurrent('withReplies: false でリスインしているフォローしていないユーザーの他人への返信が含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); + await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + test.concurrent('withReplies: true でリスインしているフォローしていないユーザーの他人への返信が含まれる', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index c31b963a9..79b5b66d9 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -633,7 +633,7 @@ describe('ユーザー', () => { { 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 as any), (expected ?? ((): true => true))()); + assert.strictEqual(selector(response as FIXME), (expected ?? ((): true => true))()); }); test('を取得することができ、Publicなロールがセットされていること', async () => { const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: userRolePublic.id }, user: alice }); diff --git a/packages/backend/test/e2e/well-known.ts b/packages/backend/test/e2e/well-known.ts index bdb298dfe..43967ba63 100644 --- a/packages/backend/test/e2e/well-known.ts +++ b/packages/backend/test/e2e/well-known.ts @@ -95,7 +95,7 @@ describe('.well-known', () => { assert.ok(res.ok); assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); - const serverInfo = await res.json() as any; + const serverInfo = await res.json() as FIXME; assert.strictEqual(serverInfo.issuer, origin); assert.strictEqual(serverInfo.authorization_endpoint, `${origin}/oauth/authorize`); assert.strictEqual(serverInfo.token_endpoint, `${origin}/oauth/token`); diff --git a/packages/backend/test/global.d.ts b/packages/backend/test/global.d.ts new file mode 100644 index 000000000..036307335 --- /dev/null +++ b/packages/backend/test/global.d.ts @@ -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; diff --git a/packages/backend/test/jest.setup.ts b/packages/backend/test/jest.setup.ts index cf5b9bf24..861bc6db6 100644 --- a/packages/backend/test/jest.setup.ts +++ b/packages/backend/test/jest.setup.ts @@ -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 () => { diff --git a/packages/backend/test/prelude/get-api-validator.ts b/packages/backend/test/prelude/get-api-validator.ts index b86a7a978..7aa7a9270 100644 --- a/packages/backend/test/prelude/get-api-validator.ts +++ b/packages/backend/test/prelude/get-api-validator.ts @@ -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]+$/); diff --git a/packages/backend/test/tsconfig.json b/packages/backend/test/tsconfig.json index 4597ff878..2b562acda 100644 --- a/packages/backend/test/tsconfig.json +++ b/packages/backend/test/tsconfig.json @@ -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, diff --git a/packages/backend/test/unit/AnnouncementService.ts b/packages/backend/test/unit/AnnouncementService.ts index 81da0fac3..ff3219b66 100644 --- a/packages/backend/test/unit/AnnouncementService.ts +++ b/packages/backend/test/unit/AnnouncementService.ts @@ -178,7 +178,7 @@ describe('AnnouncementService', () => { expect(globalEventService.publishBroadcastStream).toHaveBeenCalled(); expect(globalEventService.publishBroadcastStream.mock.lastCall![0]).toBe('announcementCreated'); - expect((globalEventService.publishBroadcastStream.mock.lastCall![1] as any).announcement).toBe(result.packed); + expect((globalEventService.publishBroadcastStream.mock.lastCall![1] as FIXME).announcement).toBe(result.packed); expect(moderationLogService.log).toHaveBeenCalled(); }); @@ -198,7 +198,7 @@ describe('AnnouncementService', () => { expect(globalEventService.publishMainStream).toHaveBeenCalled(); expect(globalEventService.publishMainStream.mock.lastCall![0]).toBe(user.id); expect(globalEventService.publishMainStream.mock.lastCall![1]).toBe('announcementCreated'); - expect((globalEventService.publishMainStream.mock.lastCall![2] as any).announcement).toBe(result.packed); + expect((globalEventService.publishMainStream.mock.lastCall![2] as FIXME).announcement).toBe(result.packed); expect(moderationLogService.log).toHaveBeenCalled(); }); }); diff --git a/packages/backend/test/unit/ApMfmService.ts b/packages/backend/test/unit/ApMfmService.ts index 2b79041c8..223110b2b 100644 --- a/packages/backend/test/unit/ApMfmService.ts +++ b/packages/backend/test/unit/ApMfmService.ts @@ -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'; @@ -21,7 +26,7 @@ describe('ApMfmService', () => { const note: MiNote = { text: 'テキスト #タグ @mention 🍊 :emoji: https://example.com', mentionedRemoteUsers: '[]', - } as any; + } as FIXME; const { content, noMisskeyContent } = apMfmService.getNoteHtml(note); @@ -33,7 +38,7 @@ describe('ApMfmService', () => { const note: MiNote = { text: '$[tada foo]', mentionedRemoteUsers: '[]', - } as any; + } as FIXME; const { content, noMisskeyContent } = apMfmService.getNoteHtml(note); diff --git a/packages/backend/test/unit/FetchInstanceMetadataService.ts b/packages/backend/test/unit/FetchInstanceMetadataService.ts index 029a333be..2b1a51b3d 100644 --- a/packages/backend/test/unit/FetchInstanceMetadataService.ts +++ b/packages/backend/test/unit/FetchInstanceMetadataService.ts @@ -7,7 +7,6 @@ process.env.NODE_ENV = 'test'; import { jest } from '@jest/globals'; import { Test } from '@nestjs/testing'; -import { Redis } from 'ioredis'; import { GlobalModule } from '@/GlobalModule.js'; import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; @@ -18,15 +17,16 @@ import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; import type { TestingModule } from '@nestjs/testing'; -function mockRedis() { - const hash = {} as any; - const set = jest.fn((key: string, value) => { - // このテストで呼び出すSETにはNXオプションが付いてる - if (hash[key]) return null; - hash[key] = value; - return 'OK'; +function mockRedisSetNX() { + const hash = {} as FIXME; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + return jest.fn((key: string, value, secondsToken: 'EX', seconds: number, nx: 'NX', callback?): Promise<'OK' | null> => { + return new Promise(resolve => { + if (hash[key]) return resolve(null); + hash[key] = value; + resolve('OK'); + }); }); - return set; } describe('FetchInstanceMetadataService', () => { @@ -34,7 +34,6 @@ describe('FetchInstanceMetadataService', () => { let fetchInstanceMetadataService: jest.Mocked; let federatedInstanceService: jest.Mocked; let httpRequestService: jest.Mocked; - let redisClient: jest.Mocked; beforeEach(async () => { app = await Test @@ -55,7 +54,7 @@ describe('FetchInstanceMetadataService', () => { } else if (token === FederatedInstanceService) { return { fetch: jest.fn() }; } else if (token === DI.redis) { - return mockRedis; + return { set: mockRedisSetNX() }; } return null; }) @@ -65,7 +64,6 @@ describe('FetchInstanceMetadataService', () => { fetchInstanceMetadataService = app.get(FetchInstanceMetadataService) as jest.Mocked; federatedInstanceService = app.get(FederatedInstanceService) as jest.Mocked; - redisClient = app.get(DI.redis) as jest.Mocked; httpRequestService = app.get(HttpRequestService) as jest.Mocked; }); @@ -74,14 +72,13 @@ describe('FetchInstanceMetadataService', () => { }); test('Lock and update', async () => { - redisClient.set = mockRedis(); const now = Date.now(); - federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } } as any); + federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } } as FIXME); httpRequestService.getJson.mockImplementation(() => { throw Error(); }); const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); - await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any); + await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as FIXME); expect(tryLockSpy).toHaveBeenCalledTimes(1); expect(unlockSpy).toHaveBeenCalledTimes(1); expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1); @@ -89,14 +86,13 @@ describe('FetchInstanceMetadataService', () => { }); test('Lock and don\'t update', async () => { - redisClient.set = mockRedis(); const now = Date.now(); - federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now } } as any); + federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now } } as FIXME); httpRequestService.getJson.mockImplementation(() => { throw Error(); }); const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); - await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any); + await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as FIXME); expect(tryLockSpy).toHaveBeenCalledTimes(1); expect(unlockSpy).toHaveBeenCalledTimes(1); expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1); @@ -104,15 +100,14 @@ describe('FetchInstanceMetadataService', () => { }); test('Do nothing when lock not acquired', async () => { - redisClient.set = mockRedis(); const now = Date.now(); - federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any); + federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as FIXME); 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); + await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as FIXME); expect(tryLockSpy).toHaveBeenCalledTimes(1); expect(unlockSpy).toHaveBeenCalledTimes(0); expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0); @@ -120,15 +115,14 @@ describe('FetchInstanceMetadataService', () => { }); 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); + federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as FIXME); 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); + await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as FIXME, true); expect(tryLockSpy).toHaveBeenCalledTimes(1); expect(unlockSpy).toHaveBeenCalledTimes(1); expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0); diff --git a/packages/backend/test/unit/FileInfoService.ts b/packages/backend/test/unit/FileInfoService.ts index 77f7320b1..f7263209e 100644 --- a/packages/backend/test/unit/FileInfoService.ts +++ b/packages/backend/test/unit/FileInfoService.ts @@ -63,7 +63,7 @@ describe('FileInfoService', () => { test('Empty file', async () => { const path = `${resources}/emptyfile`; - const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as FIXME; delete info.warnings; delete info.blurhash; delete info.sensitive; @@ -84,7 +84,7 @@ describe('FileInfoService', () => { describe('IMAGE', () => { test('Generic JPEG', async () => { const path = `${resources}/Lenna.jpg`; - const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as FIXME; delete info.warnings; delete info.blurhash; delete info.sensitive; @@ -104,7 +104,7 @@ describe('FileInfoService', () => { test('Generic APNG', async () => { const path = `${resources}/anime.png`; - const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as FIXME; delete info.warnings; delete info.blurhash; delete info.sensitive; @@ -124,7 +124,7 @@ describe('FileInfoService', () => { test('Generic AGIF', async () => { const path = `${resources}/anime.gif`; - const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as FIXME; delete info.warnings; delete info.blurhash; delete info.sensitive; @@ -144,7 +144,7 @@ describe('FileInfoService', () => { test('PNG with alpha', async () => { const path = `${resources}/with-alpha.png`; - const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as FIXME; delete info.warnings; delete info.blurhash; delete info.sensitive; @@ -164,7 +164,7 @@ describe('FileInfoService', () => { test('Generic SVG', async () => { const path = `${resources}/image.svg`; - const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as FIXME; delete info.warnings; delete info.blurhash; delete info.sensitive; @@ -185,7 +185,7 @@ describe('FileInfoService', () => { test('SVG with XML definition', async () => { // https://github.com/misskey-dev/misskey/issues/4413 const path = `${resources}/with-xml-def.svg`; - const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as FIXME; delete info.warnings; delete info.blurhash; delete info.sensitive; @@ -205,7 +205,7 @@ describe('FileInfoService', () => { test('Dimension limit', async () => { const path = `${resources}/25000x25000.png`; - const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as FIXME; delete info.warnings; delete info.blurhash; delete info.sensitive; @@ -225,7 +225,7 @@ describe('FileInfoService', () => { test('Rotate JPEG', async () => { const path = `${resources}/rotate.jpg`; - const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as FIXME; delete info.warnings; delete info.blurhash; delete info.sensitive; @@ -247,7 +247,7 @@ describe('FileInfoService', () => { describe('AUDIO', () => { test('MP3', async () => { const path = `${resources}/kick_gaba7.mp3`; - const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as FIXME; delete info.warnings; delete info.blurhash; delete info.sensitive; @@ -267,7 +267,7 @@ describe('FileInfoService', () => { test('WAV', async () => { const path = `${resources}/kick_gaba7.wav`; - const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as FIXME; delete info.warnings; delete info.blurhash; delete info.sensitive; @@ -287,7 +287,7 @@ describe('FileInfoService', () => { test('AAC', async () => { const path = `${resources}/kick_gaba7.aac`; - const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as FIXME; delete info.warnings; delete info.blurhash; delete info.sensitive; @@ -307,7 +307,7 @@ describe('FileInfoService', () => { test('FLAC', async () => { const path = `${resources}/kick_gaba7.flac`; - const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as FIXME; delete info.warnings; delete info.blurhash; delete info.sensitive; @@ -329,7 +329,7 @@ describe('FileInfoService', () => { * video/webmとして検出されてしまう test('WEBM AUDIO', async () => { const path = `${resources}/kick_gaba7.webm`; - const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as FIXME; delete info.warnings; delete info.blurhash; delete info.sensitive; diff --git a/packages/backend/test/unit/RelayService.ts b/packages/backend/test/unit/RelayService.ts index f2a67dba4..ced50be47 100644 --- a/packages/backend/test/unit/RelayService.ts +++ b/packages/backend/test/unit/RelayService.ts @@ -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 FIXME).type).toBe('Follow'); expect(queueService.deliver.mock.lastCall![2]).toBe('https://example.com'); //expect(queueService.deliver.mock.lastCall![0].username).toBe('relay.actor'); diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts index 19d03570e..485b29bd2 100644 --- a/packages/backend/test/unit/RoleService.ts +++ b/packages/backend/test/unit/RoleService.ts @@ -130,7 +130,7 @@ describe('RoleService', () => { policies: { canManageCustomEmojis: false, }, - } as any); + } as FIXME); const result = await roleService.getUserPolicies(user.id); @@ -143,7 +143,7 @@ describe('RoleService', () => { policies: { canManageCustomEmojis: true, }, - } as any); + } as FIXME); const result = await roleService.getUserPolicies(user.id); @@ -167,7 +167,7 @@ describe('RoleService', () => { policies: { canManageCustomEmojis: false, }, - } as any); + } as FIXME); const result = await roleService.getUserPolicies(user.id); @@ -202,7 +202,7 @@ describe('RoleService', () => { policies: { driveCapacityMb: 50, }, - } as any); + } as FIXME); const result = await roleService.getUserPolicies(user.id); @@ -246,7 +246,7 @@ describe('RoleService', () => { policies: { canManageCustomEmojis: false, }, - } as any); + } as FIXME); const user1Policies = await roleService.getUserPolicies(user1.id); const user2Policies = await roleService.getUserPolicies(user2.id); @@ -299,7 +299,7 @@ describe('RoleService', () => { policies: { canManageCustomEmojis: false, }, - } as any); + } as FIXME); const result = await roleService.getUserPolicies(user.id); expect(result.canManageCustomEmojis).toBe(true); diff --git a/packages/backend/test/unit/entities/UserEntityService.ts b/packages/backend/test/unit/entities/UserEntityService.ts new file mode 100644 index 000000000..bc340dcb1 --- /dev/null +++ b/packages/backend/test/unit/entities/UserEntityService.ts @@ -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 = {}, profileData: Partial = {}) { + 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); + usersRepository = app.get(DI.usersRepository); + userProfileRepository = app.get(DI.userProfilesRepository); + userMemosRepository = app.get(DI.userMemosRepository); + followingRepository = app.get(DI.followingsRepository); + followingRequestRepository = app.get(DI.followRequestsRepository); + blockingRepository = app.get(DI.blockingsRepository); + mutingRepository = app.get(DI.mutingsRepository); + renoteMutingsRepository = app.get(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 FIXME; + // 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 FIXME; + // 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 FIXME; + // 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 FIXME; + 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 FIXME; + 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 FIXME; + 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 FIXME; + 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 FIXME; + 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 FIXME; + 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 FIXME; + 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 FIXME; + 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 FIXME; + 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 FIXME; + 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 FIXME; + 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 FIXME; + 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 FIXME; + 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 FIXME; + 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 FIXME; + 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 FIXME; + 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); + } + } + }); + }); + }); +}); diff --git a/packages/backend/test/unit/misc/loader.ts b/packages/backend/test/unit/misc/loader.ts index fa3795095..2cf54e155 100644 --- a/packages/backend/test/unit/misc/loader.ts +++ b/packages/backend/test/unit/misc/loader.ts @@ -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 { diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 6d1212ce4..342501d09 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -194,7 +194,7 @@ export const page = async (user: UserToken, page: Partial }, ], eyeCatchingImageId: null, - font: 'sans-serif' as any, + font: 'sans-serif' as FIXME, hideTitleWhenPinned: false, name: '1678594845072', script: '', @@ -256,7 +256,7 @@ export const role = async (user: UserToken, role: Partial condFormula: { id: 'ebef1684-672d-49b6-ad82-1b3ec3784f85', type: 'isRemote', - } as any, + } as FIXME, description: '', displayOrder: 0, iconUrl: null, diff --git a/packages/frontend/.storybook/preview-head.html b/packages/frontend/.storybook/preview-head.html index 17d9c38c5..d2e77cb7c 100644 --- a/packages/frontend/.storybook/preview-head.html +++ b/packages/frontend/.storybook/preview-head.html @@ -1,3 +1,8 @@ + + diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index d68c96b14..1d2831f5b 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -178,14 +178,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) { @@ -220,7 +232,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'); } diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue index a5abbecea..3f7aba2fe 100644 --- a/packages/frontend/src/components/MkLink.vue +++ b/packages/frontend/src/components/MkLink.vue @@ -29,13 +29,13 @@ const self = props.url.startsWith(local); const attr = self ? 'to' : 'href'; const target = self ? null : '_blank'; -const el = ref(); +const el = ref(); 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'); }); diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 11dfb1eb1..a1d60de3d 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -121,7 +121,7 @@ SPDX-License-Identifier: AGPL-3.0-only -

{{ number(appearNote.reactionCount) }}

+

{{ number(appearNote.reactionCount) }}