Skip to content

3. Study Kasus (Socket Server)

Membaut Module Chat

terminal
npx nest g module app/chat
npx nest g service app/chat
npx nest g controller app/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 };
}

alt text

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

alt text

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,
    });
  }
}

alt text

alt text

Penjelasan ...

Membuat Posman untuk

alt text

ihsan

alt text alt text

raihan alt text alt text

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"
}

alt text alt text alt text

_Mencoba mengirim pesan dari azis ke ihsan _

terminal
{
    "sender" : 2,
    "receiver" : 1,
    "message" : "hai ihsan",
    "room_receiver" : "ihsan@gmail.com"
}

alt text alt text alt text

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"

}

alt text alt text alt text alt text

Membuat list user

controller.chat.ts
 @Get('list')
  async list() {
    return this.chat.list();
  }
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);
  }

alt text

Membuat fitur sedang mengetik

websocket.gateway.ts
@SubscribeMessage('typing')
  Typing(@MessageBody() payload: { sender: string; receiver: string,is_typing:boolean }) {

    console.log('oay', payload)
    this.server.to(payload.receiver).emit('typing.listen', payload);
  }