mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-11 03:13:46 +00:00
/wordle and /loldle now send the wordle and loldle BotFather games in an unlimited mode: each player gets their own word or champion, saved after every guess, with a New game button. Cards the bot did not record as unlimited, including the daily cards, play the daily puzzle. The helper commands keep working on the player's round. /loldledaily mirrors /wordledaily: one champion per day at 07:00 ICT, subscribe and unsubscribe, the morning recap and the live group results. The daily and unlimited machinery is shared in util/guessgame, the old in-chat wordle module is removed, and a one-time startup migration carries each player's stats and unfinished round over without changing old data.
107 lines
3.6 KiB
Go
107 lines
3.6 KiB
Go
package htmlgame
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"io/fs"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/tiennm99/tiennm99bot/internal/log"
|
|
)
|
|
|
|
// BaseURLEnv names the public https base every game page is served under.
|
|
const BaseURLEnv = "GAME_BASE_URL"
|
|
|
|
// MaxBodyBytes caps one API request body.
|
|
const MaxBodyBytes = 4 << 10
|
|
|
|
// ErrBadRequest is a body that is too big, not one JSON object, or carries
|
|
// unknown fields.
|
|
var ErrBadRequest = errors.New("htmlgame: bad request")
|
|
|
|
// ParseBaseURL accepts https://host[/path] and returns it without a trailing
|
|
// slash. Anything else is logged, naming game, and disables that game.
|
|
func ParseBaseURL(raw, game string) string {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return ""
|
|
}
|
|
u, err := url.Parse(raw)
|
|
if err != nil || u.Scheme != "https" || u.Host == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" {
|
|
log.Warn("GAME_BASE_URL must be https://host[/path]; " + game + " game disabled")
|
|
return ""
|
|
}
|
|
return strings.TrimRight(u.String(), "/")
|
|
}
|
|
|
|
// SecurityHeaders applies to every response under a game's route prefix. The
|
|
// token travels in the page URL, so no referrer may leak it. Telegram Web
|
|
// embeds games in an iframe, so framing is deliberately not restricted. API
|
|
// answers under prefix+"api/" are never cached. imgSrc adds image origins to
|
|
// the policy, for a page that shows pictures from a CDN; images still come
|
|
// from nowhere else.
|
|
func SecurityHeaders(prefix string, next http.Handler, imgSrc ...string) http.Handler {
|
|
img := strings.Join(append([]string{"'self'", "data:"}, imgSrc...), " ")
|
|
csp := "default-src 'self'; script-src 'self' https://telegram.org; style-src 'self'; img-src " + img +
|
|
"; connect-src 'self'; base-uri 'none'; form-action 'none'"
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
h := w.Header()
|
|
h.Set("Referrer-Policy", "no-referrer")
|
|
h.Set("X-Content-Type-Options", "nosniff")
|
|
h.Set("Content-Security-Policy", csp)
|
|
if strings.HasPrefix(r.URL.Path, prefix+"api/") {
|
|
h.Set("Cache-Control", "no-store")
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// WriteJSON writes v as the JSON response body with status.
|
|
func WriteJSON(w http.ResponseWriter, status int, v any) {
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(v)
|
|
}
|
|
|
|
// DecodeJSON reads one JSON object of at most MaxBodyBytes with no unknown
|
|
// fields and nothing after it. It returns ErrBadRequest otherwise; the caller
|
|
// writes its own error body.
|
|
func DecodeJSON(w http.ResponseWriter, r *http.Request, dst any) error {
|
|
r.Body = http.MaxBytesReader(w, r.Body, MaxBodyBytes)
|
|
dec := json.NewDecoder(r.Body)
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(dst); err != nil {
|
|
return ErrBadRequest
|
|
}
|
|
if _, err := dec.Token(); !errors.Is(err, io.EOF) {
|
|
return ErrBadRequest
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var assetTypes = map[string]string{
|
|
".html": "text/html; charset=utf-8",
|
|
".js": "text/javascript; charset=utf-8",
|
|
".css": "text/css; charset=utf-8",
|
|
}
|
|
|
|
// ServeAsset serves one embedded page file from fsys. The files are small
|
|
// and fixed for a binary's lifetime, so a short public cache is safe; the
|
|
// page's token lives in the query string, which does not change the content.
|
|
func ServeAsset(fsys fs.FS, name string) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, _ *http.Request) {
|
|
body, err := fs.ReadFile(fsys, name)
|
|
if err != nil {
|
|
http.NotFound(w, nil)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", assetTypes[path.Ext(name)])
|
|
w.Header().Set("Cache-Control", "public, max-age=300")
|
|
_, _ = w.Write(body)
|
|
}
|
|
}
|