diff --git a/database/contacts.go b/database/contacts.go index 118d286..72eb6c5 100644 --- a/database/contacts.go +++ b/database/contacts.go @@ -347,3 +347,17 @@ func ContactSuggestion(db *sql.DB, contactUsername string) ([]string, error) { } return suggestions, nil } + +func UpdateContactStatus(db *sql.DB, userID uuid.UUID, conversationID uuid.UUID, lastReadMessageID int) error { + query := ` + UPDATE Memberships + SET last_read_message_id = $1 + WHERE user_id = $2 + AND conversation_id = $3; + ` + _, err := db.Exec(query, lastReadMessageID, userID, conversationID) + if err != nil { + return fmt.Errorf("failed to update contact status: %w", err) + } + return nil +} diff --git a/database/groups.go b/database/groups.go index d0cc2dc..8f096f7 100644 --- a/database/groups.go +++ b/database/groups.go @@ -170,6 +170,11 @@ func RemoveUserFromGroup(db *sql.DB, userID uuid.UUID, groupID uuid.UUID) (strin WHERE conversation_id = $1 AND user_id = $2; ` + removeAdministratorPermissionsQuery := ` + DELETE FROM GroupAdmins + WHERE conversation_id = $1 AND user_id = $2; + ` + isOwner, err := IsGroupOwner(db, userID, groupID) if err != nil { return "Failed to remove user from group", fmt.Errorf("failed to check if user is group owner: %w", err) @@ -188,6 +193,12 @@ func RemoveUserFromGroup(db *sql.DB, userID uuid.UUID, groupID uuid.UUID) (strin if rowsAffected == 0 { return "User is not a member of the group", nil } + + // If user is an admin, remove admin permissions + _, err = db.Exec(removeAdministratorPermissionsQuery, groupID, userID) + if err != nil { + return "Failed to remove user from group", fmt.Errorf("failed to remove admin permissions: %w", err) + } return "Successfully removed user from group", nil } @@ -207,3 +218,34 @@ func IsGroupOwner(db *sql.DB, userID uuid.UUID, groupID uuid.UUID) (bool, error) } return isOwner, nil } + +func AddAdministrator(db *sql.DB, userID uuid.UUID, groupID uuid.UUID, grantedBy uuid.UUID) (string, error) { + query := ` + INSERT INTO GroupAdmins (conversation_id, user_id, granted_by, is_owner) + VALUES ($1, $2, $3, false) + RETURNING granted_at; + ` + isOwner, err := IsGroupOwner(db, grantedBy, groupID) + if err != nil { + return "Failed to add administrator", err + } + if !isOwner { + return "You are not the group owner", nil + } + + isMember, err := IsMember(db, userID, groupID) + if err != nil { + return "Failed to add administrator", err + } + if !isMember { + return "User is not a member of the group", nil + } + + var grantedAt string + err = db.QueryRow(query, groupID, userID, grantedBy).Scan(&grantedAt) + if err != nil { + return "Failed to add administrator", fmt.Errorf("failed to add administrator: %w", err) + } + return "Successfully added administrator", nil + +} diff --git a/main.go b/main.go index a702fb0..22763aa 100644 --- a/main.go +++ b/main.go @@ -1,11 +1,14 @@ package main import ( - "github.com/gofiber/contrib/websocket" "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/adaptor" + "github.com/gofiber/fiber/v2/middleware/cors" + socketio "github.com/googollee/go-socket.io" "log" "relay-server/database" "relay-server/router" + "relay-server/socket" "relay-server/utils" ) @@ -14,14 +17,14 @@ func main() { ErrorHandler: utils.ErrorHandler, }) - app.Use(func(c *fiber.Ctx) error { - if websocket.IsWebSocketUpgrade(c) { - c.Locals("allowed", true) - return c.Next() - } - return fiber.ErrUpgradeRequired - }) + server := socketio.NewServer(nil) + app.Use(cors.New(cors.Config{ + AllowOrigins: "http://localhost:5173", + AllowHeaders: "Origin, Content-Type, Accept, Authorization", + AllowMethods: "GET,POST,PUT,DELETE", + //AllowCredentials: true, + })) db, err := database.Init() if err != nil { log.Fatal("Failed to initialize database") @@ -34,6 +37,29 @@ func main() { log.Println("Database connection closed") }() + err = socket.InitializeSocket(server) + if err != nil { + log.Println(err) + } + go func() { + err := server.Serve() + if err != nil { + log.Println(err) + } + }() + defer func(server *socketio.Server) { + err := server.Close() + if err != nil { + log.Println("disconnected from server", err) + } + }(server) + + app.Use("/socket.io/*", adaptor.HTTPHandler(server)) + + app.Get("/socket.io/", adaptor.HTTPHandler(server)) + + // Setup routes router.SetupRoutes(app) - app.Listen(":3000") + + log.Fatal(app.Listen(":3000")) } diff --git a/model/model.go b/model/model.go index 28036f0..d15569c 100644 --- a/model/model.go +++ b/model/model.go @@ -3,7 +3,6 @@ package model import ( "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" - "time" ) type UserClaims struct { @@ -12,17 +11,17 @@ type UserClaims struct { jwt.RegisteredClaims } type Contact struct { - ID int `json:"contact_id"` - ConversationID uuid.UUID `json:"conversation_id"` - UserID uuid.UUID `json:"user_id"` - Username string `json:"username"` - LastActive *time.Time `json:"last_active"` - Type string `json:"type"` - LastReadMessageID *int `json:"last_read_message_id"` - LastMessageID *int `json:"last_message_id"` - LastMessage *time.Time `json:"last_message"` - LastMessageTime *string `json:"last_message_time"` - LastMessageSender *string `json:"last_message_sender"` + ID int `json:"contact_id"` + ConversationID uuid.UUID `json:"conversation_id"` + UserID uuid.UUID `json:"user_id"` + Username string `json:"username"` + LastActive *string `json:"last_active"` + Type string `json:"type"` + LastReadMessageID *int `json:"last_read_message_id"` + LastMessageID *int `json:"last_message_id"` + LastMessage *string `json:"last_message"` + LastMessageTime *string `json:"last_message_time"` + LastMessageSender *string `json:"last_message_sender"` } type ContactSuggestion struct { @@ -32,7 +31,7 @@ type ContactSuggestion struct { type Message struct { ID int `json:"message_id"` Message string `json:"message"` - SentAt time.Time `json:"sent_at"` + SentAt string `json:"sent_at"` Sender string `json:"sender"` SenderID uuid.UUID `json:"sender_id"` AttachmentUrl *string `json:"attachment_url"` diff --git a/router/router.go b/router/router.go index d3ff93f..8b41568 100644 --- a/router/router.go +++ b/router/router.go @@ -39,6 +39,6 @@ func SetupRoutes(app *fiber.App) { groups.Get("/getMembers/:groupID", handlers.GetMembers) // Socket group - socket := chat.Group("/ws", middleware.Protected(), logger.New()) - socket.Get("/:id") + //socket := chat.Group("/ws", middleware.Protected(), logger.New()) + //socket.Get("/:id") } diff --git a/socket/socket.go b/socket/socket.go index 4d283b4..d61477a 100644 --- a/socket/socket.go +++ b/socket/socket.go @@ -10,6 +10,11 @@ import ( socketio "github.com/googollee/go-socket.io" ) +type JoinRoomResponse struct { + Status string `json:"status"` + Message string `json:"message"` +} + // Message represents the chat message structure type Message struct { Content string `json:"message"` @@ -43,11 +48,10 @@ type SocketResponse struct { } // InitializeSocket sets up and configures the Socket.IO server -func InitializeSocket() (*socketio.Server, error) { - server := socketio.NewServer(nil) - +func InitializeSocket(server *socketio.Server) error { // Middleware for authentication server.OnConnect("/", func(s socketio.Conn) error { + s.SetContext("") token := s.RemoteHeader().Get("Authorization") if token == "" { log.Println("(socket) Not logged in") @@ -90,12 +94,12 @@ func InitializeSocket() (*socketio.Server, error) { log.Printf("(socket) Failed to get user conversations: %v\n", err) return } - + log.Println("CONVERSATIONS", conversations) // Join all conversations for _, conv := range conversations { s.Join(conv) } - s.Join(userID) // Join user's personal room + s.Join(userID) log.Printf("User: %s joined to: %v\n", username, conversations) }) @@ -178,21 +182,30 @@ func InitializeSocket() (*socketio.Server, error) { return SocketResponse{Status: "error", Message: "No user id provided"} } - err := removeUserFromGroupById(data.GroupID, data.UserID) + msg, err := database.RemoveUserFromGroup(database.DB, data.GroupID, data.UserID) + if msg != "" { + return SocketResponse{Status: "error", Message: msg} + } 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.ForEach("/", data.GroupID.String(), func(c socketio.Conn) { + // Get the context and check user ID + if ctxData, ok := c.Context().(map[string]interface{}); ok { + if userIDFromCtx, ok := ctxData["user_id"].(string); ok { + if userIDFromCtx == data.UserID.String() { + c.Leave(data.GroupID.String()) + } + } } - } + }) + groupIDstr := data.GroupID.String() + userIDstr := data.UserID.String() - server.BroadcastToRoom("", data.GroupID, "left group", data) - server.BroadcastToRoom("", data.UserID, "left group", data) + server.BroadcastToRoom("", groupIDstr, "left group", data) + server.BroadcastToRoom("", userIDstr, "left group", data) return SocketResponse{Status: "ok", Message: "Successfully removed user from group"} }) @@ -200,59 +213,118 @@ func InitializeSocket() (*socketio.Server, error) { // 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) + userIDstr := ctx["user_id"].(string) - if data.GroupID == "" { + userID, err := uuid.Parse(userIDstr) + if err != nil { + return SocketResponse{Status: "error", Message: "Invalid user id"} + } + if data.GroupID == uuid.Nil { return SocketResponse{Status: "error", Message: "No conversation id provided"} } - if data.UserID == "" { + if data.UserID == uuid.Nil { return SocketResponse{Status: "error", Message: "No user id provided"} } - isAdmin, err := isAdmin(userID, data.GroupID) + isAdmin, err := database.IsAdmin(database.DB, data.UserID, data.GroupID) if err != nil || !isAdmin { return SocketResponse{Status: "error", Message: "You are not an administrator"} } - err = addAdministrator(data.GroupID, data.UserID, userID) + msg, err := database.AddAdministrator(database.DB, data.GroupID, data.UserID, userID) if err != nil { return SocketResponse{Status: "error", Message: err.Error()} } + if msg != "" { + return SocketResponse{Status: "error", Message: msg} + } - server.BroadcastToRoom("", data.GroupID, "added administrator", data) + groupIDstr := data.GroupID.String() + server.BroadcastToRoom("", groupIDstr, "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) { + server.OnEvent("/", "message read", func(s socketio.Conn, data MessageReadData) SocketResponse { ctx := s.Context().(map[string]interface{}) - userID := ctx["user_id"].(string) + userIDstr := ctx["user_id"].(string) - if data.ConversationID == "" || data.MessageID == "" { - return + userID, err := uuid.Parse(userIDstr) + if err != nil { + return SocketResponse{Status: "error", Message: "Invalid user id"} } - err := updateContactStatus(userID, data.ConversationID, data.MessageID) + if data.ConversationID == uuid.Nil || data.MessageID == 0 { + return SocketResponse{Status: "error", Message: "Invalid conversation or message id"} + } + + err = database.UpdateContactStatus(database.DB, userID, data.ConversationID, data.MessageID) if err != nil { log.Printf("Failed to update message read status: %v\n", err) } + return SocketResponse{Status: "ok", Message: "Successfully updated message read status"} + }) + + server.OnEvent("/", "join room", func(s socketio.Conn, conversationIDstr string, callback func(JoinRoomResponse)) { + // Get user data from context + ctx := s.Context().(map[string]interface{}) + userIDstr := ctx["user_id"].(string) + username := ctx["username"].(string) + log.Printf("JOIN ROOM SDJKLSDFJKLSDFJKLSDFJKLSDFSDFJKLSLDFJKJLSDFKSDFJKLSDFJKLSDFJKLSDFJLKSFDLJKSDFJKLSDFJLKSDFJLKSDFJLKSDFJLSDFKSDFJKLSDFJKLJKL") + userID, err := uuid.Parse(userIDstr) + if err != nil { + callback(JoinRoomResponse{ + Status: "error", + Message: "Invalid user id", + }) + return + } + + conversationID, err := uuid.Parse(conversationIDstr) + if err != nil { + callback(JoinRoomResponse{ + Status: "error", + Message: "Invalid conversation id", + }) + return + } + + // Check if user is member of the conversation + isMember, err := database.IsMember(database.DB, userID, conversationID) + if err != nil { + callback(JoinRoomResponse{ + Status: "error", + Message: "Failed to check conversation membership", + }) + return + } + + if isMember { + log.Printf("Join room for: %s, room: %s", username, conversationID) + s.Join(conversationIDstr) + callback(JoinRoomResponse{ + Status: "ok", + Message: "Successfully joined to conversation", + }) + } else { + callback(JoinRoomResponse{ + Status: "error", + Message: "You are not member of this group", + }) + } + }) + + server.OnEvent("/", "*", func(s socketio.Conn, event string, data interface{}) { + log.Printf("Received event: %s, data: %v\n", event, data) + }) + + server.OnError("/", func(s socketio.Conn, e error) { + log.Printf("Error: %v\n", e) }) // 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 + return nil } - -// The following functions would need to be implemented according to your specific needs: -// - verifyJwtToken -// - isValidUsername -// - getConversationsForUser -// - insertMessage -// - deleteMessage -// - removeUserFromGroupById -// - isAdmin -// - addAdministrator -// - updateContactStatus