- Telegram bot (long polling) - /api/v1/auth/telegram (validate initData, auto-create user) - /api/v1/auth/consents (record acceptances) - users.telegram_id column, GetByTelegramID/CreateWithTelegramID - ValidateInitData (HMAC-SHA256 with WebAppData secret)
155 lines
4.0 KiB
Go
155 lines
4.0 KiB
Go
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)))
|
|
} |