diff --git a/packages/backend/src/core/entities/FollowRequestEntityService.ts b/packages/backend/src/core/entities/FollowRequestEntityService.ts index d67408873d..71947a1a48 100644 --- a/packages/backend/src/core/entities/FollowRequestEntityService.ts +++ b/packages/backend/src/core/entities/FollowRequestEntityService.ts @@ -5,6 +5,7 @@ import type { User } from '@/models/entities/User.js'; import type { FollowRequest } from '@/models/entities/FollowRequest.js'; import { UserEntityService } from './UserEntityService.js'; import { bindThis } from '@/decorators.js'; +import { Packed } from 'misskey-js'; @Injectable() export class FollowRequestEntityService { @@ -20,7 +21,7 @@ export class FollowRequestEntityService { public async pack( src: FollowRequest['id'] | FollowRequest, me?: { id: User['id'] } | null | undefined, - ) { + ): Promise> { const request = typeof src === 'object' ? src : await this.followRequestsRepository.findOneByOrFail({ id: src }); return { diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts deleted file mode 100644 index 74a308fe23..0000000000 --- a/packages/backend/src/misc/json-schema.ts +++ /dev/null @@ -1,186 +0,0 @@ -export type Packed = SchemaType; - - -import { - packedUserLiteSchema, - packedUserDetailedNotMeOnlySchema, - packedMeDetailedOnlySchema, - packedUserDetailedNotMeSchema, - packedMeDetailedSchema, - packedUserDetailedSchema, - packedUserSchema, -} from '@/models/json-schema/user.js'; -import { packedNoteSchema } from '@/models/json-schema/note.js'; -import { packedUserListSchema } from '@/models/json-schema/user-list.js'; -import { packedAppSchema } from '@/models/json-schema/app.js'; -import { packedNotificationSchema } from '@/models/json-schema/notification.js'; -import { packedDriveFileSchema } from '@/models/json-schema/drive-file.js'; -import { packedDriveFolderSchema } from '@/models/json-schema/drive-folder.js'; -import { packedFollowingSchema } from '@/models/json-schema/following.js'; -import { packedMutingSchema } from '@/models/json-schema/muting.js'; -import { packedRenoteMutingSchema } from '@/models/json-schema/renote-muting.js'; -import { packedBlockingSchema } from '@/models/json-schema/blocking.js'; -import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js'; -import { packedHashtagSchema } from '@/models/json-schema/hashtag.js'; -import { packedPageSchema } from '@/models/json-schema/page.js'; -import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js'; -import { packedChannelSchema } from '@/models/json-schema/channel.js'; -import { packedAntennaSchema } from '@/models/json-schema/antenna.js'; -import { packedClipSchema } from '@/models/json-schema/clip.js'; -import { packedFederationInstanceSchema } from '@/models/json-schema/federation-instance.js'; -import { packedQueueCountSchema } from '@/models/json-schema/queue.js'; -import { packedGalleryPostSchema } from '@/models/json-schema/gallery-post.js'; -import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/json-schema/emoji.js'; -import { packedFlashSchema } from '@/models/json-schema/flash.js'; - -export const refs = { - UserLite: packedUserLiteSchema, - UserDetailedNotMeOnly: packedUserDetailedNotMeOnlySchema, - MeDetailedOnly: packedMeDetailedOnlySchema, - UserDetailedNotMe: packedUserDetailedNotMeSchema, - MeDetailed: packedMeDetailedSchema, - UserDetailed: packedUserDetailedSchema, - User: packedUserSchema, - - UserList: packedUserListSchema, - App: packedAppSchema, - Note: packedNoteSchema, - NoteReaction: packedNoteReactionSchema, - NoteFavorite: packedNoteFavoriteSchema, - Notification: packedNotificationSchema, - DriveFile: packedDriveFileSchema, - DriveFolder: packedDriveFolderSchema, - Following: packedFollowingSchema, - Muting: packedMutingSchema, - RenoteMuting: packedRenoteMutingSchema, - Blocking: packedBlockingSchema, - Hashtag: packedHashtagSchema, - Page: packedPageSchema, - Channel: packedChannelSchema, - QueueCount: packedQueueCountSchema, - Antenna: packedAntennaSchema, - Clip: packedClipSchema, - FederationInstance: packedFederationInstanceSchema, - GalleryPost: packedGalleryPostSchema, - EmojiSimple: packedEmojiSimpleSchema, - EmojiDetailed: packedEmojiDetailedSchema, - Flash: packedFlashSchema, -}; - -type TypeStringef = 'null' | 'boolean' | 'integer' | 'number' | 'string' | 'array' | 'object' | 'any'; -type StringDefToType = - T extends 'null' ? null : - T extends 'boolean' ? boolean : - T extends 'integer' ? number : - T extends 'number' ? number : - T extends 'string' ? string | Date : - T extends 'array' ? ReadonlyArray : - T extends 'object' ? Record : - any; - -// https://swagger.io/specification/?sbsearch=optional#schema-object -type OfSchema = { - readonly anyOf?: ReadonlyArray; - readonly oneOf?: ReadonlyArray; - readonly allOf?: ReadonlyArray; -} - -export interface Schema extends OfSchema { - readonly type?: TypeStringef; - readonly nullable?: boolean; - readonly optional?: boolean; - readonly items?: Schema; - readonly properties?: Obj; - readonly required?: ReadonlyArray, string>>; - readonly description?: string; - readonly example?: any; - readonly format?: string; - readonly ref?: keyof typeof refs; - readonly enum?: ReadonlyArray; - readonly default?: (this['type'] extends TypeStringef ? StringDefToType : any) | null; - readonly maxLength?: number; - readonly minLength?: number; - readonly maximum?: number; - readonly minimum?: number; - readonly pattern?: string; -} - -type RequiredPropertyNames = { - [K in keyof s]: - // K is not optional - s[K]['optional'] extends false ? K : - // K has default value - s[K]['default'] extends null | string | number | boolean | Record ? K : - never -}[keyof s]; - -export type Obj = Record; - -// 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> = - UnionToIntersection< - { -readonly [R in RequiredPropertyNames]-?: SchemaType } & - { -readonly [R in RequiredProps[number]]-?: SchemaType } & - { -readonly [P in keyof s]?: SchemaType } - >; - -type NullOrUndefined

= - | (p['nullable'] extends true ? null : never) - | (p['optional'] extends true ? undefined : never) - | T; - -// https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection -// Get intersection from union -type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; -type PartialIntersection = Partial>; - -// https://github.com/misskey-dev/misskey/pull/8144#discussion_r785287552 -// To get union, we use `Foo extends any ? Hoge : never` -type UnionSchemaType = X extends any ? SchemaType : never; -//type UnionObjectSchemaType = X extends any ? ObjectSchemaType : never; -type UnionObjType = a[number]> = X extends any ? ObjType : never; -type ArrayUnion = T extends any ? Array : never; - -type ObjectSchemaTypeDef

= - p['ref'] extends keyof typeof refs ? Packed : - p['properties'] extends NonNullable ? - p['anyOf'] extends ReadonlyArray ? p['anyOf'][number]['required'] extends ReadonlyArray ? - UnionObjType> & ObjType> - : never - : ObjType> - : - p['anyOf'] extends ReadonlyArray ? never : // see CONTRIBUTING.md - p['allOf'] extends ReadonlyArray ? UnionToIntersection> : - any - -type ObjectSchemaType

= NullOrUndefined>; - -export type SchemaTypeDef

= - p['type'] extends 'null' ? null : - p['type'] extends 'integer' ? number : - p['type'] extends 'number' ? number : - p['type'] extends 'string' ? ( - p['enum'] extends readonly (string | null)[] ? - p['enum'][number] : - p['format'] extends 'date-time' ? string : // Dateにする?? - string - ) : - p['type'] extends 'boolean' ? boolean : - p['type'] extends 'object' ? ObjectSchemaTypeDef

: - p['type'] extends 'array' ? ( - p['items'] extends OfSchema ? ( - p['items']['anyOf'] extends ReadonlyArray ? UnionSchemaType>[] : - p['items']['oneOf'] extends ReadonlyArray ? ArrayUnion>> : - p['items']['allOf'] extends ReadonlyArray ? UnionToIntersection>>[] : - never - ) : - p['items'] extends NonNullable ? SchemaTypeDef[] : - any[] - ) : - p['anyOf'] extends ReadonlyArray ? UnionSchemaType & PartialIntersection> : - p['oneOf'] extends ReadonlyArray ? UnionSchemaType : - any; - -export type SchemaType

= NullOrUndefined>; diff --git a/packages/backend/src/server/api/endpoints/following/requests/accept.ts b/packages/backend/src/server/api/endpoints/following/requests/accept.ts index cca3e60614..f8a1d2e33c 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/accept.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/accept.ts @@ -4,51 +4,23 @@ import { GetterService } from '@/server/api/GetterService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { ApiError } from '../../../error.js'; -export const meta = { - tags: ['following', 'account'], - - requireCredential: true, - - kind: 'write:following', - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '66ce1645-d66c-46bb-8b79-96739af885bd', - }, - noFollowRequest: { - message: 'No follow request.', - code: 'NO_FOLLOW_REQUEST', - id: 'bcde4f8b-0913-4614-8881-614e522fb041', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], -} as const; - // eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint<'following/requests/accept'> { + name = 'following/requests/accept' as const; constructor( private getterService: GetterService, private userFollowingService: UserFollowingService, ) { - super(meta, paramDef, async (ps, me) => { + super(async (ps, me) => { // Fetch follower const follower = await this.getterService.getUser(ps.userId).catch(err => { - if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(this.meta.errors.noSuchUser); throw err; }); await this.userFollowingService.acceptFollowRequest(me, follower).catch(err => { - if (err.id === '8884c2dd-5795-4ac9-b27e-6a01d38190f9') throw new ApiError(meta.errors.noFollowRequest); + if (err.id === '8884c2dd-5795-4ac9-b27e-6a01d38190f9') throw new ApiError(this.meta.errors.noFollowRequest); throw err; }); diff --git a/packages/backend/src/server/api/endpoints/following/requests/cancel.ts b/packages/backend/src/server/api/endpoints/following/requests/cancel.ts index 7325e73cac..9ddf35fb55 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/cancel.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/cancel.ts @@ -8,45 +8,10 @@ import { UserFollowingService } from '@/core/UserFollowingService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; -export const meta = { - tags: ['following', 'account'], - - requireCredential: true, - - kind: 'write:following', - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '4e68c551-fc4c-4e46-bb41-7d4a37bf9dab', - }, - - followRequestNotFound: { - message: 'Follow request not found.', - code: 'FOLLOW_REQUEST_NOT_FOUND', - id: '089b125b-d338-482a-9a09-e2622ac9f8d4', - }, - }, - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'UserLite', - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], -} as const; - // eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint<'following/requests/cancel'> { + name = 'following/requests/cancel' as const; constructor( @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, @@ -55,10 +20,10 @@ export default class extends Endpoint { private getterService: GetterService, private userFollowingService: UserFollowingService, ) { - super(meta, paramDef, async (ps, me) => { + super(async (ps, me) => { // Fetch followee const followee = await this.getterService.getUser(ps.userId).catch(err => { - if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(this.meta.errors.noSuchUser); throw err; }); @@ -66,7 +31,7 @@ export default class extends Endpoint { await this.userFollowingService.cancelFollowRequest(followee, me); } catch (err) { if (err instanceof IdentifiableError) { - if (err.id === '17447091-ce07-46dd-b331-c1fd4f15b1e7') throw new ApiError(meta.errors.followRequestNotFound); + if (err.id === '17447091-ce07-46dd-b331-c1fd4f15b1e7') throw new ApiError(this.meta.errors.followRequestNotFound); } throw err; } diff --git a/packages/backend/src/server/api/endpoints/following/requests/list.ts b/packages/backend/src/server/api/endpoints/following/requests/list.ts index d68248fab9..b00840a5fb 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/list.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/list.ts @@ -5,53 +5,10 @@ import type { FollowRequestsRepository } from '@/models/index.js'; import { FollowRequestEntityService } from '@/core/entities/FollowRequestEntityService.js'; import { DI } from '@/di-symbols.js'; -export const meta = { - tags: ['following', 'account'], - - requireCredential: true, - - kind: 'read:following', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - follower: { - type: 'object', - optional: false, nullable: false, - ref: 'UserLite', - }, - followee: { - type: 'object', - optional: false, nullable: false, - ref: 'UserLite', - }, - }, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - }, - required: [], -} as const; - // eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint<'following/requests/list'> { + name = 'following/requests/list' as const; constructor( @Inject(DI.followRequestsRepository) private followRequestsRepository: FollowRequestsRepository, @@ -59,7 +16,7 @@ export default class extends Endpoint { private followRequestEntityService: FollowRequestEntityService, private queryService: QueryService, ) { - super(meta, paramDef, async (ps, me) => { + super(async (ps, me) => { const query = this.queryService.makePaginationQuery(this.followRequestsRepository.createQueryBuilder('request'), ps.sinceId, ps.untilId) .andWhere('request.followeeId = :meId', { meId: me.id }); diff --git a/packages/backend/src/server/api/endpoints/following/requests/reject.ts b/packages/backend/src/server/api/endpoints/following/requests/reject.ts index a8fdc44876..e87e573e37 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/reject.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/reject.ts @@ -4,41 +4,18 @@ import { GetterService } from '@/server/api/GetterService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { ApiError } from '../../../error.js'; -export const meta = { - tags: ['following', 'account'], - - requireCredential: true, - - kind: 'write:following', - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: 'abc2ffa6-25b2-4380-ba99-321ff3a94555', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], -} as const; - // eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint<'following/requests/reject'> { + name = 'following/requests/reject' as const; constructor( private getterService: GetterService, private userFollowingService: UserFollowingService, ) { - super(meta, paramDef, async (ps, me) => { + super(async (ps, me) => { // Fetch follower const follower = await this.getterService.getUser(ps.userId).catch(err => { - if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(this.meta.errors.noSuchUser); throw err; }); diff --git a/packages/misskey-js/src/endpoints.ts b/packages/misskey-js/src/endpoints.ts index edc4a90ca0..1d40413cf1 100644 --- a/packages/misskey-js/src/endpoints.ts +++ b/packages/misskey-js/src/endpoints.ts @@ -4702,6 +4702,125 @@ export const endpoints = { }] }, //#endregion + + //#region following + 'following/requests/accept': { + tags: ['following', 'account'], + + requireCredential: true, + + kind: 'write:following', + + errors: { + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '66ce1645-d66c-46bb-8b79-96739af885bd', + }, + noFollowRequest: { + message: 'No follow request.', + code: 'NO_FOLLOW_REQUEST', + id: 'bcde4f8b-0913-4614-8881-614e522fb041', + }, + }, + + defines: [{ + req: { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], + }, + res: undefined, + }] + }, + 'following/requests/cancel': { + tags: ['following', 'account'], + + requireCredential: true, + + kind: 'write:following', + + errors: { + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '4e68c551-fc4c-4e46-bb41-7d4a37bf9dab', + }, + + followRequestNotFound: { + message: 'Follow request not found.', + code: 'FOLLOW_REQUEST_NOT_FOUND', + id: '089b125b-d338-482a-9a09-e2622ac9f8d4', + }, + }, + + defines: [{ + req: { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], + }, + res: { + $ref: 'https://misskey-hub.net/api/schemas/UserLite', + } + }] + }, + 'following/requests/list': { + tags: ['following', 'account'], + + requireCredential: true, + + kind: 'read:following', + + defines: [{ + req: { + type: 'object', + properties: { + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + }, + required: [], + }, + res: { + type: 'array', + items: { + $ref: 'https://misskey-hub.net/api/schemas/FollowRequest', + }, + } + }] + }, + 'following/requests/reject': { + tags: ['following', 'account'], + + requireCredential: true, + + kind: 'write:following', + + errors: { + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: 'abc2ffa6-25b2-4380-ba99-321ff3a94555', + }, + }, + + defines: [{ + req: { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], + }, + res: undefined, + }] + }, + //#endregion } as const satisfies { [x: string]: IEndpointMeta; }; /** diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index 568f0828cf..5c7accd5da 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -122,11 +122,7 @@ export type Stats = { export type AuthSession = Packed<'AuthSession'>; -export type FollowRequest = { - id: ID; - follower: User; - followee: User; -}; +export type FollowRequest = Packed<'FollowRequest'>; export type FollowingFolloweePopulated = Following & { followee: UserDetailed; diff --git a/packages/misskey-js/src/schemas.ts b/packages/misskey-js/src/schemas.ts index 6475fdc15b..d9eea11f57 100644 --- a/packages/misskey-js/src/schemas.ts +++ b/packages/misskey-js/src/schemas.ts @@ -18,7 +18,10 @@ import { packedUserListSchema } from './schemas/user-list.js'; import { packedAppSchema } from './schemas/app.js'; import { packedDriveFileSchema } from './schemas/drive-file.js'; import { packedDriveFolderSchema } from './schemas/drive-folder.js'; -import { packedFollowingSchema } from './schemas/following.js'; +import { + packedFollowingSchema, + packedFollowRequestSchema, +} from './schemas/following.js'; import { packedMutingSchema } from './schemas/muting.js'; import { packedRenoteMutingSchema } from './schemas/renote-muting.js'; import { packedBlockingSchema } from './schemas/blocking.js'; @@ -82,6 +85,7 @@ export const refs = { DriveFile: packedDriveFileSchema, DriveFolder: packedDriveFolderSchema, Following: packedFollowingSchema, + FollowRequest: packedFollowRequestSchema, Muting: packedMutingSchema, RenoteMuting: packedRenoteMutingSchema, Blocking: packedBlockingSchema, diff --git a/packages/misskey-js/src/schemas/following.ts b/packages/misskey-js/src/schemas/following.ts index 74149290b1..4d6789a6b0 100644 --- a/packages/misskey-js/src/schemas/following.ts +++ b/packages/misskey-js/src/schemas/following.ts @@ -22,3 +22,19 @@ export const packedFollowingSchema = { 'followerId', ], } as const satisfies JSONSchema7Definition; + +export const packedFollowRequestSchema = { + $id: 'https://misskey-hub.net/api/schemas/FollowRequest', + + type: 'object', + properties: { + id: { $ref: 'https://misskey-hub.net/api/schemas/Id' }, + followee: { $ref: 'https://misskey-hub.net/api/schemas/UserLite' }, + follower: { $ref: 'https://misskey-hub.net/api/schemas/UserLite' }, + }, + required: [ + 'id', + 'followee', + 'follower', + ], +} as const satisfies JSONSchema7Definition;