Ctrl K
ComunidadGuías…

internal/domain/message.go

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

Archivo original ↗Preguntar a KiBot
package domain

// MessageEntityType identifies a message entity type.
type MessageEntityType string

const (
	MessageEntityBold          MessageEntityType = "bold"
	MessageEntityItalic        MessageEntityType = "italic"
	MessageEntityUnderline     MessageEntityType = "underline"
	MessageEntityStrike        MessageEntityType = "strike"
	MessageEntityCode          MessageEntityType = "code"
	MessageEntityPre           MessageEntityType = "pre"
	MessageEntityTextURL       MessageEntityType = "text_url"
	MessageEntityMentionName   MessageEntityType = "mention_name"
	MessageEntitySpoiler       MessageEntityType = "spoiler"
	MessageEntityBlockquote    MessageEntityType = "blockquote"
	MessageEntityCustomEmoji   MessageEntityType = "custom_emoji"
	MessageEntityMention       MessageEntityType = "mention"
	MessageEntityHashtag       MessageEntityType = "hashtag"
	MessageEntityCashtag       MessageEntityType = "cashtag"
	MessageEntityBotCommand    MessageEntityType = "bot_command"
	MessageEntityURL           MessageEntityType = "url"
	MessageEntityEmail         MessageEntityType = "email"
	MessageEntityPhone         MessageEntityType = "phone"
	MessageEntityBankCard      MessageEntityType = "bank_card"
	MessageEntityFormattedDate MessageEntityType = "formatted_date"
	MessageEntityDiffInsert    MessageEntityType = "diff_insert"
	MessageEntityDiffReplace   MessageEntityType = "diff_replace"
	MessageEntityDiffDelete    MessageEntityType = "diff_delete"
)

const (
	// MaxMessageTextLength matches the first-stage text message limit exposed to Telegram clients.
	MaxMessageTextLength = 4096
	// MaxMessageReplyQuoteLength matches TDesktop's quote_length_max app config default.
	MaxMessageReplyQuoteLength = 1024
	// MaxMessageReplyQuoteOffset bounds quote_offset, which is an offset inside message text, not a message id.
	MaxMessageReplyQuoteOffset = 2 * MaxMessageTextLength // UTF-16 units, including surrogate pairs.
	// MaxMessageEntityCount limits styled text entity vectors in message text and quotes.
	MaxMessageEntityCount = 256
	// MaxMessageBoxID is the largest message id safely expressible by a TL int / PostgreSQL int4.
	MaxMessageBoxID = 1<<31 - 1
	// MaxDeleteMessageIDs limits the owner-perspective id count of a single deleteMessages/updateDeleteMessages.
	// Large history cleanup goes through deleteHistory in batches, so a single RPC cannot build a huge array or durable payload.
	MaxDeleteMessageIDs = 1000
	// MaxGetMessageIDs limits the exact-ID batch of getMessages / channels.getMessages.
	MaxGetMessageIDs = 100
	// MaxDeleteHistoryBatch limits the number of message boxes a single deleteHistory actually cleans.
	// affectedHistory.Offset > 0 lets the client keep calling, and the server does not RETURN the whole history at once.
	MaxDeleteHistoryBatch = 1000
	// MaxForwardMessageIDs limits the owner-perspective id count of a single forwardMessages.
	MaxForwardMessageIDs = 100
	// MaxMessageHistoryAddOffset limits the absolute value of history/search add_offset.
	// TDesktop normally uses only small window offsets; the server must refuse to turn an oversized client value into a SQL OFFSET or slice capacity.
	MaxMessageHistoryAddOffset = 100
)

// ClampMessageHistoryAddOffset bounds Telegram history/search add_offset to a small local window.
func ClampMessageHistoryAddOffset(v int) int {
	if v > MaxMessageHistoryAddOffset {
		return MaxMessageHistoryAddOffset
	}
	if v < -MaxMessageHistoryAddOffset {
		return -MaxMessageHistoryAddOffset
	}
	return v
}

