3. Study Kasus (Socket Server)
Membaut Module Chat
Membuat Table Conversation
conversation.entity.ts
import {
BaseEntity,
Column,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import { User } from '../auth/auth.entity';
import { Message } from './message.entity';
@Entity()
export class Conversation extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => User)
@JoinColumn({ name: 'user1' })
user1: User;
@ManyToOne(() => User)
@JoinColumn({ name: 'user2' })
user2: User;
@OneToMany(() => Message, (v) => v.conversation_id)
messages: Message[];
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
created_at: Date;
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
updated_at: Date;
}
Membuat Table Message
message.entity.ts
import {
BaseEntity,
Column,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { User } from '../auth/auth.entity';
import { Conversation } from './conversation.entity';
// Import entitas Conversation
@Entity()
export class Message extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => User)
@JoinColumn({ name: 'sender' })
sender: User;
@ManyToOne(() => User)
@JoinColumn({ name: 'receiver' })
receiver: User;
@Column({ type: 'text' })
message: string;
@Column({ nullable: true })
file: string;
@Column()
is_read: number;
@ManyToOne(() => Conversation)
@JoinColumn({ name: 'conversation_id' })
conversation_id: Message;
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
created_at: Date;
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
updated_at: Date;
}
Membuat Dto
chat.dto.ts
import { OmitType, PartialType, PickType } from '@nestjs/mapped-types';
import {
IsEmail,
IsInt,
IsObject,
IsOptional,
IsString,
Length,
MinLength,
} from 'class-validator';
import { IsUnique } from 'src/utils/validator/unique.validator';
export class MessageDto {
@IsInt()
id: number;
@IsObject()
@IsOptional()
sender: { id: number };
@IsObject()
@IsOptional()
receiver: { id: number };
@IsString()
message: string;
@IsString()
file: string;
@IsOptional()
is_read : number
@IsString()
room_receiver : string
@IsString()
room_sender : string
@IsObject()
@IsOptional()
conversation_id: { id: number };
}
export class SendMessageDto extends OmitType(MessageDto, [
"id"
]) {}
export class ConversationDto {
@IsInt()
id: number;
@IsObject()
@IsOptional()
user1: { id: number };
@IsObject()
@IsOptional()
user2: { id: number };
}
Import Module WebSocket
chat.module.ts
import { Module } from '@nestjs/common';
import { ChatService } from './chat.service';
import { ChatController } from './chat.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Message } from './message.entity';
import { Conversation } from './conversation.entity';
import { WebsocketModule } from '../websocket/websocket.module';
@Module({
imports: [TypeOrmModule.forFeature([Message, Conversation] ), WebsocketModule ],
providers: [ChatService],
controllers: [ChatController]
})
export class ChatModule {}
Membuat Service untuk join Room
websocket.gateway.ts
@SubscribeMessage('join')
joinRoom(
@MessageBody('room_code') room_code: string,
@ConnectedSocket() client: Socket,
) {
console.log('join', room_code);
client.join(room_code);
this.server.emit('join.reply', {
message: `You have joined room`,
});
}
Membuat Service untuk send dan receiver di dalam room
websocket.gateway.ts
@SubscribeMessage('send_message')
sendMessage(@MessageBody() body: SendMessageDto) {
this.server.to(body.room_receiver).emit('received_message', {
msg: 'new Message',
data: body,
});
}
Menyiapkan User
Membuat Conversation_id antar user
Membuat API untuk generate conversation_id
controller.chat.ts
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
import { JwtGuard } from '../auth/auth.guard';
import { ChatService } from './chat.service';
@UseGuards(JwtGuard)
@Controller('chat')
export class ChatController {
constructor(private chat: ChatService) {}
@Post('/generate-conversation-id')
async generate(@Body('user2') user2: number) {
return this.chat.generateConversationId(user2);
}
}
service.chat.ts
import { Inject, Injectable } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { InjectRepository } from '@nestjs/typeorm';
import { ResponseSuccess } from 'src/interface/response';
import BaseResponse from 'src/utils/response/base.response';
import { Conversation } from './conversation.entity';
import { Repository } from 'typeorm';
import { randomBytes } from 'crypto';
import { Message } from './message.entity';
import { MessageGateway } from '../websocket/websocket.gateway';
@Injectable()
export class ChatService extends BaseResponse {
constructor(
@InjectRepository(Conversation)
private readonly conversationRepository: Repository<Conversation>,
@Inject(REQUEST) private req: any,
) {
super();
}
async generateConversationId(user2: number): Promise<ResponseSuccess> {
let user1 = this.req.user.id;
const code = await this.conversationRepository.findOne({
where: [
{
user1: {
id: user1,
},
user2: {
id: user2,
},
},
{
user1: {
id: user2,
},
user2: {
id: user1,
},
},
],
});
if (code === null) {
const result = await this.conversationRepository.save({
user1: {
id: user1,
},
user2: {
id: user2,
},
});
return this._success('OK', {
conversation_id:result.id,
user1,
user2,
});
}
return this._success('OK', {
conversation_id: code.id,
user1,
user2,
});
}
}
Penjelasan ...
Membuat Posman untuk
ihsan
raihan
Lakukan hal yang sama pada daffa dan aziz
_Mencoba mengirim pesan dari ihsan ke raihan _
terminal
{
"sender" : 1,
"receiver" : 3,
"message" : "hai raihan",
"room_receiver" : "raihan@gmail.com"
}
_Mencoba mengirim pesan dari azis ke ihsan _
terminal
{
"sender" : 2,
"receiver" : 1,
"message" : "hai ihsan",
"room_receiver" : "ihsan@gmail.com"
}
Penjelasan
Membuat API untuk send_message
controller.chat.ts
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
import { JwtGuard } from '../auth/auth.guard';
import { ChatService } from './chat.service';
import { SendMessageDto } from './chat.dto';
@UseGuards(JwtGuard)
@Controller('chat')
export class ChatController {
constructor(private chat: ChatService) {}
@Post('send_message')
async send_message(@Body() payload: SendMessageDto) {
return this.chat.create(payload);
}
}
service.chat.ts
import { Inject, Injectable } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { InjectRepository } from '@nestjs/typeorm';
import { ResponseSuccess } from 'src/interface/response';
import BaseResponse from 'src/utils/response/base.response';
import { Conversation } from './conversation.entity';
import { Repository } from 'typeorm';
import { randomBytes } from 'crypto';
import { Message } from './message.entity';
import { MessageGateway } from '../websocket/websocket.gateway';
import { SendMessageDto } from './chat.dto';
@Injectable()
export class ChatService extends BaseResponse {
constructor(
@InjectRepository(Conversation)
private readonly conversationRepository: Repository<Conversation>,
@InjectRepository(Message)
private readonly messageRepository: Repository<Message>,
private readonly webService: MessageGateway,
@Inject(REQUEST) private req: any,
) {
super();
}
async create(payload: SendMessageDto) {
const result = await this.messageRepository.save({
...payload,
sender: this.req.user.id,
is_read : 0
});
this.webService.create({
...result,
room_receiver: payload.room_receiver,
room_sender: this.req.user.email,
});
return this._success('OK');
}
}
websocket.gateway.ts
async create(payload: SendMessageDto) {
this.server.to(payload.room_sender).emit('received_message', payload);
this.server.to(payload.room_receiver).emit('received_message', payload);
}
payload
{
"receiver" : {
"id" : 3
},
"room_receiver" : "raihan@gmail.com",
"conversation_id" : {
"id" : 1
},
"message" : "hello raihan",
"file": "https://storage.devopsgeming.online/file-1725891827392.JPEG"
}
Membuat list user
service.chat.ts
async list(): Promise<ResponseSuccess> {
// Ambil semua percakapan berdasarkan pengguna
const conversations = await this.conversationRepository.find({
where: [
{ user1: { id: this.req.user.id } },
{ user2: { id: this.req.user.id } },
],
relations: ['user1', 'user2'], // Hanya ambil relasi user1 dan user2
select: {
user1: { id: true, nama: true, email: true },
user2: { id: true, nama: true, email: true },
},
});
// Ambil pesan terbaru untuk setiap percakapan
const conversationsWithLatestMessage = await Promise.all(
conversations.map(async (conversation) => {
const messages = await this.messageRepository
.createQueryBuilder('message')
.leftJoin('message.sender', 'sender') // Gabungkan data sender
.leftJoin('message.receiver', 'receiver') // Gabungkan data receiver
.addSelect(['sender.id', 'receiver.id']) // Pilih hanya id sender dan receiver
.where('message.conversation_id = :conversationId', {
conversationId: conversation.id,
})
.orderBy('message.created_at', 'DESC') // Urutkan dari yang terbaru
.limit(20) // Batasi hanya 10 pesan terakhir
.getMany();
// Ambil pesan terbaru
const latestMessage = messages[0] || null;
const totalMessages = await this.messageRepository
.createQueryBuilder('message')
.where('message.conversation_id = :conversationId', {
conversationId: conversation.id,
})
.getCount();
return {
...conversation,
messages,
conversation_id: conversation.id,
latestMessage,
totalMessages,
limit : 0,
pageSize : 10
};
}),
);
// Urutkan percakapan berdasarkan tanggal pesan terbaru (jika ada)
const sortedConversations = conversationsWithLatestMessage.sort((a, b) => {
const latestMessageA = a.latestMessage?.created_at || new Date(0);
const latestMessageB = b.latestMessage?.created_at || new Date(0);
return latestMessageB.getTime() - latestMessageA.getTime(); // Urutkan dari yang terbaru
});
return this._success('OK', sortedConversations);
}