From e6ec15e397e150a12486d097d4b789a98b7ae639 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 6 Jul 2025 09:54:49 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=89=B9=E5=AE=9A=E3=81=AE=E3=83=89?= =?UTF-8?q?=E3=83=A9=E3=82=A4=E3=83=96=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E3=82=92=E6=B7=BB=E4=BB=98=E3=81=97=E3=81=A6=E3=81=84=E3=82=8B?= =?UTF-8?q?=E3=83=81=E3=83=A3=E3=83=83=E3=83=88=E3=83=A1=E3=83=83=E3=82=BB?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=82=92=E4=B8=80=E8=A6=A7=E3=81=A7=E3=81=8D?= =?UTF-8?q?=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 --- CHANGELOG.md | 1 + locales/index.d.ts | 4 + locales/ja-JP.yml | 1 + .../backend/src/server/api/endpoint-list.ts | 1 + .../drive/files/attached-chat-messages.ts | 85 +++++++++++++++++++ .../frontend/src/components/MkUserList.vue | 2 +- .../frontend/src/pages/admin-file.chat.vue | 38 +++++++++ packages/frontend/src/pages/admin-file.vue | 24 ++++-- 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 | 83 ++++++++++++++++++ 13 files changed, 257 insertions(+), 6 deletions(-) create mode 100644 packages/backend/src/server/api/endpoints/drive/files/attached-chat-messages.ts create mode 100644 packages/frontend/src/pages/admin-file.chat.vue diff --git a/CHANGELOG.md b/CHANGELOG.md index 516f5f5fcd..cf422f5808 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Feat: ノートの下書き機能 - Feat: クリップ内でノートを検索できるように - Feat: Playを検索できるように +- Feat: モデレーションにおいて、特定のドライブファイルを添付しているチャットメッセージを一覧できるように ### Client - Feat: モデログを検索できるように diff --git a/locales/index.d.ts b/locales/index.d.ts index ce6ee44653..d5ec9f5d77 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -10890,6 +10890,10 @@ export interface Locale extends ILocale { * 添付されているノート */ "attachedNotes": string; + /** + * 利用 + */ + "usage": string; /** * このページは、このファイルをアップロードしたユーザーしか閲覧できません。 */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 0d2d8c4611..760f89ef64 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2885,6 +2885,7 @@ _fileViewer: url: "URL" uploadedAt: "追加日" attachedNotes: "添付されているノート" + usage: "利用" thisPageCanBeSeenFromTheAuthor: "このページは、このファイルをアップロードしたユーザーしか閲覧できません。" _externalResourceInstaller: diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts index eb83c11b39..5c4a58a6fc 100644 --- a/packages/backend/src/server/api/endpoint-list.ts +++ b/packages/backend/src/server/api/endpoint-list.ts @@ -168,6 +168,7 @@ export * as 'clips/update' from './endpoints/clips/update.js'; export * as 'drive' from './endpoints/drive.js'; export * as 'drive/files' from './endpoints/drive/files.js'; export * as 'drive/files/attached-notes' from './endpoints/drive/files/attached-notes.js'; +export * as 'drive/files/attached-chat-messages' from './endpoints/drive/files/attached-chat-messages.js'; export * as 'drive/files/check-existence' from './endpoints/drive/files/check-existence.js'; export * as 'drive/files/create' from './endpoints/drive/files/create.js'; export * as 'drive/files/delete' from './endpoints/drive/files/delete.js'; diff --git a/packages/backend/src/server/api/endpoints/drive/files/attached-chat-messages.ts b/packages/backend/src/server/api/endpoints/drive/files/attached-chat-messages.ts new file mode 100644 index 0000000000..5be477f468 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drive/files/attached-chat-messages.ts @@ -0,0 +1,85 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { DriveFilesRepository, ChatMessagesRepository } from '@/models/_.js'; +import { QueryService } from '@/core/QueryService.js'; +import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; +import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; +import { ApiError } from '../../../error.js'; + +export const meta = { + tags: ['drive', 'chat'], + + requireCredential: true, + + kind: 'read:drive', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'ChatMessage', + }, + }, + + errors: { + noSuchFile: { + message: 'No such file.', + code: 'NO_SUCH_FILE', + id: '485ce26d-f5d2-4313-9783-e689d131eafb', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + fileId: { type: 'string', format: 'misskey:id' }, + }, + required: ['fileId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + @Inject(DI.chatMessagesRepository) + private chatMessagesRepository: ChatMessagesRepository, + + private chatEntityService: ChatEntityService, + private queryService: QueryService, + private roleService: RoleService, + ) { + super(meta, paramDef, async (ps, me) => { + const file = await this.driveFilesRepository.findOneBy({ + id: ps.fileId, + userId: await this.roleService.isModerator(me) ? undefined : me.id, + }); + + if (file == null) { + throw new ApiError(meta.errors.noSuchFile); + } + + const query = this.queryService.makePaginationQuery(this.chatMessagesRepository.createQueryBuilder('message'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate); + query.andWhere('message.fileId = :fileId', { fileId: file.id }); + + const messages = await query.limit(ps.limit).getMany(); + + return await this.chatEntityService.packMessagesDetailed(messages, me); + }); + } +} diff --git a/packages/frontend/src/components/MkUserList.vue b/packages/frontend/src/components/MkUserList.vue index c639e18b1d..e3469d0fd7 100644 --- a/packages/frontend/src/components/MkUserList.vue +++ b/packages/frontend/src/components/MkUserList.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/admin-file.chat.vue b/packages/frontend/src/pages/admin-file.chat.vue new file mode 100644 index 0000000000..e451da51a3 --- /dev/null +++ b/packages/frontend/src/pages/admin-file.chat.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/packages/frontend/src/pages/admin-file.vue b/packages/frontend/src/pages/admin-file.vue index 8495642a8c..7a49ba542f 100644 --- a/packages/frontend/src/pages/admin-file.vue +++ b/packages/frontend/src/pages/admin-file.vue @@ -44,8 +44,19 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.delete }} -
- +
+ + +
{{ i18n.ts.requireAdminForView }} @@ -86,12 +97,15 @@ import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; import { iAmAdmin, iAmModerator } from '@/i.js'; +import MkTabs from '@/components/MkTabs.vue'; const tab = ref('overview'); const file = ref(null); const info = ref(null); const isSensitive = ref(false); +const usageTab = ref<'note' | 'chat'>('note'); const XNotes = defineAsyncComponent(() => import('./drive.file.notes.vue')); +const XChat = defineAsyncComponent(() => import('./admin-file.chat.vue')); const props = defineProps<{ fileId: string, @@ -147,9 +161,9 @@ const headerTabs = computed(() => [{ title: i18n.ts.overview, icon: 'ti ti-info-circle', }, iAmModerator ? { - key: 'notes', - title: i18n.ts._fileViewer.attachedNotes, - icon: 'ti ti-pencil', + key: 'usage', + title: i18n.ts._fileViewer.usage, + icon: 'ti ti-plus', } : null, iAmModerator ? { key: 'ip', title: 'IP', diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index f38e959fb2..d584efe731 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -1226,6 +1226,12 @@ type DateString = string; // @public (undocumented) type DriveFile = components['schemas']['DriveFile']; +// @public (undocumented) +type DriveFilesAttachedChatMessagesRequest = operations['drive___files___attached-chat-messages']['requestBody']['content']['application/json']; + +// @public (undocumented) +type DriveFilesAttachedChatMessagesResponse = operations['drive___files___attached-chat-messages']['responses']['200']['content']['application/json']; + // @public (undocumented) type DriveFilesAttachedNotesRequest = operations['drive___files___attached-notes']['requestBody']['content']['application/json']; @@ -1740,6 +1746,8 @@ declare namespace entities { DriveResponse, DriveFilesRequest, DriveFilesResponse, + DriveFilesAttachedChatMessagesRequest, + DriveFilesAttachedChatMessagesResponse, DriveFilesAttachedNotesRequest, DriveFilesAttachedNotesResponse, DriveFilesCheckExistenceRequest, diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index 60e238351c..4a13045592 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -2018,6 +2018,17 @@ declare module '../api.js' { credential?: string | null, ): Promise>; + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + /** * Find the notes to which the given file is attached. * diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 929cca183f..5ef493946c 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -275,6 +275,8 @@ import type { DriveResponse, DriveFilesRequest, DriveFilesResponse, + DriveFilesAttachedChatMessagesRequest, + DriveFilesAttachedChatMessagesResponse, DriveFilesAttachedNotesRequest, DriveFilesAttachedNotesResponse, DriveFilesCheckExistenceRequest, @@ -833,6 +835,7 @@ export type Endpoints = { 'clips/update': { req: ClipsUpdateRequest; res: ClipsUpdateResponse }; 'drive': { req: EmptyRequest; res: DriveResponse }; 'drive/files': { req: DriveFilesRequest; res: DriveFilesResponse }; + 'drive/files/attached-chat-messages': { req: DriveFilesAttachedChatMessagesRequest; res: DriveFilesAttachedChatMessagesResponse }; 'drive/files/attached-notes': { req: DriveFilesAttachedNotesRequest; res: DriveFilesAttachedNotesResponse }; 'drive/files/check-existence': { req: DriveFilesCheckExistenceRequest; res: DriveFilesCheckExistenceResponse }; 'drive/files/create': { req: DriveFilesCreateRequest; res: DriveFilesCreateResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index 002dfaaf30..a11bbefde5 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -278,6 +278,8 @@ export type ClipsUpdateResponse = operations['clips___update']['responses']['200 export type DriveResponse = operations['drive']['responses']['200']['content']['application/json']; export type DriveFilesRequest = operations['drive___files']['requestBody']['content']['application/json']; export type DriveFilesResponse = operations['drive___files']['responses']['200']['content']['application/json']; +export type DriveFilesAttachedChatMessagesRequest = operations['drive___files___attached-chat-messages']['requestBody']['content']['application/json']; +export type DriveFilesAttachedChatMessagesResponse = operations['drive___files___attached-chat-messages']['responses']['200']['content']['application/json']; export type DriveFilesAttachedNotesRequest = operations['drive___files___attached-notes']['requestBody']['content']['application/json']; export type DriveFilesAttachedNotesResponse = operations['drive___files___attached-notes']['responses']['200']['content']['application/json']; export type DriveFilesCheckExistenceRequest = operations['drive___files___check-existence']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index bea4090aba..df6a22ec41 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -1653,6 +1653,15 @@ export type paths = { */ post: operations['drive___files']; }; + '/drive/files/attached-chat-messages': { + /** + * drive/files/attached-chat-messages + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + post: operations['drive___files___attached-chat-messages']; + }; '/drive/files/attached-notes': { /** * drive/files/attached-notes @@ -18748,6 +18757,80 @@ export interface operations { }; }; }; + 'drive___files___attached-chat-messages': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + sinceDate?: number; + untilDate?: number; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + fileId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ChatMessage'][]; + }; + }; + /** @description Client error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; 'drive___files___attached-notes': { requestBody: { content: {