messaging

package
v0.0.0-...-8fad524 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 12, 2026 License: BSD-3-Clause Imports: 24 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ChatMessage

type ChatMessage struct {
	ID                  string    `firestore:"id"`                  // Message UUID
	EncryptedContent    string    `firestore:"encryptedContent"`    // Encrypted message content
	IsFromUser          bool      `firestore:"isFromUser"`          // true = user, false = assistant
	ChatID              string    `firestore:"chatId"`              // Chat UUID
	IsError             bool      `firestore:"isError"`             // true if error occurred
	Timestamp           time.Time `firestore:"timestamp"`           // Message timestamp
	PublicEncryptionKey string    `firestore:"publicEncryptionKey"` // Public key used (JSON string or "none")

	// Stop control fields (for AI responses that were stopped mid-generation)
	Stopped    bool   `firestore:"stopped,omitempty"`    // true if generation was stopped by user/system
	StoppedBy  string `firestore:"stoppedBy,omitempty"`  // User ID who stopped, or "system_timeout"/"system_shutdown"
	StopReason string `firestore:"stopReason,omitempty"` // Why stopped: "user_cancelled", "timeout", "error", "system_shutdown"

	// Generation state tracking (for GPT-5 Pro and other long-running models)
	Model                 string    `firestore:"model,omitempty"`                 // Model ID (e.g., "gpt-5-pro")
	GenerationState       string    `firestore:"generationState,omitempty"`       // "thinking", "completed", "failed"
	GenerationStartedAt   time.Time `firestore:"generationStartedAt,omitempty"`   // When generation started
	GenerationCompletedAt time.Time `firestore:"generationCompletedAt,omitempty"` // When generation completed/failed
	GenerationError       string    `firestore:"generationError,omitempty"`       // Error message if failed

	// Anonymizer: encrypted replacement map (original→replacement) for PII redaction
	EncryptedMaskedKeywords string `firestore:"encryptedMaskedKeywords,omitempty"`
}

ChatMessage represents a stored chat message in Firestore

type ChatTitle

type ChatTitle struct {
	Title                    string    `firestore:"title,omitempty"`                    // Plaintext title (only when encryption disabled)
	EncryptedTitle           string    `firestore:"encryptedTitle,omitempty"`           // Encrypted title (only when encryption enabled)
	TitlePublicEncryptionKey string    `firestore:"titlePublicEncryptionKey,omitempty"` // Public key used (only when encrypted)
	UpdatedAt                time.Time `firestore:"updatedAt"`                          // Last update timestamp
}

ChatTitle represents a stored chat title in Firestore IMPORTANT: Only ONE of Title or EncryptedTitle should be set, never both

type EncryptionService

type EncryptionService struct{}

EncryptionService handles ECDH + AES-GCM message encryption

func NewEncryptionService

func NewEncryptionService() *EncryptionService

NewEncryptionService creates a new encryption service

func (*EncryptionService) EncryptMessage

func (e *EncryptionService) EncryptMessage(content string, publicKeyJWK string) (string, error)

EncryptMessage encrypts message content using ECDH + HKDF + AES-256-GCM Returns base64-encoded: ephemeralPublicKey || nonce || ciphertext || tag

func (*EncryptionService) ValidatePublicKey

func (e *EncryptionService) ValidatePublicKey(publicKeyJWK string) error

ValidatePublicKey validates a JWK public key

type FirestoreClient

type FirestoreClient struct {
	// contains filtered or unexported fields
}

FirestoreClient handles Firestore operations for messages

func NewFirestoreClient

func NewFirestoreClient(client *firestore.Client) *FirestoreClient

NewFirestoreClient creates a new Firestore client wrapper

func (*FirestoreClient) GetMessage

func (f *FirestoreClient) GetMessage(ctx context.Context, userID, chatID, messageID string) (*ChatMessage, error)

GetMessage retrieves a message from Firestore

func (*FirestoreClient) GetResponseID

func (f *FirestoreClient) GetResponseID(ctx context.Context, userID, chatID string) (string, error)

GetResponseID retrieves the latest OpenAI Responses API response_id for a chat. This is used for continuing conversations with GPT-5 Pro and other stateful models.

Parameters:

  • ctx: Context for the operation
  • userID: User ID who owns the chat
  • chatID: Chat ID

Returns:

  • string: The response_id (e.g., "resp_abc123"), or empty string if not found
  • error: If retrieval failed (network error, permission denied, etc.)

