message storing, conversation storing

This commit is contained in:
Suman991 2026-04-16 17:01:08 +05:30
parent 538198840c
commit d05fb7c9f6
9 changed files with 196 additions and 60 deletions

View File

@ -77,7 +77,6 @@ export class ChatGateway
// tell everyone else someone joined
client.broadcast.emit('serverNotice', `[+] ${payload.username} joined`);
} catch (err) {
// st-4: Disconnect unauthorized clients
this.logger.warn(`[!] Rejected connection: ${err}`);
@ -104,41 +103,39 @@ export class ChatGateway
@MessageBody() body: JoinLeaveRoomDto, //roomId
@ConnectedSocket() client: Socket,
) {
// extract user
const username=client.data.user.username;
const username = client.data.user.username;
// join user in the room(front-end will provide roomId(standard appraoch))
client.join(body.roomId)
client.join(body.roomId);
// tell the joiner he successfully joined
client.emit("roomJoined",{
roomId:body.roomId,
message:`You joined room: ${body.roomId}`,
})
client.emit('roomJoined', {
roomId: body.roomId,
message: `You joined room: ${body.roomId}`,
});
// also tell others in the room except the new joiner
client.to(body.roomId).emit("roomNotice",{
client.to(body.roomId).emit('roomNotice', {
roomId: body.roomId,
message: `[+] ${username} joined the room`,
})
});
// return acknowledgement
return { success: true, roomId: body.roomId };
}
@UseGuards(WsJwtGuard)
@SubscribeMessage('leaveRoom')
handleLeaveRoom(
@MessageBody() body: JoinLeaveRoomDto, //roomId
@MessageBody() body: JoinLeaveRoomDto, //roomId
@ConnectedSocket() client: Socket,
) {
// extract user
const username=client.data.user.username;
const username = client.data.user.username;
// remove the user from the room
client.leave(body.roomId)
client.leave(body.roomId);
// confirm to the leaver
client.emit('roomLeft', { roomId: body.roomId });
@ -154,31 +151,24 @@ export class ChatGateway
@UseGuards(WsJwtGuard)
@SubscribeMessage('roomMessage')
handleRoomMessage(
@MessageBody() body:RoomMessageDto, //text:string
@ConnectedSocket() sender:Socket
){
// extract user
const username=sender.data.user.username
// Check if user is in room
const isMember = sender.rooms.has(body.roomId);
if (!isMember) {
sender.emit('roomError', {
message: 'You are not allowed',
});
return;
}
this.logger.log(`[ROOM:${body.roomId}] ${username}: ${body.text}`);
async handleRoomMessage(
@MessageBody() body: RoomMessageDto, //text:string
@ConnectedSocket() sender: Socket,
) {
//verify memebership, store chat and return event's data
const data = await this.chatService.handleRoomMessage(body, sender);
// send everone in the room(including the sender)
this.server.to(body.roomId).emit('roomMessage', {
roomId: body.roomId,
senderId: sender.id,
username,
text: body.text,
});
this.server.to(body.roomId).emit('roomMessage', data);
}
@UseGuards(WsJwtGuard)
@SubscribeMessage('startP2P')
async handleStartP2P(
@MessageBody() body: { targetUserId: string },
@ConnectedSocket() client: Socket,
) {
return this.chatService.handleStartP2P(body.targetUserId, client);
}
}

View File

@ -3,9 +3,11 @@ import { ChatService } from './chat.service';
import { ChatGateway } from './chat.gateway';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { MessagesModule } from 'src/messages/messages.module';
@Module({
imports: [
MessagesModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],

View File

@ -1,6 +1,74 @@
import { ConversationsService } from './../conversations/conversations.service';
import { MessagesService } from './../messages/messages.service';
import { Injectable } from '@nestjs/common';
import { RoomMessageDto } from './dto/room-message.dto';
import { Socket } from 'socket.io';
import { ConversationType } from 'src/conversations/schemas/conversation.schema';
@Injectable()
export class ChatService {
constructor(private readonly messagesService:MessagesService,
private readonly conversationsService:ConversationsService
){}
async handleRoomMessage(body:RoomMessageDto, sender:Socket){
// extract user
const { id: userId, username } = sender.data.user;
// Check if user is in room
const isMember = sender.rooms.has(body.roomId);
if (!isMember) {
sender.emit('roomError', {
message: 'You are not allowed',
});
return;
}
// store the message
const message=await this.messagesService.create({
conversationId:body.roomId,
senderId:userId,
text:body.text,
})
return{
roomId:body.roomId,
senderId:userId,
username,
text:body.text,
createdAt:message.createdAt,
}
}
async handleStartP2P(targetUserId: string, client: Socket) {
const senderId = client.data.user.id;
if (senderId === targetUserId) {
client.emit('roomError', { message: 'Cannot start a conversation with yourself' });
return;
}
// Find existing or create new P2P conversation
let conversation:any = await this.conversationsService.findP2p(senderId, targetUserId)
if (!conversation) {
conversation = await this.conversationsService.create({
type: ConversationType.P2P,
participants: [senderId, targetUserId],
});
}
const roomId = conversation._id.toString();
// Caller joins immediately
client.join(roomId);
client.emit('p2pReady', {
roomId, // client uses this for all future roomMessage events
targetUserId,
message: `P2P room ready`,
});
return { roomId };
}
}

View File

@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { CreateConversationDto } from './dto/create-conversation.dto';
import { UpdateConversationDto } from './dto/update-conversation.dto';
import { Conversation, ConversationDocument } from './schemas/conversation.schema';
import { Conversation, ConversationDocument, ConversationType } from './schemas/conversation.schema';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
@ -9,17 +9,24 @@ import { Model } from 'mongoose';
export class ConversationsService {
constructor(
@InjectModel(Conversation.name) private userModel: Model<ConversationDocument>,
@InjectModel(Conversation.name) private conversationModel: Model<ConversationDocument>,
) {}
create(createConversationDto: CreateConversationDto) {
return 'This action adds a new conversation';
async create(createConversationDto: CreateConversationDto):Promise<ConversationDocument> {
return await this.conversationModel.create(createConversationDto)
}
findAll() {
return `This action returns all conversations`;
}
async findP2p(senderId:string, targetUserId:string){
return await this.conversationModel.findOne({
type: ConversationType.P2P,
participants: { $all: [senderId, targetUserId], $size: 2 },
});
}
findOne(id: number) {
return `This action returns a #${id} conversation`;
}

View File

@ -1 +1,47 @@
export class CreateConversationDto {}
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { ConversationType } from '../schemas/conversation.schema';
import {
IsEnum,
IsNotEmpty,
IsOptional,
IsString,
ArrayMinSize,
ArrayUnique,
IsArray,
} from 'class-validator';
export class CreateConversationDto {
@ApiProperty({
enum: ConversationType,
enumName: 'ConversationType',
example: ConversationType.P2P,
description: 'Type of the conversation',
})
@IsEnum(ConversationType)
@IsNotEmpty()
type!: ConversationType;
@ApiProperty({
type: [String],
example: ['userId1', 'userId2'],
description: 'List of participant user IDs',
minItems: 2,
})
@IsArray()
@ArrayMinSize(2)
@ArrayUnique()
@IsString({ each: true })
@IsNotEmpty({ each: true })
participants!: string[];
@ApiPropertyOptional({
type: String,
example: 'Team Chat',
description: 'Name of the conversation (required for group, omit for p2p)',
nullable: true,
})
@IsOptional()
@IsString()
@IsNotEmpty()
name?: string;
}

View File

@ -1,7 +1,8 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { Document } from "mongoose";
export type ConversationDocument = Conversation & Document;
export enum ConversationType{
export enum ConversationType{
P2P='p2p',
GROUP='group'
}
@ -19,9 +20,9 @@ export class Conversation {
participants!:string[]
@Prop({required:false,default:null})
name!:string // for p2p no name
name?:string // for p2p no name
createdAt!:Date
createdAt!:Date // no prop
}
export const ConversationSchema = SchemaFactory.createForClass(Conversation);

View File

@ -1 +1,19 @@
export class CreateMessageDto {}
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class CreateMessageDto {
@ApiProperty({ example: 'qwertyj', description: 'mongodb comnversation id' })
@IsNotEmpty()
@IsString()
conversationId!: string;
@ApiProperty({ example: 'qwertyj', description: 'mongodb user id' })
@IsNotEmpty()
@IsString()
senderId!: string;
@ApiProperty({ example: 'Hello user', description: 'user message' })
@IsNotEmpty()
@IsString()
text!: string;
}

View File

@ -7,9 +7,11 @@ import { Model } from 'mongoose';
@Injectable()
export class MessagesService {
constructor( @InjectModel(Message.name) private MessageModel: Model<MessageDocument>,){}
create(createMessageDto: CreateMessageDto) {
return 'This action adds a new message';
constructor(
@InjectModel(Message.name) private MessageModel: Model<MessageDocument>,
) {}
async create(createMessageDto: CreateMessageDto): Promise<MessageDocument> {
return await this.MessageModel.create(createMessageDto);
}
findAll() {

View File

@ -1,19 +1,21 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
export type MessageDocument = Message & Document;
@Schema({timestamps:true})
@Schema({ timestamps: true })
export class Message {
@Prop({ required:true})
conversationId!:string
@Prop({ required: true })
conversationId!: string;
@Prop({ required:true})
senderId!:string
@Prop({ required: true })
senderId!: string;
@Prop({ required:false})
text!:string
@Prop({ required: false })
text!: string;
createdAt!:Date
// no prop
createdAt!:Date
}
export const MessageSchema = SchemaFactory.createForClass(Message);