package socket import ( "fmt" "github.com/google/uuid" "log" "relay-server/database" "relay-server/utils" socketio "github.com/googollee/go-socket.io" ) // Message represents the chat message structure type Message struct { Content string `json:"message"` Recipient string `json:"recipient"` RecipientID uuid.UUID `json:"recipient_id"` AttachmentURLs []string `json:"attachment_urls"` } // DeleteMessageData represents the message deletion structure type DeleteMessageData struct { ConversationID uuid.UUID `json:"conversation_id"` MessageID int `json:"message_id"` } // GroupUserData represents group-user operations data type GroupUserData struct { GroupID uuid.UUID `json:"group_id"` UserID uuid.UUID `json:"user_id"` } // MessageReadData represents message read status data type MessageReadData struct { ConversationID uuid.UUID `json:"conversation_id"` MessageID int `json:"message_id"` } // SocketResponse represents a standard socket response type SocketResponse struct { Status string `json:"status"` Message string `json:"message"` } // InitializeSocket sets up and configures the Socket.IO server func InitializeSocket() (*socketio.Server, error) { server := socketio.NewServer(nil) // Middleware for authentication server.OnConnect("/", func(s socketio.Conn) error { token := s.RemoteHeader().Get("Authorization") if token == "" { log.Println("(socket) Not logged in") return fmt.Errorf("not logged in") } c, err := utils.ValidateToken(token) if err != nil { log.Printf("(socket) Token verification failed: %v\n", err) return fmt.Errorf("invalid token") } if !utils.IsValidUsername(c.Username) { log.Println("(socket) Invalid username") return fmt.Errorf("invalid username") } s.SetContext(map[string]interface{}{ "username": c.Username, "user_id": c.UserID, }) log.Printf("(socket) socket id: %s, username: %s, user_id: %s\n", s.ID(), c.Username, c.UserID) return nil }) // Handle connection server.OnEvent("/", "connection", func(s socketio.Conn) { ctx := s.Context().(map[string]interface{}) username := ctx["username"].(string) userID := ctx["user_id"].(string) if !utils.IsValidUsername(username) { s.Close() return } conversations, err := database.GetUserConversations(database.DB, userID) if err != nil { log.Printf("(socket) Failed to get user conversations: %v\n", err) return } // Join all conversations for _, conv := range conversations { s.Join(conv) } s.Join(userID) // Join user's personal room log.Printf("User: %s joined to: %v\n", username, conversations) }) // Handle chat message server.OnEvent("/", "chat message", func(s socketio.Conn, msg Message) SocketResponse { ctx := s.Context().(map[string]interface{}) username := ctx["username"].(string) userIDstr := ctx["user_id"].(string) conversationIDstr := ctx["recipient_id"].(string) userID, err := uuid.Parse(userIDstr) if err != nil { return SocketResponse{Status: "error", Message: "Invalid user id"} } conversationID, err := uuid.Parse(conversationIDstr) if err != nil { return SocketResponse{Status: "error", Message: "Invalid conversation id"} } if msg.Content == "" && len(msg.AttachmentURLs) == 0 { return SocketResponse{Status: "error", Message: "No message or attachment provided"} } if msg.Recipient == "" { return SocketResponse{Status: "error", Message: "No recipient provided"} } insertedMsg, err := database.InsertMessage(database.DB, userID, conversationID, msg.Content, msg.AttachmentURLs) if err != nil { log.Printf("(socket) Failed to insert message: %v\n", err) return SocketResponse{Status: "error", Message: "Failed to insert message"} } // Emit message to recipients server.BroadcastToRoom("", msg.Recipient, "chat message", map[string]interface{}{ "sender": username, "message": insertedMsg.Message, "attachment_urls": msg.AttachmentURLs, "recipient": msg.Recipient, "message_id": insertedMsg.ID, "sender_id": userID, "sent_at": insertedMsg.SentAt, "conversation_id": insertedMsg.ConversationID, }) return SocketResponse{Status: "ok", Message: "Received message"} }) // Handle delete message server.OnEvent("/", "delete message", func(s socketio.Conn, data DeleteMessageData) SocketResponse { ctx := s.Context().(map[string]interface{}) userIDstr := ctx["user_id"].(string) userID, err := uuid.Parse(userIDstr) if err != nil { return SocketResponse{Status: "error", Message: "Invalid user id"} } if data.MessageID == 0 { return SocketResponse{Status: "error", Message: "No message id provided"} } if data.ConversationID == uuid.Nil { return SocketResponse{Status: "error", Message: "No conversation id provided"} } err = database.DeleteMessage(database.DB, userID, data.ConversationID, data.MessageID) if err != nil { return SocketResponse{Status: "error", Message: err.Error()} } conversationIDstr := data.ConversationID.String() server.BroadcastToRoom("", conversationIDstr, "delete message", data) return SocketResponse{Status: "ok", Message: "Successfully deleted message"} }) // Handle remove user from group server.OnEvent("/", "remove user from group", func(s socketio.Conn, data GroupUserData) SocketResponse { if data.GroupID == uuid.Nil { return SocketResponse{Status: "error", Message: "No group id provided"} } if data.UserID == uuid.Nil { return SocketResponse{Status: "error", Message: "No user id provided"} } err := removeUserFromGroupById(data.GroupID, data.UserID) if err != nil { return SocketResponse{Status: "error", Message: err.Error()} } // Remove user from room sockets := server.Sockets(data.GroupID) for _, socket := range sockets { if socket.Context().(map[string]interface{})["user_id"] == data.UserID { socket.Leave(data.GroupID) } } server.BroadcastToRoom("", data.GroupID, "left group", data) server.BroadcastToRoom("", data.UserID, "left group", data) return SocketResponse{Status: "ok", Message: "Successfully removed user from group"} }) // Handle administrator operations server.OnEvent("/", "added administrator", func(s socketio.Conn, data GroupUserData) SocketResponse { ctx := s.Context().(map[string]interface{}) userID := ctx["user_id"].(string) if data.GroupID == "" { return SocketResponse{Status: "error", Message: "No conversation id provided"} } if data.UserID == "" { return SocketResponse{Status: "error", Message: "No user id provided"} } isAdmin, err := isAdmin(userID, data.GroupID) if err != nil || !isAdmin { return SocketResponse{Status: "error", Message: "You are not an administrator"} } err = addAdministrator(data.GroupID, data.UserID, userID) if err != nil { return SocketResponse{Status: "error", Message: err.Error()} } server.BroadcastToRoom("", data.GroupID, "added administrator", data) return SocketResponse{Status: "ok", Message: "Successfully added administrator"} }) // Handle message read status server.OnEvent("/", "message read", func(s socketio.Conn, data MessageReadData) { ctx := s.Context().(map[string]interface{}) userID := ctx["user_id"].(string) if data.ConversationID == "" || data.MessageID == "" { return } err := updateContactStatus(userID, data.ConversationID, data.MessageID) if err != nil { log.Printf("Failed to update message read status: %v\n", err) } }) // Handle disconnection server.OnDisconnect("/", func(s socketio.Conn, reason string) { log.Printf("(socket) %s disconnected due to: %s\n", s.ID(), reason) }) return server, nil } // The following functions would need to be implemented according to your specific needs: // - verifyJwtToken // - isValidUsername // - getConversationsForUser // - insertMessage // - deleteMessage // - removeUserFromGroupById // - isAdmin // - addAdministrator // - updateContactStatus