// ValidateMessageReplyBounds validates reply fields that are independent of peer visibility.
func ValidateMessageReplyBounds(reply *MessageReply) error {
	if reply == nil {
		return nil
	}
	if reply.MessageID < 0 || reply.MessageID > MaxMessageBoxID {
		return ErrReplyMessageIDInvalid
	}
	if reply.TopMessageID < 0 || reply.TopMessageID > MaxMessageBoxID {
		return ErrReplyMessageIDInvalid
	}
	if reply.StoryID < 0 || reply.StoryID > MaxStoryID {
		return ErrReplyMessageIDInvalid
	}
	// A story reply (StoryID>0) carries no MessageID/TopMessageID; a normal reply has at least one.
	if reply.MessageID == 0 && reply.TopMessageID == 0 && reply.StoryID == 0 && reply.External == nil {
		return ErrReplyMessageIDInvalid
	}
	if reply.QuoteOffset < 0 || reply.QuoteOffset > MaxMessageReplyQuoteOffset {
		return ErrReplyMessageIDInvalid
	}
	return nil
}

// MessageEntity is a business-layer message entity, independent of TL types.
type MessageEntity struct {
	Type   MessageEntityType
	Offset int
	Length int
	// URL is used only by text_url.
	URL string
	// UserID is used only by mention_name.
	UserID int64
	// Language is used only by pre.
	Language string
	// DocumentID is used only by custom_emoji.
	DocumentID int64
	// Collapsed is used only by blockquote.
	Collapsed bool
	// Date / Relative / ShortTime / LongTime / ShortDate / LongDate / DayOfWeek
	// are used only by formatted_date.
	Date      int
	Relative  bool
	ShortTime bool
	LongTime  bool
	ShortDate bool
	LongDate  bool
	DayOfWeek bool
	// OldText is used only by AI compose diff_replace; ordinary message input does not accept this entity.
	OldText string
}

// Message is one private chat message from an account's perspective.
type Message struct {
	ID             int   // message box id from the current owner's perspective, exposed to Telegram clients
	UID            int64 // shared private-message body id, not exposed to clients
	RandomID       int64
	OwnerUserID    int64
	Peer           Peer
	From           Peer
	Date           int
	EditDate       int
	HideEdited     bool
	Out            bool
	Silent         bool
	NoForwards     bool
	Body           string
	Entities       []MessageEntity
	ReplyTo        *MessageReply
	Forward        *MessageForward
	Reactions      *ChannelMessageReactions
	Pts            int
	TTLPeriod      int
	ExpiresAt      int
	Media          *MessageMedia
	MediaUnread    bool
	ReactionUnread bool
	ViaBotID       int64
	// GroupedID is the album grouping id: the messages of one sendMultiMedia share the same non-zero value,
	// and the client renders them as a single album group. Non-album messages are always 0. Both boxes hold the same value.
	GroupedID int64
	// Effect is the message effect id (message.effect, flags2.2?long): the animation effect exclusive to a 1-1 private chat
	// (🎉/👍 etc.); both the sender and recipient boxes hold the same non-zero value and each plays it once; non-effect messages are always 0.
	// A forward carries no effect (the new message is always 0). Private chats only; groups/channels do not render it.
	Effect int64
	// ReplyMarkup is the reply/inline keyboard snapshot carried by a bot message. Only a bot outbound message may be
	// non-nil; an ordinary user message is always nil (the send-side is_bot gate). Both boxes hold the same snapshot (no per-viewer difference).
	ReplyMarkup *MessageReplyMarkup
	// RichMessage is the Layer 228 rich text message (richMessage) snapshot, optional; an ordinary message is always nil.
	RichMessage *MessageRichMessage
	// Pinned is the owner-perspective pin flag (official private-chat multi-pin semantics: each side's
	// box row holds it independently, and a non-pm_oneside operation flips both sides together).
	Pinned bool
	// Deleted is the owner-perspective soft-delete visibility marker. It is used only by update replay/difference recovery
	// to decide whether a message snapshot can still be sent; an ordinary history query should filter deleted boxes directly at the store layer.
	Deleted bool
	// SavedPeer is the Saved Messages sub-chat grouping key (message.saved_peer_id).
	// Only a self-chat box row is non-zero: a directly written note = self; a forward into saved messages = the source chat peer;
	// a backfill fallback uses the hidden-author placeholder user 2666000. A non-self-chat row is always the zero value.
	SavedPeer Peer
	PaidStars int64
}

