chat update

This commit is contained in:
morteza-mortezai
2025-11-30 14:46:22 +03:30
parent 9a8515d7ab
commit 7aa54a445b
4 changed files with 23 additions and 285 deletions
-2
View File
@@ -58,7 +58,6 @@ import { ChatbotGateway } from "../modules/chatbot/chatbot.gateway";
import { ChatbotService } from "../modules/chatbot/providers/chatbot.service"; import { ChatbotService } from "../modules/chatbot/providers/chatbot.service";
import { DataContextService } from "../modules/chatbot/providers/data-context.service"; import { DataContextService } from "../modules/chatbot/providers/data-context.service";
import { LLMService } from "../modules/chatbot/providers/llm.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 { ContactUsRepo, CreateContactUsRepo } from "../modules/contact-us/contactUs.repository";
import { ContactUsService } from "../modules/contact-us/contactUs.service"; import { ContactUsService } from "../modules/contact-us/contactUs.service";
import { CouponRepo, CouponUsageRepo, createCouponRepo, createCouponUsageRepo } from "../modules/Coupon/coupon.repository"; 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<ChatbotService>(IOCTYPES.ChatbotService).to(ChatbotService).inSingletonScope();
bind<LLMService>(IOCTYPES.ChatbotLLMService).to(LLMService).inSingletonScope(); bind<LLMService>(IOCTYPES.ChatbotLLMService).to(LLMService).inSingletonScope();
bind<DataContextService>(IOCTYPES.ChatbotDataContextService).to(DataContextService).inSingletonScope(); bind<DataContextService>(IOCTYPES.ChatbotDataContextService).to(DataContextService).inSingletonScope();
bind<WebSocketAuthService>(IOCTYPES.ChatbotWebSocketAuthService).to(WebSocketAuthService).inSingletonScope();
bind<ChatbotGateway>(IOCTYPES.ChatbotGateway).to(ChatbotGateway).inSingletonScope(); bind<ChatbotGateway>(IOCTYPES.ChatbotGateway).to(ChatbotGateway).inSingletonScope();
// #endregion // #endregion
// #region repository // #region repository
+14 -31
View File
@@ -2,26 +2,22 @@ import { Server as HttpServer } from "http";
import { inject, injectable } from "inversify"; import { inject, injectable } from "inversify";
import { Server, Socket } from "socket.io"; 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 * 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() @injectable()
export class ChatbotGateway { export class ChatbotGateway {
private io: Server; private io: Server;
private readonly logger: Logger; private readonly logger: Logger;
private readonly CHATBOT_ULID_COOKIE_NAME = "chatbot_session_id"; private readonly CHATBOT_ULID_COOKIE_NAME = "chatbot_session_id";
constructor( constructor(@inject(IOCTYPES.ChatbotService) private chatbotService: ChatbotService) {
@inject(IOCTYPES.ChatbotService) private chatbotService: ChatbotService,
@inject(IOCTYPES.ChatbotWebSocketAuthService) private wsAuthService: WebSocketAuthService,
) {
this.logger = new Logger("ChatbotGateway"); this.logger = new Logger("ChatbotGateway");
} }
@@ -43,28 +39,15 @@ export class ChatbotGateway {
private async handleConnection(socket: Socket): Promise<void> { private async handleConnection(socket: Socket): Promise<void> {
try { try {
// Try to authenticate if token is provided
const token = socket.handshake.query.token as string;
let ulid: string | undefined; let ulid: string | undefined;
if (token) { // Anonymous connection - generate or get ULID from cookie
// Authenticated connection const cookieHeader = socket.handshake.headers.cookie;
const authResult = await this.wsAuthService.authenticateClient(socket as AuthenticatedSocket); ulid = this.extractUlidFromCookie(cookieHeader);
if (!authResult.success) { if (!ulid) {
this.wsAuthService.handleAuthenticationFailure(socket, authResult.error || "Authentication failed"); ulid = ulidLib.ulid();
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 } };
} }
socket.data = { user: { id: ulid, sub: ulid } };
this.logger.info(`Client connected: ${socket.id}, ULID: ${ulid}`); this.logger.info(`Client connected: ${socket.id}, ULID: ${ulid}`);
@@ -1,7 +1,8 @@
import { injectable } from "inversify"; import { injectable } from "inversify";
import { IChatContext } from "../interfaces/chatbot.interface";
import { Logger } from "../../../core/logging/logger"; 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 * Service for retrieving relevant context data from the database
@@ -20,25 +21,14 @@ export class DataContextService {
const sources: string[] = []; const sources: string[] = [];
try { try {
// Analyze message to determine what data to fetch // Fetch all non-deleted products
// const keywords = this._extractKeywords(_message); // TODO: Use when implementing keyword-based filtering 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 products.forEach((product) => {
// For now, this returns empty data - you can extend this to fetch: 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)}` : ""}`;
// - Product data from ProductModel relevantData.push(productInfo);
// - Category data from CategoryModel sources.push("Products Database");
// - 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");
// });
// }
return { data: relevantData, sources }; return { data: relevantData, sources };
} catch (error) { } catch (error) {
@@ -46,34 +36,4 @@ export class DataContextService {
return { data: [], sources: [] }; 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;
}
}