internal/domain/media.go
Rama main · réplica de lectura del repositorio original.
package domain
import (
"path/filepath"
"strings"
"time"
)
// This file defines media-related business value objects (documents, photos, sticker sets, available reactions, message media).
// These types do not depend on tg.* at all; the rpc layer handles domain↔tg conversion.
//
// The json tags let the store layer json.Marshal directly into JSONB (message media snapshots,
// document/photo metadata). They are protocol-neutral pure data, not tg-generated types.
// MediaBackend identifies the single durable backend where blob bytes actually live.
type MediaBackend string
const (
// MediaBackendLocalFS means blob bytes live on local disk (object_key is a relative path).
MediaBackendLocalFS MediaBackend = "localfs"
// MediaBackendS3 means blob bytes live in the configured S3/MinIO bucket.
MediaBackendS3 MediaBackend = "s3"
)
// FileBlob is an index entry for a downloadable binary object: location_key → backend/object key/size/mime.
// The actual bytes are read/written by the blob backend by ObjectKey; this struct only describes location and metadata.
type FileBlob struct {
// LocationKey is the stable logical location key, derived from getFile's InputFileLocation:
// doc:<id> document body
// doc:<id>:<type> document thumbnail (PhotoSize type)
// photo:<id>:<type> one photo size
LocationKey string `json:"location_key"`
Backend MediaBackend `json:"backend"`
ObjectKey string `json:"object_key"`
Size int64 `json:"size"`
SHA256 []byte `json:"sha256,omitempty"`
MimeType string `json:"mime_type,omitempty"`
}
// UploadPart is the metadata of one part accumulated by upload.saveFilePart/saveBigFilePart.
// Part bytes do not enter PG; PG records only object_key/size/hash, and metadata and temp objects are cleaned up after assembly.
type UploadPart struct {
OwnerUserID int64
FileID int64
Part int
TotalParts int // Big: known positive total or -1 while generating; small: 0.
Big bool
Backend MediaBackend
ObjectKey string
Size int64
SHA256 []byte
}
// UploadPartUsage describes a user's current not-yet-assembled upload part usage.
type UploadPartUsage struct {
Bytes int64
Parts int
Files int
}
// UploadPartSlot describes the current state of one (owner,file_id,part) slot, used to determine the quota delta of a retry overwrite.
type UploadPartSlot struct {
ExistingBytes int64
ObjectKey string
FileParts int
Found bool
}
// UploadPartQuota limits a user's in-flight upload part usage; a field <=0 means that dimension is unlimited.
type UploadPartQuota struct {
MaxBytes int64
MaxParts int
MaxFiles int
}
// UploadedFileRef references a file a client has fully uploaded via upload.saveFilePart(Big).
// The rpc layer converts it from tg.InputFile/InputFileBig; the files service assembles the blob from it.
type UploadedFileRef struct {
OwnerUserID int64
FileID int64
Parts int
Name string
Big bool
MD5 string // small-file client md5_checksum (hex), verifiable; empty for a big file
}
// UploadedMediaKind identifies the durable object materialized from a one-shot upload file id.
type UploadedMediaKind string
const (
UploadedMediaPhoto UploadedMediaKind = "photo"
UploadedMediaDocument UploadedMediaKind = "document"
)
// UploadedMediaReceipt makes InputMediaUploaded* replayable after transient upload parts have
// been consumed. IntentHash binds the file id to the complete materialization intent (kind, part
// metadata and document spec); MediaID points at the immutable Photo/Document returned on every
// exact replay.
type UploadedMediaReceipt struct {
OwnerUserID int64
FileID int64
IntentHash []byte
Kind UploadedMediaKind
MediaID int64
CreatedAt time.Time
}
// DocumentSpec describes the metadata for creating a Document from an uploaded file (from InputMediaUploadedDocument).
type DocumentSpec struct {
MimeType string
Attributes []DocumentAttribute
Thumb *UploadedFileRef // optional thumbnail upload, produces doc:<id>:m
ForceFile bool
// NosoundVideo corresponds to inputMediaUploadedDocument.nosound_video. A real GIF is still
// converted to MP4, but when this flag is present it is persisted as an ordinary soundless video without the animated attribute (album path).
NosoundVideo bool
}
// FileDownloadRequest is the parsed download request of upload.getFile;
// LocationKey is derived by the rpc layer from tg.InputFileLocation (doc:<id> / photo:<id>:<type> etc.).
type FileDownloadRequest struct {
LocationKey string
Offset int64
Limit int
}
// FileChunk is one piece of content returned by upload.getFile. Bytes is a read-only borrow: the caller may read or
// encode within the lifetime of the FileChunk, but must not modify the content or write an append result back to the original
// backing. The file service may let concurrent cache/singleflight callers share an immutable backing,
// so a hot range is not copied into a separate payload per downloader.
type FileChunk struct {
Bytes []byte
MimeType string
Total int64
// ImmutableRange is present only when Bytes came from the exact range of a
// content-addressed immutable blob. MTProto may retain this descriptor instead
// of a second long-lived copy of Bytes until the answer is acknowledged.
ImmutableRange *ImmutableFileRange
}
// ImmutableFileRange is a capability for exact byte replay. ObjectKey names
// immutable content in Backend; RangeSHA256 protects the requested slice from
// backend corruption or an invalid implementation. It deliberately contains no
// location key, access hash, or mutable database identifier.
type ImmutableFileRange struct {
Backend MediaBackend
ObjectKey string
Offset int64
Length int
Total int64
MimeType string
RangeSHA256 [32]byte
}
// PhotoSizeKind identifies the TL variant of a PhotoSize.
type PhotoSizeKind string
const (
// PhotoSizeKindDefault → photoSize (downloadable, type/w/h/size).
PhotoSizeKindDefault PhotoSizeKind = "size"
// PhotoSizeKindStripped → photoStrippedSize (inline bytes, an instantly-shown blurred image).
PhotoSizeKindStripped PhotoSizeKind = "stripped"
// PhotoSizeKindCached → photoCachedSize (inline bytes + w/h).
PhotoSizeKindCached PhotoSizeKind = "cached"
// PhotoSizeKindPath → photoPathSize (inline svg path bytes, a vector placeholder).
PhotoSizeKindPath PhotoSizeKind = "path"
// PhotoSizeKindProgressive → photoSizeProgressive (progressive jpeg multi-segment sizes).
PhotoSizeKindProgressive PhotoSizeKind = "progressive"
// PhotoSizeKindVideo → videoSize in photo.video_sizes (animated profile video).
PhotoSizeKindVideo PhotoSizeKind = "video"
// PhotoSizeKindVideoEmojiMarkup → videoSizeEmojiMarkup in photo.video_sizes.
PhotoSizeKindVideoEmojiMarkup PhotoSizeKind = "video_emoji_markup"
// PhotoSizeKindVideoStickerMarkup → videoSizeStickerMarkup in photo.video_sizes.
PhotoSizeKindVideoStickerMarkup PhotoSizeKind = "video_sticker_markup"
)
// PhotoSize describes one rendering size of a photo/thumbnail.
type PhotoSize struct {
Kind PhotoSizeKind `json:"kind"`
Type string `json:"type"`
W int `json:"w,omitempty"`
H int `json:"h,omitempty"`
Size int `json:"size,omitempty"`
Bytes []byte `json:"bytes,omitempty"` // stripped/cached/path inline content
Sizes []int `json:"sizes,omitempty"` // progressive
VideoStartTs float64 `json:"video_start_ts,omitempty"`
EmojiID int64 `json:"emoji_id,omitempty"`
BackgroundColors []int `json:"background_colors,omitempty"`
StickerSetID int64 `json:"sticker_set_id,omitempty"`
StickerSetAccessHash int64 `json:"sticker_set_access_hash,omitempty"`
StickerSetShortName string `json:"sticker_set_short_name,omitempty"`
StickerSetSystemKey string `json:"sticker_set_system_key,omitempty"`
StickerID int64 `json:"sticker_id,omitempty"`
}
// Downloadable means the size must be pulled by the client via upload.getFile (rather than inline bytes).
func (s PhotoSize) Downloadable() bool {
return s.Kind == PhotoSizeKindDefault || s.Kind == PhotoSizeKindProgressive || s.Kind == PhotoSizeKindVideo
}
// DocumentAttributeKind identifies the TL DocumentAttribute variant.
type DocumentAttributeKind string
const (
DocAttrImageSize DocumentAttributeKind = "image_size"
DocAttrAnimated DocumentAttributeKind = "animated"
DocAttrSticker DocumentAttributeKind = "sticker"
DocAttrVideo DocumentAttributeKind = "video"
DocAttrAudio DocumentAttributeKind = "audio"
DocAttrFilename DocumentAttributeKind = "filename"
DocAttrCustomEmoji DocumentAttributeKind = "custom_emoji"
)
// DocumentAttribute is the union of the TL DocumentAttribute variants used on the main path.
type DocumentAttribute struct {
Kind DocumentAttributeKind `json:"kind"`
// image_size / video / sticker box
W int `json:"w,omitempty"`
H int `json:"h,omitempty"`
// sticker / custom_emoji
Alt string `json:"alt,omitempty"`
Mask bool `json:"mask,omitempty"`
StickerSetID int64 `json:"sticker_set_id,omitempty"`
StickerSetAccessHash int64 `json:"sticker_set_access_hash,omitempty"`
Free bool `json:"free,omitempty"` // custom_emoji
TextColor bool `json:"text_color,omitempty"` // custom_emoji
// video
Duration float64 `json:"duration,omitempty"`
RoundMessage bool `json:"round_message,omitempty"`
SupportsStreaming bool `json:"supports_streaming,omitempty"`
NoSound bool `json:"no_sound,omitempty"`
VideoCodec string `json:"video_codec,omitempty"`
// audio
AudioDuration int `json:"audio_duration,omitempty"`
Voice bool `json:"voice,omitempty"`
Title string `json:"title,omitempty"`
Performer string `json:"performer,omitempty"`
Waveform []byte `json:"waveform,omitempty"`
// filename
FileName string `json:"file_name,omitempty"`
}
// Document is a stored Telegram document (sticker, gif, file, video, audio, custom emoji, ...).
type Document struct {
ID int64 `json:"id"`
AccessHash int64 `json:"access_hash"`
FileReference []byte `json:"file_reference,omitempty"`
Date int `json:"date,omitempty"`
MimeType string `json:"mime_type,omitempty"`
Size int64 `json:"size,omitempty"`
DCID int `json:"dc_id,omitempty"`
Attributes []DocumentAttribute `json:"attributes,omitempty"`
Thumbs []PhotoSize `json:"thumbs,omitempty"`
}
// StickerSetRef returns the sticker set reference this document belongs to (if it has a sticker/custom_emoji attribute).
func (d Document) StickerSetRef() (id, accessHash int64, ok bool) {
for _, attr := range d.Attributes {
if attr.Kind == DocAttrSticker || attr.Kind == DocAttrCustomEmoji {
if attr.StickerSetID != 0 {
return attr.StickerSetID, attr.StickerSetAccessHash, true
}
}
}
return 0, 0, false
}
// IsMusic reports whether the document is a Telegram profile/shared-music song:
// documentAttributeAudio with voice=false. MIME type is intentionally not used
// as the source of truth because clients key their UI off the TL attribute.
func (d Document) IsMusic() bool {
for _, attr := range d.Attributes {
if attr.Kind == DocAttrAudio && !attr.Voice {
return true
}
}
return false
}
// IsSticker reports whether the document carries a sticker attribute
// (static / animated / video stickers all use documentAttributeSticker).
func (d Document) IsSticker() bool {
for _, attr := range d.Attributes {
if attr.Kind == DocAttrSticker {
return true
}
}
return false
}
func (d Document) IsCustomEmoji() bool {
for _, attr := range d.Attributes {
if attr.Kind == DocAttrCustomEmoji {
return true
}
}
return false
}
func (d Document) IsStickerLike() bool {
return d.IsSticker() || d.IsCustomEmoji()
}
// MaxStickerMaterialDocumentSize bounds uploaded documents that can be promoted
// into user-created sticker/custom emoji sets without server-side transcoding.
const MaxStickerMaterialDocumentSize int64 = 20 << 20
// IsStickerSetMaterial reports whether an existing Document can be used as an
// input item for stickers.createStickerSet/stickers.addStickerToSet. Already
// tagged sticker/custom emoji documents always pass. Untagged TGS/WebP/Lottie
// JSON uploads can be shaped later from their MIME/body; video uploads must
// already carry a video attribute because this layer does not transcode or
// probe containers.
func (d Document) IsStickerSetMaterial() bool {
if d.IsStickerLike() {
return true
}
if d.ID == 0 || d.AccessHash == 0 || d.Size > MaxStickerMaterialDocumentSize {
return false
}
switch d.stickerMaterialMimeOrExt() {
case "application/x-tgsticker", "image/webp", "application/json":
return true
case "video/webm", "video/mp4":
return d.hasDocumentAttribute(DocAttrVideo)
default:
return false
}
}
func (d Document) hasDocumentAttribute(kind DocumentAttributeKind) bool {
for _, attr := range d.Attributes {
if attr.Kind == kind {
return true
}
}
return false
}
// StickerSetMaterialMime returns the normalized sticker material MIME inferred
// from the document MIME type or a filename attribute.
func (d Document) StickerSetMaterialMime() string {
return d.stickerMaterialMimeOrExt()
}
func (d Document) stickerMaterialMimeOrExt() string {
mimeType := strings.ToLower(strings.TrimSpace(d.MimeType))
switch mimeType {
case "application/x-tgsticker", "image/webp", "video/webm", "video/mp4",
"application/json", "application/lottie+json", "text/json":
if mimeType == "application/lottie+json" || mimeType == "text/json" {
return "application/json"
}
return mimeType
}
for _, attr := range d.Attributes {
if attr.Kind != DocAttrFilename {
continue
}
switch strings.ToLower(filepath.Ext(strings.TrimSpace(attr.FileName))) {
case ".tgs":
return "application/x-tgsticker"
case ".webp":
return "image/webp"
case ".json":
return "application/json"
case ".webm":
return "video/webm"
case ".mp4":
return "video/mp4"
}
}
return mimeType
}
// IsGif reports whether the document is a canonical, savable Telegram GIFv.
// A Telegram GIF is a soundless MP4; a raw image/gif with only the animated attribute is not a valid saved GIF.
func (d Document) IsGif() bool {
if !strings.EqualFold(strings.TrimSpace(d.MimeType), "video/mp4") {
return false
}
var animated, video bool
for _, attr := range d.Attributes {
switch attr.Kind {
case DocAttrAnimated:
animated = true
case DocAttrVideo:
if !attr.RoundMessage && attr.W > 0 && attr.H > 0 && attr.Duration > 0 {
video = true
}
}
}
return animated && video
}
// Photo is a stored Telegram photo (avatar or image message).
type Photo struct {
ID int64 `json:"id"`
AccessHash int64 `json:"access_hash"`
FileReference []byte `json:"file_reference,omitempty"`
Date int `json:"date,omitempty"`
DCID int `json:"dc_id,omitempty"`
HasStickers bool `json:"has_stickers,omitempty"`
Sizes []PhotoSize `json:"sizes,omitempty"`
}
func ClonePhotoPtr(photo *Photo) *Photo {
if photo == nil {
return nil
}
clone := *photo
clone.FileReference = append([]byte(nil), photo.FileReference...)
clone.Sizes = append([]PhotoSize(nil), photo.Sizes...)
for i := range clone.Sizes {
clone.Sizes[i].Bytes = append([]byte(nil), photo.Sizes[i].Bytes...)
clone.Sizes[i].Sizes = append([]int(nil), photo.Sizes[i].Sizes...)
clone.Sizes[i].BackgroundColors = append([]int(nil), photo.Sizes[i].BackgroundColors...)
}
return &clone
}
// MessageMediaKind enumerates the media payloads a message can carry.
type MessageMediaKind string
const (
MessageMediaKindNone MessageMediaKind = ""
MessageMediaKindPhoto MessageMediaKind = "photo"
MessageMediaKindDocument MessageMediaKind = "document"
MessageMediaKindContact MessageMediaKind = "contact"
MessageMediaKindService MessageMediaKind = "service"
MessageMediaKindGeo MessageMediaKind = "geo"
MessageMediaKindVenue MessageMediaKind = "venue"
MessageMediaKindDice MessageMediaKind = "dice"
MessageMediaKindPoll MessageMediaKind = "poll"
MessageMediaKindGeoLive MessageMediaKind = "geo_live"
MessageMediaKindTodo MessageMediaKind = "todo"
MessageMediaKindStory MessageMediaKind = "story"
MessageMediaKindWebPage MessageMediaKind = "web_page"
MessageMediaKindGiveaway MessageMediaKind = "giveaway"
MessageMediaKindInvoice MessageMediaKind = "invoice"
MessageMediaKindWidget MessageMediaKind = "widget"
)
// MessageGiveaway is the immutable launch-card snapshot shown in the boost
// peer. Channels contains the boost peer first followed by any additional
// channels users must join. A results card is a separate lifecycle message.
type MessageGiveaway struct {
OnlyNewSubscribers bool `json:"only_new_subscribers,omitempty"`
WinnersAreVisible bool `json:"winners_are_visible,omitempty"`
Channels []int64 `json:"channels"`
CountriesISO2 []string `json:"countries_iso2,omitempty"`
PrizeDescription string `json:"prize_description,omitempty"`
Quantity int `json:"quantity"`
Stars int64 `json:"stars"`
UntilDate int `json:"until_date"`
}
// MessageTodoItem is one item in the checklist (id is a unique positive integer within the list, assigned by the client).
type MessageTodoItem struct {
ID int `json:"id"`
Title string `json:"title"`
Entities []MessageEntity `json:"entities,omitempty"`
}
// MessageTodoCompletion records who completed an item and when.
type MessageTodoCompletion struct {
ID int `json:"id"`
CompletedBy int64 `json:"completed_by"`
Date int `json:"date"`
}
// MessageTodo is a to-do checklist (messageMediaToDo). append/toggle update the snapshot wholesale via the
// editMessage media-replacement path (same pattern as live location).
type MessageTodo struct {
OthersCanAppend bool `json:"others_can_append,omitempty"`
OthersCanComplete bool `json:"others_can_complete,omitempty"`
Title string `json:"title"`
TitleEntities []MessageEntity `json:"title_entities,omitempty"`
Items []MessageTodoItem `json:"items"`
Completions []MessageTodoCompletion `json:"completions,omitempty"`
}
// MessageGeoPoint is the geographic coordinate point carried in a message (shared by messageMediaGeo/Venue).
// AccessHash is generated randomly when sending, and the client echoes it back verbatim in the upload.getWebFile map thumbnail request.
type MessageGeoPoint struct {
Lat float64 `json:"lat"`
Long float64 `json:"long"`
AccessHash int64 `json:"access_hash,omitempty"`
AccuracyRadius int `json:"accuracy_radius,omitempty"`
}
// MessageVenue is a location with venue information (messageMediaVenue).
type MessageVenue struct {
Geo MessageGeoPoint `json:"geo"`
Title string `json:"title"`
Address string `json:"address,omitempty"`
Provider string `json:"provider,omitempty"`
VenueID string `json:"venue_id,omitempty"`
VenueType string `json:"venue_type,omitempty"`
}
// MessageDice is an interactive emoji (messageMediaDice). Value is fixed once by the server when the send
// is persisted, and stays unchanged for all viewers and forward snapshots thereafter.
type MessageDice struct {
Emoticon string `json:"emoticon"`
Value int `json:"value"`
}
// MessageGeoLive is a live location (messageMediaGeoLive). The client computes expiry itself from message.date + Period;
// stopping sharing = editMessage setting Period to the elapsed duration (expiring immediately), with no server background task.
type MessageGeoLive struct {
Geo MessageGeoPoint `json:"geo"`
Heading int `json:"heading,omitempty"`
Period int `json:"period"`
ProximityNotificationRadius int `json:"proximity_notification_radius,omitempty"`
}
// MessageStory is a story shared as messageMediaStory. Peer+ID are the stable
// reference; Story is the viewer-visible snapshot captured when the message was
// sent so history/difference can replay without re-resolving the source story.
type MessageStory struct {
Peer Peer `json:"peer"`
ID int `json:"id"`
ViaMention bool `json:"via_mention,omitempty"`
Story *Story `json:"story,omitempty"`
}
// MessageWebPageState distinguishes the three TL forms of a link preview: pending (webPagePending,
// link known but preview not yet asynchronously fetched), done (webPage, a resolved preview card),
// empty (webPageEmpty, resolved but the link has no displayable content).
type MessageWebPageState string
const (
MessageWebPageStatePending MessageWebPageState = "pending"
MessageWebPageStateDone MessageWebPageState = "done"
MessageWebPageStateEmpty MessageWebPageState = "empty"
)
// MessageWebPage is a link preview attached as messageMediaWebPage. URL is the stable reference and
// the remaining fields are the snapshot captured by the server at resolution time, so history/difference can replay
// without re-fetching (same pattern as MessageStory). State decides the projection to webPagePending / webPage / webPageEmpty.
//
// ForceLargeMedia/ForceSmallMedia/Manual/Safe are the flag bits of the outer messageMediaWebPage wrapper;
// HasLargeMedia is a flag on the webPage body itself. ID must stay stable across a pending→done
// switch, since the client correlates the placeholder and the resolved result by webPage id.
type MessageWebPage struct {
State MessageWebPageState `json:"state"`
ID int64 `json:"id"`
URL string `json:"url"`
DisplayURL string `json:"display_url,omitempty"`
Hash int `json:"hash,omitempty"`
Date int `json:"date,omitempty"`
Type string `json:"type,omitempty"`
SiteName string `json:"site_name,omitempty"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
Author string `json:"author,omitempty"`
Photo *Photo `json:"photo,omitempty"`
// ComposeToneEmojiID maps webPageAttributeAiComposeTone, used by TDesktop to render
// the addstyle tone share card; 0 means an ordinary web page.
ComposeToneEmojiID int64 `json:"compose_tone_emoji_id,omitempty"`
// UniqueGift maps webPageAttributeUniqueStarGift (type telegram_nft).
UniqueGift *UniqueStarGift `json:"unique_gift,omitempty"`
// CollectionIcons maps webPageAttributeStarGiftCollection (telegram_collection).
CollectionIcons []Document `json:"collection_icons,omitempty"`
// AuctionGift and AuctionEndDate map webPageAttributeStarGiftAuction
// (telegram_auction).
AuctionGift *StarGift `json:"auction_gift,omitempty"`
AuctionEndDate int `json:"auction_end_date,omitempty"`
ForceLargeMedia bool `json:"force_large_media,omitempty"`
ForceSmallMedia bool `json:"force_small_media,omitempty"`
Manual bool `json:"manual,omitempty"`
Safe bool `json:"safe,omitempty"`
HasLargeMedia bool `json:"has_large_media,omitempty"`
}
// MessageServiceActionKind identifies a private-chat service message action.
type MessageServiceActionKind string
const MessageServiceActionGramTransfer MessageServiceActionKind = "gram_transfer"
const (
MessageServiceActionSuggestProfilePhoto MessageServiceActionKind = "suggest_profile_photo"
// MessageServiceActionHistoryClear maps messageActionHistoryClear. A private-chat
// messages.deleteHistory(just_clear) reuses the top box id at the start of the clear and
// converts it in place into an owner-local service message, so getDialogs/getHistory can
// still rebuild the chat from the real top message on a cold start.
MessageServiceActionHistoryClear MessageServiceActionKind = "history_clear"
// MessageServiceActionPinMessage maps messageActionPinMessage: the service message
// generated by a non-pm_oneside private-chat pin, with the pinned message referenced via reply_to.
MessageServiceActionPinMessage MessageServiceActionKind = "pin_message"
// MessageServiceActionPhoneCall maps messageActionPhoneCall: the call entry
// persisted after a private-chat call ends (including a missed timeout), with the sender always being the caller.
MessageServiceActionPhoneCall MessageServiceActionKind = "phone_call"
// MessageServiceActionConferenceCall maps messageActionConferenceCall:
// the private-chat invite/status service message of an ad-hoc conference call.
MessageServiceActionConferenceCall MessageServiceActionKind = "conference_call"
// MessageServiceActionBotAllowed maps messageActionBotAllowed: the service message
// left in the bot's private chat after a user authorizes the bot.
MessageServiceActionBotAllowed MessageServiceActionKind = "bot_allowed"
// MessageServiceActionWebViewDataSent maps messageActionWebViewDataSent*:
// the service message left in the private chat after a simple webview passes data back to the bot.
MessageServiceActionWebViewDataSent MessageServiceActionKind = "web_view_data_sent"
// MessageServiceActionRequestedPeer maps messageActionRequestedPeer*: after a user
// responds to the bot's request-peer button, it sends the selected peer to the bot as a recoverable service message.
MessageServiceActionRequestedPeer MessageServiceActionKind = "requested_peer"
// MessageServiceActionSetChatTheme maps messageActionSetChatTheme:
// a change to the chat theme token shared by both sides of a private chat.
MessageServiceActionSetChatTheme MessageServiceActionKind = "set_chat_theme"
// MessageServiceActionNoForwardsToggle / Request map the private-chat content-protection
// state toggle and close request. Chat-level protection cannot be written to an ordinary message's NoForwards field.
MessageServiceActionNoForwardsToggle MessageServiceActionKind = "no_forwards_toggle"
MessageServiceActionNoForwardsRequest MessageServiceActionKind = "no_forwards_request"
// MessageServiceActionStarGift maps messageActionStarGift: a Star gift was received.
// The gift snapshot (sticker/star price) is embedded in the action, so the recipient can render the bubble without an extra fetch.
MessageServiceActionStarGift MessageServiceActionKind = "star_gift"
// MessageServiceActionGiftStars maps messageActionGiftStars: fiat-purchased
// Stars credited directly to a friend, distinct from collectible Star Gifts.
MessageServiceActionGiftStars MessageServiceActionKind = "gift_stars"
// MessageServiceActionStarGiftUnique maps messageActionStarGiftUnique. The
// immutable collectible snapshot is carried by the service message so an
// exact replay/difference never depends on mutable catalog state.
MessageServiceActionStarGiftUnique MessageServiceActionKind = "star_gift_unique"
MessageServiceActionStarGiftOffer MessageServiceActionKind = "star_gift_offer"
MessageServiceActionStarGiftOfferDeclined MessageServiceActionKind = "star_gift_offer_declined"
MessageServiceActionGiftPremium MessageServiceActionKind = "gift_premium"
// MessageServiceActionPayment is messageActionPaymentSentMe: a payer settled
// an invoice of ours.
MessageServiceActionPayment MessageServiceActionKind = "payment_sent_me"
)
// MessagePhoneCallAction is the protocol-neutral payload of messageActionPhoneCall.
// Reason takes the same values as PhoneCallDiscardReason; Duration is non-zero only after the call is actually established.
type MessagePhoneCallAction struct {
CallID int64 `json:"call_id"`
Reason string `json:"reason,omitempty"`
Duration int `json:"duration,omitempty"`
Video bool `json:"video,omitempty"`
}
// MessageConferenceCallAction is the protocol-neutral payload of messageActionConferenceCall.
type MessageConferenceCallAction struct {
CallID int64 `json:"call_id"`
Missed bool `json:"missed,omitempty"`
Active bool `json:"active,omitempty"`
Video bool `json:"video,omitempty"`
Duration int `json:"duration,omitempty"`
OtherParticipants []Peer `json:"other_participants,omitempty"`
}
// MessageBotAllowedAction is the protocol-neutral payload of messageActionBotAllowed.
type MessageBotAllowedAction struct {
AttachMenu bool `json:"attach_menu,omitempty"`
FromRequest bool `json:"from_request,omitempty"`
Domain string `json:"domain,omitempty"`
}
const (
MaxWebViewDataButtonTextLen = MaxBotMenuButtonTextLen
MaxWebViewDataPayloadLen = MaxMessageTextLength
)
// MessageWebViewDataAction is the protocol-neutral payload of messageActionWebViewDataSent*.
type MessageWebViewDataAction struct {
ButtonText string `json:"button_text"`
Data string `json:"data,omitempty"`
}
// MessageRequestedPeerAction is the protocol-neutral payload of messageActionRequestedPeer*.
type MessageRequestedPeerAction struct {
ButtonID int `json:"button_id"`
Peers []Peer `json:"peers"`
// Details is the immutable, permission-gated snapshot delivered to the bot.
// It is kept separate from Peers because the sender-side MTProto action only
// exposes peer identities, while the bot-side/Bot API view may additionally
// expose the requested name, username, and profile photo.
Details []MessageRequestedPeerDetails `json:"details,omitempty"`
NameRequested bool `json:"name_requested,omitempty"`
UsernameRequested bool `json:"username_requested,omitempty"`
PhotoRequested bool `json:"photo_requested,omitempty"`
}
type MessageRequestedPeerDetails struct {
Peer Peer `json:"peer"`
FirstName string `json:"first_name,omitempty"`
LastName string `json:"last_name,omitempty"`
Title string `json:"title,omitempty"`
Username string `json:"username,omitempty"`
Photo *Photo `json:"photo,omitempty"`
}
// MessageNoForwardsAction is the protocol-neutral payload of the private-chat content-protection service action.
// ExpiresAt is only used for the read-time absolute expiry projection of a request; toggle stays 0.
type MessageNoForwardsAction struct {
PrevValue bool `json:"prev_value"`
NewValue bool `json:"new_value"`
Expired bool `json:"expired,omitempty"`
ExpiresAt int `json:"expires_at,omitempty"`
}
// MessageServiceAction is the protocol-neutral representation of a private-chat service message action.
type MessageServiceAction struct {
GramTransfer *MessageGramTransferAction `json:"gram_transfer,omitempty"`
Kind MessageServiceActionKind `json:"kind"`
Photo *Photo `json:"photo,omitempty"`
Call *MessagePhoneCallAction `json:"call,omitempty"`
ConferenceCall *MessageConferenceCallAction `json:"conference_call,omitempty"`
BotAllowed *MessageBotAllowedAction `json:"bot_allowed,omitempty"`
WebViewData *MessageWebViewDataAction `json:"web_view_data,omitempty"`
RequestedPeer *MessageRequestedPeerAction `json:"requested_peer,omitempty"`
ChatThemeEmoticon string `json:"chat_theme_emoticon,omitempty"`
NoForwards *MessageNoForwardsAction `json:"no_forwards,omitempty"`
StarGift *MessageStarGiftAction `json:"star_gift,omitempty"`
GiftStars *MessageGiftStarsAction `json:"gift_stars,omitempty"`
StarGiftUnique *MessageStarGiftUniqueAction `json:"star_gift_unique,omitempty"`
StarGiftOffer *MessageStarGiftOfferAction `json:"star_gift_offer,omitempty"`
StarGiftOfferDeclined *MessageStarGiftOfferDeclinedAction `json:"star_gift_offer_declined,omitempty"`
GiftPremium *MessageGiftPremiumAction `json:"gift_premium,omitempty"`
Payment *MessagePaymentAction `json:"payment,omitempty"`
}
// MessagePaymentAction is projected as messageActionPaymentSentMe, which is what
// a bot receives once a payer settles an invoice. It is the only place a bot
// learns the charge id, and therefore the only way it can later refund.
//
// Without it a payment settles but stays invisible to the bot, so refundStarPayment
// would be unreachable: nothing would ever supply telegram_payment_charge_id.
type MessagePaymentAction struct {
Currency string `json:"currency"`
TotalAmount int64 `json:"total_amount"`
Payload string `json:"payload,omitempty"`
ChargeID string `json:"charge_id"`
ProviderChargeID string `json:"provider_charge_id"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
}
// MessageGiftPremiumAction is projected as
// messageActionGiftPremium#48e91302 at the RPC boundary.
type MessageGiftPremiumAction struct {
Currency string `json:"currency"`
Amount int64 `json:"amount"`
Days int `json:"days"`
Message PremiumGiftMessage `json:"message,omitempty"`
}
// MessageGiftStarsAction is the immutable service-message projection. The
// recipient-only BalanceAfter field is not encoded in messageActionGiftStars;
// it lets online push and offline difference attach the matching non-PTS
// updateStarsBalance without querying mutable current state.
type MessageGiftStarsAction struct {
Currency string `json:"currency"`
Amount int64 `json:"amount"`
Stars int64 `json:"stars"`
TransactionID string `json:"transaction_id,omitempty"`
BalanceAfter int64 `json:"balance_after"`
}
// MessageStarGiftAction is the protocol-neutral payload of messageActionStarGift: the embedded gift snapshot (sticker/star price)
// lets the recipient render without an extra fetch. PeerUserID/PeerChannelID is the recipient side; when NameHidden is set the output does not expose from.
type MessageStarGiftAction struct {
GiftID int64 `json:"gift_id"`
Stars int64 `json:"stars"`
ConvertStars int64 `json:"convert_stars,omitempty"`
Title string `json:"title,omitempty"`
Sticker *Document `json:"sticker,omitempty"`
Message string `json:"message,omitempty"`
MessageEntities []MessageEntity `json:"message_entities,omitempty"`
FromUserID int64 `json:"from_user_id,omitempty"`
PeerUserID int64 `json:"peer_user_id,omitempty"`
PeerChannelID int64 `json:"peer_channel_id,omitempty"`
SavedID int64 `json:"saved_id,omitempty"`
NameHidden bool `json:"name_hidden,omitempty"`
Saved bool `json:"saved,omitempty"`
Converted bool `json:"converted,omitempty"`
CanUpgrade bool `json:"can_upgrade,omitempty"`
PrepaidUpgrade bool `json:"prepaid_upgrade,omitempty"`
PrepaidUpgradeHash string `json:"prepaid_upgrade_hash,omitempty"`
UpgradeSeparate bool `json:"upgrade_separate,omitempty"`
// UpgradePriceStars belongs to the inner StarGift.upgrade_stars field and
// is the price of a normal paid upgrade. UpgradeStars below belongs to the
// outer messageActionStarGift and is only the amount already prepaid by the
// sender. TDesktop uses these two fields to choose the paid vs free flow.
UpgradePriceStars int64 `json:"upgrade_price_stars,omitempty"`
UpgradeStars int64 `json:"upgrade_stars,omitempty"`
UpgradeMsgID int `json:"upgrade_msg_id,omitempty"`
GiftMsgID int `json:"gift_msg_id,omitempty"`
GiftNum int `json:"gift_num,omitempty"`
AuctionAcquired bool `json:"auction_acquired,omitempty"`
To Peer `json:"to,omitempty"`
}
type MessageStarGiftUniqueAction struct {
Gift UniqueStarGift `json:"gift"`
FromUserID int64 `json:"from_user_id,omitempty"`
Peer Peer `json:"peer"`
SavedID int64 `json:"saved_id,omitempty"`
Upgrade bool `json:"upgrade,omitempty"`
Saved bool `json:"saved,omitempty"`
PrepaidUpgrade bool `json:"prepaid_upgrade,omitempty"`
Transferred bool `json:"transferred,omitempty"`
Refunded bool `json:"refunded,omitempty"`
Assigned bool `json:"assigned,omitempty"`
FromOffer bool `json:"from_offer,omitempty"`
Craft bool `json:"craft,omitempty"`
CanExportAt int `json:"can_export_at,omitempty"`
TransferStars int64 `json:"transfer_stars,omitempty"`
ResaleAmount *StarGiftAmount `json:"resale_amount,omitempty"`
CanTransferAt int `json:"can_transfer_at,omitempty"`
CanResellAt int `json:"can_resell_at,omitempty"`
DropOriginalDetailsStars int64 `json:"drop_original_details_stars,omitempty"`
CanCraftAt int `json:"can_craft_at,omitempty"`
}
type MessageStarGiftOfferAction struct {
Gift UniqueStarGift `json:"gift"`
Price StarGiftAmount `json:"price"`
ExpiresAt int `json:"expires_at"`
Accepted bool `json:"accepted,omitempty"`
Declined bool `json:"declined,omitempty"`
}
type MessageStarGiftOfferDeclinedAction struct {
Gift UniqueStarGift `json:"gift"`
Price StarGiftAmount `json:"price"`
Expired bool `json:"expired,omitempty"`
}
// MessageMedia is the business representation of a message's media payload (persisted as a JSONB snapshot on the message row).
type MessageMedia struct {
Kind MessageMediaKind `json:"kind"`
Photo *Photo `json:"photo,omitempty"`
LivePhotoVideo *Document `json:"live_photo_video,omitempty"`
Document *Document `json:"document,omitempty"`
Contact *MessageContact `json:"contact,omitempty"`
ServiceAction *MessageServiceAction `json:"service_action,omitempty"`
Geo *MessageGeoPoint `json:"geo,omitempty"`
Venue *MessageVenue `json:"venue,omitempty"`
Dice *MessageDice `json:"dice,omitempty"`
Poll *MessagePoll `json:"poll,omitempty"`
GeoLive *MessageGeoLive `json:"geo_live,omitempty"`
Todo *MessageTodo `json:"todo,omitempty"`
Story *MessageStory `json:"story,omitempty"`
WebPage *MessageWebPage `json:"web_page,omitempty"`
Giveaway *MessageGiveaway `json:"giveaway,omitempty"`
Widget *MessageWidget `json:"widget,omitempty"`
Invoice *PremiumInvoice `json:"invoice,omitempty"`
// ProductInvoice is the generic bot sale shape. It is separate from Invoice
// because a Premium invoice carries a plan, not a product, and the two
// validate against different rules.
ProductInvoice *Invoice `json:"product_invoice,omitempty"`
Spoiler bool `json:"spoiler,omitempty"`
TTLSeconds int `json:"ttl_seconds,omitempty"`
Nopremium bool `json:"nopremium,omitempty"`
Voice bool `json:"voice,omitempty"`
Round bool `json:"round,omitempty"`
Video bool `json:"video,omitempty"`
// InvertMedia maps message.invert_media: the media (typically a link preview) renders above the text.
// Stored in the media snapshot rather than the message row to avoid a new message-table column; projected to tg.Message.invert_media on read.
InvertMedia bool `json:"invert_media,omitempty"`
}
// IsMusic reports whether the message media is a music document rather than a
// voice note, video, or generic file.
func (m *MessageMedia) IsMusic() bool {
return m != nil &&
m.Kind == MessageMediaKindDocument &&
m.Document != nil &&
!m.Voice &&
m.Document.IsMusic()
}
// MessageContact is a shared contact card attached to a message.
type MessageContact struct {
PhoneNumber string `json:"phone_number"`
FirstName string `json:"first_name"`
LastName string `json:"last_name,omitempty"`
Vcard string `json:"vcard,omitempty"`
UserID int64 `json:"user_id,omitempty"`
}
// IsZero means no media (used to skip an empty snapshot on persist and fall back to MessageMediaEmpty on conversion).
func (m *MessageMedia) IsZero() bool {
return m == nil || m.Kind == MessageMediaKindNone
}
// HasUnreadPayload reports whether this media participates in the media_unread ("unheard") state.
// Only voice/round have this semantics: the client renders the unheard marker only for them and reports
// readMessageContents; setting it on a photo/document only leaves a dirty state that is never cleared.
func (m *MessageMedia) HasUnreadPayload() bool {
if m.IsZero() || m.Kind != MessageMediaKindDocument {
return false
}
return m.Voice || m.Round
}
// StickerPack is an emoji→document id mapping entry (messages.stickerSet.packs).
type StickerPack struct {
Emoticon string `json:"emoticon"`
DocumentIDs []int64 `json:"document_ids"`
}
type StickerKeyword struct {
DocumentID int64 `json:"document_id"`
Keywords []string `json:"keywords"`
}
const (
// StickerSetSystemKeyEmojiDefaultStatuses is the system-set identifier for inputStickerSetEmojiDefaultStatuses:
// the "default statuses" main set of the Premium user emoji status picker
// (shared by messages.getStickerSet and account.getDefaultEmojiStatuses).
StickerSetSystemKeyEmojiDefaultStatuses = "emoji_default_statuses"
// StickerSetSystemKeyEmojiDefaultTopicIcons is the system set for default forum topic icons.
StickerSetSystemKeyEmojiDefaultTopicIcons = "emoji_default_topic_icons"
// StickerSetSystemKeyPremiumGifts is the system set for premium gifts.
StickerSetSystemKeyPremiumGifts = "premium_gifts"
// StickerSetSystemKeyTonGifts is the system set for TON gifts.
StickerSetSystemKeyTonGifts = "ton_gifts"
)
// StickerSetKind distinguishes sticker set purposes (affects getAllStickers / getEmojiStickers classification).
type StickerSetKind string
const (
StickerSetKindStickers StickerSetKind = "stickers"
StickerSetKindEmoji StickerSetKind = "emoji"
StickerSetKindMasks StickerSetKind = "masks"
// StickerSetKindSystem is a built-in set TDesktop requests via system sets such as InputStickerSetDice/AnimatedEmoji.
StickerSetKindSystem StickerSetKind = "system"
)
// StickerSet is the metadata of a sticker/custom emoji set + ordered document ids.
type StickerSet struct {
ID int64 `json:"id"`
AccessHash int64 `json:"access_hash"`
ShortName string `json:"short_name"`
Title string `json:"title"`
Count int `json:"count"`
Hash int `json:"hash"`
Kind StickerSetKind `json:"set_kind"`
Official bool `json:"official,omitempty"`
Animated bool `json:"animated,omitempty"`
Videos bool `json:"videos,omitempty"`
Emojis bool `json:"emojis,omitempty"`
Masks bool `json:"masks,omitempty"`
TextColor bool `json:"text_color,omitempty"`
Creator bool `json:"creator,omitempty"`
CreatorUserID int64 `json:"creator_user_id,omitempty"`
Installed bool `json:"installed,omitempty"`
Archived bool `json:"archived,omitempty"`
Deleted bool `json:"deleted,omitempty"`
InstalledDate int `json:"installed_date,omitempty"`
ThumbDocumentID int64 `json:"thumb_document_id,omitempty"`
Thumbs []PhotoSize `json:"thumbs,omitempty"`
ThumbDCID int `json:"thumb_dc_id,omitempty"`
ThumbVersion int `json:"thumb_version,omitempty"`
DocumentIDs []int64 `json:"document_ids,omitempty"`
Packs []StickerPack `json:"packs,omitempty"`
Keywords []StickerKeyword `json:"keywords,omitempty"`
SortOrder int `json:"sort_order,omitempty"`
Software string `json:"software,omitempty"`
// SystemKey is the stable identifier of a TDesktop system set (e.g. "animated_emoji", "dice:🎲"), used for InputStickerSet* routing.
SystemKey string `json:"system_key,omitempty"`
}
const (
MinStickerSetShortNameLen = 5
MaxStickerSetShortNameLen = 32
MaxStickerSetTitleLen = 64
MaxStickerSetItems = 120
MaxStickerSetKeywords = 20
MaxStickerSetKeywordLen = 32
)
type StickerSetItemInput struct {
DocumentID int64
DocumentAccessHash int64
Emoji string
Keywords string
}
type CreateStickerSetRequest struct {
CreatorUserID int64
Title string
ShortName string
Kind StickerSetKind
TextColor bool
ThumbDocumentID int64
ThumbAccessHash int64
Items []StickerSetItemInput
Software string
Date int
}
// ProfilePhotoKind distinguishes a user's real profile photo from the fallback
// public photo shown when privacy hides the real one.
type ProfilePhotoKind string
const (
ProfilePhotoKindProfile ProfilePhotoKind = "profile"
ProfilePhotoKindFallback ProfilePhotoKind = "fallback"
)
// ProfilePhotoRef is the minimal information needed to render an avatar (current profile/fallback/personal photo).
type ProfilePhotoRef struct {
PhotoID int64
DCID int
Stripped []byte // photoStrippedSize inline thumbnail, may be empty
Personal bool // true means a viewer-private contact avatar
HasVideo bool // true means photo.video_sizes is non-empty, requiring output of userProfilePhoto.has_video
}
// StrippedFromSizes extracts the stripped thumbnail bytes from a photo size list (used as the UserProfilePhoto/ChatPhoto placeholder).
func StrippedFromSizes(sizes []PhotoSize) []byte {
for _, s := range sizes {
if s.Kind == PhotoSizeKindStripped {
return s.Bytes
}
}
return nil
}
// PhotoHasVideo derives UserProfilePhoto.has_video from a photo size snapshot.
func PhotoHasVideo(sizes []PhotoSize) bool {
for _, s := range sizes {
switch s.Kind {
case PhotoSizeKindVideo, PhotoSizeKindVideoEmojiMarkup, PhotoSizeKindVideoStickerMarkup:
return true
}
}
return false
}
// StickerSetRefKind identifies how an InputStickerSet is resolved.
type StickerSetRefKind string
const (
StickerSetRefByID StickerSetRefKind = "id"
StickerSetRefByShortName StickerSetRefKind = "short_name"
StickerSetRefBySystem StickerSetRefKind = "system"
)
// StickerSetRef is the sticker set reference the rpc layer converts from tg.InputStickerSet.
type StickerSetRef struct {
Kind StickerSetRefKind
ID int64
AccessHash int64
ShortName string
SystemKey string
}
// AvailableReaction describes one item of messages.getAvailableReactions (real assets are referenced by document id).
type AvailableReaction struct {
Reaction string `json:"reaction"`
Title string `json:"title"`
Inactive bool `json:"inactive,omitempty"`
Premium bool `json:"premium,omitempty"`
StaticIconID int64 `json:"static_icon_id,omitempty"`
AppearAnimationID int64 `json:"appear_animation_id,omitempty"`
SelectAnimationID int64 `json:"select_animation_id,omitempty"`
ActivateAnimationID int64 `json:"activate_animation_id,omitempty"`
EffectAnimationID int64 `json:"effect_animation_id,omitempty"`
AroundAnimationID int64 `json:"around_animation_id,omitempty"`
CenterIconID int64 `json:"center_icon_id,omitempty"`
Order int `json:"order,omitempty"`
}
// AvailableEffect is a message send effect (messages.getAvailableEffects). It references three documents:
// the picker static icon / the effect sticker on the bubble / the full-screen effect animation (the latter two may be 0). It is a global static catalog,
// resident in memory after the server seeds it.
type AvailableEffect struct {
ID int64 `json:"id"`
Emoticon string `json:"emoticon"`
StaticIconID int64 `json:"static_icon_id,omitempty"`
EffectStickerID int64 `json:"effect_sticker_id"`
EffectAnimationID int64 `json:"effect_animation_id,omitempty"`
PremiumRequired bool `json:"premium_required,omitempty"`
Order int `json:"order,omitempty"`
}
// DocumentIDs collects all document ids this effect references (dropping zeros and duplicates).
func (e AvailableEffect) DocumentIDs() []int64 {
return dedupNonZeroIDs(e.StaticIconID, e.EffectStickerID, e.EffectAnimationID)
}
// DocumentIDs collects all document ids this reaction references (dropping zeros and duplicates, for bulk loading).
func (r AvailableReaction) DocumentIDs() []int64 {
raw := []int64{
r.StaticIconID, r.AppearAnimationID, r.SelectAnimationID,
r.ActivateAnimationID, r.EffectAnimationID, r.AroundAnimationID, r.CenterIconID,
}
out := make([]int64, 0, len(raw))
seen := make(map[int64]struct{}, len(raw))
for _, id := range raw {
if id == 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
out = append(out, id)
}
return out
}
// dedupNonZeroIDs returns the id list with zeros removed and duplicates dropped (order preserved).
func dedupNonZeroIDs(ids ...int64) []int64 {
out := make([]int64, 0, len(ids))
seen := make(map[int64]struct{}, len(ids))
for _, id := range ids {
if id == 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
out = append(out, id)
}
return out
}
type MessageGramTransferAction struct {
Amount int64 `json:"amount"`
// PeerAddress is the recipient's wallet address.
PeerAddress string `json:"peer_address,omitempty"`
// TransactionID is the ledger transaction id.
TransactionID string `json:"transaction_id,omitempty"`
// Comment is the note the sender wrote on the transfer.
Comment string `json:"comment,omitempty"`
// CommentEncrypted reports that the sender's note is encrypted: the client
// holds the key, the server does not, and the note stays empty.
CommentEncrypted bool `json:"comment_encrypted,omitempty"`
}