Note: Returns empty string (not error) if chat exists but has no lastResponseId field. This is normal for chats that haven't used Responses API yet.

func (*FirestoreClient) GetUserPublicKey

func (f *FirestoreClient) GetUserPublicKey(ctx context.Context, userID string) (*UserPublicKey, error)

GetUserPublicKey retrieves a user's public key Path: /users/{userId} -> accountKey field

func (*FirestoreClient) SaveChatTitle

func (f *FirestoreClient) SaveChatTitle(ctx context.Context, userID, chatID string, title *ChatTitle) error

SaveChatTitle saves/updates chat title (plaintext or encrypted) Path: /users/{userId}/chats/{chatId} IMPORTANT: This only UPDATES existing chat documents, does not create new ones IMPORTANT: Only ONE of Title or EncryptedTitle should be set, never both

func (*FirestoreClient) SaveMessage

func (f *FirestoreClient) SaveMessage(ctx context.Context, userID string, msg *ChatMessage) error

SaveMessage saves an encrypted message to Firestore Path: /chats/{userId}/{chatId}/messages/{messageId}

func (*FirestoreClient) SaveResponseID

func (f *FirestoreClient) SaveResponseID(ctx context.Context, userID, chatID, responseID string) error

SaveResponseID stores the latest OpenAI Responses API response_id for a chat. This is used for continuing conversations with GPT-5 Pro and other stateful models.

Parameters:

  • ctx: Context for the operation
  • userID: User ID who owns the chat
  • chatID: Chat ID
  • responseID: The response_id from OpenAI (e.g., "resp_abc123")

Path: /users/{userId}/chats/{chatId} Field: lastResponseId (string)

Returns:

  • error: If save failed

Note: This updates an existing chat document. If the chat doesn't exist, the update will fail gracefully (returns FailedPrecondition error).

func (*FirestoreClient) UpdateMessage

func (f *FirestoreClient) UpdateMessage(ctx context.Context, userID, chatID, messageID string, updates map[string]interface{}) error

UpdateMessage updates specific fields of an existing message in Firestore. This is used to update generation state without overwriting the entire message.

Parameters:

  • ctx: Context for the operation
  • userID: User ID who owns the message
  • chatID: Chat ID
  • messageID: Message ID
  • updates: Map of field paths to new values (e.g., {"generationState": "completed"})

Returns:

  • error: If update failed

Path: /users/{userId}/chats/{chatId}/messages/{messageId}

func (*FirestoreClient) VerifyChatOwnership

func (f *FirestoreClient) VerifyChatOwnership(ctx context.Context, userID, chatID string) error

VerifyChatOwnership checks if a user owns a specific chat Returns nil if user owns the chat, error otherwise

type JWKPublicKey

type JWKPublicKey struct {
	Crv    string   `json:"crv"`     // "P-256"
	Ext    bool     `json:"ext"`     // true
	KeyOps []string `json:"key_ops"` // []
	Kty    string   `json:"kty"`     // "EC"
	X      string   `json:"x"`       // Base64url-encoded X coordinate
	Y      string   `json:"y"`       // Base64url-encoded Y coordinate
}

JWKPublicKey represents the parsed JWK public key

type MessageToStore

type MessageToStore struct {
	UserID            string
	ChatID            string
	MessageID         string
	IsFromUser        bool
	Content           string // Plaintext content to be encrypted
	IsError           bool
	EncryptionEnabled *bool // nil = not specified (backward compat), true = enforce encryption, false = store plaintext

	// Stop control (for streaming broadcast feature)
	Stopped    bool   // true if generation was stopped mid-stream
	StoppedBy  string // User ID who stopped, or "system_timeout"/"system_shutdown"
	StopReason string // Why stopped: "user_cancelled", "timeout", "error", "system_shutdown"

	// Model and generation state (for GPT-5 Pro long-running generation tracking)
	Model                 string // Model ID (e.g., "gpt-5-pro")
	GenerationState       string // "thinking", "completed", "failed"
	GenerationStartedAt   *time.Time
	GenerationCompletedAt *time.Time
	GenerationError       string

	// Anonymizer replacement map JSON (e.g. [{"original":"John","replacement":"Mark"}])
	MaskedKeywords string
}

MessageToStore is the internal representation for messages to be stored

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service handles async message storage with encryption

func NewService

