package calendar import ( "context" "errors" "time" "github.com/jackc/pgx/v5" ) var ErrChatMessageNotFound = errors.New("chat message not found") const ( ChatChannelExternal = "external" ChatChannelInternal = "internal" ) type ChatMessage struct { ID string `json:"id"` CalendarItemID string `json:"calendar_item_id"` Channel string `json:"channel"` Body string `json:"body"` CreatedBy *string `json:"created_by"` AuthorName string `json:"author_name"` AuthorRole string `json:"author_role"` CreatedAt time.Time `json:"created_at"` EditedAt *time.Time `json:"edited_at"` DeletedAt *time.Time `json:"deleted_at"` Attachments []ChatAttachment `json:"attachments"` } type ChatAttachment struct { ID string `json:"id"` ChatMessageID string `json:"chat_message_id"` OriginalFilename string `json:"original_filename"` StoredFilename string `json:"stored_filename"` MimeType string `json:"mime_type"` SizeBytes int64 `json:"size_bytes"` StorageDriver string `json:"storage_driver"` StoragePath string `json:"-"` CreatedBy *string `json:"created_by"` CreatedAt time.Time `json:"created_at"` } type CreateChatMessageInput struct { CalendarItemID string Channel string Body string CreatedBy string } type CreateChatAttachmentInput struct { ChatMessageID string OriginalFilename string StoredFilename string MimeType string SizeBytes int64 StorageDriver string StoragePath string CreatedBy string } func (s Store) ListChatMessages(ctx context.Context, calendarItemID string, channel string) ([]ChatMessage, error) { rows, err := s.db.Query(ctx, ` SELECT calendar_item_chat_messages.id::text, calendar_item_chat_messages.calendar_item_id::text, calendar_item_chat_messages.channel, calendar_item_chat_messages.body, calendar_item_chat_messages.created_by::text, COALESCE(users.name, 'Usuario removido'), COALESCE(users.role, ''), calendar_item_chat_messages.created_at, calendar_item_chat_messages.edited_at, calendar_item_chat_messages.deleted_at FROM calendar_item_chat_messages LEFT JOIN users ON users.id = calendar_item_chat_messages.created_by WHERE calendar_item_chat_messages.calendar_item_id = $1::uuid AND calendar_item_chat_messages.channel = $2 ORDER BY calendar_item_chat_messages.created_at ASC `, calendarItemID, channel) if err != nil { return nil, err } defer rows.Close() messages := []ChatMessage{} for rows.Next() { message, err := scanChatMessage(rows) if err != nil { return nil, err } messages = append(messages, message) } if err := rows.Err(); err != nil { return nil, err } if err := s.attachChatAttachments(ctx, messages); err != nil { return nil, err } return messages, nil } func (s Store) CreateChatMessage(ctx context.Context, input CreateChatMessageInput) (ChatMessage, error) { row := s.db.QueryRow(ctx, ` INSERT INTO calendar_item_chat_messages ( calendar_item_id, channel, body, created_by ) VALUES ($1::uuid, $2, $3, NULLIF($4, '')::uuid) RETURNING calendar_item_chat_messages.id::text, calendar_item_chat_messages.calendar_item_id::text, calendar_item_chat_messages.channel, calendar_item_chat_messages.body, calendar_item_chat_messages.created_by::text, COALESCE((SELECT users.name FROM users WHERE users.id = calendar_item_chat_messages.created_by), 'Usuario removido'), COALESCE((SELECT users.role FROM users WHERE users.id = calendar_item_chat_messages.created_by), ''), calendar_item_chat_messages.created_at, calendar_item_chat_messages.edited_at, calendar_item_chat_messages.deleted_at `, input.CalendarItemID, input.Channel, input.Body, input.CreatedBy) return scanChatMessage(row) } func (s Store) FindChatMessage(ctx context.Context, id string) (ChatMessage, error) { row := s.db.QueryRow(ctx, ` SELECT calendar_item_chat_messages.id::text, calendar_item_chat_messages.calendar_item_id::text, calendar_item_chat_messages.channel, calendar_item_chat_messages.body, calendar_item_chat_messages.created_by::text, COALESCE(users.name, 'Usuario removido'), COALESCE(users.role, ''), calendar_item_chat_messages.created_at, calendar_item_chat_messages.edited_at, calendar_item_chat_messages.deleted_at FROM calendar_item_chat_messages LEFT JOIN users ON users.id = calendar_item_chat_messages.created_by WHERE calendar_item_chat_messages.id = $1::uuid `, id) message, err := scanChatMessage(row) if err != nil { return ChatMessage{}, err } messages := []ChatMessage{message} if err := s.attachChatAttachments(ctx, messages); err != nil { return ChatMessage{}, err } return messages[0], nil } func (s Store) UpdateChatMessage(ctx context.Context, id string, body string) (ChatMessage, error) { row := s.db.QueryRow(ctx, ` UPDATE calendar_item_chat_messages SET body = $2, edited_at = now() WHERE id = $1::uuid AND deleted_at IS NULL RETURNING calendar_item_chat_messages.id::text, calendar_item_chat_messages.calendar_item_id::text, calendar_item_chat_messages.channel, calendar_item_chat_messages.body, calendar_item_chat_messages.created_by::text, COALESCE((SELECT users.name FROM users WHERE users.id = calendar_item_chat_messages.created_by), 'Usuario removido'), COALESCE((SELECT users.role FROM users WHERE users.id = calendar_item_chat_messages.created_by), ''), calendar_item_chat_messages.created_at, calendar_item_chat_messages.edited_at, calendar_item_chat_messages.deleted_at `, id, body) message, err := scanChatMessage(row) if err != nil { return ChatMessage{}, err } messages := []ChatMessage{message} if err := s.attachChatAttachments(ctx, messages); err != nil { return ChatMessage{}, err } return messages[0], nil } func (s Store) DeleteChatMessage(ctx context.Context, id string) (ChatMessage, error) { row := s.db.QueryRow(ctx, ` UPDATE calendar_item_chat_messages SET deleted_at = now(), body = '' WHERE id = $1::uuid AND deleted_at IS NULL RETURNING calendar_item_chat_messages.id::text, calendar_item_chat_messages.calendar_item_id::text, calendar_item_chat_messages.channel, calendar_item_chat_messages.body, calendar_item_chat_messages.created_by::text, COALESCE((SELECT users.name FROM users WHERE users.id = calendar_item_chat_messages.created_by), 'Usuario removido'), COALESCE((SELECT users.role FROM users WHERE users.id = calendar_item_chat_messages.created_by), ''), calendar_item_chat_messages.created_at, calendar_item_chat_messages.edited_at, calendar_item_chat_messages.deleted_at `, id) return scanChatMessage(row) } func (s Store) CreateChatAttachment(ctx context.Context, input CreateChatAttachmentInput) (ChatAttachment, error) { row := s.db.QueryRow(ctx, ` INSERT INTO calendar_item_chat_attachments ( chat_message_id, original_filename, stored_filename, mime_type, size_bytes, storage_driver, storage_path, created_by ) VALUES ($1::uuid, $2, $3, $4, $5, $6, $7, NULLIF($8, '')::uuid) RETURNING id::text, chat_message_id::text, original_filename, stored_filename, mime_type, size_bytes, storage_driver, storage_path, created_by::text, created_at `, input.ChatMessageID, input.OriginalFilename, input.StoredFilename, input.MimeType, input.SizeBytes, input.StorageDriver, input.StoragePath, input.CreatedBy) return scanChatAttachment(row) } func (s Store) FindChatAttachment(ctx context.Context, id string, viewerUserID string) (ChatAttachment, error) { row := s.db.QueryRow(ctx, ` SELECT calendar_item_chat_attachments.id::text, calendar_item_chat_attachments.chat_message_id::text, calendar_item_chat_attachments.original_filename, calendar_item_chat_attachments.stored_filename, calendar_item_chat_attachments.mime_type, calendar_item_chat_attachments.size_bytes, calendar_item_chat_attachments.storage_driver, calendar_item_chat_attachments.storage_path, calendar_item_chat_attachments.created_by::text, calendar_item_chat_attachments.created_at FROM calendar_item_chat_attachments INNER JOIN calendar_item_chat_messages ON calendar_item_chat_messages.id = calendar_item_chat_attachments.chat_message_id INNER JOIN calendar_items ON calendar_items.id = calendar_item_chat_messages.calendar_item_id WHERE calendar_item_chat_attachments.id = $1::uuid AND calendar_item_chat_messages.deleted_at IS NULL AND ( $2 = '' OR ( calendar_item_chat_messages.channel = 'external' AND EXISTS ( SELECT 1 FROM client_users WHERE client_users.client_id = calendar_items.client_id AND client_users.user_id = $2::uuid ) ) ) `, id, viewerUserID) return scanChatAttachment(row) } func (s Store) attachChatAttachments(ctx context.Context, messages []ChatMessage) error { if len(messages) == 0 { return nil } ids := make([]string, 0, len(messages)) indexByID := map[string]int{} for index, message := range messages { ids = append(ids, message.ID) indexByID[message.ID] = index } rows, err := s.db.Query(ctx, ` SELECT id::text, chat_message_id::text, original_filename, stored_filename, mime_type, size_bytes, storage_driver, storage_path, created_by::text, created_at FROM calendar_item_chat_attachments WHERE chat_message_id::text = ANY($1) ORDER BY created_at ASC `, ids) if err != nil { return err } defer rows.Close() for rows.Next() { attachment, err := scanChatAttachment(rows) if err != nil { return err } index, ok := indexByID[attachment.ChatMessageID] if ok { messages[index].Attachments = append(messages[index].Attachments, attachment) } } return rows.Err() } type chatMessageScanner interface { Scan(dest ...any) error } func scanChatMessage(row chatMessageScanner) (ChatMessage, error) { var message ChatMessage err := row.Scan( &message.ID, &message.CalendarItemID, &message.Channel, &message.Body, &message.CreatedBy, &message.AuthorName, &message.AuthorRole, &message.CreatedAt, &message.EditedAt, &message.DeletedAt, ) if errors.Is(err, pgx.ErrNoRows) { return ChatMessage{}, ErrChatMessageNotFound } if err != nil { return ChatMessage{}, err } message.Attachments = []ChatAttachment{} return message, nil } type chatAttachmentScanner interface { Scan(dest ...any) error } func scanChatAttachment(row chatAttachmentScanner) (ChatAttachment, error) { var attachment ChatAttachment err := row.Scan( &attachment.ID, &attachment.ChatMessageID, &attachment.OriginalFilename, &attachment.StoredFilename, &attachment.MimeType, &attachment.SizeBytes, &attachment.StorageDriver, &attachment.StoragePath, &attachment.CreatedBy, &attachment.CreatedAt, ) if errors.Is(err, pgx.ErrNoRows) { return ChatAttachment{}, ErrAttachmentNotFound } if err != nil { return ChatAttachment{}, err } return attachment, nil }