diff options
| author | Hazelnoot <acomputerdog@gmail.com> | 2025-03-25 16:17:34 -0400 |
|---|---|---|
| committer | Hazelnoot <acomputerdog@gmail.com> | 2025-03-25 16:17:34 -0400 |
| commit | 40975719ec5bb889ab011bbc464dd0fdb09fdb68 (patch) | |
| tree | 1b8521df694b869b26c6d75c99ef885704b013b0 /packages/backend/src/core | |
| parent | merge upstream (diff) | |
| parent | Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop (diff) | |
| download | sharkey-40975719ec5bb889ab011bbc464dd0fdb09fdb68.tar.gz sharkey-40975719ec5bb889ab011bbc464dd0fdb09fdb68.tar.bz2 sharkey-40975719ec5bb889ab011bbc464dd0fdb09fdb68.zip | |
Merge branch 'misskey-develop' into merge/2025-03-24
# Conflicts:
# package.json
# packages/backend/src/core/entities/NotificationEntityService.ts
# packages/backend/src/types.ts
# packages/frontend/src/pages/admin/modlog.ModLog.vue
# packages/misskey-js/src/consts.ts
# packages/misskey-js/src/entities.ts
Diffstat (limited to 'packages/backend/src/core')
| -rw-r--r-- | packages/backend/src/core/ChatService.ts | 157 | ||||
| -rw-r--r-- | packages/backend/src/core/GlobalEventService.ts | 5 | ||||
| -rw-r--r-- | packages/backend/src/core/entities/NotificationEntityService.ts | 21 |
3 files changed, 151 insertions, 32 deletions
diff --git a/packages/backend/src/core/ChatService.ts b/packages/backend/src/core/ChatService.ts index 57e33af107..df1c384b54 100644 --- a/packages/backend/src/core/ChatService.ts +++ b/packages/backend/src/core/ChatService.ts @@ -26,11 +26,27 @@ import { Packed } from '@/misc/json-schema.js'; 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; 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( @@ -68,11 +84,13 @@ export class ChatService { private apRendererService: ApRendererService, private queueService: QueueService, private pushNotificationService: PushNotificationService, + private notificationService: NotificationService, private userBlockingService: UserBlockingService, private queryService: QueryService, private roleService: RoleService, private userFollowingService: UserFollowingService, private customEmojiService: CustomEmojiService, + private moderationLogService: ModerationLogService, ) { } @@ -199,6 +217,8 @@ export class ChatService { throw new Error('you are not a member of the room'); } + const membershipsOtherThanMe = memberships.filter(member => member.userId !== fromUser.id); + const message = { id: this.idService.gen(), fromUserId: fromUser.id, @@ -216,7 +236,7 @@ export class ChatService { this.globalEventService.publishChatRoomStream(toRoom.id, 'message', packedMessage); const redisPipeline = this.redisClient.pipeline(); - for (const membership of memberships) { + for (const membership of membershipsOtherThanMe) { if (membership.isMuted) continue; redisPipeline.set(`newRoomChatMessageExists:${membership.userId}:${toRoom.id}`, message.id); @@ -227,7 +247,7 @@ export class ChatService { // 3秒経っても既読にならなかったらイベント発行 setTimeout(async () => { const redisPipeline = this.redisClient.pipeline(); - for (const membership of memberships) { + for (const membership of membershipsOtherThanMe) { redisPipeline.get(`newRoomChatMessageExists:${membership.userId}:${toRoom.id}`); } const markers = await redisPipeline.exec(); @@ -237,12 +257,12 @@ export class ChatService { const packedMessageForTo = await this.chatEntityService.packMessageDetailed(inserted); - for (let i = 0; i < memberships.length; i++) { + for (let i = 0; i < membershipsOtherThanMe.length; i++) { const marker = markers[i][1]; if (marker == null) continue; - this.globalEventService.publishMainStream(memberships[i].userId, 'newChatMessage', packedMessageForTo); - //this.pushNotificationService.pushNotification(memberships[i].userId, 'newChatMessage', packedMessageForTo); + this.globalEventService.publishMainStream(membershipsOtherThanMe[i].userId, 'newChatMessage', packedMessageForTo); + //this.pushNotificationService.pushNotification(membershipsOtherThanMe[i].userId, 'newChatMessage', packedMessageForTo); } }, 3000); @@ -282,6 +302,20 @@ export class ChatService { } @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); @@ -330,7 +364,7 @@ export class ChatService { @bindThis public async roomTimeline(roomId: MiChatRoom['id'], limit: number, sinceId?: MiChatMessage['id'] | null, untilId?: MiChatMessage['id'] | null) { const query = this.queryService.makePaginationQuery(this.chatMessagesRepository.createQueryBuilder('message'), sinceId, untilId) - .where('message.toRoomId = :roomId', { roomId }) + .andWhere('message.toRoomId = :roomId', { roomId }) .leftJoinAndSelect('message.file', 'file') .leftJoinAndSelect('message.fromUser', 'fromUser'); @@ -489,8 +523,33 @@ 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, deleter?: MiUser) { await this.chatRoomsRepository.delete(room.id); + + if (deleter) { + const deleterIsModerator = await this.roleService.isModerator(deleter); + + if (deleterIsModerator) { + this.moderationLogService.log(deleter, 'deleteChatRoom', { + roomId: room.id, + room: room, + }); + } + } } @bindThis @@ -542,13 +601,27 @@ export class ChatService { const created = await this.chatRoomInvitationsRepository.insertOne(invitation); + this.notificationService.createNotification(inviteeId, 'chatRoomInvitationReceived', { + invitationId: invitation.id, + }, inviterId); + return created; } @bindThis + public async getSentRoomInvitationsWithPagination(roomId: MiChatRoom['id'], limit: number, sinceId?: MiChatRoomInvitation['id'] | null, untilId?: MiChatRoomInvitation['id'] | null) { + const query = this.queryService.makePaginationQuery(this.chatRoomInvitationsRepository.createQueryBuilder('invitation'), sinceId, untilId) + .andWhere('invitation.roomId = :roomId', { roomId }); + + const invitations = await query.take(limit).getMany(); + + return invitations; + } + + @bindThis public async getOwnedRoomsWithPagination(ownerId: MiUser['id'], limit: number, sinceId?: MiChatRoom['id'] | null, untilId?: MiChatRoom['id'] | null) { const query = this.queryService.makePaginationQuery(this.chatRoomsRepository.createQueryBuilder('room'), sinceId, untilId) - .where('room.ownerId = :ownerId', { ownerId }); + .andWhere('room.ownerId = :ownerId', { ownerId }); const rooms = await query.take(limit).getMany(); @@ -558,7 +631,7 @@ export class ChatService { @bindThis public async getReceivedRoomInvitationsWithPagination(userId: MiUser['id'], limit: number, sinceId?: MiChatRoomInvitation['id'] | null, untilId?: MiChatRoomInvitation['id'] | null) { const query = this.queryService.makePaginationQuery(this.chatRoomInvitationsRepository.createQueryBuilder('invitation'), sinceId, untilId) - .where('invitation.userId = :userId', { userId }) + .andWhere('invitation.userId = :userId', { userId }) .andWhere('invitation.ignored = FALSE'); const invitations = await query.take(limit).getMany(); @@ -622,7 +695,7 @@ export class ChatService { @bindThis public async getRoomMembershipsWithPagination(roomId: MiChatRoom['id'], limit: number, sinceId?: MiChatRoomMembership['id'] | null, untilId?: MiChatRoomMembership['id'] | null) { const query = this.queryService.makePaginationQuery(this.chatRoomMembershipsRepository.createQueryBuilder('membership'), sinceId, untilId) - .where('membership.roomId = :roomId', { roomId }); + .andWhere('membership.roomId = :roomId', { roomId }); const memberships = await query.take(limit).getMany(); @@ -692,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); @@ -769,9 +828,55 @@ 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 }); + .andWhere('membership.userId = :userId', { userId }); const memberships = await query.take(limit).getMany(); diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 94d6311e0d..27d8ee9891 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -172,6 +172,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/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index cea674d96c..77e6a1c7e7 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -16,6 +16,7 @@ import { bindThis } from '@/decorators.js'; import { FilterUnionByProperty, groupedNotificationTypes } from '@/types.js'; import { CacheService } from '@/core/CacheService.js'; import { RoleEntityService } from './RoleEntityService.js'; +import { ChatEntityService } from './ChatEntityService.js'; import type { OnModuleInit } from '@nestjs/common'; import type { UserEntityService } from './UserEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js'; @@ -27,6 +28,7 @@ export class NotificationEntityService implements OnModuleInit { private userEntityService: UserEntityService; private noteEntityService: NoteEntityService; private roleEntityService: RoleEntityService; + private chatEntityService: ChatEntityService; constructor( private moduleRef: ModuleRef, @@ -41,9 +43,6 @@ export class NotificationEntityService implements OnModuleInit { private followRequestsRepository: FollowRequestsRepository, private cacheService: CacheService, - - //private userEntityService: UserEntityService, - //private noteEntityService: NoteEntityService, ) { } @@ -51,6 +50,7 @@ export class NotificationEntityService implements OnModuleInit { this.userEntityService = this.moduleRef.get('UserEntityService'); this.noteEntityService = this.moduleRef.get('NoteEntityService'); this.roleEntityService = this.moduleRef.get('RoleEntityService'); + this.chatEntityService = this.moduleRef.get('ChatEntityService'); } /** @@ -59,7 +59,6 @@ export class NotificationEntityService implements OnModuleInit { async #packInternal <T extends MiNotification | MiGroupedNotification> ( src: T, meId: MiUser['id'], - options: { checkValidNotifier?: boolean; }, @@ -92,7 +91,7 @@ export class NotificationEntityService implements OnModuleInit { // if the user has been deleted, don't show this notification if (needsUser && !userIfNeed) return null; - // #region Grouped notifications + //#region Grouped notifications if (notification.type === 'reaction:grouped') { const reactions = (await Promise.all(notification.reactions.map(async reaction => { const user = hint?.packedUsers != null @@ -137,7 +136,7 @@ export class NotificationEntityService implements OnModuleInit { users, }); } - // #endregion + //#endregion const needsRole = notification.type === 'roleAssigned'; const role = needsRole @@ -151,6 +150,13 @@ export class NotificationEntityService implements OnModuleInit { return null; } + const needsChatRoomInvitation = notification.type === 'chatRoomInvitationReceived'; + const chatRoomInvitation = needsChatRoomInvitation ? await this.chatEntityService.packRoomInvitation(notification.invitationId, { id: meId }).catch(() => null) : undefined; + // if the invitation has been deleted, don't show this notification + if (needsChatRoomInvitation && !chatRoomInvitation) { + return null; + } + return await awaitAll({ id: notification.id, createdAt: new Date(notification.createdAt).toISOString(), @@ -164,6 +170,9 @@ export class NotificationEntityService implements OnModuleInit { ...(notification.type === 'roleAssigned' ? { role: role, } : {}), + ...(notification.type === 'chatRoomInvitationReceived' ? { + invitation: chatRoomInvitation, + } : {}), ...(notification.type === 'followRequestAccepted' ? { message: notification.message, } : {}), |