diff options
| author | syuilo <4439005+syuilo@users.noreply.github.com> | 2025-07-06 09:54:49 +0900 |
|---|---|---|
| committer | syuilo <4439005+syuilo@users.noreply.github.com> | 2025-07-06 09:54:49 +0900 |
| commit | e6ec15e397e150a12486d097d4b789a98b7ae639 (patch) | |
| tree | a113d1a6353eed0488bae17f2ee490974c50351e /packages | |
| parent | clean up (diff) | |
| download | misskey-e6ec15e397e150a12486d097d4b789a98b7ae639.tar.gz misskey-e6ec15e397e150a12486d097d4b789a98b7ae639.tar.bz2 misskey-e6ec15e397e150a12486d097d4b789a98b7ae639.zip | |
feat: 特定のドライブファイルを添付しているチャットメッセージを一覧できるように
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/backend/src/server/api/endpoint-list.ts | 1 | ||||
| -rw-r--r-- | packages/backend/src/server/api/endpoints/drive/files/attached-chat-messages.ts | 85 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkUserList.vue | 2 | ||||
| -rw-r--r-- | packages/frontend/src/pages/admin-file.chat.vue | 38 | ||||
| -rw-r--r-- | packages/frontend/src/pages/admin-file.vue | 24 | ||||
| -rw-r--r-- | packages/misskey-js/etc/misskey-js.api.md | 8 | ||||
| -rw-r--r-- | packages/misskey-js/src/autogen/apiClientJSDoc.ts | 11 | ||||
| -rw-r--r-- | packages/misskey-js/src/autogen/endpoint.ts | 3 | ||||
| -rw-r--r-- | packages/misskey-js/src/autogen/entities.ts | 2 | ||||
| -rw-r--r-- | packages/misskey-js/src/autogen/types.ts | 83 |
10 files changed, 251 insertions, 6 deletions
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<typeof meta, typeof paramDef> { // 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 <template #default="{ items }"> <div :class="$style.root"> - <MkUserInfo v-for="item in items" :key="item.id" class="user" :user="extractor(item)"/> + <MkUserInfo v-for="item in items" :key="item.id" :user="extractor(item)"/> </div> </template> </MkPagination> 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 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div class="_gaps"> + <MkInfo>{{ i18n.ts._fileViewer.thisPageCanBeSeenFromTheAuthor }}</MkInfo> + + <MkPagination :paginator="paginator"> + <template #default="{ items }"> + <XMessage v-for="item in items" :key="item.id" :message="item" :isSearchResult="true"/> + </template> + </MkPagination> +</div> +</template> + +<script lang="ts" setup> +import { ref, computed, markRaw } from 'vue'; +import XMessage from './chat/XMessage.vue'; +import { i18n } from '@/i18n.js'; +import MkInfo from '@/components/MkInfo.vue'; +import { Paginator } from '@/utility/paginator.js'; +import MkPagination from '@/components/MkPagination.vue'; + +const props = defineProps<{ + fileId: string; +}>(); + +const realFileId = computed(() => props.fileId); + +const paginator = markRaw(new Paginator('drive/files/attached-chat-messages', { + limit: 10, + params: { + fileId: realFileId.value, + }, +})); +</script> 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 <MkButton danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> </div> </div> - <div v-else-if="tab === 'notes' && info" class="_gaps_m"> - <XNotes :fileId="fileId"/> + <div v-else-if="tab === 'usage' && info" class="_gaps_m"> + <MkTabs + v-model:tab="usageTab" + :tabs="[{ + key: 'note', + title: 'Note', + }, { + key: 'chat', + title: 'Chat', + }]" + /> + <XNotes v-if="usageTab === 'note'" :fileId="fileId"/> + <XChat v-else-if="usageTab === 'chat'" :fileId="fileId"/> </div> <div v-else-if="tab === 'ip' && info" class="_gaps_m"> <MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo> @@ -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<Misskey.entities.DriveFile | null>(null); const info = ref<Misskey.entities.AdminDriveShowFileResponse | null>(null); const isSensitive = ref<boolean>(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 @@ -1227,6 +1227,12 @@ type DateString = string; 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']; // @public (undocumented) @@ -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 @@ -2019,6 +2019,17 @@ declare module '../api.js' { ): Promise<SwitchCaseResponseType<E, P>>; /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + request<E extends 'drive/files/attached-chat-messages', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** * Find the notes to which the given file is attached. * * **Credential required**: *Yes* / **Permission**: *read:drive* 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: { |