owpengram-server/internal/rpc/message_idempotency.go

84 lines
3 KiB
Go

package rpc
import (
"crypto/sha256"
"fmt"
"github.com/iamxvbaba/td/bin"
"github.com/iamxvbaba/td/tg"
)
// rpcRequestFingerprint 在任何自动实体补全、链接预览解析或上传媒体落库之前,
// 对客户端原始 TL request 取稳定指纹。这样 lost-response 重放不会因服务端派生
// photo/document id、pending webpage 状态等变化而被误判为另一条消息。
func rpcRequestFingerprint(req bin.Encoder) ([]byte, error) {
if req == nil {
return nil, fmt.Errorf("fingerprint rpc request: nil request")
}
var b bin.Buffer
if err := req.Encode(&b); err != nil {
return nil, fmt.Errorf("fingerprint rpc request: %w", err)
}
sum := sha256.Sum256(b.Raw())
return sum[:], nil
}
// sendMessageIdempotencyFingerprint fingerprints only the durable message intent.
// clear_draft/background/update_stickersets_order are one-shot client-side delivery
// hints: after a lost response DrKLO/TDesktop may legitimately retry the same
// random_id without them. They must not turn an exact send replay into
// RANDOM_ID_DUPLICATE.
func sendMessageIdempotencyFingerprint(req *tg.MessagesSendMessageRequest) ([]byte, error) {
if req == nil {
return nil, fmt.Errorf("fingerprint messages.sendMessage: nil request")
}
clone := *req
clone.Flags = 0
clone.ClearDraft = false
clone.Background = false
clone.UpdateStickersetsOrder = false
return rpcRequestFingerprint(&clone)
}
func sendMediaIdempotencyFingerprint(req *tg.MessagesSendMediaRequest) ([]byte, error) {
if req == nil {
return nil, fmt.Errorf("fingerprint messages.sendMedia: nil request")
}
clone := *req
clone.Flags = 0
clone.ClearDraft = false
clone.Background = false
clone.UpdateStickersetsOrder = false
return rpcRequestFingerprint(&clone)
}
// sendMultiMediaItemIdempotencyFingerprint deliberately reduces a batch to one
// InputSingleMedia. A retry containing only the failed subset therefore produces
// the same fingerprint for every surviving random_id as the original batch.
func sendMultiMediaItemIdempotencyFingerprint(req *tg.MessagesSendMultiMediaRequest, item tg.InputSingleMedia) ([]byte, error) {
if req == nil {
return nil, fmt.Errorf("fingerprint messages.sendMultiMedia item: nil request")
}
clone := *req
clone.Flags = 0
clone.ClearDraft = false
clone.Background = false
clone.UpdateStickersetsOrder = false
clone.MultiMedia = []tg.InputSingleMedia{item}
return rpcRequestFingerprint(&clone)
}
// forwardMessagesItemIdempotencyFingerprint makes the source message id and its
// paired random_id the unit of idempotency. Hashing the full ID/RandomID vectors
// incorrectly rejects a legal retry that contains only a failed subset.
func forwardMessagesItemIdempotencyFingerprint(req *tg.MessagesForwardMessagesRequest, messageID int, randomID int64) ([]byte, error) {
if req == nil {
return nil, fmt.Errorf("fingerprint messages.forwardMessages item: nil request")
}
clone := *req
clone.Flags = 0
clone.Background = false
clone.ID = []int{messageID}
clone.RandomID = []int64{randomID}
return rpcRequestFingerprint(&clone)
}