diff --git a/locales/index.d.ts b/locales/index.d.ts index f7d875d2c8..6adc66d3cd 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -10529,6 +10529,10 @@ export interface Locale extends ILocale { * ギャラリーの投稿を削除 */ "deleteGalleryPost": string; + /** + * チャットルームを削除 + */ + "deleteChatRoom": string; /** * プロキシアカウントの説明を更新 */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index efaa279454..55bb4c577b 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2791,6 +2791,7 @@ _moderationLogTypes: deletePage: "ページを削除" deleteFlash: "Playを削除" deleteGalleryPost: "ギャラリーの投稿を削除" + deleteChatRoom: "チャットルームを削除" updateProxyAccountDescription: "プロキシアカウントの説明を更新" _fileViewer: diff --git a/packages/backend/src/core/ChatService.ts b/packages/backend/src/core/ChatService.ts index 4c010b2ef7..35819a4bc2 100644 --- a/packages/backend/src/core/ChatService.ts +++ b/packages/backend/src/core/ChatService.ts @@ -27,6 +27,7 @@ import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { emojiRegex } from '@/misc/emoji-regex.js'; import { NotificationService } from '@/core/NotificationService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; const MAX_ROOM_MEMBERS = 30; const MAX_REACTIONS_PER_MESSAGE = 100; @@ -75,6 +76,7 @@ export class ChatService { private roleService: RoleService, private userFollowingService: UserFollowingService, private customEmojiService: CustomEmojiService, + private moderationLogService: ModerationLogService, ) { } @@ -285,6 +287,20 @@ export class ChatService { return this.chatMessagesRepository.findOneBy({ id: messageId, fromUserId: userId }); } + @bindThis + public async hasPermissionToViewRoomTimeline(meId: MiUser['id'], room: MiChatRoom) { + if (await this.isRoomMember(room, meId)) { + return true; + } else { + const iAmModerator = await this.roleService.isModerator({ id: meId }); + if (iAmModerator) { + return true; + } + + return false; + } + } + @bindThis public async deleteMessage(message: MiChatMessage) { await this.chatMessagesRepository.delete(message.id); @@ -493,8 +509,29 @@ export class ChatService { } @bindThis - public async deleteRoom(room: MiChatRoom) { + public async hasPermissionToDeleteRoom(meId: MiUser['id'], room: MiChatRoom) { + if (room.ownerId === meId) { + return true; + } + + const iAmModerator = await this.roleService.isModerator({ id: meId }); + if (iAmModerator) { + return true; + } + + return false; + } + + @bindThis + public async deleteRoom(room: MiChatRoom, moderator?: MiUser) { await this.chatRoomsRepository.delete(room.id); + + if (moderator) { + this.moderationLogService.log(moderator, 'deleteChatRoom', { + roomId: room.id, + room: room, + }); + } } @bindThis diff --git a/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts b/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts index ccc0030403..7aef35db04 100644 --- a/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts +++ b/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts @@ -59,7 +59,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.noSuchRoom); } - if (!(await this.chatService.isRoomMember(room, me.id))) { + if (!await this.chatService.hasPermissionToViewRoomTimeline(me.id, room)) { throw new ApiError(meta.errors.noSuchRoom); } diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/delete.ts b/packages/backend/src/server/api/endpoints/chat/rooms/delete.ts index 2ef0a778f1..1d77a06dd8 100644 --- a/packages/backend/src/server/api/endpoints/chat/rooms/delete.ts +++ b/packages/backend/src/server/api/endpoints/chat/rooms/delete.ts @@ -42,11 +42,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private chatService: ChatService, ) { super(meta, paramDef, async (ps, me) => { - const room = await this.chatService.findMyRoomById(me.id, ps.roomId); + const room = await this.chatService.findRoomById(ps.roomId); if (room == null) { throw new ApiError(meta.errors.noSuchRoom); } - await this.chatService.deleteRoom(room); + + if (!await this.chatService.hasPermissionToDeleteRoom(me.id, room)) { + throw new ApiError(meta.errors.noSuchRoom); + } + + await this.chatService.deleteRoom(room, me); }); } } diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 4e215c93c6..5d5f1e3b71 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -124,6 +124,7 @@ export const moderationLogTypes = [ 'deletePage', 'deleteFlash', 'deleteGalleryPost', + 'deleteChatRoom', 'updateProxyAccountDescription', ] as const; @@ -377,6 +378,10 @@ export type ModerationLogPayloads = { postUserUsername: string; post: any; }; + deleteChatRoom: { + roomId: string; + room: any; + }; updateProxyAccountDescription: { before: string | null; after: string | null; diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue index 9bbe5f2e42..7ab9417267 100644 --- a/packages/frontend/src/pages/admin/modlog.ModLog.vue +++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue @@ -40,6 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only 'deletePage', 'deleteFlash', 'deleteGalleryPost', + 'deleteChatRoom', ].includes(log.type) }" >{{ i18n.ts._moderationLogTypes[log.type] }}</b> @@ -80,6 +81,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-else-if="log.type === 'deletePage'">: @{{ log.info.pageUserUsername }}</span> <span v-else-if="log.type === 'deleteFlash'">: @{{ log.info.flashUserUsername }}</span> <span v-else-if="log.type === 'deleteGalleryPost'">: @{{ log.info.postUserUsername }}</span> + <span v-else-if="log.type === 'deleteChatRoom'">: @{{ log.info.room.name }}</span> </template> <template #icon> <MkAvatar :user="log.user" :class="$style.avatar"/> diff --git a/packages/frontend/src/pages/chat/message.vue b/packages/frontend/src/pages/chat/message.vue index be8be7e5d1..975d1a2be9 100644 --- a/packages/frontend/src/pages/chat/message.vue +++ b/packages/frontend/src/pages/chat/message.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkLoading/> </div> <div v-else> - <XMessage :message="message"/> + <XMessage :message="message" :isSearchResult="true"/> </div> </MkSpacer> </PageWithHeader> diff --git a/packages/frontend/src/pages/chat/room.info.vue b/packages/frontend/src/pages/chat/room.info.vue index f4f5e217dd..7e10336fd3 100644 --- a/packages/frontend/src/pages/chat/room.info.vue +++ b/packages/frontend/src/pages/chat/room.info.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only <hr> - <MkButton v-if="isOwner" danger @click="del">{{ i18n.ts._chat.deleteRoom }}</MkButton> + <MkButton v-if="isOwner || ($i.isAdmin || $i.isModerator)" danger @click="del">{{ i18n.ts._chat.deleteRoom }}</MkButton> <MkSwitch v-if="!isOwner" v-model="isMuted"> <template #label>{{ i18n.ts._chat.muteThisRoom }}</template> diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 7e73b64bb0..2c97e4b12e 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -2832,10 +2832,13 @@ type ModerationLog = { } | { type: 'deleteGalleryPost'; info: ModerationLogPayloads['deleteGalleryPost']; +} | { + type: 'deleteChatRoom'; + info: ModerationLogPayloads['deleteChatRoom']; }); // @public (undocumented) -export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "forwardAbuseReport", "updateAbuseReportNote", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount", "deletePage", "deleteFlash", "deleteGalleryPost"]; +export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "forwardAbuseReport", "updateAbuseReportNote", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount", "deletePage", "deleteFlash", "deleteGalleryPost", "deleteChatRoom"]; // @public (undocumented) type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts index 9a39e619b7..6d92915c44 100644 --- a/packages/misskey-js/src/consts.ts +++ b/packages/misskey-js/src/consts.ts @@ -14,6 +14,7 @@ import type { ReversiGameDetailed, SystemWebhook, UserLite, + ChatRoom, } from './autogen/models.js'; export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'chatRoomInvitationReceived', 'achievementEarned'] as const; @@ -165,6 +166,7 @@ export const moderationLogTypes = [ 'deletePage', 'deleteFlash', 'deleteGalleryPost', + 'deleteChatRoom', ] as const; // See: packages/backend/src/core/ReversiService.ts@L410 @@ -437,4 +439,8 @@ export type ModerationLogPayloads = { postUserUsername: string; post: GalleryPost; }; + deleteChatRoom: { + roomId: string; + room: ChatRoom; + }; }; diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index f04ab25148..ed1d89a685 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -195,6 +195,9 @@ export type ModerationLog = { } | { type: 'deleteGalleryPost'; info: ModerationLogPayloads['deleteGalleryPost']; +} | { + type: 'deleteChatRoom'; + info: ModerationLogPayloads['deleteChatRoom']; }); export type ServerStats = {