diff options
| author | syuilo <4439005+syuilo@users.noreply.github.com> | 2025-03-25 16:09:19 +0900 |
|---|---|---|
| committer | syuilo <4439005+syuilo@users.noreply.github.com> | 2025-03-25 16:09:19 +0900 |
| commit | 98554579eafd2765644c9b3ff37d33a26b6e5ceb (patch) | |
| tree | 638b04c90ab1ddbb498d84e4fdac7b046ddb89c3 /packages/backend/src | |
| parent | follow up of a01ae38a07f949cbcd5ce555cd90e8570bb985cc (diff) | |
| download | sharkey-98554579eafd2765644c9b3ff37d33a26b6e5ceb.tar.gz sharkey-98554579eafd2765644c9b3ff37d33a26b6e5ceb.tar.bz2 sharkey-98554579eafd2765644c9b3ff37d33a26b6e5ceb.zip | |
enhance: チャットのリアクションを削除できるように
Diffstat (limited to 'packages/backend/src')
4 files changed, 116 insertions, 15 deletions
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); @@ -828,6 +828,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) .where('membership.userId = :userId', { userId }); 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<typeof meta, typeof paramDef> { // 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); + }); + } +} |