mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-12 14:15:16 +00:00
/wordledaily sends a BotFather game card whose page plays one shared word per day, changing at 07:00 ICT. The server checks guesses, saves each player's board after every guess and keeps NYT-style stats. A win is reported with setGameScore, and in a group one live results message shows everyone's colour grids. /wordledaily_subscribe and /wordledaily_unsubscribe opt a chat or topic in to the 07:00 push: yesterday's recap, opening with the answer and the group's win streak, then today's card. The HTML5 game plumbing that noitu used is now shared in util/htmlgame, and the wordle word list and scoring live in wordle/wordlist.
94 lines
2.7 KiB
Go
94 lines
2.7 KiB
Go
// Package htmlgame holds the plumbing shared by the bot's Telegram HTML5
|
|
// games: the public base URL, signed Play-link tokens, the HTTP security
|
|
// headers and JSON helpers of a game's API, embedded page assets, score
|
|
// reporting with setGameScore, and a per-user request limiter.
|
|
//
|
|
// Each game keeps its own claims type, token key label, messages and routes;
|
|
// this package only owns the parts that must behave the same everywhere.
|
|
package htmlgame
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// MaxTokenBytes bounds a token before any work is spent on it.
|
|
const MaxTokenBytes = 1024
|
|
|
|
var (
|
|
// ErrBadToken is a token that is malformed, forged or names no valid
|
|
// game message.
|
|
ErrBadToken = errors.New("htmlgame: bad token")
|
|
// ErrTokenExpired is a correctly signed token past its expiry.
|
|
ErrTokenExpired = errors.New("htmlgame: token expired")
|
|
)
|
|
|
|
// Claims is what a game decodes a token into. Valid checks the game's own
|
|
// rules (for example exactly one message address); ExpiresAt is the unix
|
|
// second the token stops working.
|
|
type Claims interface {
|
|
Valid() bool
|
|
ExpiresAt() int64
|
|
}
|
|
|
|
// DeriveKey is HMAC-SHA256(key=root, label). Each game and purpose uses its
|
|
// own label, so a key derived for one never verifies for another. An empty
|
|
// root yields nil.
|
|
func DeriveKey(root []byte, label string) []byte {
|
|
if len(root) == 0 {
|
|
return nil
|
|
}
|
|
mac := hmac.New(sha256.New, root)
|
|
mac.Write([]byte(label))
|
|
return mac.Sum(nil)
|
|
}
|
|
|
|
// Sign encodes payload as base64url(json) "." base64url(HMAC-SHA256).
|
|
func Sign(key []byte, payload any) (string, error) {
|
|
raw, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
body := base64.RawURLEncoding.EncodeToString(raw)
|
|
return body + "." + base64.RawURLEncoding.EncodeToString(tokenMAC(key, body)), nil
|
|
}
|
|
|
|
func tokenMAC(key []byte, body string) []byte {
|
|
mac := hmac.New(sha256.New, key)
|
|
mac.Write([]byte(body))
|
|
return mac.Sum(nil)
|
|
}
|
|
|
|
// Verify checks the signature first, then decodes into dst (a pointer) and
|
|
// checks the claims and the expiry. It returns ErrBadToken or
|
|
// ErrTokenExpired.
|
|
func Verify(key []byte, token string, now time.Time, dst Claims) error {
|
|
if len(key) == 0 || len(token) > MaxTokenBytes {
|
|
return ErrBadToken
|
|
}
|
|
body, sig, ok := strings.Cut(token, ".")
|
|
if !ok {
|
|
return ErrBadToken
|
|
}
|
|
gotMAC, err := base64.RawURLEncoding.DecodeString(sig)
|
|
if err != nil || !hmac.Equal(gotMAC, tokenMAC(key, body)) {
|
|
return ErrBadToken
|
|
}
|
|
payload, err := base64.RawURLEncoding.DecodeString(body)
|
|
if err != nil {
|
|
return ErrBadToken
|
|
}
|
|
if err := json.Unmarshal(payload, dst); err != nil || !dst.Valid() {
|
|
return ErrBadToken
|
|
}
|
|
if now.Unix() >= dst.ExpiresAt() {
|
|
return ErrTokenExpired
|
|
}
|
|
return nil
|
|
}
|