feat: 特定のドライブファイルを添付しているチャットメッセージを一覧できるように
This commit is contained in:
parent
8430256f22
commit
e6ec15e397
|
@ -4,6 +4,7 @@
|
|||
- Feat: ノートの下書き機能
|
||||
- Feat: クリップ内でノートを検索できるように
|
||||
- Feat: Playを検索できるように
|
||||
- Feat: モデレーションにおいて、特定のドライブファイルを添付しているチャットメッセージを一覧できるように
|
||||
|
||||
### Client
|
||||
- Feat: モデログを検索できるように
|
||||
|
|
|
@ -10890,6 +10890,10 @@ export interface Locale extends ILocale {
|
|||
* 添付されているノート
|
||||
*/
|
||||
"attachedNotes": string;
|
||||
/**
|
||||
* 利用
|
||||
*/
|
||||
"usage": string;
|
||||
/**
|
||||
* このページは、このファイルをアップロードしたユーザーしか閲覧できません。
|
||||
*/
|
||||
|
|
|
@ -2885,6 +2885,7 @@ _fileViewer:
|
|||
url: "URL"
|
||||
uploadedAt: "追加日"
|
||||
attachedNotes: "添付されているノート"
|
||||
usage: "利用"
|
||||
thisPageCanBeSeenFromTheAuthor: "このページは、このファイルをアップロードしたユーザーしか閲覧できません。"
|
||||
|
||||
_externalResourceInstaller:
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -2018,6 +2018,17 @@ declare module '../api.js' {
|
|||
credential?: string | null,
|
||||
): 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.
|
||||
*
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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'];
|
||||
|
|
|
@ -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: {
|
||||
|
|
Loading…
Reference in New Issue