package database import ( "database/sql" "errors" "fmt" "github.com/google/uuid" "relay-server/model" "relay-server/utils" "strings" ) func DeleteContact(db *sql.DB, userID uuid.UUID, conversationID uuid.UUID) error { var conversationType string err := db.QueryRow( "SELECT conversation_type FROM Conversations WHERE conversation_id = $1", conversationID, ).Scan(&conversationType) if err != nil { if errors.Is(err, sql.ErrNoRows) { return utils.NewError(utils.ErrNotFound, "no contacts found for this id", fmt.Errorf("no conversation found with id: %s", conversationID)) } return utils.NewError(utils.ErrInternal, "Failed to check conversation", err) } if conversationType == "group" { // Delete from Contacts res, err := db.Exec( "DELETE FROM Contacts WHERE conversation_id = $1 AND user_id = $2", conversationID, userID, ) if err != nil { return utils.NewError(utils.ErrInternal, "Failed to delete contact", err) } rowsAffected, err := res.RowsAffected() if err != nil { return utils.NewError(utils.ErrInternal, "internal server error", fmt.Errorf("failed to verify contact deletion: %w", err)) } if rowsAffected == 0 { return utils.NewError(utils.ErrNotFound, fmt.Sprintf("no matching contact found with conversation id: %s, user id: %s", conversationID, userID), nil) } // Delete from Memberships res, err = db.Exec( "DELETE FROM Memberships WHERE conversation_id = $1 AND user_id = $2", conversationID, userID, ) if err != nil { return utils.NewError(utils.ErrInternal, "internal server error", fmt.Errorf("failed to delete membership: %w", err)) } rowsAffected, err = res.RowsAffected() if err != nil { return utils.NewError(utils.ErrInternal, "internal server error", fmt.Errorf("failed to verify membership deletion: %w", err)) } if rowsAffected == 0 { return utils.NewError(utils.ErrNotFound, "No membership found", err) } } else { res, err := db.Exec( "DELETE FROM Contacts WHERE user_id = $1 AND conversation_id = $2", userID, conversationID, ) if err != nil { return utils.NewError(utils.ErrInternal, "Failed to delete contact", err) } rowsAffected, err := res.RowsAffected() if err != nil { return utils.NewError(utils.ErrInternal, "internal server error", fmt.Errorf("failed to verify contact deletion: %w", err)) } if rowsAffected == 0 { return utils.NewError(utils.ErrNotFound, fmt.Sprintf("no matching contact found with user id: %s, conversation id: %s", userID, conversationID), nil) } } return nil } func InsertContact(db *sql.DB, userID uuid.UUID, contactID uuid.UUID, contactUsername string) (*model.Contact, error) { isSelfContact := userID == contactID var conversationID uuid.UUID if isSelfContact { err := db.QueryRow(` SELECT c.conversation_id FROM Conversations c JOIN Memberships m ON c.conversation_id = m.conversation_id WHERE c.conversation_type = 'direct' AND m.user_id = $1 AND (SELECT COUNT(*) FROM Memberships WHERE conversation_id = c.conversation_id) = 1 LIMIT 1; `, userID).Scan(&conversationID) if err != nil && !errors.Is(err, sql.ErrNoRows) { return &model.Contact{}, utils.NewError(utils.ErrInternal, "internal server error", fmt.Errorf("failed to check existing conversation: %w", err)) } if conversationID == uuid.Nil { err := db.QueryRow(` INSERT INTO Conversations (conversation_type) VALUES ('direct') RETURNING conversation_id; `).Scan(&conversationID) if err != nil { return &model.Contact{}, utils.NewError(utils.ErrInternal, "internal server error", fmt.Errorf("failed to create conversation: %w", err)) } _, err = db.Exec(` INSERT INTO Memberships (conversation_id, user_id) VALUES ($1, $2) ON CONFLICT (conversation_id, user_id) DO NOTHING; `, conversationID, userID) if err != nil { return &model.Contact{}, utils.NewError(utils.ErrInternal, "internal server error", fmt.Errorf("failed to create membership: %w", err)) } } } else { err := db.QueryRow(` SELECT c.conversation_id FROM Conversations c JOIN Memberships m1 ON c.conversation_id = m1.conversation_id JOIN Memberships m2 ON c.conversation_id = m2.conversation_id WHERE c.conversation_type = 'direct' AND ((m1.user_id = $1 AND m2.user_id = $2) OR (m1.user_id = $2 AND m2.user_id = $1)) LIMIT 1; `, userID, contactID).Scan(&conversationID) if err != nil && !errors.Is(err, sql.ErrNoRows) { return &model.Contact{}, utils.NewError(utils.ErrInternal, "internal server error", fmt.Errorf("failed to check existing conversation %w", err)) } if conversationID == uuid.Nil { err := db.QueryRow(` INSERT INTO Conversations (conversation_type) VALUES ('direct') RETURNING conversation_id; `).Scan(&conversationID) if err != nil { return &model.Contact{}, utils.NewError(utils.ErrInternal, "internal server error", fmt.Errorf("failed to create conversation: %w", err)) } _, err = db.Exec(` INSERT INTO Memberships (conversation_id, user_id) VALUES ($1, $2), ($1, $3) ON CONFLICT (conversation_id, user_id) DO NOTHING; `, conversationID, userID, contactID) if err != nil { return &model.Contact{}, utils.NewError(utils.ErrInternal, "internal server error", fmt.Errorf("failed to create memberships: %w", err)) } } } insertedContact, err := InsertContactByID(db, contactID, conversationID) if err != nil { return &model.Contact{}, err } latestMessage, err := GetLatestMessage(db, conversationID) if err != nil { return &model.Contact{}, err } contact := model.Contact{ ID: insertedContact.ID, ConversationID: insertedContact.ConversationID, UserID: insertedContact.UserID, Username: contactUsername, Type: "direct", LastMessageID: latestMessage.LastMessageID, LastMessage: latestMessage.LastMessage, LastMessageTime: latestMessage.LastMessageTime, LastMessageSender: latestMessage.LastMessageSender, } return &contact, nil } func InsertContactByID(db *sql.DB, userID uuid.UUID, conversationID uuid.UUID) (*model.Contact, error) { // First check if contact already exists var contact model.Contact err := db.QueryRow(` SELECT contact_id, conversation_id, user_id FROM Contacts WHERE user_id = $1 AND conversation_id = $2 `, userID, conversationID).Scan(&contact.ID, &contact.ConversationID, &contact.UserID) if err == nil { return &model.Contact{}, utils.NewError(utils.ErrInvalidInput, "Contact already exists", nil) } else if !errors.Is(err, sql.ErrNoRows) { return &model.Contact{}, utils.NewError(utils.ErrInternal, "internal server error", fmt.Errorf("failed to check contact existence: %w", err)) } // Insert new contact err = db.QueryRow(` INSERT INTO Contacts (user_id, conversation_id) VALUES($1, $2) RETURNING contact_id, conversation_id, user_id `, userID, conversationID).Scan(&contact.ID, &contact.ConversationID, &contact.UserID) if err != nil { return &model.Contact{}, utils.NewError(utils.ErrInternal, "Failed to create contact", err) } return &contact, nil } func GetLatestMessage(db *sql.DB, conversationId uuid.UUID) (*model.Contact, error) { var latestMessage model.Contact err := db.QueryRow(` SELECT DISTINCT ON (m.conversation_id) m.message_id AS last_message_id, m.content AS last_message, m.sent_at AS last_message_time, a.username AS last_message_sender FROM Messages m JOIN Accounts a ON m.user_id = a.user_id WHERE m.conversation_id = $1 ORDER BY m.conversation_id, m.sent_at DESC LIMIT 1; `, conversationId).Scan( &latestMessage.LastMessageID, &latestMessage.LastMessage, &latestMessage.LastMessageTime, &latestMessage.LastMessageSender, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { return &model.Contact{}, nil } return &model.Contact{}, utils.NewError(utils.ErrInternal, "Failed to get latest message", fmt.Errorf("failed to get latest message: %w", err)) } return &latestMessage, nil } func GetContacts(db *sql.DB, userID uuid.UUID) ([]*model.Contact, error) { contactsQuery := ` WITH DirectContacts AS ( SELECT DISTINCT ON (c.conversation_id) c.contact_id AS id, a.user_id AS user_id, a.username AS username, conv.last_active, c.conversation_id, conv.conversation_type AS type, requesting_member.last_read_message_id FROM Contacts c JOIN Conversations conv ON c.conversation_id = conv.conversation_id JOIN Memberships requesting_member ON requesting_member.conversation_id = conv.conversation_id AND requesting_member.user_id = $1 JOIN Memberships other_member ON other_member.conversation_id = conv.conversation_id JOIN Accounts a ON a.user_id = other_member.user_id WHERE c.user_id = $1 AND conv.conversation_type = 'direct' AND ( a.user_id != $1 OR (SELECT COUNT(*) FROM Memberships WHERE conversation_id = c.conversation_id) = 1 ) ), GroupContacts AS ( SELECT DISTINCT ON (c.conversation_id) c.contact_id AS id, NULL::uuid AS user_id, conv.name AS username, conv.last_active, c.conversation_id, conv.conversation_type AS type, m.last_read_message_id FROM Contacts c JOIN Conversations conv ON c.conversation_id = conv.conversation_id JOIN Memberships m ON m.conversation_id = conv.conversation_id AND m.user_id = $1 WHERE c.user_id = $1 AND conv.conversation_type = 'group' ) SELECT * FROM DirectContacts UNION ALL SELECT * FROM GroupContacts ORDER BY last_active DESC NULLS LAST; ` rows, err := db.Query(contactsQuery, userID) if err != nil { return []*model.Contact{}, utils.NewError(utils.ErrInternal, "Failed to get contacts", fmt.Errorf("failed to get contacts: %w", err)) } defer rows.Close() var contacts []*model.Contact for rows.Next() { contact := &model.Contact{} err := rows.Scan(&contact.ID, &contact.UserID, &contact.Username, &contact.LastActive, &contact.ConversationID, &contact.Type, &contact.LastReadMessageID) if err != nil { return []*model.Contact{}, utils.NewError(utils.ErrInternal, "internal server error", fmt.Errorf("failed to scan contact: %w", err)) } latestMessage, err := GetLatestMessage(db, contact.ConversationID) if err != nil { return []*model.Contact{}, err } contact.LastMessageID = latestMessage.LastMessageID contact.LastMessage = latestMessage.LastMessage contact.LastMessageTime = latestMessage.LastMessageTime contact.LastMessageSender = latestMessage.LastMessageSender contacts = append(contacts, contact) } if err = rows.Err(); err != nil { return []*model.Contact{}, utils.NewError(utils.ErrInternal, "internal server error", fmt.Errorf("failed to process contacts: %w", err)) } return contacts, nil } func ContactSuggestion(db *sql.DB, contactUsername string) ([]string, error) { query := ` SELECT username FROM accounts WHERE LOWER(username) LIKE $1 LIMIT 5; ` rows, err := db.Query(query, "%"+strings.ToLower(contactUsername)+"%") if err != nil && !errors.Is(err, sql.ErrNoRows) { return []string{}, utils.NewError(utils.ErrInternal, "Failed to get contact suggestions", fmt.Errorf("failed to get contact suggestions: %w", err)) } defer rows.Close() var suggestions []string for rows.Next() { var suggestion string err := rows.Scan(&suggestion) if err != nil { return []string{}, utils.NewError(utils.ErrInternal, "internal server error", fmt.Errorf("failed to scan contact suggestion: %w", err)) } suggestions = append(suggestions, suggestion) } if err = rows.Err(); err != nil { return []string{}, utils.NewError(utils.ErrInternal, "internal server error", fmt.Errorf("failed to process suggestions: %w", err)) } return suggestions, nil }