Documentation
¶
Index ¶
- Constants
- Variables
- func DeriveDmChatType(chatID *commonpb.ChatId, members []*commonpb.UserId) chatpb.ChatType
- func IsDmChatType(chatType chatpb.ChatType) bool
- func IsGroupChatID(chatID *commonpb.ChatId) bool
- func MustDeriveDmChatID(chatType chatpb.ChatType, a, b *commonpb.UserId) *commonpb.ChatId
- func MustGenerateGroupChatID() *commonpb.ChatId
- type BlocklistReader
- type Chat
- type DmFeedCursor
- type MessageRef
- type MessagingReader
- type PointerRef
- type ProfileReader
- type Server
- type Store
Constants ¶
const DmChatIDSize = 32
DmChatIDSize is the length, in bytes, of a DM chat ID: a SHA-256 digest over the DM's type and members (see MustDeriveDmChatID).
const GroupChatIDSize = 16
GroupChatIDSize is the length, in bytes, of a group chat ID: a server-generated UUID. Group membership is mutable, so a group's ID cannot be member-derived; it is an opaque random value minted at creation.
The two sizes never overlap, so a chat ID's length is its type family's discriminator: 32 bytes is a DM, 16 bytes is a group. Every DM path must reject 16-byte IDs and every group path must reject 32-byte IDs — that enforcement is what keeps the discriminator sound (an ID can never be claimed as both a derived DM and a group).
const MaxGroupChatCreationMembers = 50
MaxGroupChatCreationMembers is the largest initial member set a group chat may be created with; a larger set is rejected with ErrTooManyMembers and the caller grows the group with AddGroupMembers instead.
Creation is all-or-nothing, so the bound is what a single atomic write can cover: a DynamoDB transaction caps at 100 items, and creation spends one on the canonical record. The value is held well under that ceiling so the membership records and the canonical record always commit together — there is no partial-creation state for a caller to reconcile.
Variables ¶
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") // ErrTooManyMembers indicates that a group chat was created with more // initial members than MaxGroupChatCreationMembers allows. ErrTooManyMembers = errors.New("too many initial members") // ErrNoMembers indicates that a chat was created with an empty member set. // A chat nobody belongs to is unreachable: no one can read it, send to it, // or be added to it, since every such path gates on membership. ErrNoMembers = errors.New("chat must have at least one member") )
Functions ¶
func DeriveDmChatType ¶ added in v1.23.0
DeriveDmChatType reports which DM type's canonical derivation over the members produces chatID, letting callers that already hold a chat's members recover its type without a store read. It returns UNKNOWN when no DM type matches — including any malformed input — so callers must treat UNKNOWN as "not a derivable DM", not an error.
This works because every DM's ID commits to its type via the derivation domain. A future chat type whose ID is not member-derived (e.g. group chats) will return UNKNOWN here and needs its own discriminator.
func IsDmChatType ¶ added in v1.24.0
IsDmChatType reports whether chatType is a direct-message chat type — one with two participants and a canonical, member-derived ID — as opposed to a group or unknown chat.
func IsGroupChatID ¶ added in v1.26.0
IsGroupChatID reports whether chatID is a group chat ID, by length (see GroupChatIDSize).
func MustDeriveDmChatID ¶
MustDeriveDmChatID returns the deterministic chat ID for a DM of the given type between two users.
The ID is derived purely from the DM type and the participants, so it is stable across calls and independent of who initiates the chat: MustDeriveDmChatID(t, a, b) always equals MustDeriveDmChatID(t, 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 that encodes the DM type. Contact DMs use the bare prefix because they predate typed derivation, and their chat IDs must not change; the domains cannot alias each other because member sets are fixed-width, so the two encodings never produce equal-length hash inputs. Since the input is a sorted set, member ordering and duplicates do not affect the result. The SHA-256 digest is DmChatIDSize bytes wide by construction.
It panics on an unspecified chat type, or 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.
func MustGenerateGroupChatID ¶ added in v1.26.0
MustGenerateGroupChatID mints the ID for a new group chat: a random UUID. Group chat IDs are always generated server-side — a client-supplied ID is never trusted as a chat's identity.
Types ¶
type BlocklistReader ¶ added in v1.24.0
type BlocklistReader interface {
// GetBlocked returns which of candidateIDs the owner has blocked, as a set
// keyed by string(userID.Value). Candidates the owner has not blocked are
// absent from the map.
GetBlocked(ctx context.Context, ownerID *commonpb.UserId, candidateIDs []*commonpb.UserId) (map[string]bool, error)
}
BlocklistReader is the read slice of the blocklist domain the Chat service needs to compute per-viewer hidden state. Like the other readers it is declared here (consumer side) so the chat package need not import blocklist; the blocklist package supplies the concrete adapter.
type Chat ¶
type Chat struct {
ID *commonpb.ChatId
Type chatpb.ChatType
Members []*commonpb.UserId
Title string
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, membership, title, 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.
Members is the full, immutable member set for a DM, and is always empty for a group chat: group membership is mutable and lives in its own store records, which no path that reads the canonical record touches. A caller that needs a group's members reads them explicitly via Store.GetMembers. Title is group-only and empty for DMs.
func (*Chat) ToProto ¶
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 ¶
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 the members named in each
// ref, keyed by string(chatID.Value). Chats with no matching pointers are
// absent from the map.
Pointers(ctx context.Context, refs []PointerRef) (map[string][]*messagingpb.Pointer, error)
// LatestEventSequences returns the head event sequence of each given chat,
// keyed by string(chatID.Value). Chats at head 0 (no messages) are absent from
// the map, so a missing key means 0.
LatestEventSequences(ctx context.Context, chatIDs []*commonpb.ChatId) (map[string]uint64, 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 PointerRef ¶ added in v1.14.0
PointerRef names a chat and the members whose pointers to hydrate. The feed builds one ref per chat (with that chat's members) to batch the pointer lookup across the page.
type ProfileReader ¶ added in v1.16.0
type ProfileReader interface {
// GetPhoneNumbers returns the linked phone number for each of the given
// users that has one, keyed by string(userID.Value). Users without a linked
// phone number are absent from the map.
GetPhoneNumbers(ctx context.Context, userIDs []*commonpb.UserId) (map[string]*commonpb.PhoneNumber, error)
// GetPublicProfiles returns the public profile of each of the given users the
// profile domain knows, keyed by string(userID.Value). Unknown users are
// absent from the map. Every public field a member row shows — display name,
// profile picture, join timestamp — comes back in this one call.
//
// A member who has set neither a name nor a picture still gets an entry,
// carrying just the join timestamp. Every chat member is a user the profile
// domain knows, so hydration treats a missing entry as an error rather than
// standing in a profile of its own.
//
// Profile pictures come back with their blob metadata already resolved —
// including a short-lived download URL — so a client can render member avatars
// without a follow-up call. The URL expires; to re-mint one the client calls
// GetBlobs with an AccessContext naming that user's profile, which authorizes
// it because a profile picture is public.
//
// There is one proto per user, so a caller that fills in per-member fields
// must copy before mutating: the same user can be a member of several chats.
GetPublicProfiles(ctx context.Context, userIDs []*commonpb.UserId) (map[string]*profilepb.UserProfile, error)
}
ProfileReader is the read slice of the profile domain the Chat service needs to hydrate member profiles. Like MessagingReader it is declared here (consumer side) so the chat package need not import profile; the profile 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, profiles ProfileReader, blocklist BlocklistReader) *Server
func (*Server) GetChat ¶
func (s *Server) GetChat(ctx context.Context, req *chatpb.GetChatRequest) (*chatpb.GetChatResponse, error)
func (*Server) GetDmChatFeed ¶
func (s *Server) GetDmChatFeed(ctx context.Context, req *chatpb.GetDmChatFeedRequest) (*chatpb.GetDmChatFeedResponse, error)
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, ErrNoMembers if the member set
// is empty, and an error when the chat ID's length does not match the chat's
// type family.
//
// For a group chat (16-byte ID, type GROUP), Members become group membership
// records, equivalent to AddGroupMembers, and are written atomically with the
// canonical record: creation either fully succeeds or leaves nothing behind.
// Duplicate members collapse. A set larger than MaxGroupChatCreationMembers
// is rejected with ErrTooManyMembers rather than written non-atomically; a
// caller wanting a larger group creates it at the cap and grows it with
// AddGroupMembers.
PutChat(ctx context.Context, chat *Chat) error
// AddGroupMembers adds users as joined members of a group chat. It is
// idempotent: adding an already-joined member is a no-op that preserves
// their original join time, and re-adding a departed member rejoins them
// fresh. It returns ErrChatNotFound if the chat does not exist, and an
// error if chatID is not a group chat ID.
AddGroupMembers(ctx context.Context, chatID *commonpb.ChatId, userIDs []*commonpb.UserId) error
// RemoveGroupMember ends a user's membership in a group chat. Departure is
// a tombstone, not a deletion: the user stops being a member (IsMember
// false, excluded from GetMembers) but the record of their former
// membership is kept, and they can be re-added later. Removing a non-member
// or unknown user is a no-op. It returns an error if chatID is not a group
// chat ID.
RemoveGroupMember(ctx context.Context, chatID *commonpb.ChatId, userID *commonpb.UserId) error
// GetChatByID returns the canonical record for the chat with the given ID,
// or ErrChatNotFound. It reads only that record: Members carries a DM's
// inline participants and is always empty for a group chat, whose mutable
// membership lives in its own records. A caller that needs a group's members
// reads them explicitly via GetMembers, so the cost of enumerating a large
// group is never paid implicitly by a metadata read.
GetChatByID(ctx context.Context, chatID *commonpb.ChatId) (*Chat, error)
// GetDmFeedPage returns one page of userID's DM feed for a single chat type,
// pinned to a snapshot: the DMs of chatType 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 a single DM type because each type is its own feed (see
// GetDmChatFeedRequest.dm_chat_type). Group chats will have a parallel
// accessor, and the server merges the descending streams into one feed.
GetDmFeedPage(ctx context.Context, userID *commonpb.UserId, chatType chatpb.ChatType, snapshot time.Time, cursor *DmFeedCursor, limit int) ([]*Chat, error)
// GetMembers returns the member user IDs of a chat, or ErrChatNotFound. For
// a DM this is the canonical inline member list; for a group chat it is the
// currently joined members.
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, or when a group member has been
// removed.
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 reports advanced=false. It
// returns ErrChatNotFound if the chat does not exist.
//
// For a DM it also returns the chat's members — the set the new activity is
// fanned out to, which rides on the canonical record it must load regardless.
// A caller that goes on to broadcast the same activity can reuse this set
// instead of issuing a separate GetMembers. Members are returned on both the
// advanced and no-op paths; they are nil on error (including
// ErrChatNotFound). A group chat's membership lives in its own records, so
// members is empty and the caller reads GetMembers itself.
AdvanceLastMessage(ctx context.Context, chatID *commonpb.ChatId, messageID *messagingpb.MessageId, ts time.Time) (advanced bool, members []*commonpb.UserId, err error)
}
Store persists chats and their membership.
DM membership is fixed at creation time (the two participants) and is never mutated afterward. Group chat membership is mutable via AddGroupMembers and RemoveGroupMember. last_activity is advanced as new activity (typically messages) occurs and is the sort key for a user's chat list.
A chat ID's length discriminates its family (see DmChatIDSize and GroupChatIDSize): implementations dispatch on it, and each method rejects or misses IDs of the wrong family for its semantics.