Documentation
¶
Index ¶
- Constants
- Variables
- func DeriveDmChatType(chatID *commonpb.ChatId, members []*commonpb.UserId) chatpb.ChatType
- func IsDmChatType(chatType chatpb.ChatType) bool
- func MustDeriveDmChatID(chatType chatpb.ChatType, a, b *commonpb.UserId) *commonpb.ChatId
- type BlocklistReader
- type Chat
- type DmFeedCursor
- type MessageRef
- type MessagingReader
- type PointerRef
- type ProfileReader
- type Server
- type Store
Constants ¶
const ChatIDSize = 32
ChatIDSize is the length, in bytes, of a chat ID.
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") )
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 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 ChatIDSize 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.
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
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) 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]*phonepb.PhoneNumber, error)
// GetDisplayNames returns the display name for each of the given users that
// has one, keyed by string(userID.Value). Users without a display name are
// absent from the map.
GetDisplayNames(ctx context.Context, userIDs []*commonpb.UserId) (map[string]string, error)
// GetProfilePictures returns the profile picture of each of the given users
// that has one, keyed by string(userID.Value). Users without one are absent
// from the map.
//
// The renditions 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.
GetProfilePictures(ctx context.Context, userIDs []*commonpb.UserId) (map[string]*blobpb.Media, 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.
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 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.
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 reports advanced=false. It
// returns ErrChatNotFound if the chat does not exist.
//
// It also returns the chat's members — the set the new activity is fanned out
// to, which 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).
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.
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.