From c454a4478504d29c67a6ad0880c5e588f8712a5f Mon Sep 17 00:00:00 2001 From: tamaina Date: Mon, 3 Jul 2023 09:18:54 +0000 Subject: [PATCH] wip hashtags --- .../src/server/api/endpoints/hashtags/list.ts | 33 +---- .../server/api/endpoints/hashtags/search.ts | 30 +--- .../src/server/api/endpoints/hashtags/show.ts | 35 +---- .../server/api/endpoints/hashtags/trend.ts | 52 +------ .../server/api/endpoints/hashtags/users.ts | 33 +---- packages/misskey-js/src/endpoints.ts | 133 +++++++++++++++++- packages/misskey-js/src/entities.ts | 13 +- packages/misskey-js/src/schemas/user.ts | 8 ++ 8 files changed, 164 insertions(+), 173 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/hashtags/list.ts b/packages/backend/src/server/api/endpoints/hashtags/list.ts index 226a11de0b..15258fdd7d 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/list.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/list.ts @@ -4,44 +4,17 @@ import type { HashtagsRepository } from '@/models/index.js'; import { HashtagEntityService } from '@/core/entities/HashtagEntityService.js'; import { DI } from '@/di-symbols.js'; -export const meta = { - tags: ['hashtags'], - - requireCredential: false, - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'Hashtag', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - attachedToUserOnly: { type: 'boolean', default: false }, - attachedToLocalUserOnly: { type: 'boolean', default: false }, - attachedToRemoteUserOnly: { type: 'boolean', default: false }, - sort: { type: 'string', enum: ['+mentionedUsers', '-mentionedUsers', '+mentionedLocalUsers', '-mentionedLocalUsers', '+mentionedRemoteUsers', '-mentionedRemoteUsers', '+attachedUsers', '-attachedUsers', '+attachedLocalUsers', '-attachedLocalUsers', '+attachedRemoteUsers', '-attachedRemoteUsers'] }, - }, - required: ['sort'], -} as const; - // eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint<'hashtags/list'> { + name = 'hashtags/list' as const; constructor( @Inject(DI.hashtagsRepository) private hashtagsRepository: HashtagsRepository, private hashtagEntityService: HashtagEntityService, ) { - super(meta, paramDef, async (ps, me) => { + super(async (ps, me) => { const query = this.hashtagsRepository.createQueryBuilder('tag'); if (ps.attachedToUserOnly) query.andWhere('tag.attachedUsersCount != 0'); diff --git a/packages/backend/src/server/api/endpoints/hashtags/search.ts b/packages/backend/src/server/api/endpoints/hashtags/search.ts index 4f5f979767..f9437d0491 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/search.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/search.ts @@ -4,39 +4,15 @@ import type { HashtagsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; -export const meta = { - tags: ['hashtags'], - - requireCredential: false, - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'string', - optional: false, nullable: false, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - query: { type: 'string' }, - offset: { type: 'integer', default: 0 }, - }, - required: ['query'], -} as const; - // eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint<'hashtags/search'> { + name = 'hashtags/search' as const; constructor( @Inject(DI.hashtagsRepository) private hashtagsRepository: HashtagsRepository, ) { - super(meta, paramDef, async (ps, me) => { + super(async (ps, me) => { const hashtags = await this.hashtagsRepository.createQueryBuilder('tag') .where('tag.name like :q', { q: sqlLikeEscape(ps.query.toLowerCase()) + '%' }) .orderBy('tag.count', 'DESC') diff --git a/packages/backend/src/server/api/endpoints/hashtags/show.ts b/packages/backend/src/server/api/endpoints/hashtags/show.ts index 06b0d6e9b2..30863cf0a7 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/show.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/show.ts @@ -6,47 +6,20 @@ import { HashtagEntityService } from '@/core/entities/HashtagEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; -export const meta = { - tags: ['hashtags'], - - requireCredential: false, - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'Hashtag', - }, - - errors: { - noSuchHashtag: { - message: 'No such hashtag.', - code: 'NO_SUCH_HASHTAG', - id: '110ee688-193e-4a3a-9ecf-c167b2e6981e', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - tag: { type: 'string' }, - }, - required: ['tag'], -} as const; - // eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint<'hashtags/show'> { + name = 'hashtags/show' as const; constructor( @Inject(DI.hashtagsRepository) private hashtagsRepository: HashtagsRepository, private hashtagEntityService: HashtagEntityService, ) { - super(meta, paramDef, async (ps, me) => { + super(async (ps, me) => { const hashtag = await this.hashtagsRepository.findOneBy({ name: normalizeForSearch(ps.tag) }); if (hashtag == null) { - throw new ApiError(meta.errors.noSuchHashtag); + throw new ApiError(this.meta.errors.noSuchHashtag); } return await this.hashtagEntityService.pack(hashtag); diff --git a/packages/backend/src/server/api/endpoints/hashtags/trend.ts b/packages/backend/src/server/api/endpoints/hashtags/trend.ts index ce1cd9f01f..7b2b563633 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/trend.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/trend.ts @@ -22,57 +22,17 @@ const rangeA = 1000 * 60 * 60; // 60分 const max = 5; -export const meta = { - tags: ['hashtags'], - - requireCredential: false, - allowGet: true, - cacheSec: 60 * 1, - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - tag: { - type: 'string', - optional: false, nullable: false, - }, - chart: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'number', - optional: false, nullable: false, - }, - }, - usersCount: { - type: 'number', - optional: false, nullable: false, - }, - }, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - // eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint<'hashtags/trend'> { + name = 'hashtags/trend' as const; constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, private metaService: MetaService, ) { - super(meta, paramDef, async () => { + super(async () => { const instance = await this.metaService.fetch(true); const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t)); @@ -95,9 +55,9 @@ export default class extends Endpoint { } const tags: { - name: string; - users: Note['userId'][]; - }[] = []; + name: string; + users: Note['userId'][]; + }[] = []; for (const note of tagNotes) { for (const tag of note.tags) { diff --git a/packages/backend/src/server/api/endpoints/hashtags/users.ts b/packages/backend/src/server/api/endpoints/hashtags/users.ts index dd3549020e..4eea1001c8 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/users.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/users.ts @@ -5,44 +5,17 @@ import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; -export const meta = { - requireCredential: false, - - tags: ['hashtags', 'users'], - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'UserDetailed', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - tag: { type: 'string' }, - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt'] }, - state: { type: 'string', enum: ['all', 'alive'], default: 'all' }, - origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' }, - }, - required: ['tag', 'sort'], -} as const; - // eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint<'hashtags/users'> { + name = 'hashtags/users' as const; constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, private userEntityService: UserEntityService, ) { - super(meta, paramDef, async (ps, me) => { + super(async (ps, me) => { const query = this.usersRepository.createQueryBuilder('user') .where(':tag = ANY(user.tags)', { tag: normalizeForSearch(ps.tag) }) .andWhere('user.isSuspended = FALSE'); diff --git a/packages/misskey-js/src/endpoints.ts b/packages/misskey-js/src/endpoints.ts index 983575c00b..d29a4ddff1 100644 --- a/packages/misskey-js/src/endpoints.ts +++ b/packages/misskey-js/src/endpoints.ts @@ -1,6 +1,6 @@ import type { JSONSchema7 } from 'schema-type'; import { IEndpointMeta } from './endpoints.types.js'; -import { localUsernameSchema, passwordSchema } from './schemas/user.js'; +import { localUsernameSchema, passwordSchema, userOriginSchema, userSortingSchema } from './schemas/user.js'; import ms from 'ms'; import { chartSchemaToJSONSchema } from './schemas.js'; import { chartsSchemas } from './schemas/charts.js'; @@ -5217,6 +5217,137 @@ export const endpoints = { }], }, //#endregion + + //#region hashtags + 'hashtags/list': { + tags: ['hashtags'], + + requireCredential: false, + + defines: [{ + req: { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + attachedToUserOnly: { type: 'boolean', default: false }, + attachedToLocalUserOnly: { type: 'boolean', default: false }, + attachedToRemoteUserOnly: { type: 'boolean', default: false }, + sort: { type: 'string', enum: ['+mentionedUsers', '-mentionedUsers', '+mentionedLocalUsers', '-mentionedLocalUsers', '+mentionedRemoteUsers', '-mentionedRemoteUsers', '+attachedUsers', '-attachedUsers', '+attachedLocalUsers', '-attachedLocalUsers', '+attachedRemoteUsers', '-attachedRemoteUsers'] }, + }, + required: ['sort'], + }, + res: { + type: 'array', + items: { + $ref: 'https://misskey-hub.net/api/schemas/Hashtag', + }, + }, + }], + }, + 'hashtags/search': { + tags: ['hashtags'], + + requireCredential: false, + + defines: [{ + req: { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + query: { type: 'string' }, + offset: { type: 'integer', default: 0 }, + }, + required: ['query'], + }, + res: { + type: 'array', + items: { + type: 'string', + }, + }, + }], + }, + 'hashtags/show': { + tags: ['hashtags'], + + requireCredential: false, + + errors: { + noSuchHashtag: { + message: 'No such hashtag.', + code: 'NO_SUCH_HASHTAG', + id: '110ee688-193e-4a3a-9ecf-c167b2e6981e', + }, + }, + + defines: [{ + req: { + type: 'object', + properties: { + tag: { type: 'string' }, + }, + required: ['tag'], + }, + res: { + $ref: 'https://misskey-hub.net/api/schemas/Hashtag', + }, + }], + }, + 'hashtags/trend': { + tags: ['hashtags'], + + requireCredential: false, + allowGet: true, + cacheSec: 60 * 1, + + defines: [{ + req: undefined, + res: { + type: 'array', + items: { + type: 'object', + properties: { + tag: { type: 'string' }, + chart: { + type: 'array', + items: { type: 'number' }, + }, + usersCount: { type: 'number' }, + }, + required: ['tag', 'chart', 'usersCount'], + }, + }, + }], + }, + 'hashtags/users': { + requireCredential: false, + + tags: ['hashtags', 'users'], + + defines: [{ + req: { + type: 'object', + properties: { + tag: { type: 'string' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + sort: userSortingSchema, + state: { type: 'string', enum: ['all', 'alive'], default: 'all' }, + origin: { + ...userOriginSchema, + default: 'local', + }, + }, + required: ['tag', 'sort'], + }, + res: { + type: 'array', + items: { + $ref: 'https://misskey-hub.net/api/schemas/UserDetailed', + }, + }, + }], + }, + //#endregion } as const satisfies { [x: string]: IEndpointMeta; }; /** diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index 5c7accd5da..3aa6d02f73 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -1,4 +1,6 @@ +import { SchemaType } from "schema-type"; import { Packed } from "./schemas.js"; +import type { userOriginSchema, userSortingSchema } from "./schemas/user.js"; export type ID = Packed<'Id'>; export type DateString = string; @@ -163,13 +165,8 @@ export type Instance = { export type Signin = Packed<'SignIn'>; -export type UserSorting = - | '+follower' - | '-follower' - | '+createdAt' - | '-createdAt' - | '+updatedAt' - | '-updatedAt'; -export type OriginType = 'combined' | 'local' | 'remote'; +export type UserSorting = SchemaType; + +export type OriginType = SchemaType; export type MeSignup = TODO; diff --git a/packages/misskey-js/src/schemas/user.ts b/packages/misskey-js/src/schemas/user.ts index c865edd8be..5a7a21c36a 100644 --- a/packages/misskey-js/src/schemas/user.ts +++ b/packages/misskey-js/src/schemas/user.ts @@ -495,3 +495,11 @@ export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as con export const descriptionSchema = { type: 'string', minLength: 1, maxLength: 1500 } as const satisfies JSONSchema7; export const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const satisfies JSONSchema7; export const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const satisfies JSONSchema7; + +export const userSortingSchema = { + enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt'], +} as const satisfies JSONSchema7; + +export const userOriginSchema = { + enum: ['combined', 'local', 'remote'], +} as const satisfies JSONSchema7;