chat update
This commit is contained in:
@@ -58,7 +58,6 @@ import { ChatbotGateway } from "../modules/chatbot/chatbot.gateway";
|
||||
import { ChatbotService } from "../modules/chatbot/providers/chatbot.service";
|
||||
import { DataContextService } from "../modules/chatbot/providers/data-context.service";
|
||||
import { LLMService } from "../modules/chatbot/providers/llm.service";
|
||||
import { WebSocketAuthService } from "../modules/chatbot/providers/websocket-auth.service";
|
||||
import { ContactUsRepo, CreateContactUsRepo } from "../modules/contact-us/contactUs.repository";
|
||||
import { ContactUsService } from "../modules/contact-us/contactUs.service";
|
||||
import { CouponRepo, CouponUsageRepo, createCouponRepo, createCouponUsageRepo } from "../modules/Coupon/coupon.repository";
|
||||
@@ -205,7 +204,6 @@ const containerModules = new AsyncContainerModule(async (bind) => {
|
||||
bind<ChatbotService>(IOCTYPES.ChatbotService).to(ChatbotService).inSingletonScope();
|
||||
bind<LLMService>(IOCTYPES.ChatbotLLMService).to(LLMService).inSingletonScope();
|
||||
bind<DataContextService>(IOCTYPES.ChatbotDataContextService).to(DataContextService).inSingletonScope();
|
||||
bind<WebSocketAuthService>(IOCTYPES.ChatbotWebSocketAuthService).to(WebSocketAuthService).inSingletonScope();
|
||||
bind<ChatbotGateway>(IOCTYPES.ChatbotGateway).to(ChatbotGateway).inSingletonScope();
|
||||
// #endregion
|
||||
// #region repository
|
||||
|
||||
@@ -2,26 +2,22 @@ import { Server as HttpServer } from "http";
|
||||
|
||||
import { inject, injectable } from "inversify";
|
||||
import { Server, Socket } from "socket.io";
|
||||
|
||||
import { ChatbotService } from "./providers/chatbot.service";
|
||||
import { WebSocketAuthService } from "./providers/websocket-auth.service";
|
||||
import { SendMessageDto } from "./DTO/send-message.dto";
|
||||
import { WEBSOCKET_EVENTS } from "./constants/chatbot.constants";
|
||||
import { IOCTYPES } from "../../IOC/ioc.types";
|
||||
import { Logger } from "../../core/logging/logger";
|
||||
import { AuthenticatedSocket, WebSocketResponse } from "./interfaces/websocket.interface";
|
||||
import * as ulidLib from "ulid";
|
||||
|
||||
import { WEBSOCKET_EVENTS } from "./constants/chatbot.constants";
|
||||
import { SendMessageDto } from "./DTO/send-message.dto";
|
||||
import { AuthenticatedSocket, WebSocketResponse } from "./interfaces/websocket.interface";
|
||||
import { ChatbotService } from "./providers/chatbot.service";
|
||||
import { Logger } from "../../core/logging/logger";
|
||||
import { IOCTYPES } from "../../IOC/ioc.types";
|
||||
|
||||
@injectable()
|
||||
export class ChatbotGateway {
|
||||
private io: Server;
|
||||
private readonly logger: Logger;
|
||||
private readonly CHATBOT_ULID_COOKIE_NAME = "chatbot_session_id";
|
||||
|
||||
constructor(
|
||||
@inject(IOCTYPES.ChatbotService) private chatbotService: ChatbotService,
|
||||
@inject(IOCTYPES.ChatbotWebSocketAuthService) private wsAuthService: WebSocketAuthService,
|
||||
) {
|
||||
constructor(@inject(IOCTYPES.ChatbotService) private chatbotService: ChatbotService) {
|
||||
this.logger = new Logger("ChatbotGateway");
|
||||
}
|
||||
|
||||
@@ -43,28 +39,15 @@ export class ChatbotGateway {
|
||||
|
||||
private async handleConnection(socket: Socket): Promise<void> {
|
||||
try {
|
||||
// Try to authenticate if token is provided
|
||||
const token = socket.handshake.query.token as string;
|
||||
let ulid: string | undefined;
|
||||
|
||||
if (token) {
|
||||
// Authenticated connection
|
||||
const authResult = await this.wsAuthService.authenticateClient(socket as AuthenticatedSocket);
|
||||
if (!authResult.success) {
|
||||
this.wsAuthService.handleAuthenticationFailure(socket, authResult.error || "Authentication failed");
|
||||
return;
|
||||
}
|
||||
this.wsAuthService.emitAuthenticationSuccess(socket, authResult.user!);
|
||||
ulid = authResult.user!.id;
|
||||
} else {
|
||||
// Anonymous connection - generate or get ULID from cookie
|
||||
const cookieHeader = socket.handshake.headers.cookie;
|
||||
ulid = this.extractUlidFromCookie(cookieHeader);
|
||||
if (!ulid) {
|
||||
ulid = ulidLib.ulid();
|
||||
}
|
||||
socket.data = { user: { id: ulid, sub: ulid } };
|
||||
// Anonymous connection - generate or get ULID from cookie
|
||||
const cookieHeader = socket.handshake.headers.cookie;
|
||||
ulid = this.extractUlidFromCookie(cookieHeader);
|
||||
if (!ulid) {
|
||||
ulid = ulidLib.ulid();
|
||||
}
|
||||
socket.data = { user: { id: ulid, sub: ulid } };
|
||||
|
||||
this.logger.info(`Client connected: ${socket.id}, ULID: ${ulid}`);
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { injectable } from "inversify";
|
||||
|
||||
import { IChatContext } from "../interfaces/chatbot.interface";
|
||||
import { Logger } from "../../../core/logging/logger";
|
||||
import { ProductModel } from "../../product/models/product.model";
|
||||
import { IChatContext } from "../interfaces/chatbot.interface";
|
||||
|
||||
/**
|
||||
* Service for retrieving relevant context data from the database
|
||||
@@ -20,25 +21,14 @@ export class DataContextService {
|
||||
const sources: string[] = [];
|
||||
|
||||
try {
|
||||
// Analyze message to determine what data to fetch
|
||||
// const keywords = this._extractKeywords(_message); // TODO: Use when implementing keyword-based filtering
|
||||
// Fetch all non-deleted products
|
||||
const products = await ProductModel.find({ deleted: false }).select("title_fa title_en model description _id").lean();
|
||||
|
||||
// TODO: Implement actual data fetching based on your project's models
|
||||
// For now, this returns empty data - you can extend this to fetch:
|
||||
// - Product data from ProductModel
|
||||
// - Category data from CategoryModel
|
||||
// - Order data from OrderModel
|
||||
// - User data from UserModel
|
||||
// etc.
|
||||
|
||||
// Example: If you want to fetch product data
|
||||
// if (this.hasProductKeywords(keywords)) {
|
||||
// const products = await ProductModel.find({ ... }).limit(5);
|
||||
// products.forEach((product) => {
|
||||
// relevantData.push(`Product: ${product.title_fa} - Price: ${product.price}`);
|
||||
// sources.push("Products Database");
|
||||
// });
|
||||
// }
|
||||
products.forEach((product) => {
|
||||
const productInfo = `Product ID: ${product._id}, Title (FA): ${product.title_fa}, Title (EN): ${product.title_en}, Model: ${product.model}${product.description ? `, Description: ${product.description.substring(0, 100)}` : ""}`;
|
||||
relevantData.push(productInfo);
|
||||
sources.push("Products Database");
|
||||
});
|
||||
|
||||
return { data: relevantData, sources };
|
||||
} catch (error) {
|
||||
@@ -46,34 +36,4 @@ export class DataContextService {
|
||||
return { data: [], sources: [] };
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Use this method when implementing keyword-based filtering
|
||||
// @ts-expect-error - Method reserved for future use
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
private _extractKeywords(_message: string): string[] {
|
||||
const commonWords = ["the", "is", "at", "which", "on", "a", "an", "and", "or", "but", "in", "with", "to", "for", "of", "as", "by"];
|
||||
|
||||
return _message
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s]/g, "")
|
||||
.split(/\s+/)
|
||||
.filter((word: string) => word.length > 2 && !commonWords.includes(word));
|
||||
}
|
||||
|
||||
// Helper methods for keyword detection - can be extended
|
||||
// TODO: Implement when needed
|
||||
// private hasProductKeywords(keywords: string[]): boolean {
|
||||
// const productTerms = ["product", "item", "goods", "merchandise", "کالا", "محصول"];
|
||||
// return keywords.some(keyword => productTerms.includes(keyword));
|
||||
// }
|
||||
|
||||
// private hasOrderKeywords(keywords: string[]): boolean {
|
||||
// const orderTerms = ["order", "purchase", "buy", "cart", "سفارش", "خرید"];
|
||||
// return keywords.some(keyword => orderTerms.includes(keyword));
|
||||
// }
|
||||
|
||||
// private hasCategoryKeywords(keywords: string[]): boolean {
|
||||
// const categoryTerms = ["category", "type", "kind", "دسته", "دستهبندی"];
|
||||
// return keywords.some(keyword => categoryTerms.includes(keyword));
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
import { inject, injectable } from "inversify";
|
||||
import { Socket } from "socket.io";
|
||||
|
||||
import { WebSocketMessage } from "../../../common/enums/message.enum";
|
||||
import { jwtExpiredErr } from "../../../core/app/app.errors";
|
||||
import { Logger } from "../../../core/logging/logger";
|
||||
import { IOCTYPES } from "../../../IOC/ioc.types";
|
||||
import { TokenService } from "../../token/token.service";
|
||||
import { WEBSOCKET_EVENTS } from "../constants/chatbot.constants";
|
||||
import { AuthenticationResult, WebSocketErrorContext } from "../interfaces/websocket.interface";
|
||||
|
||||
/**
|
||||
* Service responsible for WebSocket authentication logic
|
||||
* Follows single responsibility principle and provides reusable authentication methods
|
||||
*/
|
||||
@injectable()
|
||||
export class WebSocketAuthService {
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor(@inject(IOCTYPES.TokenService) private readonly tokenService: TokenService) {
|
||||
this.logger = new Logger("WebSocketAuthService");
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts token from WebSocket client
|
||||
*/
|
||||
private extractTokenFromClient(client: Socket): string | null {
|
||||
// Try to get token from query parameters
|
||||
const token = client.handshake.query.token as string;
|
||||
if (token) {
|
||||
return token;
|
||||
}
|
||||
|
||||
// Try to get token from auth header
|
||||
const authHeader = client.handshake.headers.authorization;
|
||||
if (authHeader && typeof authHeader === "string") {
|
||||
const parts = authHeader.split(" ");
|
||||
if (parts.length === 2 && parts[0] === "Bearer") {
|
||||
return parts[1];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates a WebSocket client and returns the result
|
||||
*
|
||||
* @param client - Socket.IO client to authenticate
|
||||
* @returns Promise with authentication result
|
||||
*/
|
||||
async authenticateClient(client: Socket): Promise<AuthenticationResult> {
|
||||
try {
|
||||
const token = this.extractTokenFromClient(client);
|
||||
|
||||
if (!token) {
|
||||
this.logAuthenticationAttempt(client, false, "No token provided");
|
||||
return {
|
||||
success: false,
|
||||
error: WebSocketMessage.AUTHENTICATION_REQUIRED,
|
||||
};
|
||||
}
|
||||
|
||||
const payload = this.tokenService.verifyToken(token);
|
||||
|
||||
if (!payload?.sub) {
|
||||
this.logAuthenticationAttempt(client, false, "Invalid token payload");
|
||||
return {
|
||||
success: false,
|
||||
error: WebSocketMessage.INVALID_TOKEN,
|
||||
};
|
||||
}
|
||||
|
||||
// Store user data in client (convert sub to id for compatibility)
|
||||
client.data.user = { id: payload.sub, sub: payload.sub };
|
||||
|
||||
this.logAuthenticationAttempt(client, true, undefined, payload.sub);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
user: { id: payload.sub, sub: payload.sub },
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = this.getAuthErrorMessage(error);
|
||||
this.logAuthenticationAttempt(client, false, errorMessage);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits authentication failure event to client and disconnects
|
||||
*
|
||||
* @param client - Socket.IO client
|
||||
* @param error - Error message to send
|
||||
*/
|
||||
handleAuthenticationFailure(client: Socket, error: string): void {
|
||||
client.emit(WEBSOCKET_EVENTS.UNAUTHORIZED, {
|
||||
message: error,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Graceful disconnect with delay to ensure message is received
|
||||
setTimeout(() => {
|
||||
client.disconnect(true);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits successful authentication event to client
|
||||
*
|
||||
* @param client - Socket.IO client
|
||||
* @param user - Authenticated user payload
|
||||
*/
|
||||
emitAuthenticationSuccess(client: Socket, user: { id: string; sub: string }): void {
|
||||
client.emit(WEBSOCKET_EVENTS.AUTHENTICATED, {
|
||||
message: WebSocketMessage.AUTHENTICATED,
|
||||
userId: user.id,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates error context object for logging and monitoring
|
||||
*
|
||||
* @param client - Socket.IO client
|
||||
* @param event - Event name (optional)
|
||||
* @param data - Event data (optional)
|
||||
* @returns WebSocket error context
|
||||
*/
|
||||
createErrorContext(client: Socket, event?: WEBSOCKET_EVENTS, data?: any): WebSocketErrorContext {
|
||||
return {
|
||||
clientId: client.id,
|
||||
userId: client.data?.user?.id,
|
||||
sessionId: client.data?.sessionId,
|
||||
event,
|
||||
data,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if client is authenticated
|
||||
*
|
||||
* @param client - Socket.IO client to validate
|
||||
* @returns True if client has valid user data
|
||||
*/
|
||||
isClientAuthenticated(client: Socket): boolean {
|
||||
return !!client.data?.user?.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets user ID from authenticated client
|
||||
*
|
||||
* @param client - Socket.IO client
|
||||
* @returns User ID or undefined if not authenticated
|
||||
*/
|
||||
getUserId(client: Socket): string | undefined {
|
||||
return client.data?.user?.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs authentication attempts with structured data
|
||||
*/
|
||||
private logAuthenticationAttempt(client: Socket, success: boolean, error?: string, userId?: string): void {
|
||||
const logData = {
|
||||
clientId: client.id,
|
||||
clientIP: client.handshake.address,
|
||||
success,
|
||||
userId,
|
||||
error,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (success) {
|
||||
this.logger.info(`WebSocket authentication successful`, logData);
|
||||
} else {
|
||||
this.logger.warn(`WebSocket authentication failed`, logData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps JWT errors to user-friendly messages
|
||||
*/
|
||||
private getAuthErrorMessage(error: any): string {
|
||||
if (error instanceof jwtExpiredErr || error?.name === "TokenExpiredError") {
|
||||
return WebSocketMessage.TOKEN_EXPIRED;
|
||||
}
|
||||
|
||||
if (error?.name === "JsonWebTokenError") {
|
||||
return WebSocketMessage.INVALID_TOKEN;
|
||||
}
|
||||
|
||||
if (error?.name === "NotBeforeError") {
|
||||
return WebSocketMessage.INVALID_TOKEN;
|
||||
}
|
||||
|
||||
return WebSocketMessage.INVALID_TOKEN;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user