// NewHistoryClearMessage projects an existing owner-perspective box in place into a
// messageActionHistoryClear service message. The caller passes only the identity fields that must be kept;
// the remaining body, media, reply, reaction, TTL, pin and other payloads are all cleared per the invariant.
func NewHistoryClearMessage(ownerUserID int64, peer Peer, boxID int, uid int64, date, pts int) Message {
	self := Peer{Type: PeerTypeUser, ID: ownerUserID}
	return Message{
		ID:          boxID,
		UID:         uid,
		OwnerUserID: ownerUserID,
		Peer:        peer,
		From:        self,
		Date:        date,
		Out:         true,
		Pts:         pts,
		Media: &MessageMedia{
			Kind: MessageMediaKindService,
			ServiceAction: &MessageServiceAction{
				Kind: MessageServiceActionHistoryClear,
			},
		},
	}
}

// IsHistoryClearServiceMessage reports whether the box is already a history-clear anchor.
func IsHistoryClearServiceMessage(msg Message) bool {
	return msg.Media != nil &&
		msg.Media.Kind == MessageMediaKindService &&
		msg.Media.ServiceAction != nil &&
		msg.Media.ServiceAction.Kind == MessageServiceActionHistoryClear
}

const MessageRichBlocksLegacyLayer = 228

// MessageRichMessage is the protocol-neutral snapshot of a rich text message (richMessage): a set of IV
// PageBlocks (Blocks) + embedded resolved Photos/Documents.
//
// Blocks stores the opaque bytes of the gotd TL-serialized []tg.PageBlockClass — the PageBlock system is huge and
// input (inputRichMessage.blocks) and output (richMessage.blocks) are isomorphic and passed through verbatim, so it is not modeled per type
// in domain; the rpc layer serializes the tg.PageBlock vector ↔ bytes (domain does not depend on tg).
// As with message media, Photos/Documents store resolved snapshots (including the viewer-independent access_hash),
// and projection reuses tgPhoto/tgDocument. HTML/Markdown input is also normalized to the same set of Blocks at the RPC boundary.
//
// BlocksLayer is the exact TL profile of these persisted bytes, not the sending client's Layer. Historical records
// lack this field; 0 is the formal storage-v1 marker, always interpreted as Layer 228. New writes must explicitly store
// the profile used at encoding time, and reads must not rely on trial-decoding constructors or falling back on failure.
type MessageRichMessage struct {
	Rtl         bool       `json:"rtl,omitempty"`
	Part        bool       `json:"part,omitempty"`
	BlocksLayer int        `json:"blocks_layer,omitempty"`
	Blocks      []byte     `json:"blocks,omitempty"`
	Photos      []Photo    `json:"photos,omitempty"`
	Documents   []Document `json:"documents,omitempty"`
	// BotAPIProjection is the Bot API RichMessage JSON derived by the RPC boundary from the same
	// set of validated PageBlocks. It is not a second source of truth: the write boundary may only
	// generate it from Blocks, and the HTTP Bot API projection is read-only, avoiding a reverse dependency of the botapi package on tg types.
	BotAPIProjection []byte `json:"bot_api_projection,omitempty"`
}

// EffectiveBlocksLayer returns the deterministic storage grammar for Blocks.
// A missing JSON field is the original storage-v1 format and therefore Layer
// 228; it is not an adaptive decode fallback.
func (m *MessageRichMessage) EffectiveBlocksLayer() int {
	if m == nil || m.BlocksLayer == 0 {
		return MessageRichBlocksLegacyLayer
	}
	return m.BlocksLayer
}

// IsZero means no rich text payload (skip an empty snapshot on persist, do not send rich_message on projection).
func (m *MessageRichMessage) IsZero() bool {
	return m == nil || (len(m.Blocks) == 0 && len(m.Photos) == 0 && len(m.Documents) == 0)
}

// MessageReply describes a message reply/thread header without depending on TL types.
type MessageReply struct {
	MessageID     int
	Peer          Peer
	TopMessageID  int
	ForumTopic    bool
	QuoteText     string
	QuoteEntities []MessageEntity
	QuoteOffset   int
	// External is an immutable source snapshot resolved by the owning store.
	// It is not client input and does not authorize a source message lookup.
	External *MessageReplyExternal `json:",omitempty"`
	// StoryID > 0 means this is a reply (comment) to a story: MessageID is 0, Peer is the story author,
	// and it projects as messageReplyStoryHeader rather than an ordinary messageReplyHeader.
	StoryID int
}

// MessageForward describes the original author information of a forwarded message.
type MessageForward struct {
	From           Peer
	FromName       string
	Date           int
	ChannelPost    int
	SavedFrom      Peer
	SavedFromMsgID int
}

