messaging

package
v1.21.0 Latest Latest
Warning

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

Go to latest
Published: Jul 13, 2026 License: MIT Imports: 28 Imported by: 0

Documentation

Index

Constants

View Source
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.
	MaxReactionTypesPerMessage = 100

	// 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 time (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

View Source
const ClientMessageIDSize = 16

ClientMessageIDSize is the length, in bytes, of a client message ID.

Variables

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

View Source
var ErrInvalidEmoji = errors.New("value is not a single emoji")

ErrInvalidEmoji indicates a reaction value that is not a single emoji.

View Source
var ErrMessageNotFound = errors.New("message not found")

ErrMessageNotFound indicates that no message exists for the given chat and message ID.

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) (reactedTs time.Time, userID *commonpb.UserId, ok bool)

ReactorFromPageToken decodes the reaction timestamp and user ID from a token produced by ReactorPageToken. The ok return is false if the token is nil or malformed.

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, so the token carries the last reactor's reaction timestamp (the ordering key) followed by its user ID (a tie-breaker for equal timestamps); the next request resumes strictly after it. The token is opaque to the client, which echoes it back in options.paging_token.

func ValidateEmoji added in v1.18.0

func ValidateEmoji(value string) error

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.

const (
	EventTypeMessageSent    EventType = iota // a message was created (a send)
	EventTypeMessageEdited                   // a message's content was edited
	EventTypeMessageDeleted                  // a message was tombstoned (a delete)
)

type Media added in v1.20.0

type Media interface {
	ShareIntoChat(ctx context.Context, sharerID *commonpb.UserId, chatID *commonpb.ChatId, blobIDs []*blobpb.BlobId) error
	Resolve(ctx context.Context, ids []*blobpb.BlobId) (map[string]*blobpb.BlobMetadata, 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 metadata on read (Resolve). 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. Resolve performs no authorization — the caller has already gated on chat membership — and returns metadata 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) Clone

func (m *Message) Clone() *Message

Clone returns a deep copy of the message.

func (*Message) IsDeletable added in v1.18.0

func (m *Message) IsDeletable() bool

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

func (m *Message) IsDeleted() bool

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

func (m *Message) IsEditable() bool

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

func (m *Message) IsReactable() bool

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

func (m *Message) IsReplyable() bool

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

type PointerRef struct {
	ChatID  *commonpb.ChatId
	Members []*commonpb.UserId
}

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
	Sequence       uint64
	ReactedBySelf  bool
	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 time, see SampleFromReactors). ReactedBySelf is per-viewer and set by the read path for the requesting user; the rest of the aggregate is shareable.

func (*Reaction) ToProto added in v1.18.0

func (r *Reaction) ToProto() *messagingpb.EmojiReaction

ToProto projects the aggregate onto a messagingpb.EmojiReaction.

type ReactionRef added in v1.18.0

type ReactionRef struct {
	MessageID *messagingpb.MessageId
	Emoji     string
}

ReactionRef identifies one (message, emoji) reaction within a chat. It is the unit of the batched self-reaction lookup (see Store.GetSelfReactions): the caller derives refs from a reaction summary it already holds so the store can resolve each by exact key rather than scanning.

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

ToProto projects onto a messagingpb.ReactionSummary.

type Reactor added in v1.18.0

type Reactor struct {
	UserID    *commonpb.UserId
	ReactedTs time.Time
}

Reactor is a single user's reaction to a message, with the time they reacted.

func SampleFromReactors added in v1.18.0

func SampleFromReactors(reactors []*Reactor) []*Reactor

SampleFromReactors orders reactors by descending reaction time (ties broken by ascending user ID, for a total and stable order) 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 the most-recent-first order of 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 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,
	media Media,
	ocpData ocp_data.Provider,
	pusher push.Pusher,
	eventBus *event.Bus[*commonpb.UserId, *eventpb.Event],
) *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 NewServer

func NewServer(
	log *zap.Logger,
	authz auth.Authorizer,
	chats chat.Store,
	messages Store,
	media Media,
	sender *Sender,
) *Server

func (*Server) AddReaction added in v1.18.0

func (*Server) DeleteMessage added in v1.18.0

func (*Server) EditMessage added in v1.19.0

func (*Server) GetDelta added in v1.18.0

func (*Server) GetReactionSummaries added in v1.18.0

func (*Server) GetReactionSummary added in v1.18.0

func (*Server) GetReactors added in v1.18.0

func (*Server) RemoveReaction added in v1.18.0

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.
	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 aggregate is shareable, so
	// ReactedBySelf is left false for the caller to overlay. 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 Sequence, which
	// the removal broadcast needs). ReactedBySelf is left false — 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: ReactedBySelf is left false for the caller to
	// overlay (see GetSelfReactions). Returns an empty result (no error) when the
	// message has no reactions.
	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
	// (ReactedBySelf left false). 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 (ReactedBySelf left
	// false). 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 the subset of refs that userID has reacted to — the
	// per-viewer data behind EmojiReaction.reacted_by_self. The caller derives refs
	// from a summary it already holds, so the store resolves them by exact key in
	// one batched read. Returns an empty result (no error) when refs is empty.
	GetSelfReactions(
		ctx context.Context,
		chatID *commonpb.ChatId,
		userID *commonpb.UserId,
		refs []ReactionRef,
	) ([]ReactionRef, error)

	// GetReactors returns a page of the users who reacted to a message with emoji,
	// most-recent-first, 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.
	//
	// When consistent is true the read is strongly consistent — reflecting every
	// preceding add/remove with no propagation lag — at the cost of scaling less
	// well to large, deeply-paged reactor lists. The flag changes only consistency;
	// the ordering and paging semantics are identical either way.
	GetReactors(
		ctx context.Context,
		chatID *commonpb.ChatId,
		messageID *messagingpb.MessageId,
		emoji string,
		consistent bool,
		opts ...database.QueryOption,
	) (reactors []*Reactor, 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.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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