This commit is contained in:
syuilo
2025-03-17 18:18:04 +09:00
parent 30be29a785
commit 06bd615334
9 changed files with 144 additions and 65 deletions

View File

@@ -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();

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<MiFlash> & MiRepository<MiFlash>;
export type FlashLikesRepository = Repository<MiFlashLike> & MiRepository<MiFlashLike>;
export type UserMemoRepository = Repository<MiUserMemo> & MiRepository<MiUserMemo>;
export type ChatMessagesRepository = Repository<MiChatMessage> & MiRepository<MiChatMessage>;
export type ChatRoomsRepository = Repository<MiChatRoom> & MiRepository<MiChatRoom>;
export type ChatRoomMembershipsRepository = Repository<MiChatRoomMembership> & MiRepository<MiChatRoomMembership>;
export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord> & MiRepository<MiBubbleGameRecord>;
export type ReversiGamesRepository = Repository<MiReversiGame> & MiRepository<MiReversiGame>;

View File

@@ -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,

View File

@@ -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<typeof meta, typeof paramDef> { // 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);
}
}*/
});

View File

@@ -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<typeof meta, typeof paramDef> { // 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);
});

View File

@@ -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<typeof meta, typeof paramDef> { // 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,
})));
}*/
});