// MessageList is a message query result from an account's perspective.
type MessageList struct {
	Messages []Message
	Users    []User
	Count    int
	Hash     int64
}

// MessageFilter describes history/search query conditions.
type MessageFilter struct {
	HasPeer    bool
	Peer       Peer
	Query      string
	OffsetID   int
	OffsetDate int
	MinDate    int
	MaxDate    int
	AddOffset  int
	Limit      int
	MaxID      int
	MinID      int
	Hash       int64
	// SenderUserID intersects all other predicates; zero means no sender filter.
	SenderUserID int64
	// CountOnly ignores pagination and returns the exact filtered total without
	// loading message payloads or users. History's default limit is separate.
	CountOnly bool
	// PinnedOnly returns only pinned messages (the query path of messages.search filterPinned and
	// userFull.pinned_msg_id).
	PinnedOnly     bool
	MusicOnly      bool
	PhoneCallsOnly bool
	// MissedPhoneCallsOnly narrows PhoneCallsOnly to incoming calls that ended
	// with the protocol-level "missed" reason.
	MissedPhoneCallsOnly bool
	NeedTotalCount       bool
	// When SavedPeer is non-zero, return only the messages of that saved sub-chat in the self-chat
	// (messages.getSavedHistory); Peer must also be self.
	SavedPeer Peer
	// When SavedReactions is non-empty, return only Saved Messages carrying at least one of the tags.
	// Used only by messages.search(peer=self); an ordinary private-chat reaction does not participate in matching.
	SavedReactions []MessageReaction
	// PeerIDs restricts a global private search to these user peers. Empty is a
	// valid restricted set, so RestrictPeerIDs carries presence separately.
	PeerIDs         []int64
	RestrictPeerIDs bool
}

// SendPrivateTextRequest is the private text/media send command.
type SendPrivateTextRequest struct {
	SenderUserID    int64
	RecipientUserID int64
	RandomID        int64
	Message         string
	Entities        []MessageEntity
	Media           *MessageMedia
	Silent          bool
	NoForwards      bool
	ReplyTo         *MessageReply
	Forward         *MessageForward
	Date            int
	OriginAuthKeyID [8]byte
	OriginSessionID int64
	// OriginClientSession carries the exact request's initConnection language
	// into an in-process bot responder. Stores and idempotency fingerprints
	// intentionally ignore this ephemeral metadata.
	OriginClientSession ClientSessionMetadata
	// OriginUserID identifies the authenticated initiator when a server-generated
	// service message is authored by another user. Zero preserves the ordinary
	// send path where the sender is the initiator.
	OriginUserID     int64
	RecipientBlocked bool
	// IdempotencyFingerprint is the SHA-256 computed by the call boundary over the original, immutable send request.
	// The RPC layer should prefer the original TL request fingerprint, so server-derived fields such as link previews, dice results and
	// uploaded media do not make a legitimate replay look different; when an internal call leaves it empty, the store generates an equivalent
	// fingerprint from the domain command's immutable fields.
	IdempotencyFingerprint []byte
	// IdempotencyPreflighted is internal execution metadata.  A trusted caller sets it only
	// after a read-only replay lookup returned absent, allowing the app/store layers to avoid
	// repeating the same indexed lookup.  The transactional unique-key path still fences a
	// concurrent first writer; this flag is never part of the durable request fingerprint.
	IdempotencyPreflighted bool
	TTLPeriod              int
	ViaBotID               int64
	// GroupedID is the album grouping id (the same sendMultiMedia group shares a non-zero value; non-album is always 0).
	GroupedID int64
	// Effect is the message effect id (private-chat only, 0 means no effect; the caller has validated legality against the catalog).
	Effect int64
	// BusinessAutomationKind is internal app-layer metadata used to suppress
	// recursive greeting/away automation for server-generated replies.
	BusinessAutomationKind BusinessAutomationKind
	// ReplyMarkup is the reply/inline keyboard snapshot of a bot outbound message; an ordinary user send is always nil.
	ReplyMarkup *MessageReplyMarkup
	// RichMessage is the Layer 228 rich text message (richMessage) snapshot, optional; an ordinary message is always nil.
	RichMessage *MessageRichMessage
	PaidStars   int64
}

// HasContent reports whether the command contains a client-visible message payload.
// TDesktop rich-message posting intentionally leaves Message empty, so every send
// boundary must treat text, media, and rich_message as equivalent content sources.
func (r SendPrivateTextRequest) HasContent() bool {
	return r.Message != "" || !r.Media.IsZero() || !r.RichMessage.IsZero()
}

// PrivateSendReplayRequest identifies one already-committed private send without carrying any
// mutable or resolver-derived message fields.  The fingerprint is computed at the original
// request boundary and must be a complete SHA-256 value.
type PrivateSendReplayRequest struct {
	SenderUserID           int64
	RecipientUserID        int64
	RandomID               int64
	IdempotencyFingerprint []byte
}

// SendPrivateTextResult describes the two-sided result of one private text send.
type PaidStarsCharge struct {
	Amount           int64
	SenderBalance    int64
	RecipientBalance int64
}

type SendPrivateTextResult struct {
	SenderMessage    Message
	RecipientMessage Message
	SenderEvent      UpdateEvent
	RecipientEvent   UpdateEvent
	Duplicate        bool
	PaidStars        *PaidStarsCharge
	// ReplayDeleteEvent is the already-durable sender-side deletion that must
	// follow the first-send snapshot in an exact random_id replay. It never
	// represents a newly allocated event.
	ReplayDeleteEvent *UpdateEvent
}

// SetPrivateChatThemeRequest changes the shared theme token for a private dialog.
type SetPrivateChatThemeRequest struct {
	OwnerUserID      int64
	Peer             Peer
	Emoticon         string
	Date             int
	OriginAuthKeyID  [8]byte
	OriginSessionID  int64
	RecipientBlocked bool
}

// SetPrivateChatThemeResult describes the state change and optional service message.
type SetPrivateChatThemeResult struct {
	OwnerUserID int64
	Peer        Peer
	Emoticon    string
	Changed     bool
	Send        SendPrivateTextResult
}

// SetPrivateMessageReactionsRequest replaces the current user's reactions for one private message.
type SetPrivateMessageReactionsRequest struct {
	UserID      int64
	Peer        Peer
	MessageID   int
	Reactions   []MessageReaction
	Big         bool
	AddToRecent bool
	Date        int
	// ReactionsPerUserMax is the viewer's per-user limit tier (Premium dual tier);
	// 0 means unfilled, and the store trims to the default tier.
	ReactionsPerUserMax int
}

// PrivateMessageReactionsRequest fetches reaction summaries for exact private message ids.
type PrivateMessageReactionsRequest struct {
	OwnerUserID int64
	Peer        Peer
	IDs         []int
}

// PrivateMessageReactionsResult describes private reaction updates in owner-visible boxes.
type PrivateMessageReactionsResult struct {
	Messages  []Message
	Reactions ChannelMessageReactions
}

// ForwardPrivateMessagesRequest is the private text message forward command.
type ForwardPrivateMessagesRequest struct {
	OwnerUserID      int64
	FromPeer         Peer
	ToUserID         int64
	MessageIDs       []int
	RandomIDs        []int64
	Silent           bool
	NoForwards       bool
	DropAuthor       bool
	ReplyTo          *MessageReply
	Date             int
	OriginAuthKeyID  [8]byte
	OriginSessionID  int64
	RecipientBlocked bool
	TTLPeriod        int
}

// ForwardPrivateMessagesResult describes the owner-dimension result of one private forward.
type ForwardPrivateMessagesResult struct {
	OwnerUserID        int64
	SenderMessages     []Message
	RecipientMessages  []Message
	SenderEvents       []UpdateEvent
	RecipientEvents    []UpdateEvent
	Duplicates         []bool
	ReplayDeleteEvents []*UpdateEvent
}

const PrivateNoForwardsRequestExpirePeriod = 24 * 60 * 60

// PrivateNoForwardsState is the single content-protection authority for a pair of ordinary users.
// EnabledByUserID is 0 or one of the participants; when non-zero, chats shared by both sides are protected.
type PrivateNoForwardsState struct {
	UserLowID       int64
	UserHighID      int64
	EnabledByUserID int64
}

func (s PrivateNoForwardsState) Enabled() bool {
	return s.EnabledByUserID != 0
}

func (s PrivateNoForwardsState) ForViewer(viewerUserID int64) (myEnabled, peerEnabled bool) {
	if s.EnabledByUserID == 0 {
		return false, false
	}
	return s.EnabledByUserID == viewerUserID, s.EnabledByUserID != viewerUserID
}

