store

package
v0.3.4 Latest Latest
Warning

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

Go to latest
Published: Jul 5, 2026 License: AGPL-3.0 Imports: 11 Imported by: 0

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

View Source
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

View Source
var ErrApprovalResolved = errors.New("approval already resolved")

ErrApprovalResolved is returned when a resolve races: the row exists but is no longer pending.

View Source
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"`
	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

func Open(ctx context.Context, path string) (*Store, error)

Open returns a Store backed by the SQLite file at path, applying any pending migrations. The caller owns Close.

func (*Store) Close

func (s *Store) Close() error

Close releases the database handle.

func (*Store) CountContacts

func (s *Store) CountContacts(ctx context.Context) (int, error)

CountContacts returns the total number of stored contacts.

func (*Store) CountConversations

func (s *Store) CountConversations(ctx context.Context) (int, error)

CountConversations returns the total number of stored conversations.

func (*Store) CountMessages

func (s *Store) CountMessages(ctx context.Context) (int, error)

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

func (s *Store) CreateApproval(ctx context.Context, a Approval) error

CreateApproval inserts a new pending approval row.

func (*Store) DB

func (s *Store) DB() *sql.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

func (s *Store) DisplayName(ctx context.Context, target AliasTarget, id, fallback string) string

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) GetAlias

func (s *Store) GetAlias(ctx context.Context, target AliasTarget, id string) (string, error)

GetAlias returns the alias for a target, or ErrNotFound.

func (*Store) GetApproval

func (s *Store) GetApproval(ctx context.Context, id string) (Approval, error)

GetApproval fetches one approval by ID. Returns ErrNotFound on miss.

func (*Store) GetContact

func (s *Store) GetContact(ctx context.Context, participantID string) (Contact, error)

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

func (s *Store) GetContactByNumber(ctx context.Context, number string) (Contact, error)

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

func (s *Store) GetConversation(ctx context.Context, id string) (Conversation, error)

GetConversation fetches a single row. Returns sql.ErrNoRows on miss.

func (*Store) GetMessage

func (s *Store) GetMessage(ctx context.Context, id string) (Message, error)

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

func (s *Store) ListAliases(ctx context.Context) ([]Alias, error)

ListAliases returns all aliases ordered by target_type, target_id.

func (*Store) ListApprovals

func (s *Store) ListApprovals(ctx context.Context, status string, limit int) ([]Approval, error)

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

func (s *Store) ListMessages(ctx context.Context, opts ListMessageOpts) ([]Message, error)

ListMessages returns messages matching opts, ordered by timestamp.

func (*Store) MarkSync

func (s *Store) MarkSync(ctx context.Context, lastEventTime, connectTime time.Time) error

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

func (s *Store) RemoveAlias(ctx context.Context, target AliasTarget, id string) error

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

func (s *Store) SchemaVersion(ctx context.Context) (int, error)

SchemaVersion returns the highest applied schema migration version.

func (*Store) SearchContacts

func (s *Store) SearchContacts(ctx context.Context, query string, limit int) ([]Contact, error)

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

func (s *Store) SearchMessages(ctx context.Context, query string, limit int) ([]SearchHit, error)

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

func (s *Store) SearchMessagesRich(ctx context.Context, opts SearchOpts) ([]RichHit, error)

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

func (s *Store) SetAlias(ctx context.Context, target AliasTarget, id, alias string) error

SetAlias upserts a local alias. Empty alias is rejected — use RemoveAlias to delete.

func (*Store) SyncState

func (s *Store) SyncState(ctx context.Context) (SyncState, error)

SyncState returns the freshness row.

func (*Store) TouchSync

func (s *Store) TouchSync(ctx context.Context) error

TouchSync records that the sync loop is still active without changing the latest archived message or connection timestamp.

func (*Store) UpsertContact

func (s *Store) UpsertContact(ctx context.Context, c Contact) error

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.

func (*Store) UpsertMessage

func (s *Store) UpsertMessage(ctx context.Context, m Message) error

UpsertMessage inserts or updates a message row by ID.

type SyncState

type SyncState struct {
	LastEventTime   time.Time
	LastConnectTime time.Time
	UpdatedAt       time.Time
}

SyncState is what doctor/--json emit about freshness.

Jump to

Keyboard shortcuts

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