Ctrl K
ComunidadGuías…

internal/domain/story.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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
}