mirror of
https://activitypub.software/TransFem-org/Sharkey
synced 2024-11-25 07:25:12 +00:00
wip hashtags
This commit is contained in:
parent
41250d997b
commit
c454a44785
8 changed files with 164 additions and 173 deletions
|
@ -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');
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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; };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue