wip hashtags

This commit is contained in:
tamaina 2023-07-03 09:18:54 +00:00
parent 41250d997b
commit c454a44785
8 changed files with 164 additions and 173 deletions

View file

@ -4,44 +4,17 @@ import type { HashtagsRepository } from '@/models/index.js';
import { HashtagEntityService } from '@/core/entities/HashtagEntityService.js'; import { HashtagEntityService } from '@/core/entities/HashtagEntityService.js';
import { DI } from '@/di-symbols.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 // eslint-disable-next-line import/no-default-export
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { export default class extends Endpoint<'hashtags/list'> {
name = 'hashtags/list' as const;
constructor( constructor(
@Inject(DI.hashtagsRepository) @Inject(DI.hashtagsRepository)
private hashtagsRepository: HashtagsRepository, private hashtagsRepository: HashtagsRepository,
private hashtagEntityService: HashtagEntityService, private hashtagEntityService: HashtagEntityService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(async (ps, me) => {
const query = this.hashtagsRepository.createQueryBuilder('tag'); const query = this.hashtagsRepository.createQueryBuilder('tag');
if (ps.attachedToUserOnly) query.andWhere('tag.attachedUsersCount != 0'); if (ps.attachedToUserOnly) query.andWhere('tag.attachedUsersCount != 0');

View file

@ -4,39 +4,15 @@ import type { HashtagsRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.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 // eslint-disable-next-line import/no-default-export
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { export default class extends Endpoint<'hashtags/search'> {
name = 'hashtags/search' as const;
constructor( constructor(
@Inject(DI.hashtagsRepository) @Inject(DI.hashtagsRepository)
private hashtagsRepository: HashtagsRepository, private hashtagsRepository: HashtagsRepository,
) { ) {
super(meta, paramDef, async (ps, me) => { super(async (ps, me) => {
const hashtags = await this.hashtagsRepository.createQueryBuilder('tag') const hashtags = await this.hashtagsRepository.createQueryBuilder('tag')
.where('tag.name like :q', { q: sqlLikeEscape(ps.query.toLowerCase()) + '%' }) .where('tag.name like :q', { q: sqlLikeEscape(ps.query.toLowerCase()) + '%' })
.orderBy('tag.count', 'DESC') .orderBy('tag.count', 'DESC')

View file

@ -6,47 +6,20 @@ import { HashtagEntityService } from '@/core/entities/HashtagEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.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 // eslint-disable-next-line import/no-default-export
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { export default class extends Endpoint<'hashtags/show'> {
name = 'hashtags/show' as const;
constructor( constructor(
@Inject(DI.hashtagsRepository) @Inject(DI.hashtagsRepository)
private hashtagsRepository: HashtagsRepository, private hashtagsRepository: HashtagsRepository,
private hashtagEntityService: HashtagEntityService, private hashtagEntityService: HashtagEntityService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(async (ps, me) => {
const hashtag = await this.hashtagsRepository.findOneBy({ name: normalizeForSearch(ps.tag) }); const hashtag = await this.hashtagsRepository.findOneBy({ name: normalizeForSearch(ps.tag) });
if (hashtag == null) { if (hashtag == null) {
throw new ApiError(meta.errors.noSuchHashtag); throw new ApiError(this.meta.errors.noSuchHashtag);
} }
return await this.hashtagEntityService.pack(hashtag); return await this.hashtagEntityService.pack(hashtag);

View file

@ -22,57 +22,17 @@ const rangeA = 1000 * 60 * 60; // 60分
const max = 5; 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 // eslint-disable-next-line import/no-default-export
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { export default class extends Endpoint<'hashtags/trend'> {
name = 'hashtags/trend' as const;
constructor( constructor(
@Inject(DI.notesRepository) @Inject(DI.notesRepository)
private notesRepository: NotesRepository, private notesRepository: NotesRepository,
private metaService: MetaService, private metaService: MetaService,
) { ) {
super(meta, paramDef, async () => { super(async () => {
const instance = await this.metaService.fetch(true); const instance = await this.metaService.fetch(true);
const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t)); const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t));
@ -95,9 +55,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
} }
const tags: { const tags: {
name: string; name: string;
users: Note['userId'][]; users: Note['userId'][];
}[] = []; }[] = [];
for (const note of tagNotes) { for (const note of tagNotes) {
for (const tag of note.tags) { for (const tag of note.tags) {

View file

@ -5,44 +5,17 @@ import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.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 // eslint-disable-next-line import/no-default-export
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { export default class extends Endpoint<'hashtags/users'> {
name = 'hashtags/users' as const;
constructor( constructor(
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(async (ps, me) => {
const query = this.usersRepository.createQueryBuilder('user') const query = this.usersRepository.createQueryBuilder('user')
.where(':tag = ANY(user.tags)', { tag: normalizeForSearch(ps.tag) }) .where(':tag = ANY(user.tags)', { tag: normalizeForSearch(ps.tag) })
.andWhere('user.isSuspended = FALSE'); .andWhere('user.isSuspended = FALSE');

View file

@ -1,6 +1,6 @@
import type { JSONSchema7 } from 'schema-type'; import type { JSONSchema7 } from 'schema-type';
import { IEndpointMeta } from './endpoints.types.js'; 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 ms from 'ms';
import { chartSchemaToJSONSchema } from './schemas.js'; import { chartSchemaToJSONSchema } from './schemas.js';
import { chartsSchemas } from './schemas/charts.js'; import { chartsSchemas } from './schemas/charts.js';
@ -5217,6 +5217,137 @@ export const endpoints = {
}], }],
}, },
//#endregion //#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; }; } as const satisfies { [x: string]: IEndpointMeta; };
/** /**

View file

@ -1,4 +1,6 @@
import { SchemaType } from "schema-type";
import { Packed } from "./schemas.js"; import { Packed } from "./schemas.js";
import type { userOriginSchema, userSortingSchema } from "./schemas/user.js";
export type ID = Packed<'Id'>; export type ID = Packed<'Id'>;
export type DateString = string; export type DateString = string;
@ -163,13 +165,8 @@ export type Instance = {
export type Signin = Packed<'SignIn'>; export type Signin = Packed<'SignIn'>;
export type UserSorting = export type UserSorting = SchemaType<typeof userSortingSchema, []>;
| '+follower'
| '-follower' export type OriginType = SchemaType<typeof userOriginSchema, []>;
| '+createdAt'
| '-createdAt'
| '+updatedAt'
| '-updatedAt';
export type OriginType = 'combined' | 'local' | 'remote';
export type MeSignup = TODO; export type MeSignup = TODO;

View file

@ -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 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 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 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;