Files
tiennm99 c0231ceadf feat(wordledaily): add the daily Wordle HTML5 game
/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.
2026-10-09 17:20:02 +07:00

50 lines
1.1 KiB
Go

package htmlgame
import (
"sync"
"time"
)
// Limiter allows at most Per requests per key within any Window (a sliding
// window). It is in-memory: a restart forgets it, which only resets the
// window. Keys with no request in the last Window are pruned as it runs.
type Limiter struct {
Per int
Window time.Duration
mu sync.Mutex
hits map[int64][]time.Time
pruned time.Time
}
// Allow records a request for key at now and reports whether it is within
// the limit. A refused request is not recorded.
func (l *Limiter) Allow(key int64, now time.Time) bool {
l.mu.Lock()
defer l.mu.Unlock()
if l.hits == nil {
l.hits = map[int64][]time.Time{}
}
cutoff := now.Add(-l.Window)
if now.Sub(l.pruned) >= l.Window {
for k, ts := range l.hits {
if len(ts) == 0 || !ts[len(ts)-1].After(cutoff) {
delete(l.hits, k)
}
}
l.pruned = now
}
recent := l.hits[key]
i := 0
for i < len(recent) && !recent[i].After(cutoff) {
i++
}
recent = recent[i:]
if len(recent) >= l.Per {
l.hits[key] = recent
return false
}
l.hits[key] = append(recent, now)
return true
}