From 592c6e60a9c070c186bd03ae0371a86d33015120 Mon Sep 17 00:00:00 2001 From: mattyatea Date: Thu, 12 Oct 2023 15:25:41 +0900 Subject: [PATCH] =?UTF-8?q?=E8=89=B2=E3=80=85fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/index.d.ts | 1 + locales/ja-JP.yml | 1 + .../backend/src/server/api/EndpointsModule.ts | 4 + packages/backend/src/server/api/endpoints.ts | 2 + .../src/server/api/endpoints/users/stats.ts | 228 ++++++++++++++++++ .../frontend/src/components/MkPostForm.vue | 4 + .../global/MkMisskeyFlavoredMarkdown.ts | 19 +- .../frontend/src/components/global/MkRuby.vue | 9 +- .../src/pages/settings/account-stats.vue | 119 +++++++++ .../frontend/src/pages/settings/other.vue | 1 + packages/frontend/src/router.ts | 6 +- 11 files changed, 388 insertions(+), 6 deletions(-) create mode 100644 packages/backend/src/server/api/endpoints/users/stats.ts create mode 100644 packages/frontend/src/pages/settings/account-stats.vue diff --git a/locales/index.d.ts b/locales/index.d.ts index dfec540009..744e21607d 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -14,6 +14,7 @@ export interface Locale { "forgotPassword": string; "fetchingAsApObject": string; "ok": string; + "ruby": string; "gotIt": string; "cancel": string; "noThankYou": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index e066a36d5b..8f4b2e5ae0 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -11,6 +11,7 @@ password: "パスワード" forgotPassword: "パスワードを忘れた" fetchingAsApObject: "連合に照会中" ok: "OK" +ruby: "ルビ" gotIt: "わかった" cancel: "キャンセル" noThankYou: "やめておく" diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index a21ef69dbf..b4cdc196f8 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -330,6 +330,7 @@ import * as ep___users_following from './endpoints/users/following.js'; import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js'; import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js'; import * as ep___users_featuredNotes from './endpoints/users/featured-notes.js'; +import * as ep___users_user_stats from './endpoints/users/stats.js'; import * as ep___users_lists_create from './endpoints/users/lists/create.js'; import * as ep___users_lists_delete from './endpoints/users/lists/delete.js'; import * as ep___users_lists_list from './endpoints/users/lists/list.js'; @@ -567,6 +568,7 @@ const $i_importMuting: Provider = { provide: 'ep:i/import-muting', useClass: ep_ const $i_importUserLists: Provider = { provide: 'ep:i/import-user-lists', useClass: ep___i_importUserLists.default }; const $i_importAntennas: Provider = { provide: 'ep:i/import-antennas', useClass: ep___i_importAntennas.default }; const $i_notifications: Provider = { provide: 'ep:i/notifications', useClass: ep___i_notifications.default }; +const $i_userstats: Provider = { provide: 'ep:i/stats', useClass: ep___users_user_stats.default } const $i_pageLikes: Provider = { provide: 'ep:i/page-likes', useClass: ep___i_pageLikes.default }; const $i_pages: Provider = { provide: 'ep:i/pages', useClass: ep___i_pages.default }; const $i_pin: Provider = { provide: 'ep:i/pin', useClass: ep___i_pin.default }; @@ -816,6 +818,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $channels_timeline, $channels_unfollow, $channels_update, + $i_userstats, $channels_favorite, $channels_unfavorite, $channels_myFavorites, @@ -1103,6 +1106,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_emoji_setisSensitiveBulk, $admin_emoji_update, $admin_federation_deleteAllFiles, + $i_userstats, $admin_federation_refreshRemoteInstanceMetadata, $admin_federation_removeAllFollowing, $admin_federation_updateInstance, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 1ad9cb3a3f..3fac9552ad 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -237,6 +237,7 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js'; import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js'; import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js'; import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js'; +import * as ep___i_user_stats from './endpoints/users/stats.js'; import * as ep___invite_create from './endpoints/invite/create.js'; import * as ep___invite_delete from './endpoints/invite/delete.js'; import * as ep___invite_list from './endpoints/invite/list.js'; @@ -589,6 +590,7 @@ const eps = [ ['i/webhooks/show', ep___i_webhooks_show], ['i/webhooks/update', ep___i_webhooks_update], ['i/webhooks/delete', ep___i_webhooks_delete], + ['i/stats', ep___i_user_stats], ['invite/create', ep___invite_create], ['invite/delete', ep___invite_delete], ['invite/list', ep___invite_list], diff --git a/packages/backend/src/server/api/endpoints/users/stats.ts b/packages/backend/src/server/api/endpoints/users/stats.ts new file mode 100644 index 0000000000..f51da1baad --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/stats.ts @@ -0,0 +1,228 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import { DI } from '@/di-symbols.js'; +import type { UsersRepository, NotesRepository, FollowingsRepository, DriveFilesRepository, NoteReactionsRepository, PageLikesRepository, NoteFavoritesRepository, PollVotesRepository } from '@/models/index.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['users'], + + requireCredential: false, + + description: 'Show statistics about a user.', + + errors: { + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '9e638e45-3b25-4ef7-8f95-07e8498f1819', + }, + }, + + res: { + type: 'object', + optional: false, nullable: false, + properties: { + notesCount: { + type: 'integer', + optional: false, nullable: false, + }, + repliesCount: { + type: 'integer', + optional: false, nullable: false, + }, + renotesCount: { + type: 'integer', + optional: false, nullable: false, + }, + repliedCount: { + type: 'integer', + optional: false, nullable: false, + }, + renotedCount: { + type: 'integer', + optional: false, nullable: false, + }, + pollVotesCount: { + type: 'integer', + optional: false, nullable: false, + }, + pollVotedCount: { + type: 'integer', + optional: false, nullable: false, + }, + localFollowingCount: { + type: 'integer', + optional: false, nullable: false, + }, + remoteFollowingCount: { + type: 'integer', + optional: false, nullable: false, + }, + localFollowersCount: { + type: 'integer', + optional: false, nullable: false, + }, + remoteFollowersCount: { + type: 'integer', + optional: false, nullable: false, + }, + followingCount: { + type: 'integer', + optional: false, nullable: false, + }, + followersCount: { + type: 'integer', + optional: false, nullable: false, + }, + sentReactionsCount: { + type: 'integer', + optional: false, nullable: false, + }, + receivedReactionsCount: { + type: 'integer', + optional: false, nullable: false, + }, + noteFavoritesCount: { + type: 'integer', + optional: false, nullable: false, + }, + pageLikesCount: { + type: 'integer', + optional: false, nullable: false, + }, + pageLikedCount: { + type: 'integer', + optional: false, nullable: false, + }, + driveFilesCount: { + type: 'integer', + optional: false, nullable: false, + }, + driveUsage: { + type: 'integer', + optional: false, nullable: false, + description: 'Drive usage in bytes', + }, + }, + }, +} 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 { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + @Inject(DI.noteReactionsRepository) + private noteReactionsRepository: NoteReactionsRepository, + + @Inject(DI.pageLikesRepository) + private pageLikesRepository: PageLikesRepository, + + @Inject(DI.noteFavoritesRepository) + private noteFavoritesRepository: NoteFavoritesRepository, + + @Inject(DI.pollVotesRepository) + private pollVotesRepository: PollVotesRepository, + + private driveFileEntityService: DriveFileEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const user = await this.usersRepository.findOneBy({ id: ps.userId }); + if (user == null) { + throw new ApiError(meta.errors.noSuchUser); + } + + const result = await awaitAll({ + notesCount: this.notesRepository.createQueryBuilder('note') + .where('note.userId = :userId', { userId: user.id }) + .getCount(), + repliesCount: this.notesRepository.createQueryBuilder('note') + .where('note.userId = :userId', { userId: user.id }) + .andWhere('note.replyId IS NOT NULL') + .getCount(), + renotesCount: this.notesRepository.createQueryBuilder('note') + .where('note.userId = :userId', { userId: user.id }) + .andWhere('note.renoteId IS NOT NULL') + .getCount(), + repliedCount: this.notesRepository.createQueryBuilder('note') + .where('note.replyUserId = :userId', { userId: user.id }) + .getCount(), + renotedCount: this.notesRepository.createQueryBuilder('note') + .where('note.renoteUserId = :userId', { userId: user.id }) + .getCount(), + pollVotesCount: this.pollVotesRepository.createQueryBuilder('vote') + .where('vote.userId = :userId', { userId: user.id }) + .getCount(), + pollVotedCount: this.pollVotesRepository.createQueryBuilder('vote') + .innerJoin('vote.note', 'note') + .where('note.userId = :userId', { userId: user.id }) + .getCount(), + localFollowingCount: this.followingsRepository.createQueryBuilder('following') + .where('following.followerId = :userId', { userId: user.id }) + .andWhere('following.followeeHost IS NULL') + .getCount(), + remoteFollowingCount: this.followingsRepository.createQueryBuilder('following') + .where('following.followerId = :userId', { userId: user.id }) + .andWhere('following.followeeHost IS NOT NULL') + .getCount(), + localFollowersCount: this.followingsRepository.createQueryBuilder('following') + .where('following.followeeId = :userId', { userId: user.id }) + .andWhere('following.followerHost IS NULL') + .getCount(), + remoteFollowersCount: this.followingsRepository.createQueryBuilder('following') + .where('following.followeeId = :userId', { userId: user.id }) + .andWhere('following.followerHost IS NOT NULL') + .getCount(), + sentReactionsCount: this.noteReactionsRepository.createQueryBuilder('reaction') + .where('reaction.userId = :userId', { userId: user.id }) + .getCount(), + receivedReactionsCount: this.noteReactionsRepository.createQueryBuilder('reaction') + .innerJoin('reaction.note', 'note') + .where('note.userId = :userId', { userId: user.id }) + .getCount(), + noteFavoritesCount: this.noteFavoritesRepository.createQueryBuilder('favorite') + .where('favorite.userId = :userId', { userId: user.id }) + .getCount(), + pageLikesCount: this.pageLikesRepository.createQueryBuilder('like') + .where('like.userId = :userId', { userId: user.id }) + .getCount(), + pageLikedCount: this.pageLikesRepository.createQueryBuilder('like') + .innerJoin('like.page', 'page') + .where('page.userId = :userId', { userId: user.id }) + .getCount(), + driveFilesCount: this.driveFilesRepository.createQueryBuilder('file') + .where('file.userId = :userId', { userId: user.id }) + .getCount(), + driveUsage: this.driveFileEntityService.calcDriveUsageOf(user), + }); + + return { + ...result, + followingCount: result.localFollowingCount + result.remoteFollowingCount, + followersCount: result.localFollowersCount + result.remoteFollowersCount, + }; + }); + } +} diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index d250304efb..909ad9d666 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -86,6 +86,7 @@ SPDX-License-Identifier: AGPL-3.0-only +
@@ -849,6 +850,9 @@ async function insertEmoji(ev: MouseEvent) { function insertMfm(){ insertTextAtCursor(textareaEl, '$'); } +function insertRuby() { + insertTextAtCursor(textareaEl, '$[ruby 本文 上につくやつ]'); +} function showActions(ev) { os.popupMenu(postFormActions.map(action => ({ text: action.title, diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts index 57939e2a1d..069522c71f 100644 --- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts +++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts @@ -340,9 +340,24 @@ export default function(props: { text:base[1] }); }else if(token.children.length === 2){ + let txt,base; console.log(token.children) - const base = token.children[0].type !== 'unicodeEmoji' ? token.children[0].props.text : token.children[0].props.emoji; - const txt = token.children[1].type !== 'unicodeEmoji' ? token.children[1].props.text : token.children[1].props.emoji; + if (token.children[1].type === 'emojiCode'){ + txt = token.children[1].props.name + }else if(token.children[1].type === 'unicodeEmoji'){ + txt = token.children[1].props.emoji + }else { + txt = token.children[1].props.text + } + + if (token.children[0].type === 'emojiCode'){ + base = token.children[0].props.name + }else if(token.children[0].type === 'unicodeEmoji'){ + base = token.children[0].props.emoji + }else { + base = token.children[0].props.text + } + return h(MkRuby,{ base:base, basetype:token.children[0].type, diff --git a/packages/frontend/src/components/global/MkRuby.vue b/packages/frontend/src/components/global/MkRuby.vue index 0ea808f06d..5036af70fb 100644 --- a/packages/frontend/src/components/global/MkRuby.vue +++ b/packages/frontend/src/components/global/MkRuby.vue @@ -1,6 +1,7 @@ diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue index e2fc021099..695cc1a9e5 100644 --- a/packages/frontend/src/pages/settings/other.vue +++ b/packages/frontend/src/pages/settings/other.vue @@ -31,6 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only + {{ i18n.ts.statistics }}
diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index 6c33d0d8ee..a8e645503c 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -174,7 +174,11 @@ export const routes = [{ path: '/other', name: 'other', component: page(() => import('./pages/settings/other.vue')), - }, { + },{ + path: '/account-stats', + name: 'other', + component: page(() => import('./pages/settings/account-stats.vue')), + },{ path: '/', component: page(() => import('./pages/_empty_.vue')), }],