chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
557
internal/domain/story.go
Normal file
557
internal/domain/story.go
Normal file
|
|
@ -0,0 +1,557 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// MaxStoryID keeps story ids inside TL int / PostgreSQL int4 bounds.
|
||||
MaxStoryID = MaxMessageBoxID
|
||||
// MaxStoryIDs limits exact-id story RPCs and view increments.
|
||||
MaxStoryIDs = 200
|
||||
// MaxStoryListLimit bounds one active/archive/pinned story page.
|
||||
MaxStoryListLimit = 100
|
||||
// MaxStoryInteractionListLimit bounds one story viewer/reaction page.
|
||||
MaxStoryInteractionListLimit = 100
|
||||
// MaxStoryInteractionOffsetLength bounds server-generated owner interaction
|
||||
// cursors before they reach store-side parsing or query predicates.
|
||||
MaxStoryInteractionOffsetLength = 64
|
||||
// MaxStoryAlbumOffset bounds stories.getAlbumStories count-offset paging
|
||||
// before a future album store can turn malformed requests into deep OFFSET
|
||||
// work. DrKLO sends loadedObjects.size(), so normal UI paging stays small.
|
||||
MaxStoryAlbumOffset = 10000
|
||||
// MaxStoryPinnedToTop bounds stories.togglePinnedToTop. It matches the
|
||||
// client default stories_pinned_to_top_count_max used by TDesktop/DrKLO.
|
||||
MaxStoryPinnedToTop = 3
|
||||
// MaxStoryPrivacyFanoutTargets bounds one privacy-change fanout candidate set.
|
||||
MaxStoryPrivacyFanoutTargets = 5000
|
||||
// MaxStorySendAsChannels bounds stories.getChatsToSend channel candidates.
|
||||
MaxStorySendAsChannels = 200
|
||||
// MaxStoryMediaAreas bounds one story's overlay/click target vector.
|
||||
MaxStoryMediaAreas = 16
|
||||
// MaxStoryMediaAreaURLLength bounds mediaAreaUrl click targets stored in
|
||||
// one story snapshot.
|
||||
MaxStoryMediaAreaURLLength = 2048
|
||||
// MaxStoryStarGiftSlugLength bounds collectible gift slugs stored in one
|
||||
// story snapshot. TDesktop/DrKLO treat the slug as a deep-link path token.
|
||||
MaxStoryStarGiftSlugLength = 255
|
||||
// MaxStoryGeoAddressPartLength bounds optional mediaAreaGeoPoint address
|
||||
// labels stored in one story snapshot.
|
||||
MaxStoryGeoAddressPartLength = 256
|
||||
// MaxStoryWeatherEmojiLength bounds mediaAreaWeather emoji labels stored in
|
||||
// one story snapshot.
|
||||
MaxStoryWeatherEmojiLength = 32
|
||||
// DefaultStoryPeriod is the Layer 225 sendStory period when the optional
|
||||
// period flag is absent.
|
||||
DefaultStoryPeriod = 86400
|
||||
// MaxStoryViewQueryLength bounds q in stories.getStoryViewsList before it
|
||||
// reaches store-side LIKE/search predicates.
|
||||
MaxStoryViewQueryLength = 128
|
||||
// DefaultStoryCanSendRemaining is the development-stage story quota returned
|
||||
// by canSendStory until production limits are modeled.
|
||||
DefaultStoryCanSendRemaining = 100
|
||||
)
|
||||
|
||||
var (
|
||||
ErrStoryIDInvalid = errors.New("story id invalid")
|
||||
ErrStoryNotFound = errors.New("story not found")
|
||||
ErrStoryPeerInvalid = errors.New("story peer invalid")
|
||||
ErrStoryNotModified = errors.New("story not modified")
|
||||
ErrStoryOffsetInvalid = errors.New("story offset invalid")
|
||||
ErrStoryPeriodInvalid = errors.New("story period invalid")
|
||||
)
|
||||
|
||||
// ValidateStoryInteractionOffset validates stories.getStoryViewsList and
|
||||
// stories.getStoryReactionsList keyset cursors without depending on TL types.
|
||||
func ValidateStoryInteractionOffset(offset string, reactionsOnly bool) error {
|
||||
return validateStoryInteractionOffset(offset, reactionsOnly, false)
|
||||
}
|
||||
|
||||
// ValidateStoryReactionInteractionOffset validates stories.getStoryReactionsList
|
||||
// cursors. When forwardsFirst is set, reaction-list pages may legitimately use
|
||||
// group 1 for ordinary reaction rows after public repost rows.
|
||||
func ValidateStoryReactionInteractionOffset(offset string, forwardsFirst bool) error {
|
||||
return validateStoryInteractionOffset(offset, true, forwardsFirst)
|
||||
}
|
||||
|
||||
func validateStoryInteractionOffset(offset string, reactionsOnly, forwardsFirst bool) error {
|
||||
if offset == "" {
|
||||
return nil
|
||||
}
|
||||
if len(offset) > MaxStoryInteractionOffsetLength {
|
||||
return ErrStoryOffsetInvalid
|
||||
}
|
||||
parts := strings.Split(offset, ":")
|
||||
if len(parts) != 3 && len(parts) != 4 {
|
||||
return ErrStoryOffsetInvalid
|
||||
}
|
||||
group, err1 := strconv.Atoi(parts[0])
|
||||
date, err2 := strconv.Atoi(parts[1])
|
||||
viewerID, err3 := strconv.ParseInt(parts[2], 10, 64)
|
||||
var messageID int
|
||||
var err4 error
|
||||
if len(parts) == 4 {
|
||||
messageID, err4 = strconv.Atoi(parts[3])
|
||||
}
|
||||
reactionGroupInvalid := reactionsOnly && group != 0 && !(forwardsFirst && group == 1)
|
||||
if err1 != nil || err2 != nil || err3 != nil || err4 != nil ||
|
||||
group < 0 || group > 1 || reactionGroupInvalid ||
|
||||
date <= 0 || viewerID == 0 || messageID < 0 || messageID > MaxMessageBoxID {
|
||||
return ErrStoryOffsetInvalid
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Story is a protocol-neutral Telegram story snapshot owned by one peer.
|
||||
type Story struct {
|
||||
Owner Peer
|
||||
ID int
|
||||
RandomID int64
|
||||
Date int
|
||||
ExpireDate int
|
||||
Deleted bool
|
||||
Pinned bool
|
||||
PinnedToTopOrder int
|
||||
Public bool
|
||||
CloseFriends bool
|
||||
Contacts bool
|
||||
SelectedContacts bool
|
||||
NoForwards bool
|
||||
Edited bool
|
||||
Out bool
|
||||
PrivacyRules []PrivacyRule
|
||||
AllowUserIDs []int64
|
||||
DisallowUserIDs []int64
|
||||
Caption string
|
||||
Entities []MessageEntity
|
||||
Media *MessageMedia
|
||||
MediaAreas []StoryMediaArea
|
||||
Forward *StoryForward
|
||||
Views StoryViews
|
||||
SentReaction *MessageReaction
|
||||
}
|
||||
|
||||
// Active reports whether the story belongs in the active strip at now.
|
||||
func (s Story) Active(now int) bool {
|
||||
return !s.Deleted && s.ExpireDate > now
|
||||
}
|
||||
|
||||
// Interactable reports whether a viewer may create new view/reaction state.
|
||||
// Expired profile-pinned stories remain visible from profiles, but ordinary
|
||||
// expired stories must not accept stale interaction writes.
|
||||
func (s Story) Interactable(now int) bool {
|
||||
return !s.Deleted && (s.ExpireDate > now || s.Pinned)
|
||||
}
|
||||
|
||||
// VisibleTo reports story visibility with no relationship facts.
|
||||
func (s Story) VisibleTo(viewerUserID int64) bool {
|
||||
return s.VisibleToWithFacts(viewerUserID, false, false)
|
||||
}
|
||||
|
||||
// VisibleToWithFacts reports story visibility using already-loaded viewer facts.
|
||||
func (s Story) VisibleToWithFacts(viewerUserID int64, viewerIsContact, viewerCloseFriend bool) bool {
|
||||
return s.VisibleToWithStoryFacts(viewerUserID, viewerIsContact, viewerCloseFriend, false)
|
||||
}
|
||||
|
||||
// VisibleToWithStoryFacts reports story visibility using already-loaded viewer
|
||||
// facts, including owner-side story blocklist membership.
|
||||
func (s Story) VisibleToWithStoryFacts(viewerUserID int64, viewerIsContact, viewerCloseFriend, viewerStoryBlocked bool) bool {
|
||||
if viewerUserID == 0 {
|
||||
return false
|
||||
}
|
||||
if s.Owner.Type == PeerTypeUser && s.Owner.ID == viewerUserID {
|
||||
return true
|
||||
}
|
||||
if viewerStoryBlocked {
|
||||
return false
|
||||
}
|
||||
if int64InSet(s.DisallowUserIDs, viewerUserID) {
|
||||
return false
|
||||
}
|
||||
if int64InSet(s.AllowUserIDs, viewerUserID) {
|
||||
return true
|
||||
}
|
||||
if s.Public {
|
||||
return true
|
||||
}
|
||||
if s.Contacts && viewerIsContact {
|
||||
return true
|
||||
}
|
||||
if s.CloseFriends && viewerCloseFriend {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func int64InSet(ids []int64, needle int64) bool {
|
||||
for _, id := range ids {
|
||||
if id == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// StoryForward contains the protocol-neutral source of a reposted story.
|
||||
type StoryForward struct {
|
||||
// Source is the durable server-side source story owner used for counters
|
||||
// and owner interaction lists. From is the client-visible clickable peer
|
||||
// and may be empty when forwards privacy requires a from_name-only header.
|
||||
Source Peer
|
||||
From Peer
|
||||
FromName string
|
||||
StoryID int
|
||||
Modified bool
|
||||
}
|
||||
|
||||
// StoryViews is the owner-visible aggregate of story views and reactions.
|
||||
type StoryViews struct {
|
||||
ViewsCount int
|
||||
ForwardsCount int
|
||||
ReactionsCount int
|
||||
Reactions []ChannelMessageReactionCount
|
||||
RecentViewers []int64
|
||||
HasViewers bool
|
||||
}
|
||||
|
||||
// StoryView is one viewer's durable view/reaction row.
|
||||
type StoryView struct {
|
||||
Owner Peer
|
||||
StoryID int
|
||||
ViewerID int64
|
||||
Date int
|
||||
Reaction *MessageReaction
|
||||
// Repost is set for public repost interactions. It is protocol-neutral and
|
||||
// converted by rpc to storyViewPublicRepost.
|
||||
Repost *Story
|
||||
PublicForward *StoryPublicForward
|
||||
Blocked bool
|
||||
BlockedMyStoriesFrom bool
|
||||
}
|
||||
|
||||
// StoryPublicForward is a public channel/supergroup message that shared a story
|
||||
// via messageMediaStory. It is converted by rpc to storyViewPublicForward or
|
||||
// storyReactionPublicForward.
|
||||
type StoryPublicForward struct {
|
||||
Message ChannelMessage
|
||||
}
|
||||
|
||||
// StoryViewListRequest pages owner-visible story viewers.
|
||||
type StoryViewListRequest struct {
|
||||
ViewerUserID int64
|
||||
Owner Peer
|
||||
StoryID int
|
||||
Offset string
|
||||
Limit int
|
||||
Query string
|
||||
JustContacts bool
|
||||
ReactionsFirst bool
|
||||
ForwardsFirst bool
|
||||
}
|
||||
|
||||
// StoryViewList is a bounded page for stories.getStoryViewsList.
|
||||
type StoryViewList struct {
|
||||
Count int
|
||||
ViewsCount int
|
||||
ForwardsCount int
|
||||
ReactionsCount int
|
||||
Views []StoryView
|
||||
NextOffset string
|
||||
}
|
||||
|
||||
// StoryReactionListRequest pages peers that reacted to one story.
|
||||
type StoryReactionListRequest struct {
|
||||
ViewerUserID int64
|
||||
Owner Peer
|
||||
StoryID int
|
||||
Reaction *MessageReaction
|
||||
Offset string
|
||||
Limit int
|
||||
ForwardsFirst bool
|
||||
CanViewOwnerInteractions bool
|
||||
}
|
||||
|
||||
// StoryReactionList is a bounded page for stories.getStoryReactionsList.
|
||||
type StoryReactionList struct {
|
||||
Count int
|
||||
Reactions []StoryView
|
||||
NextOffset string
|
||||
}
|
||||
|
||||
// StoryMessageForwardListRequest pages public channel/supergroup messages that
|
||||
// shared one source story as messageMediaStory.
|
||||
type StoryMessageForwardListRequest struct {
|
||||
ViewerUserID int64
|
||||
Owner Peer
|
||||
StoryID int
|
||||
Offset string
|
||||
Limit int
|
||||
ReactionsFirst bool
|
||||
ForwardsFirst bool
|
||||
}
|
||||
|
||||
// StoryMessageForwardList is a bounded page of public message forwards.
|
||||
type StoryMessageForwardList struct {
|
||||
Count int
|
||||
Forwards []StoryView
|
||||
NextOffset string
|
||||
}
|
||||
|
||||
// StoryPublicForwardListRequest pages public repost stories for one source story.
|
||||
type StoryPublicForwardListRequest struct {
|
||||
ViewerUserID int64
|
||||
Owner Peer
|
||||
StoryID int
|
||||
Offset string
|
||||
Limit int
|
||||
}
|
||||
|
||||
// StoryPublicForwardList is a bounded page of public repost story forwards.
|
||||
type StoryPublicForwardList struct {
|
||||
Count int
|
||||
Forwards []StoryView
|
||||
NextOffset string
|
||||
}
|
||||
|
||||
// PeerStories is the story list for one owner peer from one viewer's perspective.
|
||||
type PeerStories struct {
|
||||
Peer Peer
|
||||
MaxReadID int
|
||||
Stories []Story
|
||||
Users []User
|
||||
Channels []Channel
|
||||
}
|
||||
|
||||
// StoryList is a bounded story page.
|
||||
type StoryList struct {
|
||||
Count int
|
||||
State string
|
||||
HasMore bool
|
||||
// Hidden reports that this list is the viewer's hidden story source list.
|
||||
Hidden bool
|
||||
Stories []Story
|
||||
// PinnedToTop contains the owner-visible top-pinned story IDs in display
|
||||
// order for stories.stories.pinned_to_top.
|
||||
PinnedToTop []int
|
||||
Peers []PeerStories
|
||||
Users []User
|
||||
Channels []Channel
|
||||
}
|
||||
|
||||
// StoryListCursor is the opaque getAllStories seek position after one peer.
|
||||
type StoryListCursor struct {
|
||||
Set bool
|
||||
Date int
|
||||
Peer Peer
|
||||
}
|
||||
|
||||
// StoryListDigest summarizes a complete viewer-scoped active story source list.
|
||||
type StoryListDigest struct {
|
||||
Count int
|
||||
Hash uint64
|
||||
}
|
||||
|
||||
// DigestStoryPeerList returns a deterministic digest for an already ordered
|
||||
// complete active/hidden story peer list.
|
||||
func DigestStoryPeerList(peers []PeerStories) StoryListDigest {
|
||||
h := fnv.New64a()
|
||||
fmt.Fprintf(h, "story-list|count=%d|", len(peers))
|
||||
for _, peer := range peers {
|
||||
fmt.Fprintf(h, "p=%s:%d:%d:%d|", peer.Peer.Type, peer.Peer.ID, peer.MaxReadID, len(peer.Stories))
|
||||
for _, story := range peer.Stories {
|
||||
fmt.Fprintf(
|
||||
h,
|
||||
"s=%d:%d:%d:%t:%t:%d:%t:%t:%t:%t:%t:%t:%d:%d:%#v:%#v:%#v:%#v:%#v:%#v:%#v:%#v|",
|
||||
story.ID,
|
||||
story.Date,
|
||||
story.ExpireDate,
|
||||
story.Deleted,
|
||||
story.Pinned,
|
||||
story.PinnedToTopOrder,
|
||||
story.Public,
|
||||
story.CloseFriends,
|
||||
story.Contacts,
|
||||
story.SelectedContacts,
|
||||
story.Edited,
|
||||
story.Out,
|
||||
story.Views.ViewsCount,
|
||||
story.Views.ReactionsCount,
|
||||
story.SentReaction,
|
||||
story.Caption,
|
||||
story.Entities,
|
||||
story.Media,
|
||||
story.MediaAreas,
|
||||
story.Forward,
|
||||
story.PrivacyRules,
|
||||
story.AllowUserIDs,
|
||||
)
|
||||
fmt.Fprintf(h, "disallow=%#v|noforwards=%t|", story.DisallowUserIDs, story.NoForwards)
|
||||
}
|
||||
}
|
||||
return StoryListDigest{Count: len(peers), Hash: h.Sum64()}
|
||||
}
|
||||
|
||||
// StoryReadState is the per viewer+owner read boundary.
|
||||
type StoryReadState struct {
|
||||
ViewerID int64
|
||||
Peer Peer
|
||||
MaxReadID int
|
||||
Date int
|
||||
}
|
||||
|
||||
// RecentStory is the domain counterpart of TL recentStory.
|
||||
type RecentStory struct {
|
||||
Peer Peer
|
||||
MaxID int
|
||||
Live bool
|
||||
}
|
||||
|
||||
// PeerStoryProjection is the per-viewer story summary embedded into peer objects.
|
||||
type PeerStoryProjection struct {
|
||||
Peer Peer
|
||||
Recent RecentStory
|
||||
Hidden bool
|
||||
}
|
||||
|
||||
// StoryReadResult describes a readStories mutation.
|
||||
type StoryReadResult struct {
|
||||
ViewerID int64
|
||||
Peer Peer
|
||||
MaxReadID int
|
||||
Advanced bool
|
||||
Date int
|
||||
}
|
||||
|
||||
// StoryReactionResult describes one story reaction mutation.
|
||||
type StoryReactionResult struct {
|
||||
ViewerID int64
|
||||
Peer Peer
|
||||
StoryID int
|
||||
Reaction *MessageReaction
|
||||
Story Story
|
||||
Changed bool
|
||||
Date int
|
||||
}
|
||||
|
||||
// StoryMediaAreaKind identifies a protocol-neutral story overlay area.
|
||||
type StoryMediaAreaKind string
|
||||
|
||||
const (
|
||||
StoryMediaAreaSuggestedReaction StoryMediaAreaKind = "suggested_reaction"
|
||||
StoryMediaAreaURL StoryMediaAreaKind = "url"
|
||||
StoryMediaAreaGeoPoint StoryMediaAreaKind = "geo_point"
|
||||
StoryMediaAreaVenue StoryMediaAreaKind = "venue"
|
||||
StoryMediaAreaWeather StoryMediaAreaKind = "weather"
|
||||
StoryMediaAreaChannelPost StoryMediaAreaKind = "channel_post"
|
||||
StoryMediaAreaStarGift StoryMediaAreaKind = "star_gift"
|
||||
)
|
||||
|
||||
// StoryMediaAreaCoordinates is a percentage-based story media overlay box.
|
||||
type StoryMediaAreaCoordinates struct {
|
||||
X float64
|
||||
Y float64
|
||||
W float64
|
||||
H float64
|
||||
Rotation float64
|
||||
Radius float64
|
||||
HasRadius bool
|
||||
}
|
||||
|
||||
// StoryGeoPointAddress is the protocol-neutral counterpart of geoPointAddress.
|
||||
type StoryGeoPointAddress struct {
|
||||
CountryISO2 string
|
||||
State string
|
||||
City string
|
||||
Street string
|
||||
}
|
||||
|
||||
// StoryMediaArea is a protocol-neutral media area stored with a story snapshot.
|
||||
type StoryMediaArea struct {
|
||||
Kind StoryMediaAreaKind
|
||||
Coordinates StoryMediaAreaCoordinates
|
||||
Dark bool
|
||||
Flipped bool
|
||||
Reaction *MessageReaction
|
||||
URL string
|
||||
Geo *MessageGeoPoint
|
||||
GeoAddress *StoryGeoPointAddress
|
||||
Venue *MessageVenue
|
||||
WeatherEmoji string
|
||||
TemperatureC float64
|
||||
Color int
|
||||
ChannelID int64
|
||||
MsgID int
|
||||
StarGiftSlug string
|
||||
}
|
||||
|
||||
// StoryCreateRequest creates one owner story with a store-assigned monotonic ID.
|
||||
type StoryCreateRequest struct {
|
||||
Owner Peer
|
||||
RandomID int64
|
||||
Date int
|
||||
Period int
|
||||
Pinned bool
|
||||
Public bool
|
||||
CloseFriends bool
|
||||
Contacts bool
|
||||
SelectedContacts bool
|
||||
NoForwards bool
|
||||
PrivacyRules []PrivacyRule
|
||||
AllowUserIDs []int64
|
||||
DisallowUserIDs []int64
|
||||
Caption string
|
||||
Entities []MessageEntity
|
||||
Media *MessageMedia
|
||||
MediaAreas []StoryMediaArea
|
||||
Forward *StoryForward
|
||||
}
|
||||
|
||||
// StoryCreateResult describes a sendStory mutation.
|
||||
type StoryCreateResult struct {
|
||||
Story Story
|
||||
Duplicate bool
|
||||
}
|
||||
|
||||
// StoryEditRequest applies a partial story edit.
|
||||
type StoryEditRequest struct {
|
||||
Owner Peer
|
||||
ID int
|
||||
Media *MessageMedia
|
||||
UpdateMedia bool
|
||||
Caption string
|
||||
Entities []MessageEntity
|
||||
UpdateCaption bool
|
||||
Public bool
|
||||
CloseFriends bool
|
||||
Contacts bool
|
||||
SelectedContacts bool
|
||||
PrivacyRules []PrivacyRule
|
||||
AllowUserIDs []int64
|
||||
DisallowUserIDs []int64
|
||||
UpdatePrivacy bool
|
||||
MediaAreas []StoryMediaArea
|
||||
UpdateMediaAreas bool
|
||||
}
|
||||
|
||||
// StoryEditResult describes a story edit and its previous visible facts.
|
||||
type StoryEditResult struct {
|
||||
Story Story
|
||||
Previous Story
|
||||
}
|
||||
|
||||
// StoryMutationResult describes story mutations that may emit updateStory.
|
||||
type StoryMutationResult struct {
|
||||
Peer Peer
|
||||
IDs []int
|
||||
Stories []Story
|
||||
Previous []Story
|
||||
}
|
||||
|
||||
// UpsertStoryRequest inserts or replaces a story snapshot.
|
||||
type UpsertStoryRequest struct {
|
||||
Story Story
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue