summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorsyuilo <4439005+syuilo@users.noreply.github.com>2025-07-06 09:54:49 +0900
committersyuilo <4439005+syuilo@users.noreply.github.com>2025-07-06 09:54:49 +0900
commite6ec15e397e150a12486d097d4b789a98b7ae639 (patch)
treea113d1a6353eed0488bae17f2ee490974c50351e /packages
parentclean up (diff)
downloadmisskey-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.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/attached-chat-messages.ts85
-rw-r--r--packages/frontend/src/components/MkUserList.vue2
-rw-r--r--packages/frontend/src/pages/admin-file.chat.vue38
-rw-r--r--packages/frontend/src/pages/admin-file.vue24
-rw-r--r--packages/misskey-js/etc/misskey-js.api.md8
-rw-r--r--packages/misskey-js/src/autogen/apiClientJSDoc.ts11
-rw-r--r--packages/misskey-js/src/autogen/endpoint.ts3
-rw-r--r--packages/misskey-js/src/autogen/entities.ts2
-rw-r--r--packages/misskey-js/src/autogen/types.ts83
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: {