Documentation
¶
Overview ¶
Package store is gmcli's local SQLite + FTS5 archive of conversations, messages, and contacts. It is intentionally narrow — domain code converts libgm protos into store models at the boundary and uses these helpers for upserts and queries.
All upserts use INSERT ... ON CONFLICT DO UPDATE (sqlite UPSERT) so the sync loop can replay events without dedup logic of its own.
Index ¶
- Constants
- Variables
- type Alias
- type AliasTarget
- type Approval
- type Contact
- type Conversation
- type ListConversationOpts
- type ListMessageOpts
- type Message
- type RichHit
- type RichMessage
- type SearchHit
- type SearchOpts
- type Store
- func (s *Store) Close() error
- func (s *Store) CountContacts(ctx context.Context) (int, error)
- func (s *Store) CountConversations(ctx context.Context) (int, error)
- func (s *Store) CountMessages(ctx context.Context) (int, error)
- func (s *Store) CountMessagesForConversation(ctx context.Context, conversationID string) (int, error)
- func (s *Store) CreateApproval(ctx context.Context, a Approval) error
- func (s *Store) DB() *sql.DB
- func (s *Store) DisplayName(ctx context.Context, target AliasTarget, id, fallback string) string
- func (s *Store) EnrichMessages(ctx context.Context, msgs []Message) []RichMessage
- func (s *Store) FindConversations(ctx context.Context, query string, limit int) ([]Conversation, error)
- func (s *Store) GetAlias(ctx context.Context, target AliasTarget, id string) (string, error)
- func (s *Store) GetApproval(ctx context.Context, id string) (Approval, error)
- func (s *Store) GetContact(ctx context.Context, participantID string) (Contact, error)
- func (s *Store) GetContactByNumber(ctx context.Context, number string) (Contact, error)
- func (s *Store) GetConversation(ctx context.Context, id string) (Conversation, error)
- func (s *Store) GetMessage(ctx context.Context, id string) (Message, error)
- func (s *Store) GetMessageContext(ctx context.Context, anchorID string, before, after int) ([]Message, error)
- func (s *Store) ListAliases(ctx context.Context) ([]Alias, error)
- func (s *Store) ListApprovals(ctx context.Context, status string, limit int) ([]Approval, error)
- func (s *Store) ListConversations(ctx context.Context, opts ListConversationOpts) ([]Conversation, error)
- func (s *Store) ListMessages(ctx context.Context, opts ListMessageOpts) ([]Message, error)
- func (s *Store) MarkSync(ctx context.Context, lastEventTime, connectTime time.Time) error
- func (s *Store) RemoveAlias(ctx context.Context, target AliasTarget, id string) error
- func (s *Store) ResolveApproval(ctx context.Context, id, status string, errMsg, messageID *string) error
- func (s *Store) SchemaVersion(ctx context.Context) (int, error)
- func (s *Store) SearchContacts(ctx context.Context, query string, limit int) ([]Contact, error)
- func (s *Store) SearchMessages(ctx context.Context, query string, limit int) ([]SearchHit, error)
- func (s *Store) SearchMessagesRich(ctx context.Context, opts SearchOpts) ([]RichHit, error)
- func (s *Store) SetAlias(ctx context.Context, target AliasTarget, id, alias string) error
- func (s *Store) SyncState(ctx context.Context) (SyncState, error)
- func (s *Store) TouchSync(ctx context.Context) error
- func (s *Store) UpsertContact(ctx context.Context, c Contact) error
- func (s *Store) UpsertConversation(ctx context.Context, c Conversation) error
- func (s *Store) UpsertMessage(ctx context.Context, m Message) error
- type SyncState
Constants ¶
const ( ApprovalPending = "pending" ApprovalSent = "sent" ApprovalFailed = "failed" ApprovalDenied = "denied" ApprovalCanceled = "canceled" )
Approval statuses. Lifecycle: pending -> sent | failed | denied | canceled. Terminal states never transition again.
Variables ¶
var ErrApprovalResolved = errors.New("approval already resolved")
ErrApprovalResolved is returned when a resolve races: the row exists but is no longer pending.
var ErrNotFound = errors.New("not found")
ErrNotFound is returned when a row lookup misses. Callers can use errors.Is to distinguish it from real I/O errors.
Functions ¶
This section is empty.
Types ¶
type Alias ¶
type Alias struct {
TargetType AliasTarget `json:"target_type"`
TargetID string `json:"target_id"`
Alias string `json:"alias"`
UpdatedAt time.Time `json:"updated_at"`
}
Alias is a user-set local label overriding the libgm-supplied name.
type AliasTarget ¶
type AliasTarget string
AliasTarget enumerates the things that can carry a local alias.
const ( AliasContact AliasTarget = "contact" AliasConversation AliasTarget = "conversation" )
type Approval ¶
type Approval struct {
ID string `json:"approval_id"`
ConversationID string `json:"conversation_id"`
Body string `json:"body"`
ReplyToID *string `json:"reply_to_id,omitempty"`
RequestedBy string `json:"requested_by"`
Status string `json:"status"`
Error *string `json:"error,omitempty"`
MessageID *string `json:"message_id,omitempty"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
Approval is one proposed outgoing message awaiting (or past) human review.
type Contact ¶
type Contact struct {
ParticipantID string `json:"participant_id"`
SourcePlatform string `json:"source_platform"`
ContactID string `json:"contact_id"`
Name string `json:"name"`
E164 string `json:"e164"`
FormattedNumber string `json:"formatted_number"`
AvatarColor string `json:"avatar_color"`
IsMe bool `json:"is_me"`
Alias string `json:"alias,omitempty"`
DisplayName string `json:"display_name,omitempty"`
}
Contact is the storage shape for an address-book entry. ParticipantID is the libgm-stable ID and forms the primary key; ContactID is Google's contact-database ID (may be empty for non-saved numbers).
Alias is the local user label (from the aliases table) when one is set. DisplayName is Alias if non-empty, otherwise Name — render code should always read DisplayName, never Name directly.
type Conversation ¶
type Conversation struct {
ID string `json:"conversation_id"`
SourcePlatform string `json:"source_platform"`
Name string `json:"name"`
Alias string `json:"alias,omitempty"` // local user label; overrides Name in display
IsGroup bool `json:"is_group"`
ParticipantsJSON string `json:"participants_json"`
LastMessageTimeMS int64 `json:"last_message_time_ms"`
Unread bool `json:"unread"`
Pinned bool `json:"pinned"`
Archived bool `json:"archived"`
UpdatedAt time.Time `json:"updated_at"`
}
Conversation is the storage shape for a chat thread. participants_json holds the libgm Participant array as JSON to avoid a join table at this stage — Phase 3 may normalize if query patterns demand it.
func (Conversation) DisplayName ¶
func (c Conversation) DisplayName() string
DisplayName returns the best human-readable label for a conversation: the explicit name if set, otherwise the other participants' names or numbers, otherwise the conversation ID.
func (Conversation) ParticipantNames ¶
func (c Conversation) ParticipantNames() []string
ParticipantNames lists the display names (or numbers) of everyone in the conversation except the local user.
type ListConversationOpts ¶
type ListConversationOpts struct {
Limit int // max rows; <=0 means 50
UnreadOnly bool // only conversations with unread=1
Pinned bool // only pinned threads
}
ListConversationOpts filters and paginates ListConversations.
type ListMessageOpts ¶
type ListMessageOpts struct {
ConversationID string // optional; if empty, all conversations
SenderID string // optional participant_id filter
Since time.Time // optional lower bound
Until time.Time // optional upper bound
Limit int // <=0 means 50
Order string // "asc" or "desc" (default "desc")
}
ListMessageOpts describes a message-list query. Times are inclusive.
type Message ¶
type Message struct {
ID string `json:"message_id"`
ConversationID string `json:"conversation_id"`
SourcePlatform string `json:"source_platform"`
SenderID string `json:"sender_id"`
Body *string `json:"body,omitempty"`
TimestampMS int64 `json:"timestamp_ms"`
Status int64 `json:"status"`
IsFromMe bool `json:"is_from_me"`
MediaID *string `json:"media_id,omitempty"`
MimeType *string `json:"mime_type,omitempty"`
DecryptionKey []byte `json:"-"`
ReactionsJSON *string `json:"reactions_json,omitempty"`
ReplyToID *string `json:"reply_to_id,omitempty"`
RawProto []byte `json:"-"`
}
Message is the storage shape for a single message. Body is the plaintext content if any (nil for media-only). MediaID/MimeType/DecryptionKey describe the attachment if present; the bytes themselves are only fetched by the explicit `media download` command.
type RichHit ¶
type RichHit struct {
MessageID string `json:"message_id"`
ConversationID string `json:"conversation_id"`
ConversationName string `json:"conversation_name,omitempty"`
SenderName string `json:"sender_name,omitempty"`
Body string `json:"body"`
Snippet string `json:"snippet"`
TimestampMS int64 `json:"timestamp_ms"`
TimestampISO string `json:"timestamp_iso,omitempty"`
IsFromMe bool `json:"is_from_me"`
}
RichHit is a search result enriched for direct consumption by humans and LLMs: display names and an ISO timestamp ride along so callers don't need follow-up lookups to make sense of a hit.
type RichMessage ¶
type RichMessage struct {
Message
SenderName string `json:"sender_name,omitempty"`
TimestampISO string `json:"timestamp_iso,omitempty"`
}
RichMessage is a Message plus resolved display fields, for consumers (LLMs, humans) that shouldn't need extra lookups per row.
type SearchHit ¶
type SearchHit struct {
MessageID string `json:"message_id"`
ConversationID string `json:"conversation_id"`
Body string `json:"body"`
Snippet string `json:"snippet"`
TimestampMS int64 `json:"timestamp_ms"`
IsFromMe bool `json:"is_from_me"`
}
SearchHit is one FTS result. Snippet is the FTS5-generated highlighted excerpt around the match, suitable for display in --json output.
type SearchOpts ¶
type SearchOpts struct {
Query string
ConversationID string // optional: scope to one chat
Since time.Time // optional lower bound (inclusive)
Until time.Time // optional upper bound (inclusive)
Limit int // <=0 means 50
}
SearchOpts filters SearchMessagesRich. Query is required; everything else narrows the result set.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store wraps *sql.DB with gmcli-specific helpers. Construct with Open.
func Open ¶
Open returns a Store backed by the SQLite file at path, applying any pending migrations. The caller owns Close.
func (*Store) CountContacts ¶
CountContacts returns the total number of stored contacts.
func (*Store) CountConversations ¶
CountConversations returns the total number of stored conversations.
func (*Store) CountMessages ¶
CountMessages returns the total number of stored messages.
func (*Store) CountMessagesForConversation ¶
func (s *Store) CountMessagesForConversation(ctx context.Context, conversationID string) (int, error)
CountMessagesForConversation returns the total number of stored messages in one conversation.
func (*Store) CreateApproval ¶
CreateApproval inserts a new pending approval row.
func (*Store) DB ¶
DB exposes the underlying handle. Useful for tests and ad-hoc queries; the rest of the package should prefer typed helpers.
func (*Store) DisplayName ¶
DisplayName resolves the user-facing label for a contact: alias if one is set, otherwise the contact's libgm-supplied name. Used by render code so aliases show up uniformly across all CLI surfaces.
func (*Store) EnrichMessages ¶
func (s *Store) EnrichMessages(ctx context.Context, msgs []Message) []RichMessage
EnrichMessages resolves sender display names (alias > contact name > number > raw participant id) and ISO timestamps for a message slice.
func (*Store) FindConversations ¶
func (s *Store) FindConversations(ctx context.Context, query string, limit int) ([]Conversation, error)
FindConversations resolves a person, group name, or phone number fragment to conversations, newest-activity first. It matches the conversation name, a local alias, participant names/numbers embedded in participants_json, and — via the contacts table — contact names and aliases.
func (*Store) GetApproval ¶
GetApproval fetches one approval by ID. Returns ErrNotFound on miss.
func (*Store) GetContact ¶
GetContact looks up a contact by participant_id (exact). For phone-number or contact_id lookup, see GetContactByNumber. Returns ErrNotFound on miss.
func (*Store) GetContactByNumber ¶
GetContactByNumber finds the first contact whose e164 or formatted_number matches the given query exactly. Used by `gmcli contacts show` to accept either a participant_id or a phone number.
func (*Store) GetConversation ¶
GetConversation fetches a single row. Returns sql.ErrNoRows on miss.
func (*Store) GetMessage ¶
GetMessage fetches a single message by id. Returns ErrNotFound on miss.
func (*Store) GetMessageContext ¶
func (s *Store) GetMessageContext(ctx context.Context, anchorID string, before, after int) ([]Message, error)
GetMessageContext returns up to `before` messages preceding the anchor and up to `after` messages following it, all in the same conversation. The anchor itself is always included. Result is ordered by timestamp ASC.
func (*Store) ListAliases ¶
ListAliases returns all aliases ordered by target_type, target_id.
func (*Store) ListApprovals ¶
ListApprovals returns approvals, newest first. status filters when non-empty; limit <=0 means 50.
func (*Store) ListConversations ¶
func (s *Store) ListConversations(ctx context.Context, opts ListConversationOpts) ([]Conversation, error)
ListConversations returns conversations ordered by last_message_ts DESC.
func (*Store) ListMessages ¶
ListMessages returns messages matching opts, ordered by timestamp.
func (*Store) MarkSync ¶
MarkSync updates the sync_state row. Called by the sync loop on each successful event delivery so doctor can surface a "last seen" timestamp.
func (*Store) RemoveAlias ¶
RemoveAlias deletes an alias. Returns ErrNotFound if no alias was set.
func (*Store) ResolveApproval ¶
func (s *Store) ResolveApproval(ctx context.Context, id, status string, errMsg, messageID *string) error
ResolveApproval transitions a pending approval to a terminal status, recording the sent message ID or failure detail. Returns ErrNotFound if the row does not exist and ErrApprovalResolved if it is no longer pending — guarding against double-approve races between two clients.
func (*Store) SchemaVersion ¶
SchemaVersion returns the highest applied schema migration version.
func (*Store) SearchContacts ¶
SearchContacts returns contacts matching query against name, alias, e164, or formatted_number using a case-insensitive substring match. Limit <=0 means 50.
func (*Store) SearchMessages ¶
SearchMessages runs an FTS5 MATCH against messages_fts. limit caps the result count. The query string is passed to FTS5 verbatim, so callers can use the standard syntax (phrase quotes, NEAR(), AND/OR/NOT).
func (*Store) SearchMessagesRich ¶
SearchMessagesRich runs an FTS5 search with graceful degradation: the query is tried verbatim first (full FTS5 syntax available), and if FTS5 rejects it — natural-language input with apostrophes, question marks, unbalanced quotes — it is retried with every term quoted. Results carry conversation and sender display names.
func (*Store) SetAlias ¶
SetAlias upserts a local alias. Empty alias is rejected — use RemoveAlias to delete.
func (*Store) TouchSync ¶
TouchSync records that the sync loop is still active without changing the latest archived message or connection timestamp.
func (*Store) UpsertContact ¶
UpsertContact inserts or updates a contact row by ParticipantID.
func (*Store) UpsertConversation ¶
func (s *Store) UpsertConversation(ctx context.Context, c Conversation) error
UpsertConversation inserts or updates a conversation row by ID.