chat

package
v1.13.0 Latest Latest
Warning

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

Go to latest
Published: Jun 2, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Index

Constants

View Source
const ChatIDSize = 32

ChatIDSize is the length, in bytes, of a chat ID.

Variables

View Source
var (
	// ErrChatNotFound indicates that no chat exists for the given chat ID.
	ErrChatNotFound = errors.New("chat not found")

	// ErrChatExists indicates that a chat with the given ID already exists.
	ErrChatExists = errors.New("chat already exists")
)

Functions

func MustDeriveDmChatID

func MustDeriveDmChatID(a, b *commonpb.UserId) *commonpb.ChatId

MustDeriveDmChatID returns the deterministic chat ID for a DM between two users.

The ID is derived purely from the participants, so it is stable across calls and independent of who initiates the chat: MustDeriveDmChatID(a, b) always equals MustDeriveDmChatID(b, a). This lets either user open the canonical DM without a prior lookup, and makes creation idempotent.

Derivation hashes the byte-sorted, de-duplicated set of user IDs (a DM with oneself collapses to a single member) under a domain-separation prefix. Since the input is a sorted set, member ordering and duplicates do not affect the result. The SHA-256 digest is ChatIDSize bytes wide by construction.

It panics if either user ID is not the expected fixed width, which would be a programming error: all user IDs in the system are UUIDs. Fixed-width members also make the sorted concatenation unambiguous without length prefixing.

Types

type Chat

type Chat struct {
	ID            *commonpb.ChatId
	Type          chatpb.Metadata_ChatType
	Members       []*commonpb.UserId
	LastActivity  time.Time
	LastMessageID *messagingpb.MessageId
}

Chat is the stored metadata for a chat.

It deliberately holds only the state owned by the chat domain: the chat's identity, type, immutable membership, and the last-activity timestamp used to order a user's chat list. The richer fields of chatpb.Metadata — member profiles, per-member message pointers, and the last message — live in other domains (profile, messaging) and are hydrated by the server layer.

func (*Chat) Clone

func (c *Chat) Clone() *Chat

Clone returns a deep copy of the chat.

func (*Chat) HasMember

func (c *Chat) HasMember(userID *commonpb.UserId) bool

HasMember reports whether userID is a member of the chat.

func (*Chat) ToProto

func (c *Chat) ToProto() *chatpb.Metadata

ToProto projects the stored chat onto a chatpb.Metadata. Only the fields owned by the chat domain are populated: chat_id, type, last_activity, and a Member entry per member with just user_id set. The caller is responsible for hydrating member profiles, pointers, and the last message.

type DmFeedCursor

type DmFeedCursor struct {
	LastActivity time.Time
	ChatID       *commonpb.ChatId
}

DmFeedCursor marks a position within a DM feed snapshot read. The next page resumes at the chat immediately after (LastActivity, ChatID) in the feed's descending (last_activity, chat_id) order.

type MessageRef

type MessageRef struct {
	ChatID    *commonpb.ChatId
	MessageID *messagingpb.MessageId
}

MessageRef identifies a chat's message to hydrate. The feed builds one ref per chat (its last message) to batch the lookup across the page.

type MessagingReader

type MessagingReader interface {
	// LastMessages returns the message for each ref that exists, keyed by
	// string(chatID.Value). Refs without a message are absent from the map.
	LastMessages(ctx context.Context, refs []MessageRef) (map[string]*messagingpb.Message, error)

	// Pointers returns the delivered/read pointers for each chat, keyed by
	// string(chatID.Value). Chats with no pointers are absent from the map.
	Pointers(ctx context.Context, chatIDs []*commonpb.ChatId) (map[string][]*messagingpb.Pointer, error)
}

MessagingReader is the read slice of the messaging domain the Chat service needs to hydrate feed metadata. It is declared here (consumer side) so the chat package need not import messaging, keeping the messaging→chat dependency one-way; the messaging package supplies the concrete adapter.

type Server

type Server struct {
	chatpb.UnimplementedChatServer
	// contains filtered or unexported fields
}

func NewServer

func NewServer(log *zap.Logger, authz auth.Authorizer, chats Store, messaging MessagingReader) *Server

func (*Server) GetChat

func (*Server) GetDmChatFeed

type Store

type Store interface {
	// PutChat persists a new chat and its membership. It returns ErrChatExists
	// if a chat with the same ID already exists.
	PutChat(ctx context.Context, chat *Chat) error

	// GetChatByID returns the chat with the given ID, or ErrChatNotFound.
	GetChatByID(ctx context.Context, chatID *commonpb.ChatId) (*Chat, error)

	// GetDmFeedPage returns one page of userID's DM feed pinned to a snapshot:
	// the DMs userID is a member of whose last_activity is at or before snapshot,
	// ordered by (last_activity, chat_id) descending (most recent first), at most
	// limit chats (limit <= 0 means unbounded). When cursor is nil the page starts
	// at the most recent chat in the snapshot; otherwise it resumes strictly after
	// cursor. An empty result (no error) is returned when no chats remain.
	//
	// Pinning to a fixed watermark makes a multi-page read internally consistent.
	// last_activity only ever advances to a wall-clock send time, so any chat that
	// becomes active after the snapshot moves strictly above the watermark and
	// leaves the window — it can be neither duplicated onto nor skipped within a
	// later page. Those freshly-active chats are surfaced through the live
	// MetadataUpdate event stream instead (see the Chat service's GetDmChatFeed).
	//
	// It is scoped to DMs because the per-user inbox is split by chat type (see
	// the dynamodb store). Group chats will have a parallel accessor, and the
	// server merges the two descending streams into one feed.
	GetDmFeedPage(ctx context.Context, userID *commonpb.UserId, snapshot time.Time, cursor *DmFeedCursor, limit int) ([]*Chat, error)

	// GetMembers returns the member user IDs of a chat, or ErrChatNotFound.
	GetMembers(ctx context.Context, chatID *commonpb.ChatId) ([]*commonpb.UserId, error)

	// IsMember reports whether userID is a member of chatID. It returns false
	// (no error) when the chat does not exist.
	IsMember(ctx context.Context, chatID *commonpb.ChatId, userID *commonpb.UserId) (bool, error)

	// AdvanceLastMessage records messageID as the chat's most recent message,
	// moving last_activity forward to ts and last_message_id to messageID, and
	// reports whether it advanced. The two fields are two views of the same event
	// (the newest message) and are updated together. If the stored last_activity
	// is already at or after ts, it is a no-op and returns false. It returns
	// ErrChatNotFound if the chat does not exist.
	AdvanceLastMessage(ctx context.Context, chatID *commonpb.ChatId, messageID *messagingpb.MessageId, ts time.Time) (bool, error)
}

Store persists chats and their membership.

Membership is fixed at creation time (e.g. the two participants of a DM) and is never mutated afterward. The only mutable field is last_activity, which is advanced as new activity (typically messages) occurs and is the sort key for a user's chat list.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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