owpengram-server/internal/app/ephemeral/service.go
A f49c817def feat: sync ephemeral transient messages
Sync telesrv 570ccf8 (feat(ephemeral): implement Layer 228 transient messages).

Skipped telesrv docs changes per public sync rules; normalized the public appearance seed label.
2026-07-20 16:43:27 +08:00

617 lines
23 KiB
Go

package ephemeral
import (
"bytes"
"context"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"encoding/json"
"errors"
"strings"
"time"
"telesrv/internal/domain"
"telesrv/internal/store"
)
type ChannelAccess interface {
ResolveChannel(ctx context.Context, userID, channelID int64) (domain.ChannelView, error)
GetParticipant(ctx context.Context, userID, channelID, participantUserID int64) (domain.ChannelMember, error)
GetForumTopicsByID(ctx context.Context, userID, channelID int64, ids []int) (domain.ChannelForumTopicList, error)
}
type UserDirectory interface {
ByID(ctx context.Context, currentUserID, userID int64) (domain.User, bool, error)
}
type BotCommands interface {
GetBotCommands(ctx context.Context, botUserID int64) ([]domain.BotCommand, error)
}
type Option func(*Service)
func WithClock(now func() time.Time) Option {
return func(s *Service) {
if now != nil {
s.now = now
}
}
}
func WithIDGenerator(next func() (int, error)) Option {
return func(s *Service) {
if next != nil {
s.nextID = next
}
}
}
type Service struct {
messages store.EphemeralMessageStore
channels ChannelAccess
users UserDirectory
bots BotCommands
now func() time.Time
nextID func() (int, error)
}
func NewService(messages store.EphemeralMessageStore, channels ChannelAccess, users UserDirectory, bots BotCommands, options ...Option) *Service {
s := &Service{
messages: messages,
channels: channels,
users: users,
bots: bots,
now: time.Now,
nextID: randomEphemeralID,
}
for _, option := range options {
if option != nil {
option(s)
}
}
return s
}
func (s *Service) SendFromClient(ctx context.Context, request domain.SendClientEphemeralRequest) (domain.EphemeralMessage, bool, error) {
if s == nil || s.messages == nil || s.channels == nil || s.users == nil || s.bots == nil {
return domain.EphemeralMessage{}, false, domain.ErrEphemeralInvalid
}
if request.SenderUserID <= 0 || request.ReceiverBotID <= 0 || request.SenderUserID == request.ReceiverBotID ||
request.Peer.Type != domain.PeerTypeChannel || request.Peer.ID <= 0 || request.RandomID == 0 ||
request.OriginDevice.UserID != request.SenderUserID || request.OriginDevice.BusinessAuthKeyID == ([8]byte{}) ||
request.OriginDevice.SessionID == 0 || !validContent(request.Content) {
return domain.EphemeralMessage{}, false, domain.ErrEphemeralInvalid
}
view, err := s.requireActiveGroupPair(ctx, request.SenderUserID, request.ReceiverBotID, request.Peer.ID)
if err != nil {
return domain.EphemeralMessage{}, false, err
}
receiver, found, err := s.users.ByID(ctx, request.SenderUserID, request.ReceiverBotID)
if err != nil {
return domain.EphemeralMessage{}, false, err
}
if !found || !receiver.Bot || receiver.Deleted {
return domain.EphemeralMessage{}, false, domain.ErrEphemeralReceiverInvalid
}
var replyTarget *domain.EphemeralMessage
if request.ReplyToEphemeralID != 0 {
target, found, err := s.messages.GetEphemeralMessage(ctx, request.Peer, request.ReplyToEphemeralID, s.now())
if err != nil {
return domain.EphemeralMessage{}, false, err
}
if !found || target.Deleted || target.SenderUserID != request.ReceiverBotID || target.ReceiverUserID != request.SenderUserID {
return domain.EphemeralMessage{}, false, domain.ErrEphemeralReplyExpired
}
if target.OriginDevice.BusinessAuthKeyID != ([8]byte{}) && target.OriginDevice.BusinessAuthKeyID != request.OriginDevice.BusinessAuthKeyID {
return domain.EphemeralMessage{}, false, domain.ErrEphemeralDeviceMismatch
}
if request.TopMessageID != 0 && request.TopMessageID != target.TopMessageID {
return domain.EphemeralMessage{}, false, domain.ErrEphemeralPeerInvalid
}
request.TopMessageID = target.TopMessageID
replyTarget = &target
} else {
allowed, err := s.isEphemeralCommand(ctx, receiver, request.Content.Message)
if err != nil {
return domain.EphemeralMessage{}, false, err
}
if !allowed {
return domain.EphemeralMessage{}, false, domain.ErrEphemeralCommandInvalid
}
}
if err := s.validateForumTopic(ctx, request.SenderUserID, view, request.TopMessageID); err != nil {
return domain.EphemeralMessage{}, false, err
}
message, fresh, err := s.create(ctx, domain.EphemeralMessage{
Peer: request.Peer,
SenderUserID: request.SenderUserID,
ReceiverUserID: request.ReceiverBotID,
RandomID: request.RandomID,
TopMessageID: request.TopMessageID,
ReplyToEphemeralID: request.ReplyToEphemeralID,
Content: request.Content,
OriginDevice: request.OriginDevice,
PayloadHash: clientPayloadHash(request),
})
if err == nil && replyTarget != nil {
message.BotAPIReply = replyTarget
}
return message, fresh, err
}
func (s *Service) SendFromBot(ctx context.Context, request domain.SendBotEphemeralRequest) (domain.EphemeralMessage, bool, error) {
return s.sendFromBot(ctx, request, func(context.Context) (domain.EphemeralContent, error) {
return request.Content, nil
})
}
// SendFromBotLazy authorizes the bot, receiver, chat and eligible action before
// materializing content. The RPC edge uses it for URL/upload media so an
// unauthorized target cannot consume file storage, network or decoder work.
func (s *Service) SendFromBotLazy(ctx context.Context, request domain.SendBotEphemeralRequest, build func(context.Context) (domain.EphemeralContent, error)) (domain.EphemeralMessage, bool, error) {
if build == nil {
return domain.EphemeralMessage{}, false, domain.ErrEphemeralInvalid
}
return s.sendFromBot(ctx, request, build)
}
func (s *Service) sendFromBot(ctx context.Context, request domain.SendBotEphemeralRequest, build func(context.Context) (domain.EphemeralContent, error)) (domain.EphemeralMessage, bool, error) {
if s == nil || s.messages == nil || s.channels == nil || s.users == nil {
return domain.EphemeralMessage{}, false, domain.ErrEphemeralInvalid
}
if request.BotUserID <= 0 || request.ReceiverUserID <= 0 || request.BotUserID == request.ReceiverUserID ||
request.Peer.Type != domain.PeerTypeChannel || request.Peer.ID <= 0 {
return domain.EphemeralMessage{}, false, domain.ErrEphemeralInvalid
}
view, err := s.requireActiveGroupPair(ctx, request.BotUserID, request.ReceiverUserID, request.Peer.ID)
if err != nil {
return domain.EphemeralMessage{}, false, err
}
bot, found, err := s.users.ByID(ctx, request.BotUserID, request.BotUserID)
if err != nil {
return domain.EphemeralMessage{}, false, err
}
if !found || !bot.Bot || bot.Deleted {
return domain.EphemeralMessage{}, false, domain.ErrEphemeralSenderInvalid
}
receiver, found, err := s.users.ByID(ctx, request.BotUserID, request.ReceiverUserID)
if err != nil {
return domain.EphemeralMessage{}, false, err
}
if !found || receiver.Bot || receiver.Deleted {
return domain.EphemeralMessage{}, false, domain.ErrEphemeralReceiverInvalid
}
now := s.now()
var targetDevice domain.EphemeralDevice
var replyTarget *domain.EphemeralMessage
if request.ActionMessageID != 0 && request.CallbackQueryID != 0 {
return domain.EphemeralMessage{}, false, domain.ErrEphemeralInvalid
}
if request.CallbackQueryID != 0 {
action, found, err := s.messages.GetEphemeralCallbackAction(ctx, request.BotUserID, request.CallbackQueryID, now)
if err != nil {
return domain.EphemeralMessage{}, false, err
}
if !found || action.UserID != request.ReceiverUserID || action.Peer != request.Peer || !now.Before(action.ExpiresAt) {
return domain.EphemeralMessage{}, false, domain.ErrEphemeralReplyExpired
}
targetDevice = action.Device
if request.TopMessageID != 0 && request.TopMessageID != action.TopMessageID {
return domain.EphemeralMessage{}, false, domain.ErrEphemeralPeerInvalid
}
request.TopMessageID = action.TopMessageID
} else if request.ActionMessageID != 0 {
action, found, err := s.messages.GetEphemeralMessage(ctx, request.Peer, request.ActionMessageID, now)
if err != nil {
return domain.EphemeralMessage{}, false, err
}
if !found || action.Deleted || action.SenderUserID != request.ReceiverUserID || action.ReceiverUserID != request.BotUserID ||
now.Sub(action.CreatedAt) < 0 || now.Sub(action.CreatedAt) > domain.EphemeralReplyWindow {
return domain.EphemeralMessage{}, false, domain.ErrEphemeralReplyExpired
}
targetDevice = action.OriginDevice
replyTarget = &action
if request.TopMessageID != 0 && request.TopMessageID != action.TopMessageID {
return domain.EphemeralMessage{}, false, domain.ErrEphemeralPeerInvalid
}
request.TopMessageID = action.TopMessageID
if request.ReplyToEphemeralID == 0 {
request.ReplyToEphemeralID = action.ID
}
} else {
if view.Self.Role != domain.ChannelRoleCreator && view.Self.Role != domain.ChannelRoleAdmin {
return domain.EphemeralMessage{}, false, domain.ErrEphemeralForbidden
}
}
if request.ReplyToEphemeralID != 0 {
var reply domain.EphemeralMessage
found := false
if replyTarget != nil && replyTarget.ID == request.ReplyToEphemeralID {
reply, found = *replyTarget, true
} else {
var err error
reply, found, err = s.messages.GetEphemeralMessage(ctx, request.Peer, request.ReplyToEphemeralID, now)
if err != nil {
return domain.EphemeralMessage{}, false, err
}
}
if !found || reply.Deleted || !sameEphemeralParticipants(reply, request.BotUserID, request.ReceiverUserID) {
return domain.EphemeralMessage{}, false, domain.ErrEphemeralReplyExpired
}
if targetDevice.BusinessAuthKeyID != ([8]byte{}) && reply.OriginDevice.BusinessAuthKeyID != ([8]byte{}) &&
targetDevice.BusinessAuthKeyID != reply.OriginDevice.BusinessAuthKeyID {
return domain.EphemeralMessage{}, false, domain.ErrEphemeralDeviceMismatch
}
if request.TopMessageID != 0 && request.TopMessageID != reply.TopMessageID {
return domain.EphemeralMessage{}, false, domain.ErrEphemeralPeerInvalid
}
request.TopMessageID = reply.TopMessageID
replyTarget = &reply
}
if err := s.validateForumTopic(ctx, request.BotUserID, view, request.TopMessageID); err != nil {
return domain.EphemeralMessage{}, false, err
}
content, err := build(ctx)
if err != nil {
return domain.EphemeralMessage{}, false, err
}
if !validContent(content) {
return domain.EphemeralMessage{}, false, domain.ErrEphemeralInvalid
}
request.Content = content
if request.RandomID == 0 {
request.RandomID, err = randomEphemeralRandomID()
if err != nil {
return domain.EphemeralMessage{}, false, err
}
}
message, fresh, err := s.create(ctx, domain.EphemeralMessage{
Peer: request.Peer,
SenderUserID: request.BotUserID,
ReceiverUserID: request.ReceiverUserID,
RandomID: request.RandomID,
TopMessageID: request.TopMessageID,
ReplyToEphemeralID: request.ReplyToEphemeralID,
Content: request.Content,
OriginDevice: targetDevice,
PayloadHash: botPayloadHash(request),
})
if err == nil && replyTarget != nil {
message.BotAPIReply = replyTarget
}
return message, fresh, err
}
func (s *Service) EditFromBot(ctx context.Context, botUserID int64, peer domain.Peer, id int, content domain.EphemeralContent) (domain.EphemeralMessage, error) {
now := s.now()
message, found, err := s.messages.GetEphemeralMessage(ctx, peer, id, now)
if err != nil {
return domain.EphemeralMessage{}, err
}
if !found {
return domain.EphemeralMessage{}, domain.ErrEphemeralNotFound
}
if message.SenderUserID != botUserID {
return domain.EphemeralMessage{}, domain.ErrEphemeralForbidden
}
return s.messages.EditEphemeralMessage(ctx, peer, id, message.Version, content, int(now.Unix()), now)
}
func (s *Service) EditFieldsFromBot(ctx context.Context, botUserID, receiverUserID int64, peer domain.Peer, id int, mode domain.EphemeralEditMode, fields domain.EditEphemeralFields) (domain.EphemeralMessage, error) {
return s.editFieldsFromBot(ctx, botUserID, receiverUserID, peer, id, mode, func(context.Context) (domain.EditEphemeralFields, error) {
return fields, nil
})
}
// EditFieldsFromBotLazy performs the identity/ownership lookup before building
// replacement media. This keeps invalid edit requests off the remote-fetch and
// blob-materialization paths while preserving a single CAS write on success.
func (s *Service) EditFieldsFromBotLazy(ctx context.Context, botUserID, receiverUserID int64, peer domain.Peer, id int, mode domain.EphemeralEditMode, build func(context.Context) (domain.EditEphemeralFields, error)) (domain.EphemeralMessage, error) {
if build == nil {
return domain.EphemeralMessage{}, domain.ErrEphemeralInvalid
}
return s.editFieldsFromBot(ctx, botUserID, receiverUserID, peer, id, mode, build)
}
func (s *Service) editFieldsFromBot(ctx context.Context, botUserID, receiverUserID int64, peer domain.Peer, id int, mode domain.EphemeralEditMode, build func(context.Context) (domain.EditEphemeralFields, error)) (domain.EphemeralMessage, error) {
now := s.now()
message, found, err := s.messages.GetEphemeralMessage(ctx, peer, id, now)
if err != nil {
return domain.EphemeralMessage{}, err
}
if !found {
return domain.EphemeralMessage{}, domain.ErrEphemeralNotFound
}
if message.SenderUserID != botUserID || message.ReceiverUserID != receiverUserID {
return domain.EphemeralMessage{}, domain.ErrEphemeralForbidden
}
fields, err := build(ctx)
if err != nil {
return domain.EphemeralMessage{}, err
}
switch mode {
case domain.EphemeralEditText:
if message.Content.Media != nil || !message.Content.RichMessage.IsZero() || !fields.SetMessage {
return domain.EphemeralMessage{}, domain.ErrEphemeralInvalid
}
case domain.EphemeralEditCaption:
if message.Content.Media == nil || !fields.SetMessage {
return domain.EphemeralMessage{}, domain.ErrEphemeralInvalid
}
case domain.EphemeralEditMedia:
if message.Content.Media == nil || !fields.SetMedia {
return domain.EphemeralMessage{}, domain.ErrEphemeralInvalid
}
case domain.EphemeralEditReplyMarkup:
if !fields.SetReplyMarkup || fields.SetMessage || fields.SetMedia {
return domain.EphemeralMessage{}, domain.ErrEphemeralInvalid
}
default:
return domain.EphemeralMessage{}, domain.ErrEphemeralInvalid
}
content := message.Content
if fields.SetMessage {
content.Message = fields.Message
content.Entities = append([]domain.MessageEntity(nil), fields.Entities...)
}
if fields.SetMedia {
content.Media = fields.Media
}
if fields.SetReplyMarkup {
content.ReplyMarkup = fields.ReplyMarkup
}
if !validContent(content) {
return domain.EphemeralMessage{}, domain.ErrEphemeralInvalid
}
return s.messages.EditEphemeralMessage(ctx, peer, id, message.Version, content, int(now.Unix()), now)
}
func (s *Service) Delete(ctx context.Context, actorUserID, receiverUserID int64, peer domain.Peer, id int) (domain.EphemeralMessage, bool, error) {
return s.delete(ctx, actorUserID, receiverUserID, nil, peer, id)
}
func (s *Service) DeleteFromDevice(ctx context.Context, actorUserID, receiverUserID int64, device domain.EphemeralDevice, peer domain.Peer, id int) (domain.EphemeralMessage, bool, error) {
if device.UserID != actorUserID || device.BusinessAuthKeyID == ([8]byte{}) || device.SessionID == 0 {
return domain.EphemeralMessage{}, false, domain.ErrEphemeralForbidden
}
return s.delete(ctx, actorUserID, receiverUserID, &device, peer, id)
}
func (s *Service) delete(ctx context.Context, actorUserID, receiverUserID int64, device *domain.EphemeralDevice, peer domain.Peer, id int) (domain.EphemeralMessage, bool, error) {
now := s.now()
message, found, err := s.messages.GetEphemeralMessage(ctx, peer, id, now)
if err != nil {
return domain.EphemeralMessage{}, false, err
}
if !found {
return domain.EphemeralMessage{}, false, domain.ErrEphemeralNotFound
}
if message.ReceiverUserID != receiverUserID || (actorUserID != message.SenderUserID && actorUserID != message.ReceiverUserID) {
return domain.EphemeralMessage{}, false, domain.ErrEphemeralForbidden
}
if device != nil && message.OriginDevice.UserID == actorUserID && message.OriginDevice.BusinessAuthKeyID != ([8]byte{}) &&
message.OriginDevice.BusinessAuthKeyID != device.BusinessAuthKeyID {
return domain.EphemeralMessage{}, false, domain.ErrEphemeralDeviceMismatch
}
return s.messages.DeleteEphemeralMessage(ctx, peer, id, message.Version, now)
}
func (s *Service) Callback(ctx context.Context, userID int64, device domain.EphemeralDevice, peer domain.Peer, id int, data []byte) (domain.EphemeralCallback, error) {
if len(data) > domain.MaxEphemeralCallbackDataBytes || userID <= 0 || device.UserID != userID ||
device.BusinessAuthKeyID == ([8]byte{}) || device.SessionID == 0 {
return domain.EphemeralCallback{}, domain.ErrEphemeralCallbackInvalid
}
now := s.now()
message, found, err := s.messages.GetEphemeralMessage(ctx, peer, id, now)
if err != nil {
return domain.EphemeralCallback{}, err
}
if !found || message.Deleted || message.ReceiverUserID != userID {
return domain.EphemeralCallback{}, domain.ErrEphemeralCallbackInvalid
}
if !ephemeralMarkupContainsCallback(message.Content.ReplyMarkup, data) {
return domain.EphemeralCallback{}, domain.ErrEphemeralCallbackInvalid
}
if message.OriginDevice.BusinessAuthKeyID != ([8]byte{}) && message.OriginDevice.BusinessAuthKeyID != device.BusinessAuthKeyID {
return domain.EphemeralCallback{}, domain.ErrEphemeralDeviceMismatch
}
return domain.EphemeralCallback{
Message: message,
BotUserID: message.SenderUserID,
UserID: userID,
Peer: peer,
Data: append([]byte(nil), data...),
Device: device,
OccurredAt: now,
}, nil
}
func (s *Service) PutCallbackAction(ctx context.Context, action domain.EphemeralCallbackAction) (bool, error) {
if s == nil || s.messages == nil {
return false, domain.ErrEphemeralInvalid
}
return s.messages.PutEphemeralCallbackAction(ctx, action)
}
func (s *Service) ReportTarget(ctx context.Context, userID int64, device domain.EphemeralDevice, peer domain.Peer, id int) (domain.EphemeralMessage, error) {
if userID <= 0 || device.UserID != userID || device.BusinessAuthKeyID == ([8]byte{}) || device.SessionID == 0 {
return domain.EphemeralMessage{}, domain.ErrEphemeralForbidden
}
message, found, err := s.messages.GetEphemeralMessage(ctx, peer, id, s.now())
if err != nil {
return domain.EphemeralMessage{}, err
}
if !found || message.Deleted || message.ReceiverUserID != userID {
return domain.EphemeralMessage{}, domain.ErrEphemeralNotFound
}
if message.OriginDevice.BusinessAuthKeyID != ([8]byte{}) && message.OriginDevice.BusinessAuthKeyID != device.BusinessAuthKeyID {
return domain.EphemeralMessage{}, domain.ErrEphemeralDeviceMismatch
}
return message, nil
}
func ephemeralMarkupContainsCallback(markup *domain.MessageReplyMarkup, data []byte) bool {
if markup == nil || markup.Kind() != domain.MessageReplyMarkupInline {
return false
}
for _, row := range markup.Inline {
for _, button := range row {
if button.Type == domain.MarkupButtonCallback && bytes.Equal(button.Data, data) {
return true
}
}
}
return false
}
func sameEphemeralParticipants(message domain.EphemeralMessage, first, second int64) bool {
return (message.SenderUserID == first && message.ReceiverUserID == second) ||
(message.SenderUserID == second && message.ReceiverUserID == first)
}
func (s *Service) create(ctx context.Context, message domain.EphemeralMessage) (domain.EphemeralMessage, bool, error) {
now := s.now()
message.Date = int(now.Unix())
message.CreatedAt = now
message.ExpiresAt = now.Add(domain.EphemeralMessageRetention)
message.Version = 1
for attempt := 0; attempt < domain.MaxEphemeralCreateAttempts; attempt++ {
id, err := s.nextID()
if err != nil {
return domain.EphemeralMessage{}, false, err
}
message.ID = id
created, fresh, err := s.messages.CreateEphemeralMessage(ctx, message)
if !errors.Is(err, domain.ErrEphemeralIDCollision) {
return created, fresh, err
}
}
return domain.EphemeralMessage{}, false, domain.ErrEphemeralIDCollision
}
func (s *Service) requireActiveGroupPair(ctx context.Context, viewerUserID, otherUserID, channelID int64) (domain.ChannelView, error) {
view, err := s.channels.ResolveChannel(ctx, viewerUserID, channelID)
if err != nil {
return domain.ChannelView{}, err
}
if view.Channel.Deleted || view.Channel.Broadcast || view.Channel.Monoforum || view.Self.Status != domain.ChannelMemberActive {
return domain.ChannelView{}, domain.ErrEphemeralPeerInvalid
}
other, err := s.channels.GetParticipant(ctx, viewerUserID, channelID, otherUserID)
if err != nil {
return domain.ChannelView{}, err
}
if other.Status != domain.ChannelMemberActive {
return domain.ChannelView{}, domain.ErrEphemeralReceiverInvalid
}
return view, nil
}
func (s *Service) validateForumTopic(ctx context.Context, userID int64, view domain.ChannelView, topMessageID int) error {
if topMessageID == 0 {
return nil
}
if !view.Channel.Forum || topMessageID < 0 || topMessageID > domain.MaxMessageBoxID {
return domain.ErrEphemeralPeerInvalid
}
topics, err := s.channels.GetForumTopicsByID(ctx, userID, view.Channel.ID, []int{topMessageID})
if err != nil {
return err
}
if len(topics.Topics) != 1 || topics.Topics[0].TopicID != topMessageID || topics.Topics[0].Hidden {
return domain.ErrEphemeralPeerInvalid
}
if topics.Topics[0].Closed && view.Self.Role != domain.ChannelRoleAdmin && view.Self.Role != domain.ChannelRoleCreator {
return domain.ErrEphemeralForbidden
}
return nil
}
func (s *Service) isEphemeralCommand(ctx context.Context, bot domain.User, message string) (bool, error) {
command, username, ok := parseCommand(message)
if !ok || (username != "" && !strings.EqualFold(username, bot.Username)) {
return false, nil
}
commands, err := s.bots.GetBotCommands(ctx, bot.ID)
if err != nil {
return false, err
}
for _, candidate := range commands {
if candidate.Ephemeral && strings.EqualFold(candidate.Command, command) {
return true, nil
}
}
return false, nil
}
func parseCommand(message string) (command, username string, ok bool) {
fields := strings.Fields(strings.TrimSpace(message))
if len(fields) == 0 || len(fields[0]) < 2 || fields[0][0] != '/' {
return "", "", false
}
parts := strings.SplitN(fields[0][1:], "@", 2)
command = strings.ToLower(parts[0])
if command == "" {
return "", "", false
}
if len(parts) == 2 {
username = strings.TrimPrefix(strings.ToLower(parts[1]), "@")
if username == "" {
return "", "", false
}
}
return command, username, true
}
func validContent(content domain.EphemeralContent) bool {
return domain.ValidateEphemeralContent(content) == nil
}
func clientPayloadHash(request domain.SendClientEphemeralRequest) [32]byte {
return payloadHash(struct {
SenderUserID, ReceiverBotID int64
Peer domain.Peer
QueryID, RandomID int64
TopMessageID, ReplyID int
Content domain.EphemeralContent
Device domain.EphemeralDevice
}{request.SenderUserID, request.ReceiverBotID, request.Peer, request.QueryID, request.RandomID,
request.TopMessageID, request.ReplyToEphemeralID, request.Content, request.OriginDevice})
}
func botPayloadHash(request domain.SendBotEphemeralRequest) [32]byte {
return payloadHash(request)
}
func payloadHash(value any) [32]byte {
raw, err := json.Marshal(value)
if err != nil {
return sha256.Sum256([]byte("invalid-ephemeral-payload"))
}
return sha256.Sum256(raw)
}
func randomEphemeralID() (int, error) {
var raw [4]byte
if _, err := rand.Read(raw[:]); err != nil {
return 0, err
}
value := binary.LittleEndian.Uint32(raw[:]) & 0x7fffffff
if value == 0 {
value = 1
}
return int(value), nil
}
func randomEphemeralRandomID() (int64, error) {
var raw [8]byte
if _, err := rand.Read(raw[:]); err != nil {
return 0, err
}
value := int64(binary.LittleEndian.Uint64(raw[:]))
if value == 0 {
value = 1
}
return value, nil
}