This commit is contained in:
syuilo
2025-03-23 11:41:31 +09:00
parent ff4568e1d6
commit b043455152
9 changed files with 187 additions and 9 deletions

View File

@@ -255,20 +255,27 @@ export class ChatService {
await redisPipeline.exec(); await redisPipeline.exec();
} }
@bindThis
public findMyMessageById(userId: MiUser['id'], messageId: MiChatMessage['id']) {
return this.chatMessagesRepository.findOneBy({ id: messageId, fromUserId: userId });
}
@bindThis @bindThis
public async deleteMessage(message: MiChatMessage) { public async deleteMessage(message: MiChatMessage) {
await this.chatMessagesRepository.delete(message.id); await this.chatMessagesRepository.delete(message.id);
if (message.toUserId) { if (message.toUserId) {
const fromUser = await this.usersRepository.findOneByOrFail({ id: message.fromUserId }); const [fromUser, toUser] = await Promise.all([
const toUser = await this.usersRepository.findOneByOrFail({ id: message.toUserId }); this.usersRepository.findOneByOrFail({ id: message.fromUserId }),
this.usersRepository.findOneByOrFail({ id: message.toUserId }),
]);
if (this.userEntityService.isLocalUser(fromUser)) this.globalEventService.publishChatStream(message.fromUserId, message.toUserId, 'deleted', message.id); if (this.userEntityService.isLocalUser(fromUser)) this.globalEventService.publishChatStream(message.fromUserId, message.toUserId, 'deleted', message.id);
if (this.userEntityService.isLocalUser(toUser)) this.globalEventService.publishChatStream(message.toUserId, message.fromUserId, 'deleted', message.id); if (this.userEntityService.isLocalUser(toUser)) this.globalEventService.publishChatStream(message.toUserId, message.fromUserId, 'deleted', message.id);
if (this.userEntityService.isLocalUser(fromUser) && this.userEntityService.isRemoteUser(toUser)) { if (this.userEntityService.isLocalUser(fromUser) && this.userEntityService.isRemoteUser(toUser)) {
const activity = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${message.id}`), fromUser)); //const activity = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${message.id}`), fromUser));
this.queueService.deliver(fromUser, activity, toUser.inbox); //this.queueService.deliver(fromUser, activity, toUser.inbox);
} }
}/* else if (message.toRoomId) { }/* else if (message.toRoomId) {
this.globalEventService.publishRoomChatStream(message.toRoomId, 'deleted', message.id); this.globalEventService.publishRoomChatStream(message.toRoomId, 'deleted', message.id);

View File

@@ -397,6 +397,7 @@ export * as 'users/search-by-username-and-host' from './endpoints/users/search-b
export * as 'users/show' from './endpoints/users/show.js'; export * as 'users/show' from './endpoints/users/show.js';
export * as 'users/update-memo' from './endpoints/users/update-memo.js'; export * as 'users/update-memo' from './endpoints/users/update-memo.js';
export * as 'chat/messages/create' from './endpoints/chat/messages/create.js'; export * as 'chat/messages/create' from './endpoints/chat/messages/create.js';
export * as 'chat/messages/delete' from './endpoints/chat/messages/delete.js';
export * as 'chat/messages/timeline' from './endpoints/chat/messages/timeline.js'; export * as 'chat/messages/timeline' from './endpoints/chat/messages/timeline.js';
export * as 'chat/history' from './endpoints/chat/history.js'; export * as 'chat/history' from './endpoints/chat/history.js';
export * as 'v2/admin/emoji/list' from './endpoints/v2/admin/emoji/list.js'; export * as 'v2/admin/emoji/list' from './endpoints/v2/admin/emoji/list.js';

View File

@@ -0,0 +1,54 @@
/*
* 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 { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js';
import { ChatService } from '@/core/ChatService.js';
import { ChatMessageEntityService } from '@/core/entities/ChatMessageEntityService.js';
import { ApiError } from '@/server/api/error.js';
export const meta = {
tags: ['chat'],
requireCredential: true,
kind: 'write:chat',
res: {
},
errors: {
noSuchMessage: {
message: 'No such message.',
code: 'NO_SUCH_MESSAGE',
id: '36b67f0e-66a6-414b-83df-992a55294f17',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
messageId: { type: 'string', format: 'misskey:id' },
},
required: ['messageId'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
const message = await this.chatService.findMyMessageById(me.id, ps.messageId);
if (message == null) {
throw new ApiError(meta.errors.noSuchMessage);
}
await this.chatService.deleteMessage(message);
});
}
}

View File

@@ -26,15 +26,18 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed, defineAsyncComponent } from 'vue';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { url } from '@@/js/config.js';
import type { MenuItem } from '@/types/menu.js';
import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js'; import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
import MkUrlPreview from '@/components/MkUrlPreview.vue'; import MkUrlPreview from '@/components/MkUrlPreview.vue';
import { ensureSignin } from '@/i.js'; import { ensureSignin } from '@/i.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkFukidashi from '@/components/MkFukidashi.vue'; import MkFukidashi from '@/components/MkFukidashi.vue';
import * as os from '@/os.js';
const $i = ensureSignin(); const $i = ensureSignin();
@@ -47,10 +50,36 @@ const props = defineProps<{
const isMe = computed(() => props.message.fromUserId === $i.id); const isMe = computed(() => props.message.fromUserId === $i.id);
const urls = computed(() => props.message.text ? extractUrlFromMfm(mfm.parse(props.message.text)) : []); const urls = computed(() => props.message.text ? extractUrlFromMfm(mfm.parse(props.message.text)) : []);
function del(): void { function showMenu(ev: MouseEvent) {
misskeyApi('chat/messages/delete', { const menu: MenuItem[] = [];
messageId: props.message.id, if (isMe.value) {
}); menu.push({
text: i18n.ts.delete,
icon: 'ti ti-trash',
danger: true,
action: () => {
misskeyApi('chat/messages/delete', {
messageId: props.message.id,
});
},
});
} else {
menu.push({
text: i18n.ts.reportAbuse,
icon: 'ti ti-exclamation-circle',
action: () => {
const localUrl = `${url}/chat/messages/${props.message.id}`;
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
user: props.user,
initialComment: `${localUrl}\n-----\n`,
}, {
closed: () => dispose(),
});
},
});
}
os.popupMenu(menu, ev.currentTarget ?? ev.target);
} }
</script> </script>

View File

@@ -965,6 +965,12 @@ type ChatMessagesCreateRequest = operations['chat___messages___create']['request
// @public (undocumented) // @public (undocumented)
type ChatMessagesCreateResponse = operations['chat___messages___create']['responses']['200']['content']['application/json']; type ChatMessagesCreateResponse = operations['chat___messages___create']['responses']['200']['content']['application/json'];
// @public (undocumented)
type ChatMessagesDeleteRequest = operations['chat___messages___delete']['requestBody']['content']['application/json'];
// @public (undocumented)
type ChatMessagesDeleteResponse = operations['chat___messages___delete']['responses']['200']['content']['application/json'];
// @public (undocumented) // @public (undocumented)
type ChatMessagesTimelineRequest = operations['chat___messages___timeline']['requestBody']['content']['application/json']; type ChatMessagesTimelineRequest = operations['chat___messages___timeline']['requestBody']['content']['application/json'];
@@ -1475,6 +1481,8 @@ declare namespace entities {
ChatHistoryResponse, ChatHistoryResponse,
ChatMessagesCreateRequest, ChatMessagesCreateRequest,
ChatMessagesCreateResponse, ChatMessagesCreateResponse,
ChatMessagesDeleteRequest,
ChatMessagesDeleteResponse,
ChatMessagesTimelineRequest, ChatMessagesTimelineRequest,
ChatMessagesTimelineResponse, ChatMessagesTimelineResponse,
ClipsAddNoteRequest, ClipsAddNoteRequest,

View File

@@ -1556,6 +1556,17 @@ declare module '../api.js' {
credential?: string | null, credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>; ): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:chat*
*/
request<E extends 'chat/messages/delete', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/** /**
* No description provided. * No description provided.
* *

View File

@@ -211,6 +211,8 @@ import type {
ChatHistoryResponse, ChatHistoryResponse,
ChatMessagesCreateRequest, ChatMessagesCreateRequest,
ChatMessagesCreateResponse, ChatMessagesCreateResponse,
ChatMessagesDeleteRequest,
ChatMessagesDeleteResponse,
ChatMessagesTimelineRequest, ChatMessagesTimelineRequest,
ChatMessagesTimelineResponse, ChatMessagesTimelineResponse,
ClipsAddNoteRequest, ClipsAddNoteRequest,
@@ -734,6 +736,7 @@ export type Endpoints = {
'charts/users': { req: ChartsUsersRequest; res: ChartsUsersResponse }; 'charts/users': { req: ChartsUsersRequest; res: ChartsUsersResponse };
'chat/history': { req: ChatHistoryRequest; res: ChatHistoryResponse }; 'chat/history': { req: ChatHistoryRequest; res: ChatHistoryResponse };
'chat/messages/create': { req: ChatMessagesCreateRequest; res: ChatMessagesCreateResponse }; 'chat/messages/create': { req: ChatMessagesCreateRequest; res: ChatMessagesCreateResponse };
'chat/messages/delete': { req: ChatMessagesDeleteRequest; res: ChatMessagesDeleteResponse };
'chat/messages/timeline': { req: ChatMessagesTimelineRequest; res: ChatMessagesTimelineResponse }; 'chat/messages/timeline': { req: ChatMessagesTimelineRequest; res: ChatMessagesTimelineResponse };
'clips/add-note': { req: ClipsAddNoteRequest; res: EmptyResponse }; 'clips/add-note': { req: ClipsAddNoteRequest; res: EmptyResponse };
'clips/create': { req: ClipsCreateRequest; res: ClipsCreateResponse }; 'clips/create': { req: ClipsCreateRequest; res: ClipsCreateResponse };

View File

@@ -214,6 +214,8 @@ export type ChatHistoryRequest = operations['chat___history']['requestBody']['co
export type ChatHistoryResponse = operations['chat___history']['responses']['200']['content']['application/json']; export type ChatHistoryResponse = operations['chat___history']['responses']['200']['content']['application/json'];
export type ChatMessagesCreateRequest = operations['chat___messages___create']['requestBody']['content']['application/json']; export type ChatMessagesCreateRequest = operations['chat___messages___create']['requestBody']['content']['application/json'];
export type ChatMessagesCreateResponse = operations['chat___messages___create']['responses']['200']['content']['application/json']; export type ChatMessagesCreateResponse = operations['chat___messages___create']['responses']['200']['content']['application/json'];
export type ChatMessagesDeleteRequest = operations['chat___messages___delete']['requestBody']['content']['application/json'];
export type ChatMessagesDeleteResponse = operations['chat___messages___delete']['responses']['200']['content']['application/json'];
export type ChatMessagesTimelineRequest = operations['chat___messages___timeline']['requestBody']['content']['application/json']; export type ChatMessagesTimelineRequest = operations['chat___messages___timeline']['requestBody']['content']['application/json'];
export type ChatMessagesTimelineResponse = operations['chat___messages___timeline']['responses']['200']['content']['application/json']; export type ChatMessagesTimelineResponse = operations['chat___messages___timeline']['responses']['200']['content']['application/json'];
export type ClipsAddNoteRequest = operations['clips___add-note']['requestBody']['content']['application/json']; export type ClipsAddNoteRequest = operations['clips___add-note']['requestBody']['content']['application/json'];

View File

@@ -1376,6 +1376,15 @@ export type paths = {
*/ */
post: operations['chat___messages___create']; post: operations['chat___messages___create'];
}; };
'/chat/messages/delete': {
/**
* chat/messages/delete
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:chat*
*/
post: operations['chat___messages___delete'];
};
'/chat/messages/timeline': { '/chat/messages/timeline': {
/** /**
* chat/messages/timeline * chat/messages/timeline
@@ -13844,6 +13853,60 @@ export type operations = {
}; };
}; };
}; };
/**
* chat/messages/delete
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:chat*
*/
chat___messages___delete: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
messageId: string;
};
};
};
responses: {
/** @description OK (with results) */
200: {
content: {
'application/json': unknown;
};
};
/** @description Client error */
400: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
/** /**
* chat/messages/timeline * chat/messages/timeline
* @description No description provided. * @description No description provided.