package telegram import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "sort" "strconv" "strings" "time" "github.com/google/uuid" ) // WebAppUser — пользователь из Telegram WebApp type WebAppUser struct { ID int64 `json:"id"` FirstName string `json:"first_name"` LastName string `json:"last_name,omitempty"` Username string `json:"username,omitempty"` LanguageCode string `json:"language_code,omitempty"` IsBot bool `json:"is_bot,omitempty"` IsPremium bool `json:"is_premium,omitempty"` PhotoURL string `json:"photo_url,omitempty"` } // InitData — сырой initData от Telegram WebApp type InitData struct { QueryID string `json:"query_id,omitempty"` User *WebAppUser `json:"user,omitempty"` Receiver *WebAppUser `json:"receiver,omitempty"` Chat *Chat `json:"chat,omitempty"` ChatType string `json:"chat_type,omitempty"` ChatInst string `json:"chat_instance,omitempty"` StartParam string `json:"start_param,omitempty"` CanSendAfter int `json:"can_send_after,omitempty"` AuthDate int64 `json:"auth_date"` Hash string `json:"hash"` Signature string `json:"signature,omitempty"` } type Chat struct { ID int64 `json:"id"` Type string `json:"type"` Title string `json:"title,omitempty"` Username string `json:"username,omitempty"` } // ValidateInitData — проверка подписи Telegram func ValidateInitData(botToken string, initData string, maxAge time.Duration) (*InitData, error) { parts := strings.Split(initData, "&") if len(parts) == 0 { return nil, errors.New("empty initData") } m := make(map[string]string) for _, p := range parts { kv := strings.SplitN(p, "=", 2) if len(kv) != 2 { continue } m[kv[0]] = kv[1] } hash, ok := m["hash"] if !ok { return nil, errors.New("no hash") } delete(m, "hash") // сортируем ключи keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Strings(keys) // собираем data-check-string var b strings.Builder for i, k := range keys { if i > 0 { b.WriteByte('\n') } b.WriteString(k) b.WriteByte('=') b.WriteString(m[k]) } checkString := b.String() // secret = HMAC-SHA256(bot_token, "WebAppData") secret := hmac.New(sha256.New, []byte("WebAppData")) secret.Write([]byte(botToken)) secretBytes := secret.Sum(nil) // hash = HMAC-SHA256(check_string, secret) h := hmac.New(sha256.New, secretBytes) h.Write([]byte(checkString)) computed := hex.EncodeToString(h.Sum(nil)) if !hmac.Equal([]byte(computed), []byte(hash)) { return nil, errors.New("invalid signature") } // парсим initData var data InitData data.Hash = hash if v, ok := m["auth_date"]; ok { ts, err := strconv.ParseInt(v, 10, 64) if err != nil { return nil, errors.New("bad auth_date") } data.AuthDate = ts // проверка свежести if maxAge > 0 { if time.Now().Unix()-ts > int64(maxAge.Seconds()) { return nil, errors.New("initData expired") } } } if v, ok := m["query_id"]; ok { data.QueryID = v } if v, ok := m["chat_type"]; ok { data.ChatType = v } if v, ok := m["chat_instance"]; ok { data.ChatInst = v } if v, ok := m["start_param"]; ok { data.StartParam = v } if v, ok := m["can_send_after"]; ok { n, _ := strconv.Atoi(v) data.CanSendAfter = n } if v, ok := m["user"]; ok { var u WebAppUser if err := json.Unmarshal([]byte(v), &u); err == nil { data.User = &u } } if v, ok := m["receiver"]; ok { var u WebAppUser if err := json.Unmarshal([]byte(v), &u); err == nil { data.Receiver = &u } } if v, ok := m["chat"]; ok { var c Chat if err := json.Unmarshal([]byte(v), &c); err == nil { data.Chat = &c } } return &data, nil } // UserIDFromTelegram — стабильный UUID из tg_id func UserIDFromTelegram(tgID int64) uuid.UUID { ns := uuid.MustParse("1d4a3f50-2a8a-4f55-b1a3-b7e0c1f4e6a0") return uuid.NewSHA1(ns, []byte(strconv.FormatInt(tgID, 10))) }