// TogglePrivateNoForwardsRequest is the atomic state + service message command for private content protection.
type TogglePrivateNoForwardsRequest struct {
	ActorUserID     int64
	PeerUserID      int64
	Enabled         bool
	RequestMsgID    int
	RandomID        int64
	Date            int
	OriginAuthKeyID [8]byte
	OriginSessionID int64
}

// TogglePrivateNoForwardsResult returns both the committed authoritative state and this call's real service message.
// Changed=false with an empty Send means the officially defined no-op.
type TogglePrivateNoForwardsResult struct {
	State   PrivateNoForwardsState
	Changed bool
	Send    SendPrivateTextResult
}

// ReadHistoryRequest is the messages.readHistory command from an account's perspective.
type ReadHistoryRequest struct {
	OwnerUserID     int64
	Peer            Peer
	MaxID           int
	Date            int
	OriginAuthKeyID [8]byte
	OriginSessionID int64
}

// ReadHistoryResult describes the business result of one chat read operation.
type ReadHistoryResult struct {
	OwnerUserID      int64
	Peer             Peer
	MaxID            int
	StillUnreadCount int
	ChannelPts       int
	Changed          bool
	InboxEvent       UpdateEvent
	OutboxChanged    bool
	OutboxUserID     int64
	OutboxEvent      UpdateEvent
}

// ReadMessageContentsRequest marks media/mention contents as read for exact owner-visible messages.
type ReadMessageContentsRequest struct {
	OwnerUserID     int64
	IDs             []int
	Date            int
	OriginAuthKeyID [8]byte
	OriginSessionID int64
}

// ReadMessageContentsResult contains owner-visible message IDs whose content unread state changed.
type ReadMessageContentsResult struct {
	OwnerUserID int64
	MessageIDs  []int
	Event       UpdateEvent
	// SenderEvents are the content read receipts for the peer senders: after the reader finishes a voice/round,
	// each original sender receives an updateReadMessagesContents, where messages uses the sender's
	// own-perspective box id.
	SenderEvents []UpdateEvent
}

// OutboxReadDateRequest is the messages.getOutboxReadDate query.
type OutboxReadDateRequest struct {
	OwnerUserID int64
	Peer        Peer
	ID          int
}

// EditMessageRequest is the command to edit a sent private message from an account's perspective.
// A non-nil Media replaces the message media snapshot wholesale (the only current caller is live location update/stop,
// and the rpc layer limits the media kind); nil means a text-only edit.
type EditMessageRequest struct {
	OwnerUserID     int64
	Peer            Peer
	ID              int
	Message         string
	Entities        []MessageEntity
	Media           *MessageMedia
	EditDate        int
	HideEdited      bool
	OriginAuthKeyID [8]byte
	OriginSessionID int64
	// When SetReplyMarkup is set, replace reply_markup (a nil/empty ReplyMarkup = clear the keyboard);
	// when unset, the original markup is preserved. The RPC layer sets it only when a bot edits its own message (P3).
	SetReplyMarkup bool
	ReplyMarkup    *MessageReplyMarkup
	// When SetRichMessage is set, replace rich_message (a nil/empty RichMessage = clear the rich text).
	// TDesktop EditRichMessage carries only f_rich_message, not f_message; the store must not mistake it for an empty edit.
	SetRichMessage bool
	RichMessage    *MessageRichMessage
	// A non-zero ViaBotEditBotID allows the corresponding bot to edit inline private messages sent through it.
	ViaBotEditBotID int64
	// AllowTodoParticipantMutation lets a checklist participant replace the todo media snapshot through an
	// edit event under the others_can_* grant. Only the RPC todo handler sets it; the store still restricts it to
	// todo->todo with an unchanged body/entities/markup, preventing an ordinary message from being edited without permission.
	AllowTodoParticipantMutation bool
	// When WebPageResolve is set it is a server-internal "replace link preview in place": only the media is replaced (body/
	// entities/edit_date untouched, so it is not marked "edited") and UpdateEventWebPage is generated rather than edit_message.
	// Idempotency guard: replace only when the target's current media is still a pending link preview with ID==ExpectedWebPageID,
	// otherwise return ErrMessageNotModified (the message was deleted/changed/resolved).
	WebPageResolve    bool
	ExpectedWebPageID int64
	// WidgetData patches only a bot widget's public/chat data in place
	// (messages.editMessageWidgetData). Like WebPageResolve it leaves
	// body/entities/edit_date untouched, so the message is not marked edited and
	// updateMessageWidgetData is generated instead of edit_message.
	WidgetData *MessageWidgetDataPatch
}

