Files
relay-server/socket/socket.go
2025-02-11 16:34:59 +01:00

259 lines
7.8 KiB
Go

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