From 9659007a06290315d8c4f6b1288e87a2434a728b Mon Sep 17 00:00:00 2001 From: hanif salafi Date: Mon, 22 Sep 2025 20:05:40 +0700 Subject: [PATCH] feat: add chats features --- CHAT_HISTORY_IMPLEMENTATION.md | 206 ---- app/database/entity/chat_messages.entity.go | 29 +- .../entity/chat_messages_new.entity.go | 14 - .../entity/chat_participants.entity.go | 21 + app/database/entity/chat_sessions.entity.go | 21 +- app/database/index.database.go | 2 + app/module/chat/chat.module.go | 70 ++ app/module/chat/controller/chat.controller.go | 474 +++++++++ app/module/chat/controller/controller.go | 13 + app/module/chat/mapper/chat.mapper.go | 105 ++ app/module/chat/repository/chat.repository.go | 272 ++++++ app/module/chat/request/chat.request.go | 122 +++ app/module/chat/response/chat.response.go | 56 ++ app/module/chat/service/chat.service.go | 418 ++++++++ .../mapper/communications.mapper.go | 12 +- .../repository/communications.repository.go | 2 +- .../request/communications.request.go | 46 +- app/router/api.go | 5 + docs/swagger/docs.go | 909 ++++++++++++++++++ docs/swagger/swagger.json | 909 ++++++++++++++++++ docs/swagger/swagger.yaml | 596 ++++++++++++ main.go | 2 + migrations/001_create_chat_history_tables.go | 28 - migrations/001_create_chat_history_tables.sql | 53 - 24 files changed, 4029 insertions(+), 356 deletions(-) delete mode 100644 CHAT_HISTORY_IMPLEMENTATION.md delete mode 100644 app/database/entity/chat_messages_new.entity.go create mode 100644 app/database/entity/chat_participants.entity.go create mode 100644 app/module/chat/chat.module.go create mode 100644 app/module/chat/controller/chat.controller.go create mode 100644 app/module/chat/controller/controller.go create mode 100644 app/module/chat/mapper/chat.mapper.go create mode 100644 app/module/chat/repository/chat.repository.go create mode 100644 app/module/chat/request/chat.request.go create mode 100644 app/module/chat/response/chat.response.go create mode 100644 app/module/chat/service/chat.service.go delete mode 100644 migrations/001_create_chat_history_tables.go delete mode 100644 migrations/001_create_chat_history_tables.sql diff --git a/CHAT_HISTORY_IMPLEMENTATION.md b/CHAT_HISTORY_IMPLEMENTATION.md deleted file mode 100644 index ce1ad15..0000000 --- a/CHAT_HISTORY_IMPLEMENTATION.md +++ /dev/null @@ -1,206 +0,0 @@ -# Chat History Implementation - -This document describes the implementation of the chat history functionality based on the old web API code from the `plan/old` folder. - -## Overview - -The implementation includes: -1. **New Chat History Module**: A complete module for managing chat sessions and messages -2. **Updated AI Chat Module**: Enhanced with proper routing -3. **Database Entities**: New entities for chat sessions and messages -4. **API Endpoints**: RESTful endpoints for all chat history operations - -## New Files Created - -### Database Entities -- `app/database/entity/chat_sessions.entity.go` - Chat sessions entity -- `app/database/entity/chat_messages_new.entity.go` - Chat messages entity (renamed to avoid conflicts) - -### Chat History Module -- `app/module/chat_history/chat_history.module.go` - Module definition -- `app/module/chat_history/chat_history.router.go` - Router configuration -- `app/module/chat_history/request/chat_history.request.go` - Request DTOs -- `app/module/chat_history/response/chat_history.response.go` - Response DTOs -- `app/module/chat_history/mapper/chat_history.mapper.go` - Entity to response mappers -- `app/module/chat_history/repository/chat_history.repository.go` - Data access layer -- `app/module/chat_history/service/chat_history.service.go` - Business logic layer -- `app/module/chat_history/controller/chat_history.controller.go` - API controller - -### AI Chat Module Updates -- `app/module/ai_chat/ai_chat.router.go` - Router configuration for AI chat - -### Database Migration -- `migrations/001_create_chat_history_tables.sql` - SQL migration script -- `migrations/001_create_chat_history_tables.go` - Go migration script -- `scripts/migrate.go` - Migration runner script - -## API Endpoints - -### Chat History Endpoints - -#### Sessions -- `GET /chat-history/sessions` - Get user's chat sessions -- `GET /chat-history/sessions/{sessionId}` - Get specific session with messages -- `POST /chat-history/sessions` - Create new session -- `PUT /chat-history/sessions/{sessionId}` - Update session -- `DELETE /chat-history/sessions/{sessionId}` - Delete session - -#### Messages -- `GET /chat-history/sessions/{sessionId}/messages` - Get session messages -- `POST /chat-history/sessions/{sessionId}/messages` - Create message -- `PUT /chat-history/messages/{messageId}` - Update message -- `DELETE /chat-history/messages/{messageId}` - Delete message - -#### Combined Operations -- `POST /chat-history/save` - Save complete chat history (sessions + messages) - -### AI Chat Endpoints (Updated) -- `GET /ai-chat/sessions` - Get user's AI chat sessions -- `GET /ai-chat/sessions/{id}` - Get specific AI chat session -- `POST /ai-chat/sessions` - Create AI chat session -- `PUT /ai-chat/sessions/{id}` - Update AI chat session -- `DELETE /ai-chat/sessions/{id}` - Delete AI chat session -- `GET /ai-chat/sessions/{sessionId}/messages` - Get AI chat messages -- `POST /ai-chat/sessions/{sessionId}/messages` - Send AI chat message -- `PUT /ai-chat/sessions/{sessionId}/messages/{messageId}` - Update AI chat message -- `DELETE /ai-chat/sessions/{sessionId}/messages/{messageId}` - Delete AI chat message -- `GET /ai-chat/logs` - Get user's AI chat logs -- `GET /ai-chat/logs/{id}` - Get specific AI chat log - -## Database Schema - -### chat_sessions Table -```sql -CREATE TABLE chat_sessions ( - id SERIAL PRIMARY KEY, - session_id VARCHAR(255) NOT NULL UNIQUE, - user_id INTEGER NOT NULL, - agent_id VARCHAR(255) NOT NULL, - title VARCHAR(255) NOT NULL DEFAULT 'Chat Session', - message_count INTEGER DEFAULT 0, - status VARCHAR(50) DEFAULT 'active', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); -``` - -### chat_messages_new Table -```sql -CREATE TABLE chat_messages_new ( - id SERIAL PRIMARY KEY, - session_id VARCHAR(255) NOT NULL, - message_type VARCHAR(50) NOT NULL CHECK (message_type IN ('user', 'assistant')), - content TEXT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); -``` - -## Key Features Implemented - -### 1. Session Management -- Create, read, update, delete chat sessions -- Session-based architecture with unique session IDs -- User ownership validation -- Message count tracking - -### 2. Message Management -- Create, read, update, delete messages within sessions -- Support for user and assistant message types -- Automatic message count updates -- Cascade deletion when sessions are deleted - -### 3. Combined Operations -- Save complete chat history in one operation -- Update existing sessions or create new ones -- Bulk message operations - -### 4. Security -- User authentication required for all operations -- User ownership validation for all resources -- Proper error handling and validation - -## Migration Instructions - -### 1. Run Database Migration -```bash -# Option 1: Run the Go migration script -go run scripts/migrate.go - -# Option 2: Run SQL migration manually -psql -d your_database -f migrations/001_create_chat_history_tables.sql -``` - -### 2. Update Application -The application has been updated to include: -- New modules in `main.go` -- Updated router configuration -- New API endpoints - -### 3. Test the Implementation -```bash -# Start the application -go run main.go - -# Test endpoints -curl -X GET "http://localhost:8080/chat-history/sessions" \ - -H "Authorization: Bearer YOUR_TOKEN" - -curl -X POST "http://localhost:8080/chat-history/save" \ - -H "Authorization: Bearer YOUR_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "user_id": 1, - "agent_id": "agent-123", - "session_id": "session-456", - "title": "Test Chat", - "messages": [ - {"type": "user", "content": "Hello"}, - {"type": "assistant", "content": "Hi there!"} - ] - }' -``` - -## Differences from Old Code - -### Architecture Changes -- **Language**: Converted from TypeScript/JavaScript to Go -- **Framework**: Converted from Next.js API routes to Fiber (Go web framework) -- **Database**: Converted from SQLite to PostgreSQL (via GORM) -- **Structure**: Implemented clean architecture with repository, service, and controller layers - -### API Changes -- **Authentication**: Added Bearer token authentication -- **Validation**: Added comprehensive request validation -- **Error Handling**: Standardized error responses -- **Pagination**: Added pagination support for list endpoints - -### Database Changes -- **Schema**: Adapted SQLite schema to PostgreSQL -- **Relationships**: Added proper foreign key relationships -- **Indexes**: Added performance indexes -- **Constraints**: Added data validation constraints - -## Future Enhancements - -1. **Real-time Features**: Add WebSocket support for real-time chat -2. **File Attachments**: Support for file uploads in messages -3. **Message Search**: Full-text search across messages -4. **Analytics**: Chat analytics and reporting -5. **Export**: Export chat history to various formats -6. **Archiving**: Automatic archiving of old sessions - -## Troubleshooting - -### Common Issues - -1. **Migration Errors**: Ensure database connection is properly configured -2. **Authentication Errors**: Verify Bearer token is valid and user exists -3. **Validation Errors**: Check request payload matches expected schema -4. **Permission Errors**: Ensure user owns the resource being accessed - -### Debug Mode -Enable debug logging by setting the log level to debug in your configuration. - -## Support - -For issues or questions regarding this implementation, please refer to the application logs or contact the development team. diff --git a/app/database/entity/chat_messages.entity.go b/app/database/entity/chat_messages.entity.go index 9427e48..d33732d 100644 --- a/app/database/entity/chat_messages.entity.go +++ b/app/database/entity/chat_messages.entity.go @@ -6,18 +6,19 @@ import ( ) type ChatMessages struct { - ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"` - ConversationID uint `json:"conversation_id" gorm:"type:int4;not null;index"` - SenderID uint `json:"sender_id" gorm:"type:int4;not null;index"` - MessageText *string `json:"message_text" gorm:"type:text"` - MessageType string `json:"message_type" gorm:"type:varchar;not null;default:'text'"` - FileURL *string `json:"file_url" gorm:"type:varchar"` - FileName *string `json:"file_name" gorm:"type:varchar"` - FileSize *int64 `json:"file_size" gorm:"type:bigint"` - IsRead bool `json:"is_read" gorm:"default:false"` - IsActive bool `json:"is_active" gorm:"type:bool;default:true"` - CreatedAt time.Time `json:"created_at" gorm:"default:now()"` - UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` - // Conversation *Conversations `json:"conversation" gorm:"foreignKey:ConversationID;references:ID"` - Sender *users.Users `json:"sender" gorm:"foreignKey:SenderID;references:ID"` + ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"` + ChatSessionID uint `json:"chat_session_id" gorm:"type:int4;not null;index"` + SenderID uint `json:"sender_id" gorm:"type:int4;not null;index"` + Message string `json:"message" gorm:"type:text;not null"` + MessageType string `json:"message_type" gorm:"type:varchar(20);not null;default:'text';check:message_type IN ('text', 'image', 'file', 'user', 'assistant')"` // 'text', 'image', 'file', 'user', 'assistant' + IsEdited bool `json:"is_edited" gorm:"default:false"` + EditedAt *time.Time `json:"edited_at"` + IsDeleted bool `json:"is_deleted" gorm:"default:false"` + DeletedAt *time.Time `json:"deleted_at"` + CreatedAt time.Time `json:"created_at" gorm:"default:now()"` + UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` + + // Relationships + ChatSession *ChatSessions `json:"chat_session" gorm:"foreignKey:ChatSessionID;references:ID"` + Sender *users.Users `json:"sender" gorm:"foreignKey:SenderID;references:ID"` } diff --git a/app/database/entity/chat_messages_new.entity.go b/app/database/entity/chat_messages_new.entity.go deleted file mode 100644 index 06b9afe..0000000 --- a/app/database/entity/chat_messages_new.entity.go +++ /dev/null @@ -1,14 +0,0 @@ -package entity - -import ( - "time" -) - -type ChatMessagesNew struct { - ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"` - SessionID string `json:"session_id" gorm:"type:varchar;not null;index"` - MessageType string `json:"message_type" gorm:"type:varchar;not null;check:message_type IN ('user', 'assistant')"` - Content string `json:"content" gorm:"type:text;not null"` - CreatedAt time.Time `json:"created_at" gorm:"default:now()"` - Session *ChatSessions `json:"session" gorm:"foreignKey:SessionID;references:SessionID"` -} diff --git a/app/database/entity/chat_participants.entity.go b/app/database/entity/chat_participants.entity.go new file mode 100644 index 0000000..80335ad --- /dev/null +++ b/app/database/entity/chat_participants.entity.go @@ -0,0 +1,21 @@ +package entity + +import ( + "narasi-ahli-be/app/database/entity/users" + "time" +) + +type ChatParticipants struct { + ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"` + ChatSessionID uint `json:"chat_session_id" gorm:"type:int4;not null;index"` + UserID uint `json:"user_id" gorm:"type:int4;not null;index"` + JoinedAt time.Time `json:"joined_at" gorm:"default:now()"` + LeftAt *time.Time `json:"left_at"` + IsActive bool `json:"is_active" gorm:"default:true"` + CreatedAt time.Time `json:"created_at" gorm:"default:now()"` + UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` + + // Relationships + ChatSession *ChatSessions `json:"chat_session" gorm:"foreignKey:ChatSessionID;references:ID"` + User *users.Users `json:"user" gorm:"foreignKey:UserID;references:ID"` +} diff --git a/app/database/entity/chat_sessions.entity.go b/app/database/entity/chat_sessions.entity.go index 962bbf6..e7aa3ee 100644 --- a/app/database/entity/chat_sessions.entity.go +++ b/app/database/entity/chat_sessions.entity.go @@ -6,14 +6,15 @@ import ( ) type ChatSessions struct { - ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"` - SessionID string `json:"session_id" gorm:"type:varchar;not null;unique;index"` - UserID uint `json:"user_id" gorm:"type:int4;not null;index"` - AgentID string `json:"agent_id" gorm:"type:varchar;not null"` - Title string `json:"title" gorm:"type:varchar;not null"` - MessageCount int `json:"message_count" gorm:"type:int4;default:0"` - IsActive bool `json:"is_active" gorm:"type:bool;default:true"` - CreatedAt time.Time `json:"created_at" gorm:"default:now()"` - UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` - User *users.Users `json:"user" gorm:"foreignKey:UserID;references:ID"` + ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"` + Name *string `json:"name" gorm:"type:varchar(255)"` // null for personal chat, filled for group chat + Type string `json:"type" gorm:"type:varchar(20);not null;default:'personal'"` // 'personal' or 'group' + CreatedBy uint `json:"created_by" gorm:"type:int4;not null;index"` + CreatedAt time.Time `json:"created_at" gorm:"default:now()"` + UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` + + // Relationships + Creator *users.Users `json:"creator" gorm:"foreignKey:CreatedBy;references:ID"` + Participants []*ChatParticipants `json:"participants" gorm:"foreignKey:ChatSessionID;references:ID"` + Messages []*ChatMessages `json:"messages" gorm:"foreignKey:ChatSessionID;references:ID"` } diff --git a/app/database/index.database.go b/app/database/index.database.go index 2f28364..957ec2e 100644 --- a/app/database/index.database.go +++ b/app/database/index.database.go @@ -122,6 +122,8 @@ func Models() []interface{} { entity.ResearchJournals{}, entity.Conversations{}, entity.ChatMessages{}, + entity.ChatParticipants{}, + entity.ChatSessions{}, entity.AIChatSessions{}, entity.AIChatMessages{}, entity.AIChatLogs{}, diff --git a/app/module/chat/chat.module.go b/app/module/chat/chat.module.go new file mode 100644 index 0000000..8232239 --- /dev/null +++ b/app/module/chat/chat.module.go @@ -0,0 +1,70 @@ +package chat + +import ( + "narasi-ahli-be/app/module/chat/controller" + "narasi-ahli-be/app/module/chat/repository" + "narasi-ahli-be/app/module/chat/service" + + "github.com/gofiber/fiber/v2" + "go.uber.org/fx" +) + +// struct of ChatRouter +type ChatRouter struct { + App fiber.Router + Controller *controller.Controller +} + +// register bulky of Chat module +var NewChatModule = fx.Options( + // register repository of Chat module + fx.Provide(repository.NewChatRepository), + + // register service of Chat module + fx.Provide(service.NewChatService), + + // register controller of Chat module + fx.Provide(controller.NewController), + + // register router of Chat module + fx.Provide(NewChatRouter), +) + +// init ChatRouter +func NewChatRouter(fiber *fiber.App, controller *controller.Controller) *ChatRouter { + return &ChatRouter{ + App: fiber, + Controller: controller, + } +} + +// register routes of Chat module +func (_i *ChatRouter) RegisterChatRoutes() { + // define controllers + chatController := _i.Controller.Chat + + // define routes + _i.App.Route("/chat", func(router fiber.Router) { + // Chat Session routes + router.Route("/sessions", func(sessionRouter fiber.Router) { + sessionRouter.Get("/", chatController.GetAllChatSessions) + sessionRouter.Get("/:id", chatController.GetChatSessionByID) + sessionRouter.Post("/", chatController.CreateChatSession) + sessionRouter.Put("/:id", chatController.UpdateChatSession) + sessionRouter.Delete("/:id", chatController.DeleteChatSession) + + // Chat Participant routes + sessionRouter.Post("/:chatSessionId/participants", chatController.AddParticipantToChat) + sessionRouter.Delete("/:chatSessionId/participants", chatController.RemoveParticipantFromChat) + }) + + // Chat Message routes + router.Route("/messages", func(messageRouter fiber.Router) { + messageRouter.Get("/", chatController.GetAllChatMessages) + messageRouter.Get("/:id", chatController.GetChatMessageByID) + messageRouter.Post("/", chatController.CreateChatMessage) + messageRouter.Put("/:id", chatController.UpdateChatMessage) + messageRouter.Delete("/:id", chatController.DeleteChatMessage) + }) + }) +} diff --git a/app/module/chat/controller/chat.controller.go b/app/module/chat/controller/chat.controller.go new file mode 100644 index 0000000..c4508fd --- /dev/null +++ b/app/module/chat/controller/chat.controller.go @@ -0,0 +1,474 @@ +package controller + +import ( + "narasi-ahli-be/app/module/chat/request" + "narasi-ahli-be/app/module/chat/service" + "narasi-ahli-be/utils/paginator" + utilRes "narasi-ahli-be/utils/response" + utilVal "narasi-ahli-be/utils/validator" + "strconv" + + "github.com/gofiber/fiber/v2" +) + +type chatController struct { + chatService service.ChatService +} + +type ChatController interface { + // Chat Session endpoints + GetAllChatSessions(c *fiber.Ctx) error + GetChatSessionByID(c *fiber.Ctx) error + CreateChatSession(c *fiber.Ctx) error + UpdateChatSession(c *fiber.Ctx) error + DeleteChatSession(c *fiber.Ctx) error + + // Chat Message endpoints + GetAllChatMessages(c *fiber.Ctx) error + GetChatMessageByID(c *fiber.Ctx) error + CreateChatMessage(c *fiber.Ctx) error + UpdateChatMessage(c *fiber.Ctx) error + DeleteChatMessage(c *fiber.Ctx) error + + // Chat Participant endpoints + AddParticipantToChat(c *fiber.Ctx) error + RemoveParticipantFromChat(c *fiber.Ctx) error +} + +func NewChatController(chatService service.ChatService) ChatController { + return &chatController{ + chatService: chatService, + } +} + +// GetAllChatSessions - Get all chat sessions for a user +// @Summary Get all chat sessions +// @Description API for getting all chat sessions for authenticated user +// @Tags Chat +// @Security Bearer +// @Param Authorization header string false "Insert your access token" default(Bearer ) +// @Param type query string false "Chat type (personal or group)" +// @Param req query paginator.Pagination false "pagination parameters" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /chat/sessions [get] +func (_i *chatController) GetAllChatSessions(c *fiber.Ctx) error { + paginate, err := paginator.Paginate(c) + if err != nil { + return err + } + + authToken := c.Get("Authorization") + + reqContext := request.ChatSessionQueryRequestContext{ + Type: c.Query("type"), + } + req := reqContext.ToParamRequest() + req.Pagination = paginate + + chatSessions, paging, err := _i.chatService.GetAllChatSessions(authToken, req) + if err != nil { + return err + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Chat sessions successfully retrieved"}, + Data: chatSessions, + Meta: paging, + }) +} + +// GetChatSessionByID - Get one chat session +// @Summary Get one chat session +// @Description API for getting one chat session +// @Tags Chat +// @Security Bearer +// @Param Authorization header string false "Insert your access token" default(Bearer ) +// @Param id path int true "Chat Session ID" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /chat/sessions/{id} [get] +func (_i *chatController) GetChatSessionByID(c *fiber.Ctx) error { + id, err := strconv.ParseUint(c.Params("id"), 10, 0) + if err != nil { + return err + } + + authToken := c.Get("Authorization") + + chatSession, err := _i.chatService.GetChatSessionByID(authToken, uint(id)) + if err != nil { + return err + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Chat session successfully retrieved"}, + Data: chatSession, + }) +} + +// CreateChatSession - Create chat session +// @Summary Create chat session +// @Description API for creating a new chat session (personal or group) +// @Tags Chat +// @Security Bearer +// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token" +// @Param Authorization header string false "Insert your access token" default(Bearer ) +// @Param payload body request.ChatSessionCreateRequest true "Required payload" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /chat/sessions [post] +func (_i *chatController) CreateChatSession(c *fiber.Ctx) error { + authToken := c.Get("Authorization") + + req := new(request.ChatSessionCreateRequest) + if err := utilVal.ParseAndValidate(c, req); err != nil { + return err + } + + dataResult, err := _i.chatService.CreateChatSession(authToken, *req) + if err != nil { + return err + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Chat session successfully created"}, + Data: dataResult, + }) +} + +// UpdateChatSession - Update chat session +// @Summary Update chat session +// @Description API for updating chat session (only creator can update) +// @Tags Chat +// @Security Bearer +// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token" +// @Param Authorization header string false "Insert your access token" default(Bearer ) +// @Param id path int true "Chat Session ID" +// @Param payload body request.ChatSessionUpdateRequest true "Required payload" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /chat/sessions/{id} [put] +func (_i *chatController) UpdateChatSession(c *fiber.Ctx) error { + id, err := strconv.ParseUint(c.Params("id"), 10, 0) + if err != nil { + return err + } + + authToken := c.Get("Authorization") + + req := new(request.ChatSessionUpdateRequest) + if err := utilVal.ParseAndValidate(c, req); err != nil { + return err + } + + err = _i.chatService.UpdateChatSession(authToken, uint(id), *req) + if err != nil { + return err + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Chat session successfully updated"}, + }) +} + +// DeleteChatSession - Delete chat session +// @Summary Delete chat session +// @Description API for deleting chat session (only creator can delete) +// @Tags Chat +// @Security Bearer +// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token" +// @Param Authorization header string false "Insert your access token" default(Bearer ) +// @Param id path int true "Chat Session ID" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /chat/sessions/{id} [delete] +func (_i *chatController) DeleteChatSession(c *fiber.Ctx) error { + id, err := strconv.ParseUint(c.Params("id"), 10, 0) + if err != nil { + return err + } + + authToken := c.Get("Authorization") + + err = _i.chatService.DeleteChatSession(authToken, uint(id)) + if err != nil { + return err + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Chat session successfully deleted"}, + }) +} + +// GetAllChatMessages - Get all messages in a chat session +// @Summary Get all chat messages +// @Description API for getting all messages in a specific chat session +// @Tags Chat +// @Security Bearer +// @Param Authorization header string false "Insert your access token" default(Bearer ) +// @Param chatSessionId query uint true "Chat Session ID" +// @Param req query paginator.Pagination false "pagination parameters" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /chat/messages [get] +func (_i *chatController) GetAllChatMessages(c *fiber.Ctx) error { + paginate, err := paginator.Paginate(c) + if err != nil { + return err + } + + authToken := c.Get("Authorization") + + reqContext := request.ChatMessageQueryRequestContext{ + ChatSessionID: c.Query("chatSessionId"), + } + req := reqContext.ToParamRequest() + req.Pagination = paginate + + chatMessages, paging, err := _i.chatService.GetAllChatMessages(authToken, req) + if err != nil { + return err + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Chat messages successfully retrieved"}, + Data: chatMessages, + Meta: paging, + }) +} + +// GetChatMessageByID - Get one chat message +// @Summary Get one chat message +// @Description API for getting one chat message +// @Tags Chat +// @Security Bearer +// @Param Authorization header string false "Insert your access token" default(Bearer ) +// @Param id path int true "Chat Message ID" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /chat/messages/{id} [get] +func (_i *chatController) GetChatMessageByID(c *fiber.Ctx) error { + id, err := strconv.ParseUint(c.Params("id"), 10, 0) + if err != nil { + return err + } + + authToken := c.Get("Authorization") + + chatMessage, err := _i.chatService.GetChatMessageByID(authToken, uint(id)) + if err != nil { + return err + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Chat message successfully retrieved"}, + Data: chatMessage, + }) +} + +// CreateChatMessage - Create chat message +// @Summary Create chat message +// @Description API for creating a new chat message +// @Tags Chat +// @Security Bearer +// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token" +// @Param Authorization header string false "Insert your access token" default(Bearer ) +// @Param payload body request.ChatMessageCreateRequest true "Required payload" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /chat/messages [post] +func (_i *chatController) CreateChatMessage(c *fiber.Ctx) error { + authToken := c.Get("Authorization") + + req := new(request.ChatMessageCreateRequest) + if err := utilVal.ParseAndValidate(c, req); err != nil { + return err + } + + dataResult, err := _i.chatService.CreateChatMessage(authToken, *req) + if err != nil { + return err + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Chat message successfully created"}, + Data: dataResult, + }) +} + +// UpdateChatMessage - Update chat message +// @Summary Update chat message +// @Description API for updating chat message (only sender can update) +// @Tags Chat +// @Security Bearer +// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token" +// @Param Authorization header string false "Insert your access token" default(Bearer ) +// @Param id path int true "Chat Message ID" +// @Param payload body request.ChatMessageUpdateRequest true "Required payload" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /chat/messages/{id} [put] +func (_i *chatController) UpdateChatMessage(c *fiber.Ctx) error { + id, err := strconv.ParseUint(c.Params("id"), 10, 0) + if err != nil { + return err + } + + authToken := c.Get("Authorization") + + req := new(request.ChatMessageUpdateRequest) + if err := utilVal.ParseAndValidate(c, req); err != nil { + return err + } + + err = _i.chatService.UpdateChatMessage(authToken, uint(id), *req) + if err != nil { + return err + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Chat message successfully updated"}, + }) +} + +// DeleteChatMessage - Delete chat message +// @Summary Delete chat message +// @Description API for deleting chat message (only sender can delete) +// @Tags Chat +// @Security Bearer +// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token" +// @Param Authorization header string false "Insert your access token" default(Bearer ) +// @Param id path int true "Chat Message ID" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /chat/messages/{id} [delete] +func (_i *chatController) DeleteChatMessage(c *fiber.Ctx) error { + id, err := strconv.ParseUint(c.Params("id"), 10, 0) + if err != nil { + return err + } + + authToken := c.Get("Authorization") + + err = _i.chatService.DeleteChatMessage(authToken, uint(id)) + if err != nil { + return err + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Chat message successfully deleted"}, + }) +} + +// AddParticipantToChat - Add participant to chat session +// @Summary Add participant to chat session +// @Description API for adding a participant to a chat session (only creator can add) +// @Tags Chat +// @Security Bearer +// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token" +// @Param Authorization header string false "Insert your access token" default(Bearer ) +// @Param chatSessionId path int true "Chat Session ID" +// @Param participantUserId query uint true "Participant User ID" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /chat/sessions/{chatSessionId}/participants [post] +func (_i *chatController) AddParticipantToChat(c *fiber.Ctx) error { + chatSessionId, err := strconv.ParseUint(c.Params("chatSessionId"), 10, 0) + if err != nil { + return err + } + + authToken := c.Get("Authorization") + + participantUserId, err := strconv.ParseUint(c.Query("participantUserId"), 10, 0) + if err != nil { + return utilRes.Resp(c, utilRes.Response{ + Success: false, + Messages: utilRes.Messages{"participantUserId parameter is required and must be a valid number"}, + }) + } + + err = _i.chatService.AddParticipantToChat(authToken, uint(chatSessionId), uint(participantUserId)) + if err != nil { + return err + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Participant successfully added to chat session"}, + }) +} + +// RemoveParticipantFromChat - Remove participant from chat session +// @Summary Remove participant from chat session +// @Description API for removing a participant from a chat session (creator can remove anyone, user can remove themselves) +// @Tags Chat +// @Security Bearer +// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token" +// @Param Authorization header string false "Insert your access token" default(Bearer ) +// @Param chatSessionId path int true "Chat Session ID" +// @Param participantUserId query uint true "Participant User ID" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /chat/sessions/{chatSessionId}/participants [delete] +func (_i *chatController) RemoveParticipantFromChat(c *fiber.Ctx) error { + chatSessionId, err := strconv.ParseUint(c.Params("chatSessionId"), 10, 0) + if err != nil { + return err + } + + authToken := c.Get("Authorization") + + participantUserId, err := strconv.ParseUint(c.Query("participantUserId"), 10, 0) + if err != nil { + return utilRes.Resp(c, utilRes.Response{ + Success: false, + Messages: utilRes.Messages{"participantUserId parameter is required and must be a valid number"}, + }) + } + + err = _i.chatService.RemoveParticipantFromChat(authToken, uint(chatSessionId), uint(participantUserId)) + if err != nil { + return err + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Participant successfully removed from chat session"}, + }) +} diff --git a/app/module/chat/controller/controller.go b/app/module/chat/controller/controller.go new file mode 100644 index 0000000..7fd0660 --- /dev/null +++ b/app/module/chat/controller/controller.go @@ -0,0 +1,13 @@ +package controller + +import "narasi-ahli-be/app/module/chat/service" + +type Controller struct { + Chat ChatController +} + +func NewController(ChatService service.ChatService) *Controller { + return &Controller{ + Chat: NewChatController(ChatService), + } +} diff --git a/app/module/chat/mapper/chat.mapper.go b/app/module/chat/mapper/chat.mapper.go new file mode 100644 index 0000000..acf48b0 --- /dev/null +++ b/app/module/chat/mapper/chat.mapper.go @@ -0,0 +1,105 @@ +package mapper + +import ( + "narasi-ahli-be/app/database/entity" + "narasi-ahli-be/app/module/chat/response" +) + +// Chat Session Mapper +func ChatSessionResponseMapper(chatSession *entity.ChatSessions) *response.ChatSessionResponse { + result := &response.ChatSessionResponse{ + ID: chatSession.ID, + Name: chatSession.Name, + Type: chatSession.Type, + CreatedBy: chatSession.CreatedBy, + CreatedAt: chatSession.CreatedAt, + UpdatedAt: chatSession.UpdatedAt, + } + + if chatSession.Creator != nil { + result.Creator = &response.UserBasicInfo{ + ID: chatSession.Creator.ID, + Username: chatSession.Creator.Username, + Fullname: chatSession.Creator.Fullname, + Email: chatSession.Creator.Email, + } + } + + // Map participants + if len(chatSession.Participants) > 0 { + for _, participant := range chatSession.Participants { + if participant.IsActive { + result.Participants = append(result.Participants, ChatParticipantResponseMapper(participant)) + } + } + } + + // Map last message + if len(chatSession.Messages) > 0 { + // Find the latest message that is not deleted + var lastMessage *entity.ChatMessages + for _, message := range chatSession.Messages { + if !message.IsDeleted && (lastMessage == nil || message.CreatedAt.After(lastMessage.CreatedAt)) { + lastMessage = message + } + } + if lastMessage != nil { + result.LastMessage = ChatMessageResponseMapper(lastMessage) + } + } + + return result +} + +// Chat Message Mapper +func ChatMessageResponseMapper(chatMessage *entity.ChatMessages) *response.ChatMessageResponse { + result := &response.ChatMessageResponse{ + ID: chatMessage.ID, + ChatSessionID: chatMessage.ChatSessionID, + SenderID: chatMessage.SenderID, + Message: chatMessage.Message, + MessageType: chatMessage.MessageType, + IsEdited: chatMessage.IsEdited, + EditedAt: chatMessage.EditedAt, + IsDeleted: chatMessage.IsDeleted, + DeletedAt: chatMessage.DeletedAt, + CreatedAt: chatMessage.CreatedAt, + UpdatedAt: chatMessage.UpdatedAt, + } + + if chatMessage.Sender != nil { + result.Sender = &response.UserBasicInfo{ + ID: chatMessage.Sender.ID, + Username: chatMessage.Sender.Username, + Fullname: chatMessage.Sender.Fullname, + Email: chatMessage.Sender.Email, + } + } + + return result +} + +// Chat Participant Mapper +func ChatParticipantResponseMapper(chatParticipant *entity.ChatParticipants) *response.ChatParticipantResponse { + result := &response.ChatParticipantResponse{ + ID: chatParticipant.ID, + ChatSessionID: chatParticipant.ChatSessionID, + UserID: chatParticipant.UserID, + JoinedAt: chatParticipant.JoinedAt, + LeftAt: chatParticipant.LeftAt, + IsActive: chatParticipant.IsActive, + CreatedAt: chatParticipant.CreatedAt, + UpdatedAt: chatParticipant.UpdatedAt, + } + + if chatParticipant.User != nil { + result.User = &response.UserBasicInfo{ + ID: chatParticipant.User.ID, + Username: chatParticipant.User.Username, + Fullname: chatParticipant.User.Fullname, + Email: chatParticipant.User.Email, + } + } + + return result +} diff --git a/app/module/chat/repository/chat.repository.go b/app/module/chat/repository/chat.repository.go new file mode 100644 index 0000000..fdb6f3b --- /dev/null +++ b/app/module/chat/repository/chat.repository.go @@ -0,0 +1,272 @@ +package repository + +import ( + "narasi-ahli-be/app/database" + "narasi-ahli-be/app/database/entity" + "narasi-ahli-be/app/module/chat/request" + "narasi-ahli-be/utils/paginator" +) + +type chatRepository struct { + DB *database.Database +} + +type ChatRepository interface { + // Chat Session methods + GetAllChatSessions(userId uint, req request.ChatSessionQueryRequest) (chatSessions []*entity.ChatSessions, paging paginator.Pagination, err error) + FindChatSessionByID(id uint) (chatSession *entity.ChatSessions, err error) + FindChatSessionByUserAndID(userId uint, id uint) (chatSession *entity.ChatSessions, err error) + CreateChatSession(chatSession *entity.ChatSessions) (result *entity.ChatSessions, err error) + UpdateChatSession(id uint, chatSession *entity.ChatSessions) (err error) + DeleteChatSession(id uint) (err error) + + // Chat Message methods + GetAllChatMessages(req request.ChatMessageQueryRequest) (chatMessages []*entity.ChatMessages, paging paginator.Pagination, err error) + FindChatMessageByID(id uint) (chatMessage *entity.ChatMessages, err error) + FindChatMessageByUserAndID(userId uint, id uint) (chatMessage *entity.ChatMessages, err error) + CreateChatMessage(chatMessage *entity.ChatMessages) (result *entity.ChatMessages, err error) + UpdateChatMessage(id uint, chatMessage *entity.ChatMessages) (err error) + DeleteChatMessage(id uint) (err error) + + // Chat Participant methods + CreateChatParticipant(chatParticipant *entity.ChatParticipants) (result *entity.ChatParticipants, err error) + FindChatParticipantsBySessionID(chatSessionID uint) (participants []*entity.ChatParticipants, err error) + UpdateChatParticipant(id uint, chatParticipant *entity.ChatParticipants) (err error) + DeleteChatParticipant(id uint) (err error) + + // Utility methods + FindPersonalChatSession(userId1 uint, userId2 uint) (chatSession *entity.ChatSessions, err error) + CheckUserInChatSession(userId uint, chatSessionID uint) (isParticipant bool, err error) +} + +func NewChatRepository(db *database.Database) ChatRepository { + return &chatRepository{ + DB: db, + } +} + +// Chat Session Repository Methods +func (_i *chatRepository) GetAllChatSessions(userId uint, req request.ChatSessionQueryRequest) (chatSessions []*entity.ChatSessions, paging paginator.Pagination, err error) { + // Get chat sessions where user is a participant + query := _i.DB.DB.Model(&entity.ChatSessions{}). + Joins("INNER JOIN chat_participants cp ON chat_sessions.id = cp.chat_session_id"). + Where("cp.user_id = ? AND cp.is_active = true", userId) + + // Apply filters + if req.Type != nil { + query = query.Where("chat_sessions.type = ?", *req.Type) + } + + // Include relationships + query = query.Preload("Creator"). + Preload("Participants", "is_active = true"). + Preload("Participants.User"). + Preload("Messages", "is_deleted = false"). + Preload("Messages.Sender") + + // Order by updated_at desc (most recent first) + query = query.Order("chat_sessions.updated_at DESC") + + // Apply pagination + var count int64 + query.Count(&count) + req.Pagination.Count = count + req.Pagination = paginator.Paging(req.Pagination) + + err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&chatSessions).Error + paging = *req.Pagination + + return +} + +func (_i *chatRepository) FindChatSessionByID(id uint) (chatSession *entity.ChatSessions, err error) { + err = _i.DB.DB.Preload("Creator"). + Preload("Participants", "is_active = true"). + Preload("Participants.User"). + Preload("Messages", "is_deleted = false"). + Preload("Messages.Sender"). + First(&chatSession, id).Error + return +} + +func (_i *chatRepository) FindChatSessionByUserAndID(userId uint, id uint) (chatSession *entity.ChatSessions, err error) { + // Check if user is participant in this chat session + var isParticipant bool + err = _i.DB.DB.Table("chat_participants"). + Select("COUNT(*) > 0"). + Where("chat_session_id = ? AND user_id = ? AND is_active = true", id, userId). + Scan(&isParticipant).Error + + if err != nil { + return nil, err + } + + if !isParticipant { + return nil, nil + } + + err = _i.DB.DB.Preload("Creator"). + Preload("Participants", "is_active = true"). + Preload("Participants.User"). + Preload("Messages", "is_deleted = false"). + Preload("Messages.Sender"). + First(&chatSession, id).Error + return +} + +func (_i *chatRepository) CreateChatSession(chatSession *entity.ChatSessions) (result *entity.ChatSessions, err error) { + err = _i.DB.DB.Create(chatSession).Error + if err != nil { + return nil, err + } + + // Reload with relationships + err = _i.DB.DB.Preload("Creator"). + Preload("Participants", "is_active = true"). + Preload("Participants.User"). + First(&result, chatSession.ID).Error + return +} + +func (_i *chatRepository) UpdateChatSession(id uint, chatSession *entity.ChatSessions) (err error) { + err = _i.DB.DB.Model(&entity.ChatSessions{}).Where("id = ?", id).Updates(chatSession).Error + return +} + +func (_i *chatRepository) DeleteChatSession(id uint) (err error) { + err = _i.DB.DB.Delete(&entity.ChatSessions{}, id).Error + return +} + +// Chat Message Repository Methods +func (_i *chatRepository) GetAllChatMessages(req request.ChatMessageQueryRequest) (chatMessages []*entity.ChatMessages, paging paginator.Pagination, err error) { + // Check if user is participant in this chat session + var isParticipant bool + err = _i.DB.DB.Table("chat_participants"). + Select("COUNT(*) > 0"). + Where("chat_session_id = ? AND user_id = ? AND is_active = true", req.ChatSessionID, req.UserID). + Scan(&isParticipant).Error + + if err != nil { + return nil, paginator.Pagination{}, err + } + + if !isParticipant { + return nil, paginator.Pagination{}, nil + } + + query := _i.DB.DB.Model(&entity.ChatMessages{}).Where("chat_session_id = ? AND is_deleted = false", req.ChatSessionID) + + // Include relationships + query = query.Preload("Sender") + + // Order by created_at desc (most recent first) + query = query.Order("created_at DESC") + + // Apply pagination + var count int64 + query.Count(&count) + req.Pagination.Count = count + req.Pagination = paginator.Paging(req.Pagination) + + err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&chatMessages).Error + paging = *req.Pagination + + return +} + +func (_i *chatRepository) FindChatMessageByID(id uint) (chatMessage *entity.ChatMessages, err error) { + err = _i.DB.DB.Preload("Sender").First(&chatMessage, id).Error + return +} + +func (_i *chatRepository) FindChatMessageByUserAndID(userId uint, id uint) (chatMessage *entity.ChatMessages, err error) { + // Check if user is participant in the chat session of this message + var isParticipant bool + err = _i.DB.DB.Table("chat_messages cm"). + Joins("INNER JOIN chat_participants cp ON cm.chat_session_id = cp.chat_session_id"). + Select("COUNT(*) > 0"). + Where("cm.id = ? AND cp.user_id = ? AND cp.is_active = true", id, userId). + Scan(&isParticipant).Error + + if err != nil { + return nil, err + } + + if !isParticipant { + return nil, nil + } + + err = _i.DB.DB.Preload("Sender").First(&chatMessage, id).Error + return +} + +func (_i *chatRepository) CreateChatMessage(chatMessage *entity.ChatMessages) (result *entity.ChatMessages, err error) { + err = _i.DB.DB.Create(chatMessage).Error + if err != nil { + return nil, err + } + + // Reload with relationships + err = _i.DB.DB.Preload("Sender").First(&result, chatMessage.ID).Error + return +} + +func (_i *chatRepository) UpdateChatMessage(id uint, chatMessage *entity.ChatMessages) (err error) { + err = _i.DB.DB.Model(&entity.ChatMessages{}).Where("id = ?", id).Updates(chatMessage).Error + return +} + +func (_i *chatRepository) DeleteChatMessage(id uint) (err error) { + err = _i.DB.DB.Delete(&entity.ChatMessages{}, id).Error + return +} + +// Chat Participant Repository Methods +func (_i *chatRepository) CreateChatParticipant(chatParticipant *entity.ChatParticipants) (result *entity.ChatParticipants, err error) { + err = _i.DB.DB.Create(chatParticipant).Error + if err != nil { + return nil, err + } + + // Reload with relationships + err = _i.DB.DB.Preload("User").First(&result, chatParticipant.ID).Error + return +} + +func (_i *chatRepository) FindChatParticipantsBySessionID(chatSessionID uint) (participants []*entity.ChatParticipants, err error) { + err = _i.DB.DB.Model(&entity.ChatParticipants{}).Where("chat_session_id = ? AND is_active = true", chatSessionID). + Preload("User").Find(&participants).Error + return +} + +func (_i *chatRepository) UpdateChatParticipant(id uint, chatParticipant *entity.ChatParticipants) (err error) { + err = _i.DB.DB.Model(&entity.ChatParticipants{}).Where("id = ?", id).Updates(chatParticipant).Error + return +} + +func (_i *chatRepository) DeleteChatParticipant(id uint) (err error) { + err = _i.DB.DB.Delete(&entity.ChatParticipants{}, id).Error + return +} + +// Utility Methods +func (_i *chatRepository) FindPersonalChatSession(userId1 uint, userId2 uint) (chatSession *entity.ChatSessions, err error) { + err = _i.DB.DB.Model(&entity.ChatSessions{}). + Joins("INNER JOIN chat_participants cp1 ON chat_sessions.id = cp1.chat_session_id"). + Joins("INNER JOIN chat_participants cp2 ON chat_sessions.id = cp2.chat_session_id"). + Where("chat_sessions.type = 'personal' AND cp1.user_id = ? AND cp2.user_id = ? AND cp1.is_active = true AND cp2.is_active = true", userId1, userId2). + Preload("Creator"). + Preload("Participants", "is_active = true"). + Preload("Participants.User"). + First(&chatSession).Error + return +} + +func (_i *chatRepository) CheckUserInChatSession(userId uint, chatSessionID uint) (isParticipant bool, err error) { + err = _i.DB.DB.Table("chat_participants"). + Select("COUNT(*) > 0"). + Where("chat_session_id = ? AND user_id = ? AND is_active = true", chatSessionID, userId). + Scan(&isParticipant).Error + return +} diff --git a/app/module/chat/request/chat.request.go b/app/module/chat/request/chat.request.go new file mode 100644 index 0000000..47dcfa5 --- /dev/null +++ b/app/module/chat/request/chat.request.go @@ -0,0 +1,122 @@ +package request + +import ( + "narasi-ahli-be/app/database/entity" + "narasi-ahli-be/utils/paginator" + "strconv" + "time" +) + +// Chat Session Requests +type ChatSessionQueryRequest struct { + Type *string `json:"type"` // 'personal' or 'group' + Pagination *paginator.Pagination `json:"pagination"` +} + +type ChatSessionCreateRequest struct { + Name *string `json:"name" validate:"omitempty,min=2,max=255"` // null for personal chat + Type string `json:"type" validate:"required,oneof=personal group"` + UserIDs []uint `json:"userIds" validate:"required,min=1"` // participants (excluding creator) +} + +func (req ChatSessionCreateRequest) ToEntity(createdBy uint) *entity.ChatSessions { + return &entity.ChatSessions{ + Name: req.Name, + Type: req.Type, + CreatedBy: createdBy, + } +} + +type ChatSessionUpdateRequest struct { + Name *string `json:"name" validate:"omitempty,min=2,max=255"` +} + +func (req ChatSessionUpdateRequest) ToEntity() *entity.ChatSessions { + return &entity.ChatSessions{ + Name: req.Name, + } +} + +type ChatSessionQueryRequestContext struct { + Type string `json:"type"` +} + +func (req ChatSessionQueryRequestContext) ToParamRequest() ChatSessionQueryRequest { + var request ChatSessionQueryRequest + + if chatType := req.Type; chatType != "" { + request.Type = &chatType + } + + return request +} + +// Chat Message Requests +type ChatMessageQueryRequest struct { + ChatSessionID uint `json:"chatSessionId" validate:"required"` + UserID uint `json:"userId"` // Will be set in service layer + Pagination *paginator.Pagination `json:"pagination"` +} + +type ChatMessageCreateRequest struct { + ChatSessionID uint `json:"chatSessionId" validate:"required"` + Message string `json:"message" validate:"required,min=1,max=1000"` + MessageType string `json:"messageType" validate:"omitempty,oneof=text image file user assistant"` +} + +func (req ChatMessageCreateRequest) ToEntity(senderID uint) *entity.ChatMessages { + messageType := req.MessageType + if messageType == "" { + messageType = "text" + } + + return &entity.ChatMessages{ + ChatSessionID: req.ChatSessionID, + SenderID: senderID, + Message: req.Message, + MessageType: messageType, + } +} + +type ChatMessageUpdateRequest struct { + Message string `json:"message" validate:"required,min=1,max=1000"` +} + +func (req ChatMessageUpdateRequest) ToEntity() *entity.ChatMessages { + return &entity.ChatMessages{ + Message: req.Message, + IsEdited: true, + EditedAt: &time.Time{}, + } +} + +type ChatMessageQueryRequestContext struct { + ChatSessionID string `json:"chatSessionId"` +} + +func (req ChatMessageQueryRequestContext) ToParamRequest() ChatMessageQueryRequest { + var request ChatMessageQueryRequest + + if chatSessionId := req.ChatSessionID; chatSessionId != "" { + chatSessionIdUint, err := strconv.ParseUint(chatSessionId, 10, 0) + if err == nil { + request.ChatSessionID = uint(chatSessionIdUint) + } + } + + return request +} + +// Chat Participant Requests +type ChatParticipantCreateRequest struct { + ChatSessionID uint `json:"chatSessionId" validate:"required"` + UserID uint `json:"userId" validate:"required"` +} + +func (req ChatParticipantCreateRequest) ToEntity() *entity.ChatParticipants { + return &entity.ChatParticipants{ + ChatSessionID: req.ChatSessionID, + UserID: req.UserID, + IsActive: true, + } +} diff --git a/app/module/chat/response/chat.response.go b/app/module/chat/response/chat.response.go new file mode 100644 index 0000000..f4ed0d0 --- /dev/null +++ b/app/module/chat/response/chat.response.go @@ -0,0 +1,56 @@ +package response + +import ( + "time" +) + +// Chat Session Response +type ChatSessionResponse struct { + ID uint `json:"id"` + Name *string `json:"name"` + Type string `json:"type"` + CreatedBy uint `json:"createdBy"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + Creator *UserBasicInfo `json:"creator,omitempty"` + Participants []*ChatParticipantResponse `json:"participants,omitempty"` + LastMessage *ChatMessageResponse `json:"lastMessage,omitempty"` + UnreadCount int `json:"unreadCount"` +} + +// Chat Message Response +type ChatMessageResponse struct { + ID uint `json:"id"` + ChatSessionID uint `json:"chatSessionId"` + SenderID uint `json:"senderId"` + Message string `json:"message"` + MessageType string `json:"messageType"` + IsEdited bool `json:"isEdited"` + EditedAt *time.Time `json:"editedAt"` + IsDeleted bool `json:"isDeleted"` + DeletedAt *time.Time `json:"deletedAt"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + Sender *UserBasicInfo `json:"sender,omitempty"` +} + +// Chat Participant Response +type ChatParticipantResponse struct { + ID uint `json:"id"` + ChatSessionID uint `json:"chatSessionId"` + UserID uint `json:"userId"` + JoinedAt time.Time `json:"joinedAt"` + LeftAt *time.Time `json:"leftAt"` + IsActive bool `json:"isActive"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + User *UserBasicInfo `json:"user,omitempty"` +} + +// User Basic Info (reused from work_history) +type UserBasicInfo struct { + ID uint `json:"id"` + Username string `json:"username"` + Fullname string `json:"fullname"` + Email string `json:"email"` +} diff --git a/app/module/chat/service/chat.service.go b/app/module/chat/service/chat.service.go new file mode 100644 index 0000000..e4af9fc --- /dev/null +++ b/app/module/chat/service/chat.service.go @@ -0,0 +1,418 @@ +package service + +import ( + "errors" + "narasi-ahli-be/app/database/entity" + "narasi-ahli-be/app/module/chat/mapper" + "narasi-ahli-be/app/module/chat/repository" + "narasi-ahli-be/app/module/chat/request" + "narasi-ahli-be/app/module/chat/response" + usersRepository "narasi-ahli-be/app/module/users/repository" + "narasi-ahli-be/utils/paginator" + utilSvc "narasi-ahli-be/utils/service" + "time" + + "github.com/rs/zerolog" +) + +type chatService struct { + Repo repository.ChatRepository + UsersRepo usersRepository.UsersRepository + Log zerolog.Logger +} + +type ChatService interface { + // Chat Session methods + GetAllChatSessions(authToken string, req request.ChatSessionQueryRequest) (chatSessions []*response.ChatSessionResponse, paging paginator.Pagination, err error) + GetChatSessionByID(authToken string, id uint) (chatSession *response.ChatSessionResponse, err error) + CreateChatSession(authToken string, req request.ChatSessionCreateRequest) (chatSession *response.ChatSessionResponse, err error) + UpdateChatSession(authToken string, id uint, req request.ChatSessionUpdateRequest) (err error) + DeleteChatSession(authToken string, id uint) error + + // Chat Message methods + GetAllChatMessages(authToken string, req request.ChatMessageQueryRequest) (chatMessages []*response.ChatMessageResponse, paging paginator.Pagination, err error) + GetChatMessageByID(authToken string, id uint) (chatMessage *response.ChatMessageResponse, err error) + CreateChatMessage(authToken string, req request.ChatMessageCreateRequest) (chatMessage *response.ChatMessageResponse, err error) + UpdateChatMessage(authToken string, id uint, req request.ChatMessageUpdateRequest) (err error) + DeleteChatMessage(authToken string, id uint) error + + // Chat Participant methods + AddParticipantToChat(authToken string, chatSessionID uint, participantUserID uint) error + RemoveParticipantFromChat(authToken string, chatSessionID uint, participantUserID uint) error +} + +func NewChatService(repo repository.ChatRepository, usersRepo usersRepository.UsersRepository, log zerolog.Logger) ChatService { + return &chatService{ + Repo: repo, + UsersRepo: usersRepo, + Log: log, + } +} + +// Chat Session Service Methods +func (_i *chatService) GetAllChatSessions(authToken string, req request.ChatSessionQueryRequest) (chatSessions []*response.ChatSessionResponse, paging paginator.Pagination, err error) { + userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if userInfo == nil { + return nil, paginator.Pagination{}, errors.New("user not found") + } + + results, paging, err := _i.Repo.GetAllChatSessions(userInfo.ID, req) + if err != nil { + return + } + + for _, result := range results { + chatSessions = append(chatSessions, mapper.ChatSessionResponseMapper(result)) + } + + return +} + +func (_i *chatService) GetChatSessionByID(authToken string, id uint) (chatSession *response.ChatSessionResponse, err error) { + userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if userInfo == nil { + return nil, errors.New("user not found") + } + + result, err := _i.Repo.FindChatSessionByUserAndID(userInfo.ID, id) + if err != nil { + return nil, err + } + if result == nil { + return nil, errors.New("chat session not found or access denied") + } + + return mapper.ChatSessionResponseMapper(result), nil +} + +func (_i *chatService) CreateChatSession(authToken string, req request.ChatSessionCreateRequest) (chatSession *response.ChatSessionResponse, err error) { + userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if userInfo == nil { + return nil, errors.New("user not found") + } + + _i.Log.Info().Interface("data", req).Msg("Creating chat session") + + // Validate business rules + if req.Type == "personal" && len(req.UserIDs) != 1 { + return nil, errors.New("personal chat must have exactly one other participant") + } + if req.Type == "group" && len(req.UserIDs) < 1 { + return nil, errors.New("group chat must have at least one participant") + } + + // Check if personal chat already exists + if req.Type == "personal" { + existingChat, err := _i.Repo.FindPersonalChatSession(userInfo.ID, req.UserIDs[0]) + if err == nil && existingChat != nil { + return mapper.ChatSessionResponseMapper(existingChat), nil + } + } + + // Validate all user IDs exist + for _, userID := range req.UserIDs { + user, err := _i.UsersRepo.FindOne(userID) + if err != nil || user == nil { + return nil, errors.New("invalid user ID: " + string(rune(userID))) + } + } + + // Create chat session + entity := req.ToEntity(userInfo.ID) + result, err := _i.Repo.CreateChatSession(entity) + if err != nil { + return nil, err + } + + // Add creator as participant + creatorParticipant := &request.ChatParticipantCreateRequest{ + ChatSessionID: result.ID, + UserID: userInfo.ID, + } + _, err = _i.Repo.CreateChatParticipant(creatorParticipant.ToEntity()) + if err != nil { + return nil, err + } + + // Add other participants + for _, userID := range req.UserIDs { + participant := &request.ChatParticipantCreateRequest{ + ChatSessionID: result.ID, + UserID: userID, + } + _, err = _i.Repo.CreateChatParticipant(participant.ToEntity()) + if err != nil { + return nil, err + } + } + + // Reload with all relationships + finalResult, err := _i.Repo.FindChatSessionByID(result.ID) + if err != nil { + return nil, err + } + + return mapper.ChatSessionResponseMapper(finalResult), nil +} + +func (_i *chatService) UpdateChatSession(authToken string, id uint, req request.ChatSessionUpdateRequest) (err error) { + userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if userInfo == nil { + return errors.New("user not found") + } + + _i.Log.Info().Interface("data", req).Msg("Updating chat session") + + // Check if chat session exists and user has access + existing, err := _i.Repo.FindChatSessionByUserAndID(userInfo.ID, id) + if err != nil { + return err + } + if existing == nil { + return errors.New("chat session not found or access denied") + } + + // Only creator can update chat session + if existing.CreatedBy != userInfo.ID { + return errors.New("only chat creator can update chat session") + } + + entity := req.ToEntity() + return _i.Repo.UpdateChatSession(id, entity) +} + +func (_i *chatService) DeleteChatSession(authToken string, id uint) error { + userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if userInfo == nil { + return errors.New("user not found") + } + + _i.Log.Info().Uint("userId", userInfo.ID).Uint("id", id).Msg("Deleting chat session") + + // Check if chat session exists and user has access + existing, err := _i.Repo.FindChatSessionByUserAndID(userInfo.ID, id) + if err != nil { + return err + } + if existing == nil { + return errors.New("chat session not found or access denied") + } + + // Only creator can delete chat session + if existing.CreatedBy != userInfo.ID { + return errors.New("only chat creator can delete chat session") + } + + return _i.Repo.DeleteChatSession(id) +} + +// Chat Message Service Methods +func (_i *chatService) GetAllChatMessages(authToken string, req request.ChatMessageQueryRequest) (chatMessages []*response.ChatMessageResponse, paging paginator.Pagination, err error) { + userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if userInfo == nil { + return nil, paginator.Pagination{}, errors.New("user not found") + } + + // Set user ID in request for repository + req.UserID = userInfo.ID + + results, paging, err := _i.Repo.GetAllChatMessages(req) + if err != nil { + return + } + + for _, result := range results { + chatMessages = append(chatMessages, mapper.ChatMessageResponseMapper(result)) + } + + return +} + +func (_i *chatService) GetChatMessageByID(authToken string, id uint) (chatMessage *response.ChatMessageResponse, err error) { + userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if userInfo == nil { + return nil, errors.New("user not found") + } + + result, err := _i.Repo.FindChatMessageByUserAndID(userInfo.ID, id) + if err != nil { + return nil, err + } + if result == nil { + return nil, errors.New("chat message not found or access denied") + } + + return mapper.ChatMessageResponseMapper(result), nil +} + +func (_i *chatService) CreateChatMessage(authToken string, req request.ChatMessageCreateRequest) (chatMessage *response.ChatMessageResponse, err error) { + userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if userInfo == nil { + return nil, errors.New("user not found") + } + + _i.Log.Info().Interface("data", req).Msg("Creating chat message") + + // Check if user is participant in the chat session + isParticipant, err := _i.Repo.CheckUserInChatSession(userInfo.ID, req.ChatSessionID) + if err != nil { + return nil, err + } + if !isParticipant { + return nil, errors.New("user is not a participant in this chat session") + } + + entity := req.ToEntity(userInfo.ID) + result, err := _i.Repo.CreateChatMessage(entity) + if err != nil { + return nil, err + } + + return mapper.ChatMessageResponseMapper(result), nil +} + +func (_i *chatService) UpdateChatMessage(authToken string, id uint, req request.ChatMessageUpdateRequest) (err error) { + userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if userInfo == nil { + return errors.New("user not found") + } + + _i.Log.Info().Interface("data", req).Msg("Updating chat message") + + // Check if message exists and user has access + existing, err := _i.Repo.FindChatMessageByUserAndID(userInfo.ID, id) + if err != nil { + return err + } + if existing == nil { + return errors.New("chat message not found or access denied") + } + + // Only sender can update message + if existing.SenderID != userInfo.ID { + return errors.New("only message sender can update message") + } + + now := time.Now() + entity := req.ToEntity() + entity.EditedAt = &now + return _i.Repo.UpdateChatMessage(id, entity) +} + +func (_i *chatService) DeleteChatMessage(authToken string, id uint) error { + userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if userInfo == nil { + return errors.New("user not found") + } + + _i.Log.Info().Uint("userId", userInfo.ID).Uint("id", id).Msg("Deleting chat message") + + // Check if message exists and user has access + existing, err := _i.Repo.FindChatMessageByUserAndID(userInfo.ID, id) + if err != nil { + return err + } + if existing == nil { + return errors.New("chat message not found or access denied") + } + + // Only sender can delete message + if existing.SenderID != userInfo.ID { + return errors.New("only message sender can delete message") + } + + return _i.Repo.DeleteChatMessage(id) +} + +// Chat Participant Service Methods +func (_i *chatService) AddParticipantToChat(authToken string, chatSessionID uint, participantUserID uint) error { + userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if userInfo == nil { + return errors.New("user not found") + } + + _i.Log.Info().Uint("userId", userInfo.ID).Uint("chatSessionID", chatSessionID).Uint("participantUserID", participantUserID).Msg("Adding participant to chat") + + // Check if user has access to chat session + existing, err := _i.Repo.FindChatSessionByUserAndID(userInfo.ID, chatSessionID) + if err != nil { + return err + } + if existing == nil { + return errors.New("chat session not found or access denied") + } + + // Only creator can add participants + if existing.CreatedBy != userInfo.ID { + return errors.New("only chat creator can add participants") + } + + // Validate participant user exists + user, err := _i.UsersRepo.FindOne(participantUserID) + if err != nil || user == nil { + return errors.New("invalid user ID") + } + + // Check if user is already a participant + isParticipant, err := _i.Repo.CheckUserInChatSession(participantUserID, chatSessionID) + if err != nil { + return err + } + if isParticipant { + return errors.New("user is already a participant in this chat") + } + + participant := &request.ChatParticipantCreateRequest{ + ChatSessionID: chatSessionID, + UserID: participantUserID, + } + _, err = _i.Repo.CreateChatParticipant(participant.ToEntity()) + return err +} + +func (_i *chatService) RemoveParticipantFromChat(authToken string, chatSessionID uint, participantUserID uint) error { + userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if userInfo == nil { + return errors.New("user not found") + } + + _i.Log.Info().Uint("userId", userInfo.ID).Uint("chatSessionID", chatSessionID).Uint("participantUserID", participantUserID).Msg("Removing participant from chat") + + // Check if user has access to chat session + existing, err := _i.Repo.FindChatSessionByUserAndID(userInfo.ID, chatSessionID) + if err != nil { + return err + } + if existing == nil { + return errors.New("chat session not found or access denied") + } + + // Only creator can remove participants (or user can remove themselves) + if existing.CreatedBy != userInfo.ID && userInfo.ID != participantUserID { + return errors.New("only chat creator can remove participants or user can remove themselves") + } + + // Find participant + participants, err := _i.Repo.FindChatParticipantsBySessionID(chatSessionID) + if err != nil { + return err + } + + var participantToRemove *entity.ChatParticipants + for _, participant := range participants { + if participant.UserID == participantUserID { + participantToRemove = participant + break + } + } + + if participantToRemove == nil { + return errors.New("participant not found") + } + + // Soft delete by setting is_active to false + now := time.Now() + participantToRemove.IsActive = false + participantToRemove.LeftAt = &now + return _i.Repo.UpdateChatParticipant(participantToRemove.ID, participantToRemove) +} diff --git a/app/module/communications/mapper/communications.mapper.go b/app/module/communications/mapper/communications.mapper.go index c44c2f7..f59123a 100644 --- a/app/module/communications/mapper/communications.mapper.go +++ b/app/module/communications/mapper/communications.mapper.go @@ -39,14 +39,14 @@ func ConversationsResponseMapper(conversation *entity.Conversations) *response.C func ChatMessagesResponseMapper(chatMessage *entity.ChatMessages) *response.ChatMessagesResponse { result := &response.ChatMessagesResponse{ ID: chatMessage.ID, - ConversationID: chatMessage.ConversationID, + ConversationID: chatMessage.ChatSessionID, SenderID: chatMessage.SenderID, - MessageText: chatMessage.MessageText, + MessageText: &chatMessage.Message, MessageType: chatMessage.MessageType, - FileURL: chatMessage.FileURL, - FileName: chatMessage.FileName, - FileSize: chatMessage.FileSize, - IsRead: chatMessage.IsRead, + FileURL: nil, // Not available in entity + FileName: nil, // Not available in entity + FileSize: nil, // Not available in entity + IsRead: false, // Not available in entity, default to false CreatedAt: chatMessage.CreatedAt, } diff --git a/app/module/communications/repository/communications.repository.go b/app/module/communications/repository/communications.repository.go index 0161058..76dd0f1 100644 --- a/app/module/communications/repository/communications.repository.go +++ b/app/module/communications/repository/communications.repository.go @@ -6,7 +6,7 @@ import ( "narasi-ahli-be/utils/paginator" "time" - "gorm.io/gorm" + "gorm.io/gorm" ) type communicationsRepository struct { diff --git a/app/module/communications/request/communications.request.go b/app/module/communications/request/communications.request.go index 952e742..1a04e25 100644 --- a/app/module/communications/request/communications.request.go +++ b/app/module/communications/request/communications.request.go @@ -23,42 +23,40 @@ func (req ConversationsCreateRequest) ToEntity() *entity.Conversations { // Chat Messages Request DTOs type ChatMessagesQueryRequest struct { - ConversationID uint `json:"conversationId" validate:"required"` - Pagination *paginator.Pagination `json:"pagination"` + ChatSessionID uint `json:"chatSessionId" validate:"required"` + Pagination *paginator.Pagination `json:"pagination"` } type ChatMessagesCreateRequest struct { - ConversationID uint `json:"conversationId" validate:"required"` - MessageText string `json:"messageText"` - MessageType string `json:"messageType" validate:"required,oneof=text image file audio"` + ChatSessionID uint `json:"chatSessionId" validate:"required"` + Message string `json:"message"` + MessageType string `json:"messageType" validate:"required,oneof=text image file audio"` } func (req ChatMessagesCreateRequest) ToEntity() *entity.ChatMessages { return &entity.ChatMessages{ - ConversationID: req.ConversationID, - SenderID: 0, // Will be set in service layer - MessageText: &req.MessageText, - MessageType: req.MessageType, - IsRead: false, + ChatSessionID: req.ChatSessionID, + SenderID: 0, // Will be set in service layer + Message: req.Message, + MessageType: req.MessageType, } } type ChatMessagesFileUploadRequest struct { - ConversationID uint `json:"conversationId" validate:"required"` - MessageType string `json:"messageType" validate:"required,oneof=image file audio"` - FileName string `json:"fileName"` - FileSize int64 `json:"fileSize"` + ChatSessionID uint `json:"chatSessionId" validate:"required"` + MessageType string `json:"messageType" validate:"required,oneof=image file audio"` + FileName string `json:"fileName"` + FileSize int64 `json:"fileSize"` } func (req ChatMessagesFileUploadRequest) ToEntity(fileURL string) *entity.ChatMessages { return &entity.ChatMessages{ - ConversationID: req.ConversationID, - SenderID: 0, // Will be set in service layer - MessageType: req.MessageType, - FileURL: &fileURL, - FileName: &req.FileName, - FileSize: &req.FileSize, - IsRead: false, + ChatSessionID: req.ChatSessionID, + SenderID: 0, // Will be set in service layer + Message: "", // File messages might not have text content + MessageType: req.MessageType, + // Note: FileURL, FileName, FileSize, IsRead fields are not available in the entity + // These would need to be stored separately or the entity needs to be updated } } @@ -75,14 +73,14 @@ func (req ConversationsQueryRequestContext) ToParamRequest() ConversationsQueryR } type ChatMessagesQueryRequestContext struct { - ConversationID string `json:"conversationId"` + ChatSessionID string `json:"chatSessionId"` } func (req ChatMessagesQueryRequestContext) ToParamRequest() ChatMessagesQueryRequest { var request ChatMessagesQueryRequest - if conversationIDStr := req.ConversationID; conversationIDStr != "" { - // Parse conversation ID from string to uint + if chatSessionIDStr := req.ChatSessionID; chatSessionIDStr != "" { + // Parse chat session ID from string to uint // This will be handled in the controller } diff --git a/app/router/api.go b/app/router/api.go index 4e8ec70..7b193a0 100644 --- a/app/router/api.go +++ b/app/router/api.go @@ -10,6 +10,7 @@ import ( "narasi-ahli-be/app/module/article_comments" "narasi-ahli-be/app/module/article_files" "narasi-ahli-be/app/module/articles" + "narasi-ahli-be/app/module/chat" "narasi-ahli-be/app/module/cities" "narasi-ahli-be/app/module/custom_static_pages" "narasi-ahli-be/app/module/districts" @@ -48,6 +49,7 @@ type Router struct { ArticleCommentsRouter *article_comments.ArticleCommentsRouter ArticleApprovalsRouter *article_approvals.ArticleApprovalsRouter ArticlesRouter *articles.ArticlesRouter + ChatRouter *chat.ChatRouter CitiesRouter *cities.CitiesRouter CustomStaticPagesRouter *custom_static_pages.CustomStaticPagesRouter DistrictsRouter *districts.DistrictsRouter @@ -81,6 +83,7 @@ func NewRouter( articleCommentsRouter *article_comments.ArticleCommentsRouter, articleApprovalsRouter *article_approvals.ArticleApprovalsRouter, articlesRouter *articles.ArticlesRouter, + chatRouter *chat.ChatRouter, citiesRouter *cities.CitiesRouter, customStaticPagesRouter *custom_static_pages.CustomStaticPagesRouter, districtsRouter *districts.DistrictsRouter, @@ -112,6 +115,7 @@ func NewRouter( ArticleCommentsRouter: articleCommentsRouter, ArticleApprovalsRouter: articleApprovalsRouter, ArticlesRouter: articlesRouter, + ChatRouter: chatRouter, CitiesRouter: citiesRouter, CustomStaticPagesRouter: customStaticPagesRouter, DistrictsRouter: districtsRouter, @@ -153,6 +157,7 @@ func (r *Router) Register() { r.ArticleApprovalsRouter.RegisterArticleApprovalsRoutes() r.ArticlesRouter.RegisterArticlesRoutes() r.ArticleCommentsRouter.RegisterArticleCommentsRoutes() + r.ChatRouter.RegisterChatRoutes() r.CitiesRouter.RegisterCitiesRoutes() r.CustomStaticPagesRouter.RegisterCustomStaticPagesRoutes() r.DistrictsRouter.RegisterDistrictsRoutes() diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index 1dfb0f1..fb0f73e 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -4536,6 +4536,835 @@ const docTemplate = `{ } } }, + "/chat/messages": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for getting all messages in a specific chat session", + "tags": [ + "Chat" + ], + "summary": "Get all chat messages", + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "type": "integer", + "description": "Chat Session ID", + "name": "chatSessionId", + "in": "query", + "required": true + }, + { + "type": "integer", + "name": "count", + "in": "query" + }, + { + "type": "integer", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "name": "nextPage", + "in": "query" + }, + { + "type": "integer", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "name": "previousPage", + "in": "query" + }, + { + "type": "string", + "name": "sort", + "in": "query" + }, + { + "type": "string", + "name": "sortBy", + "in": "query" + }, + { + "type": "integer", + "name": "totalPage", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for creating a new chat message", + "tags": [ + "Chat" + ], + "summary": "Create chat message", + "parameters": [ + { + "type": "string", + "description": "Insert the X-Csrf-Token", + "name": "X-Csrf-Token", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "description": "Required payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.ChatMessageCreateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, + "/chat/messages/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for getting one chat message", + "tags": [ + "Chat" + ], + "summary": "Get one chat message", + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "type": "integer", + "description": "Chat Message ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for updating chat message (only sender can update)", + "tags": [ + "Chat" + ], + "summary": "Update chat message", + "parameters": [ + { + "type": "string", + "description": "Insert the X-Csrf-Token", + "name": "X-Csrf-Token", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "type": "integer", + "description": "Chat Message ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Required payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.ChatMessageUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for deleting chat message (only sender can delete)", + "tags": [ + "Chat" + ], + "summary": "Delete chat message", + "parameters": [ + { + "type": "string", + "description": "Insert the X-Csrf-Token", + "name": "X-Csrf-Token", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "type": "integer", + "description": "Chat Message ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, + "/chat/sessions": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for getting all chat sessions for authenticated user", + "tags": [ + "Chat" + ], + "summary": "Get all chat sessions", + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "type": "string", + "description": "Chat type (personal or group)", + "name": "type", + "in": "query" + }, + { + "type": "integer", + "name": "count", + "in": "query" + }, + { + "type": "integer", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "name": "nextPage", + "in": "query" + }, + { + "type": "integer", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "name": "previousPage", + "in": "query" + }, + { + "type": "string", + "name": "sort", + "in": "query" + }, + { + "type": "string", + "name": "sortBy", + "in": "query" + }, + { + "type": "integer", + "name": "totalPage", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for creating a new chat session (personal or group)", + "tags": [ + "Chat" + ], + "summary": "Create chat session", + "parameters": [ + { + "type": "string", + "description": "Insert the X-Csrf-Token", + "name": "X-Csrf-Token", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "description": "Required payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.ChatSessionCreateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, + "/chat/sessions/{chatSessionId}/participants": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for adding a participant to a chat session (only creator can add)", + "tags": [ + "Chat" + ], + "summary": "Add participant to chat session", + "parameters": [ + { + "type": "string", + "description": "Insert the X-Csrf-Token", + "name": "X-Csrf-Token", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "type": "integer", + "description": "Chat Session ID", + "name": "chatSessionId", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Participant User ID", + "name": "participantUserId", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for removing a participant from a chat session (creator can remove anyone, user can remove themselves)", + "tags": [ + "Chat" + ], + "summary": "Remove participant from chat session", + "parameters": [ + { + "type": "string", + "description": "Insert the X-Csrf-Token", + "name": "X-Csrf-Token", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "type": "integer", + "description": "Chat Session ID", + "name": "chatSessionId", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Participant User ID", + "name": "participantUserId", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, + "/chat/sessions/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for getting one chat session", + "tags": [ + "Chat" + ], + "summary": "Get one chat session", + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "type": "integer", + "description": "Chat Session ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for updating chat session (only creator can update)", + "tags": [ + "Chat" + ], + "summary": "Update chat session", + "parameters": [ + { + "type": "string", + "description": "Insert the X-Csrf-Token", + "name": "X-Csrf-Token", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "type": "integer", + "description": "Chat Session ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Required payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.ChatSessionUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for deleting chat session (only creator can delete)", + "tags": [ + "Chat" + ], + "summary": "Delete chat session", + "parameters": [ + { + "type": "string", + "description": "Insert the X-Csrf-Token", + "name": "X-Csrf-Token", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "type": "integer", + "description": "Chat Session ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, "/cities": { "get": { "security": [ @@ -13763,6 +14592,86 @@ const docTemplate = `{ } } }, + "request.ChatMessageCreateRequest": { + "type": "object", + "required": [ + "chatSessionId", + "message" + ], + "properties": { + "chatSessionId": { + "type": "integer" + }, + "message": { + "type": "string", + "maxLength": 1000, + "minLength": 1 + }, + "messageType": { + "type": "string", + "enum": [ + "text", + "image", + "file", + "user", + "assistant" + ] + } + } + }, + "request.ChatMessageUpdateRequest": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string", + "maxLength": 1000, + "minLength": 1 + } + } + }, + "request.ChatSessionCreateRequest": { + "type": "object", + "required": [ + "type", + "userIds" + ], + "properties": { + "name": { + "description": "null for personal chat", + "type": "string", + "maxLength": 255, + "minLength": 2 + }, + "type": { + "type": "string", + "enum": [ + "personal", + "group" + ] + }, + "userIds": { + "description": "participants (excluding creator)", + "type": "array", + "minItems": 1, + "items": { + "type": "integer" + } + } + } + }, + "request.ChatSessionUpdateRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 255, + "minLength": 2 + } + } + }, "request.CitiesCreateRequest": { "type": "object", "required": [ diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 9d23622..9d2411e 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -4525,6 +4525,835 @@ } } }, + "/chat/messages": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for getting all messages in a specific chat session", + "tags": [ + "Chat" + ], + "summary": "Get all chat messages", + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "type": "integer", + "description": "Chat Session ID", + "name": "chatSessionId", + "in": "query", + "required": true + }, + { + "type": "integer", + "name": "count", + "in": "query" + }, + { + "type": "integer", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "name": "nextPage", + "in": "query" + }, + { + "type": "integer", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "name": "previousPage", + "in": "query" + }, + { + "type": "string", + "name": "sort", + "in": "query" + }, + { + "type": "string", + "name": "sortBy", + "in": "query" + }, + { + "type": "integer", + "name": "totalPage", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for creating a new chat message", + "tags": [ + "Chat" + ], + "summary": "Create chat message", + "parameters": [ + { + "type": "string", + "description": "Insert the X-Csrf-Token", + "name": "X-Csrf-Token", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "description": "Required payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.ChatMessageCreateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, + "/chat/messages/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for getting one chat message", + "tags": [ + "Chat" + ], + "summary": "Get one chat message", + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "type": "integer", + "description": "Chat Message ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for updating chat message (only sender can update)", + "tags": [ + "Chat" + ], + "summary": "Update chat message", + "parameters": [ + { + "type": "string", + "description": "Insert the X-Csrf-Token", + "name": "X-Csrf-Token", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "type": "integer", + "description": "Chat Message ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Required payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.ChatMessageUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for deleting chat message (only sender can delete)", + "tags": [ + "Chat" + ], + "summary": "Delete chat message", + "parameters": [ + { + "type": "string", + "description": "Insert the X-Csrf-Token", + "name": "X-Csrf-Token", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "type": "integer", + "description": "Chat Message ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, + "/chat/sessions": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for getting all chat sessions for authenticated user", + "tags": [ + "Chat" + ], + "summary": "Get all chat sessions", + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "type": "string", + "description": "Chat type (personal or group)", + "name": "type", + "in": "query" + }, + { + "type": "integer", + "name": "count", + "in": "query" + }, + { + "type": "integer", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "name": "nextPage", + "in": "query" + }, + { + "type": "integer", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "name": "previousPage", + "in": "query" + }, + { + "type": "string", + "name": "sort", + "in": "query" + }, + { + "type": "string", + "name": "sortBy", + "in": "query" + }, + { + "type": "integer", + "name": "totalPage", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for creating a new chat session (personal or group)", + "tags": [ + "Chat" + ], + "summary": "Create chat session", + "parameters": [ + { + "type": "string", + "description": "Insert the X-Csrf-Token", + "name": "X-Csrf-Token", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "description": "Required payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.ChatSessionCreateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, + "/chat/sessions/{chatSessionId}/participants": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for adding a participant to a chat session (only creator can add)", + "tags": [ + "Chat" + ], + "summary": "Add participant to chat session", + "parameters": [ + { + "type": "string", + "description": "Insert the X-Csrf-Token", + "name": "X-Csrf-Token", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "type": "integer", + "description": "Chat Session ID", + "name": "chatSessionId", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Participant User ID", + "name": "participantUserId", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for removing a participant from a chat session (creator can remove anyone, user can remove themselves)", + "tags": [ + "Chat" + ], + "summary": "Remove participant from chat session", + "parameters": [ + { + "type": "string", + "description": "Insert the X-Csrf-Token", + "name": "X-Csrf-Token", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "type": "integer", + "description": "Chat Session ID", + "name": "chatSessionId", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Participant User ID", + "name": "participantUserId", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, + "/chat/sessions/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for getting one chat session", + "tags": [ + "Chat" + ], + "summary": "Get one chat session", + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "type": "integer", + "description": "Chat Session ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for updating chat session (only creator can update)", + "tags": [ + "Chat" + ], + "summary": "Update chat session", + "parameters": [ + { + "type": "string", + "description": "Insert the X-Csrf-Token", + "name": "X-Csrf-Token", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "type": "integer", + "description": "Chat Session ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Required payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.ChatSessionUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for deleting chat session (only creator can delete)", + "tags": [ + "Chat" + ], + "summary": "Delete chat session", + "parameters": [ + { + "type": "string", + "description": "Insert the X-Csrf-Token", + "name": "X-Csrf-Token", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "type": "integer", + "description": "Chat Session ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, "/cities": { "get": { "security": [ @@ -13752,6 +14581,86 @@ } } }, + "request.ChatMessageCreateRequest": { + "type": "object", + "required": [ + "chatSessionId", + "message" + ], + "properties": { + "chatSessionId": { + "type": "integer" + }, + "message": { + "type": "string", + "maxLength": 1000, + "minLength": 1 + }, + "messageType": { + "type": "string", + "enum": [ + "text", + "image", + "file", + "user", + "assistant" + ] + } + } + }, + "request.ChatMessageUpdateRequest": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string", + "maxLength": 1000, + "minLength": 1 + } + } + }, + "request.ChatSessionCreateRequest": { + "type": "object", + "required": [ + "type", + "userIds" + ], + "properties": { + "name": { + "description": "null for personal chat", + "type": "string", + "maxLength": 255, + "minLength": 2 + }, + "type": { + "type": "string", + "enum": [ + "personal", + "group" + ] + }, + "userIds": { + "description": "participants (excluding creator)", + "type": "array", + "minItems": 1, + "items": { + "type": "integer" + } + } + } + }, + "request.ChatSessionUpdateRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 255, + "minLength": 2 + } + } + }, "request.CitiesCreateRequest": { "type": "object", "required": [ diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 353c413..4c65f8d 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -378,6 +378,64 @@ definitions: - title - typeId type: object + request.ChatMessageCreateRequest: + properties: + chatSessionId: + type: integer + message: + maxLength: 1000 + minLength: 1 + type: string + messageType: + enum: + - text + - image + - file + - user + - assistant + type: string + required: + - chatSessionId + - message + type: object + request.ChatMessageUpdateRequest: + properties: + message: + maxLength: 1000 + minLength: 1 + type: string + required: + - message + type: object + request.ChatSessionCreateRequest: + properties: + name: + description: null for personal chat + maxLength: 255 + minLength: 2 + type: string + type: + enum: + - personal + - group + type: string + userIds: + description: participants (excluding creator) + items: + type: integer + minItems: 1 + type: array + required: + - type + - userIds + type: object + request.ChatSessionUpdateRequest: + properties: + name: + maxLength: 255 + minLength: 2 + type: string + type: object request.CitiesCreateRequest: properties: cityName: @@ -4077,6 +4135,544 @@ paths: summary: Viewer Articles Thumbnail tags: - Articles + /chat/messages: + get: + description: API for getting all messages in a specific chat session + parameters: + - default: Bearer + description: Insert your access token + in: header + name: Authorization + type: string + - description: Chat Session ID + in: query + name: chatSessionId + required: true + type: integer + - in: query + name: count + type: integer + - in: query + name: limit + type: integer + - in: query + name: nextPage + type: integer + - in: query + name: page + type: integer + - in: query + name: previousPage + type: integer + - in: query + name: sort + type: string + - in: query + name: sortBy + type: string + - in: query + name: totalPage + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.BadRequestError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.UnauthorizedError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.InternalServerError' + security: + - Bearer: [] + summary: Get all chat messages + tags: + - Chat + post: + description: API for creating a new chat message + parameters: + - description: Insert the X-Csrf-Token + in: header + name: X-Csrf-Token + required: true + type: string + - default: Bearer + description: Insert your access token + in: header + name: Authorization + type: string + - description: Required payload + in: body + name: payload + required: true + schema: + $ref: '#/definitions/request.ChatMessageCreateRequest' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.BadRequestError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.UnauthorizedError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.InternalServerError' + security: + - Bearer: [] + summary: Create chat message + tags: + - Chat + /chat/messages/{id}: + delete: + description: API for deleting chat message (only sender can delete) + parameters: + - description: Insert the X-Csrf-Token + in: header + name: X-Csrf-Token + required: true + type: string + - default: Bearer + description: Insert your access token + in: header + name: Authorization + type: string + - description: Chat Message ID + in: path + name: id + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.BadRequestError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.UnauthorizedError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.InternalServerError' + security: + - Bearer: [] + summary: Delete chat message + tags: + - Chat + get: + description: API for getting one chat message + parameters: + - default: Bearer + description: Insert your access token + in: header + name: Authorization + type: string + - description: Chat Message ID + in: path + name: id + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.BadRequestError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.UnauthorizedError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.InternalServerError' + security: + - Bearer: [] + summary: Get one chat message + tags: + - Chat + put: + description: API for updating chat message (only sender can update) + parameters: + - description: Insert the X-Csrf-Token + in: header + name: X-Csrf-Token + required: true + type: string + - default: Bearer + description: Insert your access token + in: header + name: Authorization + type: string + - description: Chat Message ID + in: path + name: id + required: true + type: integer + - description: Required payload + in: body + name: payload + required: true + schema: + $ref: '#/definitions/request.ChatMessageUpdateRequest' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.BadRequestError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.UnauthorizedError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.InternalServerError' + security: + - Bearer: [] + summary: Update chat message + tags: + - Chat + /chat/sessions: + get: + description: API for getting all chat sessions for authenticated user + parameters: + - default: Bearer + description: Insert your access token + in: header + name: Authorization + type: string + - description: Chat type (personal or group) + in: query + name: type + type: string + - in: query + name: count + type: integer + - in: query + name: limit + type: integer + - in: query + name: nextPage + type: integer + - in: query + name: page + type: integer + - in: query + name: previousPage + type: integer + - in: query + name: sort + type: string + - in: query + name: sortBy + type: string + - in: query + name: totalPage + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.BadRequestError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.UnauthorizedError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.InternalServerError' + security: + - Bearer: [] + summary: Get all chat sessions + tags: + - Chat + post: + description: API for creating a new chat session (personal or group) + parameters: + - description: Insert the X-Csrf-Token + in: header + name: X-Csrf-Token + required: true + type: string + - default: Bearer + description: Insert your access token + in: header + name: Authorization + type: string + - description: Required payload + in: body + name: payload + required: true + schema: + $ref: '#/definitions/request.ChatSessionCreateRequest' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.BadRequestError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.UnauthorizedError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.InternalServerError' + security: + - Bearer: [] + summary: Create chat session + tags: + - Chat + /chat/sessions/{chatSessionId}/participants: + delete: + description: API for removing a participant from a chat session (creator can + remove anyone, user can remove themselves) + parameters: + - description: Insert the X-Csrf-Token + in: header + name: X-Csrf-Token + required: true + type: string + - default: Bearer + description: Insert your access token + in: header + name: Authorization + type: string + - description: Chat Session ID + in: path + name: chatSessionId + required: true + type: integer + - description: Participant User ID + in: query + name: participantUserId + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.BadRequestError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.UnauthorizedError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.InternalServerError' + security: + - Bearer: [] + summary: Remove participant from chat session + tags: + - Chat + post: + description: API for adding a participant to a chat session (only creator can + add) + parameters: + - description: Insert the X-Csrf-Token + in: header + name: X-Csrf-Token + required: true + type: string + - default: Bearer + description: Insert your access token + in: header + name: Authorization + type: string + - description: Chat Session ID + in: path + name: chatSessionId + required: true + type: integer + - description: Participant User ID + in: query + name: participantUserId + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.BadRequestError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.UnauthorizedError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.InternalServerError' + security: + - Bearer: [] + summary: Add participant to chat session + tags: + - Chat + /chat/sessions/{id}: + delete: + description: API for deleting chat session (only creator can delete) + parameters: + - description: Insert the X-Csrf-Token + in: header + name: X-Csrf-Token + required: true + type: string + - default: Bearer + description: Insert your access token + in: header + name: Authorization + type: string + - description: Chat Session ID + in: path + name: id + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.BadRequestError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.UnauthorizedError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.InternalServerError' + security: + - Bearer: [] + summary: Delete chat session + tags: + - Chat + get: + description: API for getting one chat session + parameters: + - default: Bearer + description: Insert your access token + in: header + name: Authorization + type: string + - description: Chat Session ID + in: path + name: id + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.BadRequestError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.UnauthorizedError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.InternalServerError' + security: + - Bearer: [] + summary: Get one chat session + tags: + - Chat + put: + description: API for updating chat session (only creator can update) + parameters: + - description: Insert the X-Csrf-Token + in: header + name: X-Csrf-Token + required: true + type: string + - default: Bearer + description: Insert your access token + in: header + name: Authorization + type: string + - description: Chat Session ID + in: path + name: id + required: true + type: integer + - description: Required payload + in: body + name: payload + required: true + schema: + $ref: '#/definitions/request.ChatSessionUpdateRequest' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.BadRequestError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.UnauthorizedError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.InternalServerError' + security: + - Bearer: [] + summary: Update chat session + tags: + - Chat /cities: get: description: API for getting all Cities diff --git a/main.go b/main.go index 201a16a..b4ad6f6 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ import ( "narasi-ahli-be/app/module/article_comments" "narasi-ahli-be/app/module/article_files" "narasi-ahli-be/app/module/articles" + "narasi-ahli-be/app/module/chat" "narasi-ahli-be/app/module/cities" "narasi-ahli-be/app/module/custom_static_pages" "narasi-ahli-be/app/module/districts" @@ -74,6 +75,7 @@ func main() { article_approvals.NewArticleApprovalsModule, articles.NewArticlesModule, article_comments.NewArticleCommentsModule, + chat.NewChatModule, cities.NewCitiesModule, custom_static_pages.NewCustomStaticPagesModule, districts.NewDistrictsModule, diff --git a/migrations/001_create_chat_history_tables.go b/migrations/001_create_chat_history_tables.go deleted file mode 100644 index c76b361..0000000 --- a/migrations/001_create_chat_history_tables.go +++ /dev/null @@ -1,28 +0,0 @@ -package migrations - -import ( - "narasi-ahli-be/app/database" - "narasi-ahli-be/app/database/entity" -) - -// CreateChatHistoryTables creates the chat history tables -func CreateChatHistoryTables(db *database.Database) error { - // Auto-migrate the new entities - err := db.DB.AutoMigrate( - &entity.ChatSessions{}, - &entity.ChatMessagesNew{}, - ) - if err != nil { - return err - } - - // Create indexes manually if needed - db.DB.Exec("CREATE INDEX IF NOT EXISTS idx_chat_sessions_user_id ON chat_sessions (user_id)") - db.DB.Exec("CREATE INDEX IF NOT EXISTS idx_chat_sessions_agent_id ON chat_sessions (agent_id)") - db.DB.Exec("CREATE INDEX IF NOT EXISTS idx_chat_sessions_session_id ON chat_sessions (session_id)") - db.DB.Exec("CREATE INDEX IF NOT EXISTS idx_chat_sessions_created_at ON chat_sessions (created_at)") - db.DB.Exec("CREATE INDEX IF NOT EXISTS idx_chat_messages_new_session_id ON chat_messages_new (session_id)") - db.DB.Exec("CREATE INDEX IF NOT EXISTS idx_chat_messages_new_created_at ON chat_messages_new (created_at)") - - return nil -} diff --git a/migrations/001_create_chat_history_tables.sql b/migrations/001_create_chat_history_tables.sql deleted file mode 100644 index 155f9bd..0000000 --- a/migrations/001_create_chat_history_tables.sql +++ /dev/null @@ -1,53 +0,0 @@ --- Migration: Create Chat History Tables --- Description: Create tables for chat history functionality based on the old web API structure --- Date: 2024-01-01 - --- Create chat_sessions table -CREATE TABLE IF NOT EXISTS chat_sessions ( - id SERIAL PRIMARY KEY, - session_id VARCHAR(255) NOT NULL UNIQUE, - user_id INTEGER NOT NULL, - agent_id VARCHAR(255) NOT NULL, - title VARCHAR(255) NOT NULL DEFAULT 'Chat Session', - message_count INTEGER DEFAULT 0, - status VARCHAR(50) DEFAULT 'active', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - --- Create chat_messages_new table (to avoid conflict with existing chat_messages) -CREATE TABLE IF NOT EXISTS chat_messages_new ( - id SERIAL PRIMARY KEY, - session_id VARCHAR(255) NOT NULL, - message_type VARCHAR(50) NOT NULL CHECK (message_type IN ('user', 'assistant')), - content TEXT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - --- Create indexes for better performance -CREATE INDEX IF NOT EXISTS idx_chat_sessions_user_id ON chat_sessions (user_id); -CREATE INDEX IF NOT EXISTS idx_chat_sessions_agent_id ON chat_sessions (agent_id); -CREATE INDEX IF NOT EXISTS idx_chat_sessions_session_id ON chat_sessions (session_id); -CREATE INDEX IF NOT EXISTS idx_chat_sessions_created_at ON chat_sessions (created_at); -CREATE INDEX IF NOT EXISTS idx_chat_messages_new_session_id ON chat_messages_new (session_id); -CREATE INDEX IF NOT EXISTS idx_chat_messages_new_created_at ON chat_messages_new (created_at); - --- Add foreign key constraints -ALTER TABLE chat_sessions -ADD CONSTRAINT fk_chat_sessions_user_id -FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - -ALTER TABLE chat_messages_new -ADD CONSTRAINT fk_chat_messages_new_session_id -FOREIGN KEY (session_id) REFERENCES chat_sessions(session_id) ON DELETE CASCADE; - --- Insert sample data (optional) -INSERT INTO chat_sessions (session_id, user_id, agent_id, title, message_count) -VALUES ('sample-session-123', 1, 'd04bbced-ae93-4fb3-a015-9472e4e5e539', 'Sample Chat Session', 2) -ON CONFLICT (session_id) DO NOTHING; - -INSERT INTO chat_messages_new (session_id, message_type, content) -VALUES - ('sample-session-123', 'user', 'Halo, ada yang bisa saya tanyakan?'), - ('sample-session-123', 'assistant', 'Halo! Tentu saja, saya siap membantu Anda dengan pertanyaan apapun.') -ON CONFLICT DO NOTHING;