// MessageWidgetDataPatch is the in-place widget data change carried by an edit.
// A nil Public/Char pointer field means "leave unchanged".
type MessageWidgetDataPatch struct {
	PublicData *string
	ChatData   *string
}

// EditedMessageForUser describes the effect of one edit on a given owner's perspective.
type EditedMessageForUser struct {
	UserID  int64
	Message Message
	Event   UpdateEvent
}

// EditMessageResult describes the owner-dimension result after a message edit.
type EditMessageResult struct {
	OwnerUserID int64
	Edited      []EditedMessageForUser
}

// Self returns the edit result for the current requesting account.
func (r EditMessageResult) Self() EditedMessageForUser {
	for _, item := range r.Edited {
		if item.UserID == r.OwnerUserID {
			return item
		}
	}
	return EditedMessageForUser{UserID: r.OwnerUserID}
}

// Changed reports whether this edit actually affected any owner's perspective.
func (r EditMessageResult) Changed() bool {
	return len(r.Edited) > 0
}

// DeleteMessagesRequest is the command to delete messages by id from an account's perspective.
type DeleteMessagesRequest struct {
	OwnerUserID     int64
	IDs             []int
	Revoke          bool
	Date            int
	OriginAuthKeyID [8]byte
	OriginSessionID int64
}

// DeleteHistoryRequest is the command to clear a peer's history from an account's perspective.
type DeleteHistoryRequest struct {
	OwnerUserID int64
	Peer        Peer
	MaxID       int
	// MinDate/MaxDate are the closed interval of a "delete by date" (unix seconds, 0 means unbounded);
	// the client destroys locally before sending the request, so a server that silently ignores it would resurrect the deletions.
	MinDate         int
	MaxDate         int
	JustClear       bool
	Revoke          bool
	Date            int
	OriginAuthKeyID [8]byte
	OriginSessionID int64
}

// DeletedMessagesForUser describes the effect of one deletion on a given owner's perspective.
type DeletedMessagesForUser struct {
	UserID     int64
	MessageIDs []int
	// Event keeps a single delete_messages event for the ordinary deleteMessages path and
	// the replay receipt; just_clear additionally carries read/edit events in Events.
	Event  UpdateEvent
	Events []UpdateEvent
	// Pts/PtsCount is the affected watermark summary of this method for that owner.
	// For an ordinary delete it equals Event; for just_clear it equals the pts of the last real update and
	// the sum of the delete/read/edit three-segment pts_count.
	Pts      int
	PtsCount int
}

// AffectedPts returns the final PTS and this call's total increment that messages.Affected* should use.
func (d DeletedMessagesForUser) AffectedPts() (int, int) {
	if d.Pts != 0 {
		return d.Pts, d.PtsCount
	}
	return d.Event.Pts, d.Event.PtsCount
}

// DeleteMessagesResult describes the owner-dimension result after message deletion.
type DeleteMessagesResult struct {
	OwnerUserID int64
	Deleted     []DeletedMessagesForUser
	Offset      int
}

// Self returns the deletion result for the current requesting account.
func (r DeleteMessagesResult) Self() DeletedMessagesForUser {
	for _, item := range r.Deleted {
		if item.UserID == r.OwnerUserID {
			return item
		}
	}
	return DeletedMessagesForUser{UserID: r.OwnerUserID}
}

// Changed reports whether this deletion actually affected any owner's perspective.
func (r DeleteMessagesResult) Changed() bool {
	for _, item := range r.Deleted {
		pts, _ := item.AffectedPts()
		if len(item.MessageIDs) > 0 || pts > 0 {
			return true
		}
	}
	return false
}

// PinPrivateMessageRequest is the private-chat command of messages.updatePinnedMessage
// (including Saved Messages = self peer).
type PinPrivateMessageRequest struct {
	OwnerUserID int64
	Peer        Peer
	MessageID   int
	Pinned      bool
	// PmOneside pins only on this side (when the official private-chat pin box's "pin for the other side too" is unchecked);
	// it does not flip the peer and generates no service message. unpin has no such semantics and always clears both sides.
	PmOneside bool
	Silent    bool
	// RecipientBlocked applies the normal private-service delivery policy.
	RecipientBlocked bool
	Date             int
	OriginAuthKeyID  [8]byte
	OriginSessionID  int64
}