func NewService(firestoreClient *firestore.Client, logger *logger.Logger) *Service

NewService creates a new message storage service

func (*Service) EncryptContent

func (s *Service) EncryptContent(content string, publicKeyJWK string) (string, error)

EncryptContent exposes encryption function for title service

func (*Service) GetPublicKey

func (s *Service) GetPublicKey(ctx context.Context, userID string) (*UserPublicKey, error)

GetPublicKey exposes getPublicKey for title service

func (*Service) GetResponseID

func (s *Service) GetResponseID(ctx context.Context, userID, chatID string) (string, error)

GetResponseID retrieves the latest OpenAI Responses API response_id for a chat. This is used for continuing conversations with GPT-5 Pro and other stateful models.

Parameters:

  • ctx: Context for the operation
  • userID: User ID who owns the chat
  • chatID: Chat ID

Returns:

  • string: The response_id (e.g., "resp_abc123"), or empty string if not found
  • error: If retrieval failed

func (*Service) SaveResponseID

func (s *Service) SaveResponseID(ctx context.Context, userID, chatID, responseID string) error

SaveResponseID stores the latest OpenAI Responses API response_id for a chat. This is used for continuing conversations with GPT-5 Pro and other stateful models.

Parameters:

  • ctx: Context for the operation
  • userID: User ID who owns the chat
  • chatID: Chat ID
  • responseID: The response_id from OpenAI (e.g., "resp_abc123")

Returns:

  • error: If save failed

func (*Service) SaveThinkingMessage

func (s *Service) SaveThinkingMessage(ctx context.Context, userID, chatID, messageID, model string, encryptionEnabled *bool) error

SaveThinkingMessage saves a placeholder message for long-running generations (GPT-5 Pro). This allows clients to detect in-progress generation when reconnecting.

The message is saved immediately when streaming starts with:

  • generationState: "thinking"
  • encryptedContent: empty (will be updated when complete)
  • generationStartedAt: current timestamp

Parameters:

  • ctx: Context for the operation
  • userID: User ID who owns the message
  • chatID: Chat ID
  • messageID: AI message ID
  • model: Model ID (e.g., "gpt-5-pro")
  • encryptionEnabled: Whether to encrypt (can be nil for backward compat)

Returns:

  • error: If save failed

func (*Service) Shutdown

func (s *Service) Shutdown()

Shutdown gracefully shuts down the service

func (*Service) StoreMessageAsync

func (s *Service) StoreMessageAsync(ctx context.Context, msg MessageToStore) error

StoreMessageAsync queues a message for async storage

func (*Service) UpdateGenerationStateSync

func (s *Service) UpdateGenerationStateSync(ctx context.Context, userID, chatID, messageID, state, errorMsg string) error

UpdateGenerationStateSync updates a message's generation state synchronously.

This is used by the background polling worker to update Firestore state as OpenAI's response status changes.

Unlike StoreMessageAsync, this method updates Firestore directly without going through the async worker queue. This ensures critical state transitions (thinking → completed/failed) are saved immediately.

Parameters:

  • ctx: Context for the operation
  • userID: User ID who owns the message
  • chatID: Chat ID
  • messageID: AI message ID
  • state: New state ("thinking", "completed", "failed")
  • errorMsg: Error message (only if state="failed")

Returns:

  • error: If update failed

func (*Service) UpdateMessageGenerationState

func (s *Service) UpdateMessageGenerationState(ctx context.Context, userID, chatID, messageID, state, errorMsg string) error

UpdateMessageGenerationState updates a message's generation state. Used to mark messages as "completed" or "failed" after generation finishes.

This method updates an existing message in Firestore - it does NOT create a new message. The full message content should already be stored via the normal StoreMessageAsync flow.

Parameters:

  • ctx: Context for the operation
  • userID: User ID who owns the message
  • chatID: Chat ID
  • messageID: AI message ID
  • state: New state ("completed" or "failed")
  • errorMsg: Error message (only if state="failed")

Returns:

  • error: If update failed

type UserPublicKey

type UserPublicKey struct {
	CreatedAt time.Time `firestore:"createdAt"`
	Public    string    `firestore:"public"` // JWK JSON string (EC P-256)
	UpdatedAt time.Time `firestore:"updatedAt"`
	Version   int       `firestore:"version"` // Key version number
}

UserPublicKey represents a user's ECDSA P-256 public key

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL