From 7ef7f575a475d10d11503bcde4a27f5b45b6f9b2 Mon Sep 17 00:00:00 2001 From: lqvp <183242690+lqvp@users.noreply.github.com> Date: Sun, 22 Dec 2024 01:54:39 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E3=83=AA=E3=82=A2=E3=82=AF=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=B3=E3=81=AE=E7=B5=B1=E8=A8=88=E3=82=92=E8=A6=8B?= =?UTF-8?q?=E3=82=8C=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/server/api/endpoint-list.ts | 1 + .../server/api/endpoints/reactions-stat.ts | 78 ++++++++++++ packages/frontend/src/navbar.ts | 5 + .../frontend/src/pages/reactions-stat.vue | 111 ++++++++++++++++++ packages/frontend/src/router/definition.ts | 4 + packages/misskey-js/etc/misskey-js.api.md | 8 ++ .../misskey-js/src/autogen/apiClientJSDoc.ts | 11 ++ packages/misskey-js/src/autogen/endpoint.ts | 3 + packages/misskey-js/src/autogen/entities.ts | 2 + packages/misskey-js/src/autogen/types.ts | 66 +++++++++++ 10 files changed, 289 insertions(+) create mode 100644 packages/backend/src/server/api/endpoints/reactions-stat.ts create mode 100644 packages/frontend/src/pages/reactions-stat.vue diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts index 28f7cfea04..40ddcc7182 100644 --- a/packages/backend/src/server/api/endpoint-list.ts +++ b/packages/backend/src/server/api/endpoint-list.ts @@ -339,6 +339,7 @@ export * as 'pages/update' from './endpoints/pages/update.js'; export * as 'ping' from './endpoints/ping.js'; export * as 'pinned-users' from './endpoints/pinned-users.js'; export * as 'promo/read' from './endpoints/promo/read.js'; +export * as 'reactions-stat' from './endpoints/reactions-stat.js'; export * as 'renote-mute/create' from './endpoints/renote-mute/create.js'; export * as 'renote-mute/delete' from './endpoints/renote-mute/delete.js'; export * as 'renote-mute/list' from './endpoints/renote-mute/list.js'; diff --git a/packages/backend/src/server/api/endpoints/reactions-stat.ts b/packages/backend/src/server/api/endpoints/reactions-stat.ts new file mode 100644 index 0000000000..313ea4b69a --- /dev/null +++ b/packages/backend/src/server/api/endpoints/reactions-stat.ts @@ -0,0 +1,78 @@ +/* + * SPDX-FileCopyrightText: lqvp + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { NoteReactionsRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + tags: ['meta'], + + requireCredential: true, + kind: 'read:account', + + res: { + type: 'array', + items: { + type: 'object', + properties: { + reaction: { + type: 'string', + optional: false, + }, + count: { + type: 'number', + optional: false, + }, + }, + }, + }, + + errors: { + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '27e494ba-2ac2-48e8-893b-10d4d8c2387b', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + site: { type: 'boolean', default: false }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.noteReactionsRepository) + private noteReactionsRepository: NoteReactionsRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const query = + this.noteReactionsRepository.createQueryBuilder('nr') + .select('nr.reaction', 'reaction') + .addSelect('count(nr.id)', 'count') + .groupBy('nr.reaction') + .orderBy('count', 'DESC') + .limit(100); + + if (!ps.site) { + query.where('nr.userId = :id', { id: me.id }); + } + + const res = await query.getRawMany(); + + return res.map(x => ({ + reaction: x.reaction, + count: x.count, + })); + }); + } +} diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts index 096d404a57..768585bc9a 100644 --- a/packages/frontend/src/navbar.ts +++ b/packages/frontend/src/navbar.ts @@ -176,6 +176,11 @@ export const navbarItemDef = reactive({ show: computed(() => $i != null), to: `/@${$i?.username}`, }, + reactionStat: { + title: i18n.ts.reactionsStat, + icon: 'ti ti-chart-bar', + to: '/reactions-stat', + }, cacheClear: { title: i18n.ts.clearCache, icon: 'ti ti-trash', diff --git a/packages/frontend/src/pages/reactions-stat.vue b/packages/frontend/src/pages/reactions-stat.vue new file mode 100644 index 0000000000..70ad255de4 --- /dev/null +++ b/packages/frontend/src/pages/reactions-stat.vue @@ -0,0 +1,111 @@ + + + + + + + diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts index 732b209a36..0c39a292a2 100644 --- a/packages/frontend/src/router/definition.ts +++ b/packages/frontend/src/router/definition.ts @@ -577,6 +577,10 @@ const routes: RouteDef[] = [{ }, { path: '/timeline', component: page(() => import('@/pages/timeline.vue')), +}, { + path: '/reactions-stat', + component: page(() => import('@/pages/reactions-stat.vue')), + loginRequired: true, }, { name: 'index', path: '/', diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index ac7babb250..2064c81bbd 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -1724,6 +1724,8 @@ declare namespace entities { PingResponse, PinnedUsersResponse, PromoReadRequest, + ReactionsStatRequest, + ReactionsStatResponse, RenoteMuteCreateRequest, RenoteMuteDeleteRequest, RenoteMuteListRequest, @@ -2937,6 +2939,12 @@ type QueueStats = { // @public (undocumented) type QueueStatsLog = QueueStats[]; +// @public (undocumented) +type ReactionsStatRequest = operations['reactions-stat']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ReactionsStatResponse = operations['reactions-stat']['responses']['200']['content']['application/json']; + // @public (undocumented) type RenoteMuteCreateRequest = operations['renote-mute___create']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index 6bace3924c..c767e30b81 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -3671,6 +3671,17 @@ declare module '../api.js' { credential?: string | null, ): Promise>; + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + /** * No description provided. * diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index a9903b9139..00a971014c 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -489,6 +489,8 @@ import type { PingResponse, PinnedUsersResponse, PromoReadRequest, + ReactionsStatRequest, + ReactionsStatResponse, RenoteMuteCreateRequest, RenoteMuteDeleteRequest, RenoteMuteListRequest, @@ -915,6 +917,7 @@ export type Endpoints = { 'ping': { req: EmptyRequest; res: PingResponse }; 'pinned-users': { req: EmptyRequest; res: PinnedUsersResponse }; 'promo/read': { req: PromoReadRequest; res: EmptyResponse }; + 'reactions-stat': { req: ReactionsStatRequest; res: ReactionsStatResponse }; 'renote-mute/create': { req: RenoteMuteCreateRequest; res: EmptyResponse }; 'renote-mute/delete': { req: RenoteMuteDeleteRequest; res: EmptyResponse }; 'renote-mute/list': { req: RenoteMuteListRequest; res: RenoteMuteListResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index b7639abca8..c2e97d8e11 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -492,6 +492,8 @@ export type PagesUpdateRequest = operations['pages___update']['requestBody']['co export type PingResponse = operations['ping']['responses']['200']['content']['application/json']; export type PinnedUsersResponse = operations['pinned-users']['responses']['200']['content']['application/json']; export type PromoReadRequest = operations['promo___read']['requestBody']['content']['application/json']; +export type ReactionsStatRequest = operations['reactions-stat']['requestBody']['content']['application/json']; +export type ReactionsStatResponse = operations['reactions-stat']['responses']['200']['content']['application/json']; export type RenoteMuteCreateRequest = operations['renote-mute___create']['requestBody']['content']['application/json']; export type RenoteMuteDeleteRequest = operations['renote-mute___delete']['requestBody']['content']['application/json']; export type RenoteMuteListRequest = operations['renote-mute___list']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index e42a163288..c221947493 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -3176,6 +3176,15 @@ export type paths = { */ post: operations['promo___read']; }; + '/reactions-stat': { + /** + * reactions-stat + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + post: operations['reactions-stat']; + }; '/renote-mute/create': { /** * renote-mute/create @@ -24848,6 +24857,63 @@ export type operations = { }; }; }; + /** + * reactions-stat + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + 'reactions-stat': { + requestBody: { + content: { + 'application/json': { + /** @default false */ + site?: boolean; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + reaction: string; + count: number; + }[]; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; /** * renote-mute/create * @description No description provided.