// PinnedMessagesForUser describes the effect of a pin state change on a given owner's perspective.
type PinnedMessagesForUser struct {
	UserID     int64
	Peer       Peer
	MessageIDs []int
	Pinned     bool
	Event      UpdateEvent
}

// MaxUnpinAllBatch limits the pin count a single unpinAllMessages actually clears;
// the rest is cleared by the client continuing per affectedHistory.Offset>0, keeping a single
// updatePinnedMessages messages vector bounded.
const MaxUnpinAllBatch = 1000

// PinPrivateMessageResult describes the owner-dimension result of a private pin/unpin.
// An empty Updated means the state did not change (an idempotent no-op that burns no pts).
type PinPrivateMessageResult struct {
	OwnerUserID int64
	Updated     []PinnedMessagesForUser
	// ServiceMessage is committed atomically with a changed shared private pin.
	ServiceMessage SendPrivateTextResult
	// A non-zero Offset means unpinAll still has remaining batches to clear.
	Offset int
}

// Self returns the pin result for the current requesting account.
func (r PinPrivateMessageResult) Self() PinnedMessagesForUser {
	for _, item := range r.Updated {
		if item.UserID == r.OwnerUserID {
			return item
		}
	}
	return PinnedMessagesForUser{UserID: r.OwnerUserID}
}

// Changed reports whether this operation actually flipped the pin state for any owner's perspective.
func (r PinPrivateMessageResult) Changed() bool {
	for _, item := range r.Updated {
		if len(item.MessageIDs) > 0 {
			return true
		}
	}
	return false
}

// UnpinAllPrivateMessagesRequest is the private-chat command of messages.unpinAllMessages.
type UnpinAllPrivateMessagesRequest struct {
	OwnerUserID     int64
	Peer            Peer
	Date            int
	OriginAuthKeyID [8]byte
	OriginSessionID int64
}

// ScheduledMessage is a pending account-owned message that has not entered
// normal peer history yet.
type ScheduledMessage struct {
	OwnerUserID          int64
	ID                   int
	Peer                 Peer
	RandomID             int64
	Message              string
	Entities             []MessageEntity
	Media                *MessageMedia
	RichMessage          *MessageRichMessage
	Silent               bool
	NoForwards           bool
	ReplyTo              *MessageReply
	Forward              *MessageForward
	SendAs               *Peer
	ScheduleDate         int
	ScheduleRepeatPeriod int
	PaidStars            int64
	CreatedAt            int
	UpdatedAt            int
	State                string
	SentMessageID        int
}

// ScheduledMessageList is a bounded scheduled queue page.
type ScheduledMessageList struct {
	Messages []ScheduledMessage
	Count    int
	Hash     int64
}

// ScheduleMessageRequest creates one scheduled message.
type ScheduleMessageRequest struct {
	OwnerUserID          int64
	Peer                 Peer
	RandomID             int64
	Message              string
	Entities             []MessageEntity
	Media                *MessageMedia
	RichMessage          *MessageRichMessage
	Silent               bool
	NoForwards           bool
	ReplyTo              *MessageReply
	Forward              *MessageForward
	SendAs               *Peer
	ScheduleDate         int
	ScheduleRepeatPeriod int
	PaidStars            int64
	Date                 int
}

// EditScheduledMessageRequest updates one pending scheduled message before it
// enters normal history.
type EditScheduledMessageRequest struct {
	OwnerUserID    int64
	Peer           Peer
	ID             int
	SetMessage     bool
	Message        string
	Entities       []MessageEntity
	SetRichMessage bool
	RichMessage    *MessageRichMessage
	ScheduleDate   int
	Date           int
}

// ScheduledMessageFilter selects scheduled messages for one owner/peer.
type ScheduledMessageFilter struct {
	OwnerUserID int64
	Peer        Peer
	IDs         []int
	Limit       int
	Hash        int64
}

// ScheduledMessageClaim describes a due/manual scheduled dispatch claim.
type ScheduledMessageClaim struct {
	OwnerUserID int64
	Peer        Peer
	IDs         []int
	Now         int
	Limit       int
	LeaseUntil  int
}