diff --git a/packages/backend/src/core/ChatService.ts b/packages/backend/src/core/ChatService.ts index 1a552e7ac4..9eedfb6acb 100644 --- a/packages/backend/src/core/ChatService.ts +++ b/packages/backend/src/core/ChatService.ts @@ -54,14 +54,14 @@ export class ChatService { public async createMessage(params: { fromUser: { id: MiUser['id']; host: MiUser['host']; }; toUser?: MiUser | null; - //toGroup?: MiUserGroup | null; + //toRoom?: MiUserRoom | null; text?: string | null; file?: MiDriveFile | null; uri?: string | null; }) { - const { fromUser, toUser /*toGroup*/ } = params; + const { fromUser, toUser /*toRoom*/ } = params; - if (toUser == null /*&& toGroup == null*/) { + if (toUser == null /*&& toRoom == null*/) { throw new Error('recipient is required'); } @@ -76,7 +76,7 @@ export class ChatService { id: this.idService.gen(), fromUserId: fromUser.id, toUserId: toUser ? toUser.id : null, - //toGroupId: recipientGroup ? recipientGroup.id : null, + //toRoomId: recipientRoom ? recipientRoom.id : null, text: params.text ? params.text.trim() : null, fileId: params.file ? params.file.id : null, reads: [], @@ -102,12 +102,12 @@ export class ChatService { // 相手のストリーム this.globalEventService.publishChatStream(toUser.id, fromUser.id, 'message', packedMessage); } - }/* else if (toGroup) { + }/* else if (toRoom) { // グループのストリーム - this.globalEventService.publishGroupChatStream(toGroup.id, 'message', messageObj); + this.globalEventService.publishRoomChatStream(toRoom.id, 'message', messageObj); // メンバーのストリーム - const joinings = await this.userGroupJoiningsRepository.findBy({ userGroupId: toGroup.id }); + const joinings = await this.userRoomJoiningsRepository.findBy({ userRoomId: toRoom.id }); for (const joining of joinings) { this.globalEventService.publishChatIndexStream(joining.userId, 'message', messageObj); this.globalEventService.publishMainStream(joining.userId, 'chatMessage', messageObj); @@ -124,8 +124,8 @@ export class ChatService { const packedMessageForTo = await this.chatMessageEntityService.pack(inserted, toUser); this.globalEventService.publishMainStream(toUser.id, 'newChatMessage', packedMessageForTo); this.pushNotificationService.pushNotification(toUser.id, 'newChatMessage', packedMessageForTo); - }/* else if (toGroup) { - const joinings = await this.userGroupJoiningsRepository.findBy({ userGroupId: toGroup.id, userId: Not(fromUser.id) }); + }/* else if (toRoom) { + const joinings = await this.userRoomJoiningsRepository.findBy({ userRoomId: toRoom.id, userId: Not(fromUser.id) }); for (const joining of joinings) { if (freshMessage.reads.includes(joining.userId)) return; // 既読 this.globalEventService.publishMainStream(joining.userId, 'newChatMessage', messageObj); @@ -186,28 +186,28 @@ export class ChatService { 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); } - }/* else if (message.groupId) { - this.globalEventService.publishGroupChatStream(message.groupId, 'deleted', message.id); + }/* else if (message.roomId) { + this.globalEventService.publishRoomChatStream(message.roomId, 'deleted', message.id); }*/ } /* @bindThis - public async readGroupChatMessage( + public async readRoomChatMessage( userId: MiUser['id'], - groupId: MiUserGroup['id'], + roomId: MiUserRoom['id'], messageIds: MiChatMessage['id'][], ) { if (messageIds.length === 0) return; // check joined - const joining = await this.userGroupJoiningsRepository.findOneBy({ + const joining = await this.userRoomJoiningsRepository.findOneBy({ userId: userId, - userGroupId: groupId, + userRoomId: roomId, }); if (joining == null) { - throw new IdentifiableError('930a270c-714a-46b2-b776-ad27276dc569', 'Access denied (group).'); + throw new IdentifiableError('930a270c-714a-46b2-b776-ad27276dc569', 'Access denied (room).'); } const messages = await this.chatMessagesRepository.findBy({ @@ -232,7 +232,7 @@ export class ChatService { } // Publish event - this.globalEventService.publishGroupChatStream(groupId, 'read', { + this.globalEventService.publishRoomChatStream(roomId, 'read', { ids: reads, userId: userId, }); @@ -245,14 +245,14 @@ export class ChatService { } else { // そのグループにおいて未読がなければイベント発行 const unreadExist = await this.chatMessagesRepository.createQueryBuilder('message') - .where('message.groupId = :groupId', { groupId: groupId }) + .where('message.roomId = :roomId', { roomId: roomId }) .andWhere('message.userId != :userId', { userId: userId }) .andWhere('NOT (:userId = ANY(message.reads))', { userId: userId }) .andWhere('message.createdAt > :joinedAt', { joinedAt: joining.createdAt }) // 自分が加入する前の会話については、未読扱いしない .getOne().then(x => x != null); if (!unreadExist) { - this.pushNotificationService.pushNotification(userId, 'readAllChatMessagesOfARoom', { groupId }); + this.pushNotificationService.pushNotification(userId, 'readAllChatMessagesOfARoom', { roomId }); } } } @@ -300,7 +300,7 @@ export class ChatService { .where('message.fromUserId = :meId', { meId: meId }) .orWhere('message.toUserId = :meId', { meId: meId }); })) - .andWhere('message.groupId IS NULL') + .andWhere('message.roomId IS NULL') .andWhere(`message.fromUserId NOT IN (${ mutingQuery.getQuery() })`) .andWhere(`message.toUserId NOT IN (${ mutingQuery.getQuery() })`); @@ -324,27 +324,27 @@ export class ChatService { } @bindThis - public async groupHistory(meId: MiUser['id'], limit: number) { + public async roomHistory(meId: MiUser['id'], limit: number) { /* - const groups = await this.userGroupJoiningsRepository.findBy({ + const rooms = await this.userRoomJoiningsRepository.findBy({ userId: meId, - }).then(xs => xs.map(x => x.userGroupId)); + }).then(xs => xs.map(x => x.userRoomId)); - if (groups.length === 0) { + if (rooms.length === 0) { return []; } const history: MiChatMessage[] = []; for (let i = 0; i < limit; i++) { - const found = history.map(m => m.groupId!); + const found = history.map(m => m.roomId!); const query = this.chatMessagesRepository.createQueryBuilder('message') .orderBy('message.id', 'DESC') - .where('message.groupId IN (:...groups)', { groups: groups }); + .where('message.roomId IN (:...rooms)', { rooms: rooms }); if (found.length > 0) { - query.andWhere('message.groupId NOT IN (:...found)', { found: found }); + query.andWhere('message.roomId NOT IN (:...found)', { found: found }); } const message = await query.getOne(); diff --git a/packages/backend/src/models/ChatMessage.ts b/packages/backend/src/models/ChatMessage.ts index 964d8135b5..3d2b64268e 100644 --- a/packages/backend/src/models/ChatMessage.ts +++ b/packages/backend/src/models/ChatMessage.ts @@ -7,6 +7,7 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typ import { id } from './util/id.js'; import { MiUser } from './User.js'; import { MiDriveFile } from './DriveFile.js'; +import { MiChatRoom } from './ChatRoom.js'; @Entity('chat_message') export class MiChatMessage { @@ -37,19 +38,17 @@ export class MiChatMessage { @JoinColumn() public toUser: MiUser | null; - /* @Index() @Column({ ...id(), nullable: true, }) - public toGroupId: MiUserGroup['id'] | null; + public toRoomId: MiChatRoom['id'] | null; - @ManyToOne(type => MiUserGroup, { + @ManyToOne(type => MiChatRoom, { onDelete: 'CASCADE', }) @JoinColumn() - public toGroup: MiUserGroup | null; - */ + public toRoom: MiChatRoom | null; @Column('varchar', { length: 4096, nullable: true, diff --git a/packages/backend/src/models/ChatRoom.ts b/packages/backend/src/models/ChatRoom.ts new file mode 100644 index 0000000000..a0b9708e75 --- /dev/null +++ b/packages/backend/src/models/ChatRoom.ts @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('chat_room') +export class MiChatRoom { + @PrimaryColumn(id()) + public id: string; + + @Column('varchar', { + length: 256, + }) + public name: string; + + @Index() + @Column({ + ...id(), + }) + public ownerId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public owner: MiUser | null; +} diff --git a/packages/backend/src/models/ChatRoomMembership.ts b/packages/backend/src/models/ChatRoomMembership.ts new file mode 100644 index 0000000000..c527f5d795 --- /dev/null +++ b/packages/backend/src/models/ChatRoomMembership.ts @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiChatRoom } from './ChatRoom.js'; + +@Entity('chat_room_membership') +export class MiChatRoomMembership { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column({ + ...id(), + }) + public userId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: MiUser | null; + + @Index() + @Column({ + ...id(), + }) + public roomId: MiChatRoom['id']; + + @ManyToOne(type => MiChatRoom, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public room: MiChatRoom | null; +} diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index b98f8cb8b7..d1d664dd20 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -76,6 +76,8 @@ import { MiFlash } from '@/models/Flash.js'; import { MiFlashLike } from '@/models/FlashLike.js'; import { MiUserListFavorite } from '@/models/UserListFavorite.js'; import { MiChatMessage } from '@/models/ChatMessage.js'; +import { MiChatRoom } from '@/models/ChatRoom.js'; +import { MiChatRoomMembership } from '@/models/ChatRoomMembership.js'; import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; import { MiReversiGame } from '@/models/ReversiGame.js'; import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js'; @@ -193,6 +195,8 @@ export { MiFlashLike, MiUserMemo, MiChatMessage, + MiChatRoom, + MiChatRoomMembership, MiBubbleGameRecord, MiReversiGame, }; @@ -266,5 +270,7 @@ export type FlashsRepository = Repository & MiRepository; export type FlashLikesRepository = Repository & MiRepository; export type UserMemoRepository = Repository & MiRepository; export type ChatMessagesRepository = Repository & MiRepository; +export type ChatRoomsRepository = Repository & MiRepository; +export type ChatRoomMembershipsRepository = Repository & MiRepository; export type BubbleGameRecordsRepository = Repository & MiRepository; export type ReversiGamesRepository = Repository & MiRepository; diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index 96d4ba6a0a..a08635ac99 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -77,6 +77,8 @@ import { MiFlash } from '@/models/Flash.js'; import { MiFlashLike } from '@/models/FlashLike.js'; import { MiUserMemo } from '@/models/UserMemo.js'; import { MiChatMessage } from '@/models/ChatMessage.js'; +import { MiChatRoom } from '@/models/ChatRoom.js'; +import { MiChatRoomMembership } from '@/models/ChatRoomMembership.js'; import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; import { MiReversiGame } from '@/models/ReversiGame.js'; @@ -238,6 +240,8 @@ export const entities = [ MiFlashLike, MiUserMemo, MiChatMessage, + MiChatRoom, + MiChatRoomMembership, MiBubbleGameRecord, MiReversiGame, ...charts, diff --git a/packages/backend/src/server/api/endpoints/chat/messages/create.ts b/packages/backend/src/server/api/endpoints/chat/messages/create.ts index 1facf3f67a..f98f2991e5 100644 --- a/packages/backend/src/server/api/endpoints/chat/messages/create.ts +++ b/packages/backend/src/server/api/endpoints/chat/messages/create.ts @@ -45,15 +45,15 @@ export const meta = { id: '11795c64-40ea-4198-b06e-3c873ed9039d', }, - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', + noSuchRoom: { + message: 'No such room.', + code: 'NO_SUCH_ROOM', id: 'c94e2a5d-06aa-4914-8fa6-6a42e73d6537', }, - groupAccessDenied: { - message: 'You can not send messages to groups that you have not joined.', - code: 'GROUP_ACCESS_DENIED', + roomAccessDenied: { + message: 'You can not send messages to rooms that you have not joined.', + code: 'ROOM_ACCESS_DENIED', id: 'd96b3cca-5ad1-438b-ad8b-02f931308fbd', }, @@ -130,22 +130,22 @@ export default class extends Endpoint { // eslint- text: ps.text, file: file, }); - }/* else if (ps.groupId != null) { - // Fetch recipient (group) - recipientGroup = await this.userGroupsRepository.findOneBy({ id: ps.groupId! }); + }/* else if (ps.roomId != null) { + // Fetch recipient (room) + recipientRoom = await this.userRoomsRepository.findOneBy({ id: ps.roomId! }); - if (recipientGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); + if (recipientRoom == null) { + throw new ApiError(meta.errors.noSuchRoom); } // check joined - const joining = await this.userGroupJoiningsRepository.findOneBy({ + const joining = await this.userRoomJoiningsRepository.findOneBy({ userId: me.id, - userGroupId: recipientGroup.id, + userRoomId: recipientRoom.id, }); if (joining == null) { - throw new ApiError(meta.errors.groupAccessDenied); + throw new ApiError(meta.errors.roomAccessDenied); } }*/ }); diff --git a/packages/backend/src/server/api/endpoints/chat/messages/history.ts b/packages/backend/src/server/api/endpoints/chat/messages/history.ts index 23434c32cd..163c286be3 100644 --- a/packages/backend/src/server/api/endpoints/chat/messages/history.ts +++ b/packages/backend/src/server/api/endpoints/chat/messages/history.ts @@ -35,7 +35,7 @@ export const paramDef = { type: 'object', properties: { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - group: { type: 'boolean', default: false }, + room: { type: 'boolean', default: false }, }, } as const; @@ -46,7 +46,7 @@ export default class extends Endpoint { // eslint- private chatService: ChatService, ) { super(meta, paramDef, async (ps, me) => { - const history = ps.group ? await this.chatService.groupHistory(me.id, ps.limit) : await this.chatService.userHistory(me.id, ps.limit); + const history = ps.room ? await this.chatService.roomHistory(me.id, ps.limit) : await this.chatService.userHistory(me.id, ps.limit); return await this.chatMessageEntityService.packMany(history, me); }); diff --git a/packages/backend/src/server/api/endpoints/chat/messages/timeline.ts b/packages/backend/src/server/api/endpoints/chat/messages/timeline.ts index 834acf0757..bd8f347d01 100644 --- a/packages/backend/src/server/api/endpoints/chat/messages/timeline.ts +++ b/packages/backend/src/server/api/endpoints/chat/messages/timeline.ts @@ -35,15 +35,15 @@ export const meta = { id: '11795c64-40ea-4198-b06e-3c873ed9039d', }, - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', + noSuchRoom: { + message: 'No such room.', + code: 'NO_SUCH_ROOM', id: 'c4d9f88c-9270-4632-b032-6ed8cee36f7f', }, - groupAccessDenied: { - message: 'You can not read messages of groups that you have not joined.', - code: 'GROUP_ACCESS_DENIED', + roomAccessDenied: { + message: 'You can not read messages of rooms that you have not joined.', + code: 'ROOM_ACCESS_DENIED', id: 'a053a8dd-a491-4718-8f87-50775aad9284', }, }, @@ -76,36 +76,36 @@ export default class extends Endpoint { // eslint- const messages = await this.chatService.userTimeline(me.id, other.id, ps.sinceId, ps.untilId, ps.limit); return await this.chatMessageEntityService.packLiteMany(messages); - }/* else if (ps.groupId != null) { - // Fetch recipient (group) - const recipientGroup = await this.userGroupRepository.findOneBy({ id: ps.groupId }); + }/* else if (ps.roomId != null) { + // Fetch recipient (room) + const recipientRoom = await this.userRoomRepository.findOneBy({ id: ps.roomId }); - if (recipientGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); + if (recipientRoom == null) { + throw new ApiError(meta.errors.noSuchRoom); } // check joined - const joining = await this.userGroupJoiningsRepository.findOneBy({ + const joining = await this.userRoomJoiningsRepository.findOneBy({ userId: me.id, - userGroupId: recipientGroup.id, + userRoomId: recipientRoom.id, }); if (joining == null) { - throw new ApiError(meta.errors.groupAccessDenied); + throw new ApiError(meta.errors.roomAccessDenied); } const query = this.queryService.makePaginationQuery(this.messagingMessagesRepository.createQueryBuilder('message'), ps.sinceId, ps.untilId) - .andWhere('message.groupId = :groupId', { groupId: recipientGroup.id }); + .andWhere('message.roomId = :roomId', { roomId: recipientRoom.id }); const messages = await query.take(ps.limit).getMany(); // Mark all as read if (ps.markAsRead) { - this.messagingService.readGroupMessagingMessage(me.id, recipientGroup.id, messages.map(x => x.id)); + this.messagingService.readRoomMessagingMessage(me.id, recipientRoom.id, messages.map(x => x.id)); } return await Promise.all(messages.map(message => this.messagingMessageEntityService.pack(message, me, { - populateGroup: false, + populateRoom: false, }))); }*/ });