Documentation
¶
Index ¶
- Constants
- Variables
- func IDFromPageToken(token *commonpb.PagingToken) (messageID uint64, ok bool)
- func NewChatMessagingReader(store Store) chat.MessagingReader
- func NewMessageDeletedEvent(msg *messagingpb.Message) *messagingpb.Event
- func NewMessageEditedEvent(msg *messagingpb.Message) *messagingpb.Event
- func NewMessageSentEvent(msg *messagingpb.Message) *messagingpb.Event
- func PageTokenFromID(messageID *messagingpb.MessageId) *commonpb.PagingToken
- func ReactorFromPageToken(token *commonpb.PagingToken) (version uint64, ok bool)
- func ReactorLess(a, b *Reactor) bool
- func ReactorPageToken(reactor *Reactor) *commonpb.PagingToken
- func ValidateEmoji(value string) error
- type EventType
- type Media
- type Message
- type MessageRef
- type PointerRef
- type Reaction
- type ReactionSummary
- type Reactor
- type SelfReaction
- type Sender
- type Server
- func (s *Server) AddReaction(ctx context.Context, req *messagingpb.AddReactionRequest) (*messagingpb.AddReactionResponse, error)
- func (s *Server) AdvancePointer(ctx context.Context, req *messagingpb.AdvancePointerRequest) (*messagingpb.AdvancePointerResponse, error)
- func (s *Server) DeleteMessage(ctx context.Context, req *messagingpb.DeleteMessageRequest) (*messagingpb.DeleteMessageResponse, error)
- func (s *Server) EditMessage(ctx context.Context, req *messagingpb.EditMessageRequest) (*messagingpb.EditMessageResponse, error)
- func (s *Server) GetDelta(req *messagingpb.GetDeltaRequest, stream messagingpb.Messaging_GetDeltaServer) error
- func (s *Server) GetMessage(ctx context.Context, req *messagingpb.GetMessageRequest) (*messagingpb.GetMessageResponse, error)
- func (s *Server) GetMessages(ctx context.Context, req *messagingpb.GetMessagesRequest) (*messagingpb.GetMessagesResponse, error)
- func (s *Server) GetReactionSummaries(ctx context.Context, req *messagingpb.GetReactionSummariesRequest) (*messagingpb.GetReactionSummariesResponse, error)
- func (s *Server) GetReactionSummary(ctx context.Context, req *messagingpb.GetReactionSummaryRequest) (*messagingpb.GetReactionSummaryResponse, error)
- func (s *Server) GetReactors(ctx context.Context, req *messagingpb.GetReactorsRequest) (*messagingpb.GetReactorsResponse, error)
- func (s *Server) NotifyIsTyping(ctx context.Context, req *messagingpb.NotifyIsTypingRequest) (*messagingpb.NotifyIsTypingResponse, error)
- func (s *Server) RemoveReaction(ctx context.Context, req *messagingpb.RemoveReactionRequest) (*messagingpb.RemoveReactionResponse, error)
- func (s *Server) SendMessage(ctx context.Context, req *messagingpb.SendMessageRequest) (*messagingpb.SendMessageResponse, error)
- type Store
Constants ¶
const ( // MaxReactionTypesPerMessage caps how many distinct emoji may react to a // single message. The (N+1)th distinct emoji is rejected with // TOO_MANY_REACTION_TYPES; a re-add of an already-present emoji is unaffected. // // The cap bounds what one message can cost every reader: a page of // summaries carries every active emoji of every message in it, with a // sample each, and the viewer's self overlay resolves one key per (message, // emoji). Twenty is what the largest chat products render under a message // and keeps a full page of capped messages well inside a gRPC response. // Lowering it later is safe: the cap blocks activating a new emoji and // never touches what a message already has. MaxReactionTypesPerMessage = 20 // MaxSampleReactors bounds the sample of reactors surfaced inline on an // EmojiReaction (e.g. for rendering a few avatars). The sample is the most // recent reactors by reaction order (see SampleFromReactors), so a viewer sees // who reacted most recently. The full reactor list is fetched on demand via // GetReactors. MaxSampleReactors = 8 // MaxStoredSampleReactors bounds how many reactor entries a store retains for a // reaction's sample. Once the retained set is full, a new reactor evicts the // least-recent entry, keeping it the most-recent reactors. It is twice the // surfaced size so that reactors leaving (the sample is not backfilled on // removal) rarely depletes the retained set below MaxSampleReactors; reads still // surface only the most-recent MaxSampleReactors. MaxStoredSampleReactors = 2 * MaxSampleReactors )
Reaction-aggregate bounds
const ClientMessageIDSize = 16
ClientMessageIDSize is the length, in bytes, of a client message ID.
Variables ¶
var ErrEventSequenceConflict = errors.New("message event sequence conflict")
ErrEventSequenceConflict indicates an optimistic-concurrency failure on a message mutation (edit/delete): the message's current event_sequence no longer matches the expected value the caller supplied, so the mutation was rejected rather than clobbering newer state. The store returns the message's current state alongside this error so the caller can surface it.
var ErrInvalidEmoji = errors.New("value is not a single emoji")
ErrInvalidEmoji indicates a reaction value that is not a single emoji.
var ErrMessageNotFound = errors.New("message not found")
ErrMessageNotFound indicates that no message exists for the given chat and message ID.
var ErrSelfReactionsGroupOnly = errors.New("self reactions are read for groups only")
ErrSelfReactionsGroupOnly is returned by Store.GetSelfReactions for a DM: a store keeps no per-viewer reaction rows for a DM (see there), so the read has nothing to answer from and refuses rather than report the viewer reacted to nothing.
var StoredPointerTypes = []messagingpb.Pointer_Type{ messagingpb.Pointer_DELIVERED, messagingpb.Pointer_READ, }
StoredPointerTypes are the only pointer types persisted, for any chat type: DELIVERED and READ. SENT is client-side and never stored, so enumerating these per member addresses every pointer that can exist for a chat. Treat as read-only.
Functions ¶
func IDFromPageToken ¶
func IDFromPageToken(token *commonpb.PagingToken) (messageID uint64, ok bool)
IDFromPageToken decodes the message ID from a token produced by PageTokenFromID. The ok return is false if the token is nil or malformed.
func NewChatMessagingReader ¶
func NewChatMessagingReader(store Store) chat.MessagingReader
NewChatMessagingReader returns a chat.MessagingReader backed by the given messaging store, for wiring the Chat service.
func NewMessageDeletedEvent ¶ added in v1.18.0
func NewMessageDeletedEvent(msg *messagingpb.Message) *messagingpb.Event
NewMessageDeletedEvent builds the event-log entry for a tombstoned message: a single Event carrying one message_deleted mutation. The event's sequence is the message's (now advanced) event_sequence and its count is 1 — a delete consumes exactly one event-log point, advancing the event-log head without minting a message ID. The mutation carries the materialized tombstone (content replaced with DeletedContent). msg is referenced, not copied; callers pass a proto they own.
func NewMessageEditedEvent ¶ added in v1.19.0
func NewMessageEditedEvent(msg *messagingpb.Message) *messagingpb.Event
NewMessageEditedEvent builds the event-log entry for an edited message: a single Event carrying one message_edited mutation. The event's sequence is the message's (now advanced) event_sequence and its count is 1 — an edit consumes exactly one event-log point, advancing the event-log head without minting a message ID. The mutation carries the materialized message (content replaced, last_edited_ts set), and the event's ts is that edit time. msg is referenced, not copied; callers pass a proto they own.
func NewMessageSentEvent ¶ added in v1.18.0
func NewMessageSentEvent(msg *messagingpb.Message) *messagingpb.Event
NewMessageSentEvent builds the event-log entry for a freshly sent message: a single Event carrying one message_sent mutation. While every event is a new message, the event's sequence is the message's event_sequence (which equals its message ID) and its count is 1 — a send consumes exactly one event-log point. msg is referenced, not copied; callers pass a proto they own.
func PageTokenFromID ¶
func PageTokenFromID(messageID *messagingpb.MessageId) *commonpb.PagingToken
PageTokenFromID encodes a message ID as a paging token. The token is the identifier of the last message in a page; the next request resumes strictly after it. Shared by the store implementations and callers.
func ReactorFromPageToken ¶ added in v1.18.0
func ReactorFromPageToken(token *commonpb.PagingToken) (version uint64, ok bool)
ReactorFromPageToken decodes the version cursor from a token produced by ReactorPageToken. The ok return is false if the token is nil or malformed.
func ReactorLess ¶ added in v1.31.0
ReactorLess orders reactors most-recent-first: descending Version, the order in which they reacted. Versions are unique within an emoji, so the tie-break on ascending user ID only ever decides between reactors of different emoji (or malformed input) and exists to keep the order total. Shared by the stores and the sample so every reactor list agrees.
func ReactorPageToken ¶ added in v1.18.0
func ReactorPageToken(reactor *Reactor) *commonpb.PagingToken
ReactorPageToken encodes a reactor as the server-issued cursor returned in GetReactorsResponse.paging_token. Reactors are returned most-recent-first, i.e. by descending Reactor.Version, and versions are unique within an emoji, so the token is that version alone; the next page resumes strictly below it. The token is opaque to the client, which echoes it back in options.paging_token.
func ValidateEmoji ¶ added in v1.18.0
ValidateEmoji enforces that a reaction value is exactly one emoji grapheme and nothing else. The protobuf layer only bounds the value's size (code points and bytes); true emoji validity is enforced here, against the Unicode emoji set, so arbitrary text can't be smuggled in as a reaction.
Types ¶
type EventType ¶ added in v1.18.0
type EventType uint8
EventType is the kind of mutation an event-log entry records, mirroring the messagingpb.Mutation oneof. It is stored on each event row so the log can be read or filtered by what happened (e.g. deletions only) without joining to the message. The zero value is EventTypeMessageSent, so a create is the default.
type Media ¶ added in v1.20.0
type Media interface {
ResolveRenditions(ctx context.Context, ids []*blobpb.BlobId) (map[string][]*blobpb.Rendition, error)
}
Media is the blob-side integration messaging uses for media: it shares the blobs a message references into the chat on send (ShareIntoChat) and resolves their rendition sets on read (ResolveRenditions). It is implemented by blob.Integration.
ShareIntoChat returns blob.ErrBlobNotShareable when a referenced blob may not be attached (unknown, not owned by the sender, or not a READY original), in which case nothing is granted. ResolveRenditions performs no authorization — the caller has already gated on chat membership — and returns each original's full rendition set keyed by string(BlobId.Value), omitting unknown or not-yet-READY ids.
type Message ¶
type Message struct {
ChatID *commonpb.ChatId
ID *messagingpb.MessageId
SenderID *commonpb.UserId // nil for system messages
Content []*messagingpb.Content
Timestamp time.Time
UnreadSeq uint64
EventSequence uint64
LastEditedTs time.Time // zero until the message is edited; a delete leaves it untouched
}
Message is a stored chat message.
ID, UnreadSeq, and EventSequence are server-assigned by the store at PutMessage time. ID is a per-chat gapless sequence number that is the message's canonical identity, sort key, and pagination cursor. UnreadSeq is a separate per-chat running count of unread-eligible messages. EventSequence is the per-chat event-log sequence at which the message reached its current state; while every event is a new message it equals ID, and it diverges once edits and deletes advance the event log without minting an ID (see messagingpb.Message for the full semantics).
func (*Message) IsDeletable ¶ added in v1.18.0
IsDeletable reports whether this message may be tombstoned via DeleteMessage. Like IsReplyable this is a whitelist of user-authored conversational content, so content types added later (and non-conversational ones like system messages) are non-deletable until explicitly allowed. Cash payment messages are excluded: the payment is a settled record and the tombstone path is for ordinary chat content. A Deleted tombstone is itself non-deletable; the DeleteMessage RPC short-circuits an already-deleted message as an idempotent no-op (see IsDeleted) before this check, so it never reaches here.
func (*Message) IsDeleted ¶ added in v1.18.0
IsDeleted reports whether this message has already been tombstoned — its content replaced with a single DeletedContent. A delete targeting an already-deleted message is an idempotent no-op (see the DeleteMessage RPC).
func (*Message) IsEditable ¶ added in v1.19.0
IsEditable reports whether this message's content may be replaced via EditMessage. Like IsDeletable this is a whitelist of user-authored conversational content, so content types added later (and non-conversational ones like system messages) are non-editable until explicitly allowed. Cash payment messages are excluded, as is a Deleted tombstone — both are terminal records, not editable chat content; the DeleteMessage tombstone falls through to the default here, so editing an already-deleted message is rejected with CANNOT_EDIT.
func (*Message) IsReactable ¶ added in v1.18.0
IsReactable reports whether this message may be the target of an emoji reaction. Like IsReplyable this is a whitelist, so content types added later (and non-conversational ones like system messages) are non-reactable until explicitly allowed. A Deleted tombstone remains reactable — it is still a real message in the thread.
func (*Message) IsReplyable ¶ added in v1.18.0
IsReplyable reports whether this message may be the target of a reply. Only user-facing messages are replyable; this is a whitelist so that content types added later (and non-conversational ones like system messages) are treated as non-replyable until explicitly allowed. Deleted messages remain replyable — the tombstone is still a real message in the thread.
func (*Message) ToProto ¶
func (m *Message) ToProto() *messagingpb.Message
ToProto projects the stored message onto a messagingpb.Message.
type MessageRef ¶
type MessageRef struct {
ChatID *commonpb.ChatId
MessageID *messagingpb.MessageId
}
MessageRef identifies a single message within a chat. It is the unit of a cross-chat batch read (see Store.GetMessagesByRefs) — e.g. one ref per chat to fetch every chat's last message for the feed.
type PointerRef ¶ added in v1.14.0
PointerRef requests a chat's stored pointers (StoredPointerTypes) for the given members. It is the unit of the batched cross-chat pointer read (see Store.GetPointersForChats), mirroring MessageRef on the message path: the caller enumerates exactly which pointers to hydrate so the store can address them by key rather than scanning each chat's partition.
type Reaction ¶ added in v1.18.0
type Reaction struct {
Emoji string
Count uint64
Version uint64
Self *Reactor
SampleReactors []*Reactor
}
Reaction is the aggregate state of a single emoji on a message: how many users reacted with it, a monotonic version that advances on every change to it, and a bounded sample of reactors (the most recent by reaction order, see SampleFromReactors). Self is per-viewer and set by the read path for the requesting user; the rest of the aggregate is shareable.
Version is state, not a sequence of deltas, in the sense of chat.RosterSummary: every real transition on the emoji — a reactor added or removed — moves it by exactly one, an idempotent no-op leaves it alone, and a client keeps the greater value. It is per emoji, so a client watermarks each (message, emoji) independently. A store keeps it across the emoji emptying and being re-added, so a re-add can never look stale.
Self is the viewer's own Reactor entry — the version at which their current reaction was added and when — nil when they do not react with the emoji. It is their entry in the emoji's reactor order whether or not they still sit in the sample — where they rank among the reactors, which ReactionUpdate for the emoji was theirs — so a client can render itself among the reactors without a second read. Its presence answers "did I react"; its version is not the watermark for that toggle: a summary is a snapshot at Version, and every transition of the viewer's at or below it is already folded into Self, so Version is the value to gate live updates by.
In an AddReaction result Self.Version equals Version. In a summary read the two come from separate strongly consistent reads, aggregate first, so Self.Version is normally at most Version but can exceed it by the viewer's own add landing between the two reads; then Self is the newer truth and the aggregate is a transition behind, which the add's ReactionUpdate (or the next refresh) reconciles. The rows behind the overlay are written in the add's own transaction, so the two never disagree about a completed transition. It projects onto EmojiReaction.self_reactor.
func (*Reaction) ToProto ¶ added in v1.18.0
func (r *Reaction) ToProto() *messagingpb.EmojiReaction
ToProto projects the aggregate onto a messagingpb.EmojiReaction.
type ReactionSummary ¶ added in v1.18.0
type ReactionSummary struct {
MessageID *messagingpb.MessageId
Reactions []*Reaction
}
ReactionSummary pairs a message with its non-empty reaction aggregates, the unit returned by the batch reaction-summary reads. It projects onto a messagingpb.ReactionSummary.
func (*ReactionSummary) ToProto ¶ added in v1.18.0
func (s *ReactionSummary) ToProto() *messagingpb.ReactionSummary
ToProto projects onto a messagingpb.ReactionSummary.
type Reactor ¶ added in v1.18.0
Reactor is a single user's reaction to a message, with the time they reacted.
Version is the emoji aggregate's version at which this reaction was added — the transition that created it (see Reaction.Version). Within one (message, emoji) it is unique, so it is the reactor list's ordering key and paging cursor: "most recent first" means descending Version, which is reaction order by definition and involves no wall clock. ReactedTs is a display value only; a store assigns it from the server clock at the add and never orders by it.
func SampleFromReactors ¶ added in v1.18.0
SampleFromReactors orders reactors most-recent-first (see ReactorLess) and returns the first MaxSampleReactors — the deterministic, most-recent sample surfaced on a reaction aggregate even when a store retains up to MaxStoredSampleReactors. The ordering matches GetReactors. It mutates the given slice's order; callers pass a slice they own.
func (*Reactor) ToProto ¶ added in v1.18.0
func (r *Reactor) ToProto() *messagingpb.Reactor
ToProto projects the reactor onto a messagingpb.Reactor.
type SelfReaction ¶ added in v1.31.0
type SelfReaction struct {
MessageID *messagingpb.MessageId
Emoji string
Version uint64
ReactedTs time.Time
}
SelfReaction is one reaction a viewer currently holds in a chat, as the per-viewer overlay read reports it (see Store.GetSelfReactions): the (message, emoji) it sits on, the version that added it and when — the viewer's own Reactor.Version and Reactor.ReactedTs, from which the caller builds Reaction.Self.
type Sender ¶ added in v1.14.0
type Sender struct {
// contains filtered or unexported fields
}
Sender is the engine behind a message send: it persists the message and performs every side effect — advancing the sender's read pointer, bumping the chat's last message, and broadcasting (with pushes) to members. It carries no authentication or transport concerns, so internal callers (e.g. injecting a cash message after a payment) can construct just a Sender rather than the full gRPC Server. The Server holds one and delegates SendMessage to it.
func NewSender ¶ added in v1.14.0
func NewSender( log *zap.Logger, badges badge.Store, chats chat.Store, messages Store, profiles profile.Store, blocklists blocklist.Store, media Media, ocpData ocp_data.Provider, pusher push.Pusher, userEventBus *event.Bus[*commonpb.UserId, *eventpb.Event], chatEventBus *event.Bus[*commonpb.ChatId, *eventpb.ChatEvent], ) *Sender
func (*Sender) Send ¶ added in v1.14.0
func (s *Sender) Send( ctx context.Context, chatID *commonpb.ChatId, senderID *commonpb.UserId, content []*messagingpb.Content, clientMessageID *messagingpb.ClientMessageId, countsTowardUnread bool, ) (*messagingpb.Message, error)
Send persists content as a message in the chat and performs every side effect of a send: it advances the sender's own read pointer past the message, records the message as the chat's most recent, and broadcasts the resulting update to all members. It is the shared core behind the SendMessage RPC and internal, server-authored sends — the latter bypass the RPC's client-side content and membership checks (e.g. injecting a cash message after a payment settles).
senderID may be nil to denote a system message, in which case no read pointer is advanced. countsTowardUnread controls whether the message advances the chat's unread sequence: true for user-authored messages (the sender doesn't see their own message as unread because their read pointer is advanced past it), false for messages that shouldn't bump anyone's unread count. Sends are idempotent on (chatID, clientMessageID): a retry returns the originally persisted message and skips the side effects, which already ran on the first send — re-running them would duplicate pushes to members.
type Server ¶
type Server struct {
messagingpb.UnimplementedMessagingServer
// contains filtered or unexported fields
}
func (*Server) AddReaction ¶ added in v1.18.0
func (s *Server) AddReaction(ctx context.Context, req *messagingpb.AddReactionRequest) (*messagingpb.AddReactionResponse, error)
func (*Server) AdvancePointer ¶
func (s *Server) AdvancePointer(ctx context.Context, req *messagingpb.AdvancePointerRequest) (*messagingpb.AdvancePointerResponse, error)
func (*Server) DeleteMessage ¶ added in v1.18.0
func (s *Server) DeleteMessage(ctx context.Context, req *messagingpb.DeleteMessageRequest) (*messagingpb.DeleteMessageResponse, error)
func (*Server) EditMessage ¶ added in v1.19.0
func (s *Server) EditMessage(ctx context.Context, req *messagingpb.EditMessageRequest) (*messagingpb.EditMessageResponse, error)
func (*Server) GetDelta ¶ added in v1.18.0
func (s *Server) GetDelta(req *messagingpb.GetDeltaRequest, stream messagingpb.Messaging_GetDeltaServer) error
func (*Server) GetMessage ¶
func (s *Server) GetMessage(ctx context.Context, req *messagingpb.GetMessageRequest) (*messagingpb.GetMessageResponse, error)
func (*Server) GetMessages ¶
func (s *Server) GetMessages(ctx context.Context, req *messagingpb.GetMessagesRequest) (*messagingpb.GetMessagesResponse, error)
func (*Server) GetReactionSummaries ¶ added in v1.18.0
func (s *Server) GetReactionSummaries(ctx context.Context, req *messagingpb.GetReactionSummariesRequest) (*messagingpb.GetReactionSummariesResponse, error)
func (*Server) GetReactionSummary ¶ added in v1.18.0
func (s *Server) GetReactionSummary(ctx context.Context, req *messagingpb.GetReactionSummaryRequest) (*messagingpb.GetReactionSummaryResponse, error)
func (*Server) GetReactors ¶ added in v1.18.0
func (s *Server) GetReactors(ctx context.Context, req *messagingpb.GetReactorsRequest) (*messagingpb.GetReactorsResponse, error)
func (*Server) NotifyIsTyping ¶
func (s *Server) NotifyIsTyping(ctx context.Context, req *messagingpb.NotifyIsTypingRequest) (*messagingpb.NotifyIsTypingResponse, error)
func (*Server) RemoveReaction ¶ added in v1.18.0
func (s *Server) RemoveReaction(ctx context.Context, req *messagingpb.RemoveReactionRequest) (*messagingpb.RemoveReactionResponse, error)
func (*Server) SendMessage ¶
func (s *Server) SendMessage(ctx context.Context, req *messagingpb.SendMessageRequest) (*messagingpb.SendMessageResponse, error)
type Store ¶
type Store interface {
// PutMessage assigns the next gapless message ID for the chat and persists
// the message, returning the persisted message with its assigned ID and
// unread sequence.
//
// It is idempotent on (chatID, clientMessageID): a retried send with the
// same client message ID returns the originally persisted message without
// assigning a new ID. created reports whether this call persisted the
// message (false on a retry), so callers can skip one-time side effects
// like pushes.
//
// countsTowardUnread controls the unread sequence: when true the message's
// unread_seq is the previous value + 1; when false it carries the previous
// value forward (for messages that shouldn't bump anyone's unread count).
//
// senderID may be nil to denote a system message.
PutMessage(
ctx context.Context,
chatID *commonpb.ChatId,
senderID *commonpb.UserId,
content []*messagingpb.Content,
ts time.Time,
clientMessageID *messagingpb.ClientMessageId,
countsTowardUnread bool,
) (msg *Message, created bool, err error)
// EditMessage replaces a message's content with the given content and stamps
// editedTs as its last-edited time, advances the chat's event-log head, and
// re-stamps the message's event_sequence to that new head — the message ID and
// unread_seq are left untouched, so the per-chat ID sequence stays gapless. Like
// DeleteMessage it advances the event-log head without minting a message ID, so
// event_sequence diverges from message ID.
//
// It is an optimistic-concurrency operation: the edit is applied only if the
// message's current event_sequence still equals expectedEventSeq. On a mismatch
// nothing is modified and it returns the message's current state alongside
// ErrEventSequenceConflict; there is no last-writer-wins path. It returns
// ErrMessageNotFound if no such message exists. On success it returns the edited
// message at its new event_sequence.
EditMessage(
ctx context.Context,
chatID *commonpb.ChatId,
messageID *messagingpb.MessageId,
content []*messagingpb.Content,
editedTs time.Time,
expectedEventSeq uint64,
) (*Message, error)
// DeleteMessage tombstones a message: it replaces the message's content with a
// single DeletedContent (carrying deletedTs and deletedBy), advances the chat's
// event-log head, and re-stamps the message's event_sequence to that new head —
// the message ID and unread_seq are left untouched, so the per-chat ID sequence
// stays gapless. This is the first operation that advances the event-log head
// without minting a message ID, so event_sequence diverges from message ID here.
//
// It is an optimistic-concurrency operation: the tombstone is applied only if
// the message's current event_sequence still equals expectedEventSeq. On a
// mismatch nothing is modified and it returns the message's current state
// alongside ErrEventSequenceConflict; there is no last-writer-wins path. It
// returns ErrMessageNotFound if no such message exists. On success it returns
// the tombstoned message at its new event_sequence.
//
// deletedBy may be nil to denote a system-level deletion (e.g. moderation).
DeleteMessage(
ctx context.Context,
chatID *commonpb.ChatId,
messageID *messagingpb.MessageId,
deletedBy *commonpb.UserId,
deletedTs time.Time,
expectedEventSeq uint64,
) (*Message, error)
// GetMessage returns a single message by ID, or ErrMessageNotFound.
GetMessage(ctx context.Context, chatID *commonpb.ChatId, messageID *messagingpb.MessageId) (*Message, error)
// MessageExists reports whether a message exists in the chat. It is a
// lightweight existence check that does not read or decode the message body,
// for callers (e.g. the reaction read paths) that only need to distinguish a
// missing message and don't need its content. The read is strongly
// consistent: it reflects every send that completed before it. Its callers
// gate an action on a message the user is looking at, and a message just
// delivered on the stream is one another server committed moments ago —
// the case a lagging read would report as missing.
MessageExists(ctx context.Context, chatID *commonpb.ChatId, messageID *messagingpb.MessageId) (bool, error)
// GetMessages returns a page of messages for a chat ordered by message ID
// (ascending by default), paged via the provided query options. The paging
// token's value is the message ID of the last message from the previous
// page (see PageTokenFromID). Returns an empty result (no error) when the
// chat has no messages.
GetMessages(ctx context.Context, chatID *commonpb.ChatId, opts ...database.QueryOption) ([]*Message, error)
// GetMessagesByRefs returns the messages identified by the given refs that
// exist, across any number of chats. Refs without a matching message are
// omitted and duplicate refs collapse. Results are ordered by
// (chatID, message ID), so refs within a single chat come back ascending by
// ID. It returns an empty result (no error) when refs is empty.
//
// It is the batch read behind the DM feed, where it fetches every chat's last
// message in one call (one ref per chat). For the single-chat case the caller
// builds refs from a shared chat ID.
GetMessagesByRefs(ctx context.Context, refs []MessageRef) ([]*Message, error)
// GetEventDelta reads up to limit event-log entries with event_sequence in
// (afterEventSeq, headEventSeq], ascending, and returns the current state of the
// messages they concern — the page primitive behind GetDelta's catch-up. The
// event log stores only a thin descriptor per event (message_id, type, ts); each
// referenced message is joined to its current materialized state here.
//
// Superseded events are dropped: when a scanned event's message has since changed
// again (its current event_sequence is greater than that event's sequence), the
// stale entry is skipped because a newer event — later in this delta, or, if past
// head, on the live stream — carries the up-to-date state. So a message appears at
// most once, at its latest in-range event, in its current state.
//
// nextCursor is the highest event_sequence scanned (<= headEventSeq), whether or
// not it survived the drop — the caller advances its cursor to it so a fully
// superseded page still makes progress rather than re-reading. It equals
// afterEventSeq when the range is empty. The log is gapless and read consistently,
// so advancing by nextCursor never skips an event. limit <= 0 uses the store's
// default page size.
GetEventDelta(ctx context.Context, chatID *commonpb.ChatId, afterEventSeq, headEventSeq uint64, limit int) (msgs []*Message, nextCursor uint64, err error)
// GetLatestEventSequence returns the chat's current head event sequence — the
// highest event_sequence assigned in the chat, or 0 when the chat has no
// messages. It bounds GetDelta catch-up: a client whose cursor equals this
// value is at the head.
//
// While every event is a new message (no edits or deletes yet) the event
// sequence advances in lockstep with the message ID, so this equals the
// chat's highest message ID. The two are distinct concepts: once edits and
// deletes advance the event sequence without minting a new message ID, they
// diverge.
GetLatestEventSequence(ctx context.Context, chatID *commonpb.ChatId) (uint64, error)
// GetLatestEventSequencesForChats returns the head event sequence of each of
// the given chats, keyed by string(chatID.Value). It is the cross-chat batch
// counterpart to GetLatestEventSequence, used to hydrate Metadata.latest_event_sequence
// for the DM feed in one call rather than one read per chat. A chat with no
// messages (head 0) is absent from the map and duplicate chat IDs collapse, so
// callers must treat a missing key as 0. Returns an empty map (no error) when
// chatIDs is empty.
GetLatestEventSequencesForChats(ctx context.Context, chatIDs []*commonpb.ChatId) (map[string]uint64, error)
// GetPointers returns all delivered/read pointers for a chat. Returns an
// empty result (no error) when the chat has no pointers.
GetPointers(ctx context.Context, chatID *commonpb.ChatId) ([]*messagingpb.Pointer, error)
// GetPointersForChats returns the stored pointers (StoredPointerTypes) for the
// members named in each ref, keyed by string(chatID.Value). It is the
// cross-chat batch counterpart to GetPointers, used to hydrate member pointers
// for the DM feed. Unlike GetPointers it addresses pointers by exact key
// (chat × member × StoredPointerTypes) in a single batched read rather than
// scanning each chat's partition; since those are the only types ever stored,
// this is exhaustive. Chats with no matching pointers are absent from the map
// and duplicate (chat, member) pairs collapse. Returns an empty map (no error)
// when refs is empty.
GetPointersForChats(ctx context.Context, refs []PointerRef) (map[string][]*messagingpb.Pointer, error)
// AdvancePointer moves a member's pointer of the given type forward to
// newValue. Pointers are monotonic: a request to move a pointer to a value
// at or before its current value is a no-op. It always returns the pointer's
// current state (carrying its last-advanced ts), whether or not this call
// moved it; the bool reports whether it advanced. The pointer is nil only
// alongside a non-nil error.
//
// It does not verify that newValue references an existing message. Callers
// with a caller-supplied newValue must check existence first (see
// MessageExists and the AdvancePointer RPC); callers that already know the
// message exists — e.g. the sender's own READ pointer right after PutMessage
// returns the message it just wrote — can advance directly.
AdvancePointer(
ctx context.Context,
chatID *commonpb.ChatId,
userID *commonpb.UserId,
pointerType messagingpb.Pointer_Type,
newValue *messagingpb.MessageId,
) (*messagingpb.Pointer, bool, error)
// AddReaction records userID's reaction with emoji on a message and returns
// the emoji's aggregate after the add. The result goes back to the reactor
// alone, so unlike the summary reads it is already the caller's view:
// Self is the caller's own entry, at the version that added their reaction
// — the new Version on a real add, the earlier one on a re-add — and when:
// ts on a real add, the original add's time on a re-add (ts is ignored). It
// is idempotent on (chat, message, emoji, user): a re-add
// returns the current aggregate with created false and changes nothing.
// created reports whether this call actually added the reaction (false on a
// re-add), so callers can skip the broadcast.
//
// tooManyTypes is true (with a nil reaction) when adding this emoji would
// exceed MaxReactionTypesPerMessage distinct emoji on the message; the add is
// rejected. Re-adding an already-present emoji never trips the cap.
//
// It does not verify the message exists or is reactable — the caller checks
// that first (see Message.IsReactable).
AddReaction(
ctx context.Context,
chatID *commonpb.ChatId,
messageID *messagingpb.MessageId,
userID *commonpb.UserId,
emoji string,
ts time.Time,
) (reaction *Reaction, created bool, tooManyTypes bool, err error)
// RemoveReaction removes userID's reaction with emoji from a message. It is
// idempotent: removing a reaction that isn't present returns removed false and
// changes nothing. removed reports whether this call actually removed the
// reaction, so callers can skip the broadcast.
//
// reaction is the emoji's aggregate after the removal, with Count possibly 0
// when the last reactor left (it still carries the advanced Version, which
// the removal broadcast needs). Self is left nil — which is also
// the correct overlay for the caller, who just removed their reaction. It is
// nil only when the emoji has no aggregate at all (a pure no-op).
RemoveReaction(
ctx context.Context,
chatID *commonpb.ChatId,
messageID *messagingpb.MessageId,
userID *commonpb.UserId,
emoji string,
) (reaction *Reaction, removed bool, err error)
// GetReactionSummary returns the per-emoji aggregates for a single message,
// one entry per distinct emoji that currently has at least one reactor. The
// aggregates are shareable: Self is left nil for the caller to
// overlay (see GetSelfReactions). Returns an empty result (no error) when the
// message has no reactions.
//
// The read is strongly consistent, as every summary read is: it reflects
// every add and remove that completed before it. A stale count would be
// harmless — the client keeps the greater version — but a stale *set* is
// not: a reader who reacts and then refreshes would find their emoji
// missing, with no version to tell "not yet visible" from "removed", and
// the strongly consistent self overlay would then have nothing to mark.
GetReactionSummary(
ctx context.Context,
chatID *commonpb.ChatId,
messageID *messagingpb.MessageId,
) ([]*Reaction, error)
// GetReactionSummariesByRefs returns one summary per requested message,
// deduplicated and ordered by message ID. A message with no reactions (or
// unknown) is echoed with an empty Reactions slice rather than omitted, so the
// caller gets an answer for every requested ID. Aggregates are shareable
// (Self left nil) and the read is strongly consistent (see
// GetReactionSummary). Returns an empty result (no error) when messageIDs is
// empty.
GetReactionSummariesByRefs(
ctx context.Context,
chatID *commonpb.ChatId,
messageIDs []*messagingpb.MessageId,
) ([]*ReactionSummary, error)
// GetReactionSummaries returns one summary per message in a page of the chat's
// messages, ordered by message ID and paged via the query options (the paging
// token is a message ID, as in GetMessages). The page spans messages, not just
// reacted ones: a message with no reactions is returned with an empty Reactions
// slice rather than skipped. Aggregates are shareable (Self left
// false) and the read is strongly consistent (see GetReactionSummary).
// Returns an empty result (no error) when the page is empty.
GetReactionSummaries(
ctx context.Context,
chatID *commonpb.ChatId,
opts ...database.QueryOption,
) ([]*ReactionSummary, error)
// GetSelfReactions returns every reaction userID currently holds on the given
// messages of a group, each with the version that added it and when — the
// per-viewer data behind Reaction.Self and EmojiReaction.self_reactor. It is
// the viewer's half of a summary read: the aggregates are shareable and
// leave Self unset, and this answers it for one viewer across a
// whole page. The read is addressed by the viewer, not by every (message,
// emoji) on the page, so its cost follows how much the viewer reacted rather
// than how reacted the page is, and it depends on nothing but the message
// IDs, so it can run alongside the aggregate read. Message IDs are
// deduplicated and an unknown ID contributes nothing; the result is in no
// particular order. Returns an empty result (no error) when messageIDs is
// empty. The read is strongly consistent: the rows it reads are written and
// deleted in the add's and remove's own transaction, so it reflects every
// transition of the viewer's that completed before it.
//
// It is for groups only and returns ErrSelfReactionsGroupOnly for a DM. A
// DM's two members can never outgrow an emoji's sample, so its overlay is
// read off the aggregates the caller already holds (see
// Server.applySelfReactions), and a store keeps no per-viewer rows for a DM
// — a write per reaction that nothing would read.
GetSelfReactions(
ctx context.Context,
chatID *commonpb.ChatId,
userID *commonpb.UserId,
messageIDs []*messagingpb.MessageId,
) ([]SelfReaction, error)
// GetReactors returns a page of the users who reacted to a message with emoji,
// most-recent-first (see ReactorLess), paged via the query options (the paging
// token is a ReactorPageToken). It also returns hasMore, whether further pages
// remain. Returns an empty result (no error) when the message has no reactors
// for the emoji. The read is strongly consistent: it reflects every add and
// remove that completed before it.
//
// version is the emoji aggregate's version (Reaction.Version), read BEFORE the
// page, so the page reflects at least every transition up to it and possibly
// more. That direction is the safe one: a client holding version may apply live
// updates for the emoji whose version exceeds it, and any it re-applies are
// idempotent (an add of a reactor already listed dedupes, a remove of one
// already absent is a no-op), whereas a version newer than the page would make
// it drop an update the page is missing. It is 0 when the emoji has no
// aggregate. A page cannot read the two atomically, so the order of the reads
// is the contract.
GetReactors(
ctx context.Context,
chatID *commonpb.ChatId,
messageID *messagingpb.MessageId,
emoji string,
opts ...database.QueryOption,
) (reactors []*Reactor, version uint64, hasMore bool, err error)
}
Store persists chat messages and message-history pointers.
Each chat has its own gapless message ID sequence. Sends are made idempotent by a client-generated message ID, so a retried send returns the originally persisted message rather than assigning a new ID.