// Copyright (c) 2025 Shivaram Lingamneni // released under the MIT license package history import ( "errors" "io" "time" ) var ( ErrDisallowed = errors.New("disallowed") ErrNotFound = errors.New("not found") ) // Database is an interface for persistent history storage backends. type Database interface { // Close closes the database connection and releases resources. io.Closer // AddChannelItem adds a history item for a channel. // target is the casefolded channel name. // account is the sender's casefolded account name ("" for no account). AddChannelItem(target string, item Item, account string) error // AddDirectMessage adds a history item for a direct message. // All identifiers are casefolded; account identifiers are "" for no account. AddDirectMessage(sender, senderAccount, recipient, recipientAccount string, item Item) error // DeleteMsgid deletes a message by its msgid. // accountName is the unfolded account name, or "*" to skip // account validation DeleteMsgid(msgid, accountName string) error // MakeSequence creates a Sequence for querying history. // target is the primary target (channel or account), casefolded. // correspondent is the casefolded DM correspondent (empty for channels). // cutoff is the earliest time to include in results. MakeSequence(target, correspondent string, cutoff time.Time) Sequence // ListChannels returns the timestamp of the latest message in each // of the given channels (specified as casefolded names). ListChannels(cfchannels []string) (results []TargetListing, err error) // ListCorrespondents lists the DM correspondents associated with an account, // in order to implement CHATHISTORY TARGETS. ListCorrespondents(cftarget string, start, end time.Time, limit int) ([]TargetListing, error) // these are for theoretical GDPR compliance, not actual chat functionality, // and are not essential: // Forget enqueues an account (casefolded) for message deletion. // This is used for GDPR-style "right to be forgotten" requests. // The actual deletion happens asynchronously. Forget(account string) // Export exports all messages for an account (casefolded) to the given writer. Export(account string, writer io.Writer) } type noopDatabase struct{} // NewNoopDatabase returns a Database implementation that does nothing. func NewNoopDatabase() Database { return noopDatabase{} } func (n noopDatabase) Close() error { return nil } func (n noopDatabase) AddChannelItem(target string, item Item, account string) error { return nil } func (n noopDatabase) AddDirectMessage(sender, senderAccount, recipient, recipientAccount string, item Item) error { return nil } func (n noopDatabase) DeleteMsgid(msgid, accountName string) error { return nil } func (n noopDatabase) Forget(account string) { // no-op } func (n noopDatabase) Export(account string, writer io.Writer) { // no-op } func (n noopDatabase) ListChannels(cfchannels []string) (results []TargetListing, err error) { return nil, nil } func (n noopDatabase) ListCorrespondents(target string, start, end time.Time, limit int) (results []TargetListing, err error) { return nil, nil } func (n noopDatabase) MakeSequence(target, correspondent string, cutoff time.Time) Sequence { return noopSequence{} } // noopSequence is a no-op implementation of Sequence. // XXX: this should never be accessed, because if persistent history is disabled, // we should always be working with a bufferSequence instead. But we might as well // be defensive in case there's an edge case where (noopDatabase).MakeSequence ends // up getting called. type noopSequence struct{} func (n noopSequence) Between(start, end Selector, limit int) (results []Item, err error) { return nil, nil } func (n noopSequence) Around(start Selector, limit int) (results []Item, err error) { return nil, nil } func (n noopSequence) Cutoff() time.Time { return time.Time{} } func (n noopSequence) Ephemeral() bool { return false // we're pretending to be an empty database }