From 98554579eafd2765644c9b3ff37d33a26b6e5ceb Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 25 Mar 2025 16:09:19 +0900 Subject: [PATCH] =?UTF-8?q?enhance:=20=E3=83=81=E3=83=A3=E3=83=83=E3=83=88?= =?UTF-8?q?=E3=81=AE=E3=83=AA=E3=82=A2=E3=82=AF=E3=82=B7=E3=83=A7=E3=83=B3?= =?UTF-8?q?=E3=82=92=E5=89=8A=E9=99=A4=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/core/ChatService.ts | 76 +++++++++++++++---- .../backend/src/core/GlobalEventService.ts | 5 ++ .../backend/src/server/api/endpoint-list.ts | 1 + .../api/endpoints/chat/messages/unreact.ts | 49 ++++++++++++ packages/frontend/src/pages/chat/XMessage.vue | 11 ++- packages/frontend/src/pages/chat/room.vue | 12 +++ 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 | 64 ++++++++++++++++ 11 files changed, 226 insertions(+), 16 deletions(-) create mode 100644 packages/backend/src/server/api/endpoints/chat/messages/unreact.ts diff --git a/packages/backend/src/core/ChatService.ts b/packages/backend/src/core/ChatService.ts index 6884fdfdb6..32f7896cdd 100644 --- a/packages/backend/src/core/ChatService.ts +++ b/packages/backend/src/core/ChatService.ts @@ -33,6 +33,20 @@ const MAX_ROOM_MEMBERS = 30; const MAX_REACTIONS_PER_MESSAGE = 100; const isCustomEmojiRegexp = /^:([\w+-]+)(?:@\.)?:$/; +// TODO: ReactionServiceのやつと共通化 +function normalizeEmojiString(x: string) { + const match = emojiRegex.exec(x); + if (match) { + // 合字を含む1つの絵文字 + const unicode = match[0]; + + // 異体字セレクタ除去 + return unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, ''); + } else { + throw new Error('invalid emoji'); + } +} + @Injectable() export class ChatService { constructor( @@ -751,24 +765,10 @@ export class ChatService { public async react(messageId: MiChatMessage['id'], userId: MiUser['id'], reaction_: string) { let reaction; - // TODO: ReactionServiceのやつと共通化 - function normalize(x: string) { - const match = emojiRegex.exec(x); - if (match) { - // 合字を含む1つの絵文字 - const unicode = match[0]; - - // 異体字セレクタ除去 - return unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, ''); - } else { - throw new Error('invalid emoji'); - } - } - const custom = reaction_.match(isCustomEmojiRegexp); if (custom == null) { - reaction = normalize(reaction_); + reaction = normalizeEmojiString(reaction_); } else { const name = custom[1]; const emoji = (await this.customEmojiService.localEmojisCache.fetch()).get(name); @@ -827,6 +827,52 @@ export class ChatService { } } + @bindThis + public async unreact(messageId: MiChatMessage['id'], userId: MiUser['id'], reaction_: string) { + let reaction; + + const custom = reaction_.match(isCustomEmojiRegexp); + + if (custom == null) { + reaction = normalizeEmojiString(reaction_); + } else { // 削除されたカスタム絵文字のリアクションを削除したいかもしれないので絵文字の存在チェックはする必要なし + const name = custom[1]; + reaction = `:${name}:`; + } + + // NOTE: 自分のリアクションを(あれば)削除するだけなので諸々の権限チェックは必要なし + + const message = await this.chatMessagesRepository.findOneByOrFail({ id: messageId }); + + const room = message.toRoomId ? await this.chatRoomsRepository.findOneByOrFail({ id: message.toRoomId }) : null; + + await this.chatMessagesRepository.createQueryBuilder().update() + .set({ + reactions: () => `array_remove("reactions", '${userId}/${reaction}')`, + }) + .where('id = :id', { id: message.id }) + .execute(); + + // TODO: 実際に削除が行われたときのみイベントを発行する + + if (room) { + this.globalEventService.publishChatRoomStream(room.id, 'unreact', { + messageId: message.id, + user: await this.userEntityService.pack(userId), + reaction, + }); + } else { + this.globalEventService.publishChatUserStream(message.fromUserId, message.toUserId!, 'unreact', { + messageId: message.id, + reaction, + }); + this.globalEventService.publishChatUserStream(message.toUserId!, message.fromUserId, 'unreact', { + messageId: message.id, + reaction, + }); + } + } + @bindThis public async getMyMemberships(userId: MiUser['id'], limit: number, sinceId?: MiChatRoomMembership['id'] | null, untilId?: MiChatRoomMembership['id'] | null) { const query = this.queryService.makePaginationQuery(this.chatRoomMembershipsRepository.createQueryBuilder('membership'), sinceId, untilId) diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 4da3b8bc78..f85d302774 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -167,6 +167,11 @@ export interface ChatEventTypes { user?: Packed<'UserLite'>; messageId: MiChatMessage['id']; }; + unreact: { + reaction: string; + user?: Packed<'UserLite'>; + messageId: MiChatMessage['id']; + }; } export interface ReversiEventTypes { diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts index c8cb87e0ab..34aaef3cc7 100644 --- a/packages/backend/src/server/api/endpoint-list.ts +++ b/packages/backend/src/server/api/endpoint-list.ts @@ -401,6 +401,7 @@ export * as 'chat/messages/create-to-room' from './endpoints/chat/messages/creat export * as 'chat/messages/delete' from './endpoints/chat/messages/delete.js'; export * as 'chat/messages/show' from './endpoints/chat/messages/show.js'; export * as 'chat/messages/react' from './endpoints/chat/messages/react.js'; +export * as 'chat/messages/unreact' from './endpoints/chat/messages/unreact.js'; export * as 'chat/messages/user-timeline' from './endpoints/chat/messages/user-timeline.js'; export * as 'chat/messages/room-timeline' from './endpoints/chat/messages/room-timeline.js'; export * as 'chat/messages/search' from './endpoints/chat/messages/search.js'; diff --git a/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts b/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts new file mode 100644 index 0000000000..4eb25259fb --- /dev/null +++ b/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts @@ -0,0 +1,49 @@ +/* + * 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 { ChatService } from '@/core/ChatService.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: 'c39ea42f-e3ca-428a-ad57-390e0a711595', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + messageId: { type: 'string', format: 'misskey:id' }, + reaction: { type: 'string' }, + }, + required: ['messageId', 'reaction'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private chatService: ChatService, + ) { + super(meta, paramDef, async (ps, me) => { + await this.chatService.unreact(ps.messageId, me.id, ps.reaction); + }); + } +} diff --git a/packages/frontend/src/pages/chat/XMessage.vue b/packages/frontend/src/pages/chat/XMessage.vue index bc348c6fb1..3dae772db1 100644 --- a/packages/frontend/src/pages/chat/XMessage.vue +++ b/packages/frontend/src/pages/chat/XMessage.vue @@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only :moveClass="prefer.s.animation ? $style.transition_reaction_move : ''" tag="div" :class="$style.reactions" > -
+
m.id === ctx.messageId); + if (message) { + const index = message.reactions.findIndex(r => r.reaction === ctx.reaction && r.user.id === ctx.user.id); + if (index !== -1) { + message.reactions.splice(index, 1); + } + } +} + function onIndicatorClick() { showIndicator.value = false; } diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 2c97e4b12e..cc397e2270 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -1001,6 +1001,12 @@ type ChatMessagesShowRequest = operations['chat___messages___show']['requestBody // @public (undocumented) type ChatMessagesShowResponse = operations['chat___messages___show']['responses']['200']['content']['application/json']; +// @public (undocumented) +type ChatMessagesUnreactRequest = operations['chat___messages___unreact']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ChatMessagesUnreactResponse = operations['chat___messages___unreact']['responses']['200']['content']['application/json']; + // @public (undocumented) type ChatMessagesUserTimelineRequest = operations['chat___messages___user-timeline']['requestBody']['content']['application/json']; @@ -1613,6 +1619,8 @@ declare namespace entities { ChatMessagesSearchResponse, ChatMessagesShowRequest, ChatMessagesShowResponse, + ChatMessagesUnreactRequest, + ChatMessagesUnreactResponse, ChatMessagesUserTimelineRequest, ChatMessagesUserTimelineResponse, ChatRoomsCreateRequest, diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index e5d0b94a10..ae97084116 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -1622,6 +1622,17 @@ declare module '../api.js' { credential?: string | null, ): Promise>; + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:chat* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + /** * No description provided. * diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 1dfdb53320..26d22d273c 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -223,6 +223,8 @@ import type { ChatMessagesSearchResponse, ChatMessagesShowRequest, ChatMessagesShowResponse, + ChatMessagesUnreactRequest, + ChatMessagesUnreactResponse, ChatMessagesUserTimelineRequest, ChatMessagesUserTimelineResponse, ChatRoomsCreateRequest, @@ -780,6 +782,7 @@ export type Endpoints = { 'chat/messages/room-timeline': { req: ChatMessagesRoomTimelineRequest; res: ChatMessagesRoomTimelineResponse }; 'chat/messages/search': { req: ChatMessagesSearchRequest; res: ChatMessagesSearchResponse }; 'chat/messages/show': { req: ChatMessagesShowRequest; res: ChatMessagesShowResponse }; + 'chat/messages/unreact': { req: ChatMessagesUnreactRequest; res: ChatMessagesUnreactResponse }; 'chat/messages/user-timeline': { req: ChatMessagesUserTimelineRequest; res: ChatMessagesUserTimelineResponse }; 'chat/rooms/create': { req: ChatRoomsCreateRequest; res: ChatRoomsCreateResponse }; 'chat/rooms/delete': { req: ChatRoomsDeleteRequest; res: ChatRoomsDeleteResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index 0e3ca7202e..6f3b2aa983 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -226,6 +226,8 @@ export type ChatMessagesSearchRequest = operations['chat___messages___search'][' export type ChatMessagesSearchResponse = operations['chat___messages___search']['responses']['200']['content']['application/json']; export type ChatMessagesShowRequest = operations['chat___messages___show']['requestBody']['content']['application/json']; export type ChatMessagesShowResponse = operations['chat___messages___show']['responses']['200']['content']['application/json']; +export type ChatMessagesUnreactRequest = operations['chat___messages___unreact']['requestBody']['content']['application/json']; +export type ChatMessagesUnreactResponse = operations['chat___messages___unreact']['responses']['200']['content']['application/json']; export type ChatMessagesUserTimelineRequest = operations['chat___messages___user-timeline']['requestBody']['content']['application/json']; export type ChatMessagesUserTimelineResponse = operations['chat___messages___user-timeline']['responses']['200']['content']['application/json']; export type ChatRoomsCreateRequest = operations['chat___rooms___create']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 3b52a70ccf..c91fedf2fa 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -1430,6 +1430,15 @@ export type paths = { */ post: operations['chat___messages___show']; }; + '/chat/messages/unreact': { + /** + * chat/messages/unreact + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:chat* + */ + post: operations['chat___messages___unreact']; + }; '/chat/messages/user-timeline': { /** * chat/messages/user-timeline @@ -14424,6 +14433,61 @@ export type operations = { }; }; }; + /** + * chat/messages/unreact + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:chat* + */ + chat___messages___unreact: { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + messageId: string; + reaction: 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/user-timeline * @description No description provided.