mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 17:02:42 +00:00
The game-side half of Mischief Makers M3. gogobee polls Pete for storefront orders and opens the contract itself — the money, the eligibility, the fight are all its own, exactly as a Matrix !mischief buy. - roster push now carries each buyer's advisory euro balance (keyed by localpart, a separate keyspace from the anonymous board token) and the live tier catalog, so the storefront renders gogobee's current prices and never hardcodes one - placeWebMischief: the fulfilment path, debit-first-then-refund so the money state is a pure function of the order guid. DebitIdem + an order_guid stamp on the contract make a retried claim neither double-charge nor double-open - resolveRosterToken recomputes the one-way board token per live player to name a mark; the buyer is @<username>:<homeserver> from Authentik's preferred_username - a 30s poll loop drives it; a lost verdict just leaves the order pending to re-run, so the loop is its own retry and needs no durable queue - euro.HasExternalTx lets the affordability gate tell a first attempt (no debt for a mischief buy, like Matrix) from a retry of one already paid order_guid column added to mischief_contracts (schema + idempotent ALTER).
87 lines
3.1 KiB
Go
87 lines
3.1 KiB
Go
package plugin
|
|
|
|
// The mischief storefront's game-side loop.
|
|
//
|
|
// A buyer places a hit on Pete's web board; Pete records the intent and waits.
|
|
// We poll for those orders, do the real work against our own ledger — the money,
|
|
// the eligibility, the contract — and hand back a verdict Pete files against the
|
|
// order. Pete has no route into this box, which is why the traffic runs this way:
|
|
// we ask for work, we don't get told about it.
|
|
//
|
|
// The order guid is the idempotency key end to end (see placeWebMischief), so
|
|
// every step here is safe to repeat. That is the whole reason the loop can be
|
|
// this simple: a failed claim just leaves the order pending, and the next tick
|
|
// re-runs it as a no-op. The loop is its own retry, and needs no durable queue.
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"gogobee/internal/peteclient"
|
|
)
|
|
|
|
const (
|
|
mischiefPollInterval = 30 * time.Second
|
|
mischiefPollTimeout = 20 * time.Second
|
|
)
|
|
|
|
// peteMischiefTicker polls Pete for storefront orders and fulfils them. Started
|
|
// alongside the other adventure tickers; exits on stopCh.
|
|
func (p *AdventurePlugin) peteMischiefTicker() {
|
|
if !peteclient.Enabled() {
|
|
return // no Pete wire configured; the storefront half is simply off
|
|
}
|
|
ticker := time.NewTicker(mischiefPollInterval)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-p.stopCh:
|
|
return
|
|
case <-ticker.C:
|
|
p.pollMischiefOrders()
|
|
}
|
|
}
|
|
}
|
|
|
|
// pollMischiefOrders fetches the pending orders and fulfils each in turn.
|
|
func (p *AdventurePlugin) pollMischiefOrders() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), mischiefPollTimeout)
|
|
defer cancel()
|
|
|
|
orders, err := peteclient.PendingMischief(ctx)
|
|
if err != nil {
|
|
// A Pete that predates the storefront answers 404 here; a wire blip is the
|
|
// same. Either way there is nothing to do this tick, and the next one tries
|
|
// again. Debug, not warn — this must be quiet when Pete simply hasn't
|
|
// shipped the endpoint yet.
|
|
slog.Debug("mischief: poll failed", "err", err)
|
|
return
|
|
}
|
|
|
|
for _, order := range orders {
|
|
p.fulfilMischiefOrder(ctx, order)
|
|
}
|
|
}
|
|
|
|
// fulfilMischiefOrder places one order's contract and files the verdict. A
|
|
// transient failure (Retry) is left pending for the next poll; a real verdict is
|
|
// pushed back so the buyer sees why. A failed claim-push is not fatal — the order
|
|
// stays pending and the next poll re-runs it, which the guid makes a no-op.
|
|
func (p *AdventurePlugin) fulfilMischiefOrder(ctx context.Context, order peteclient.MischiefOrder) {
|
|
res := p.placeWebMischief(order)
|
|
if res.Retry {
|
|
return // leave it pending; try again next tick
|
|
}
|
|
if err := peteclient.ClaimMischief(ctx, order.GUID, res.Status, res.Detail); err != nil {
|
|
// The contract is placed (or the buyer refunded) on our side, but Pete
|
|
// hasn't heard the verdict. It stays pending there and we'll re-offer it;
|
|
// placeWebMischief will short-circuit on the stamped order guid and we'll
|
|
// re-file. So this is a warn, not a lost order.
|
|
slog.Warn("mischief: claim verdict push failed, will re-file next poll",
|
|
"order", order.GUID, "status", res.Status, "err", err)
|
|
return
|
|
}
|
|
slog.Info("mischief: web order fulfilled", "order", order.GUID, "status", res.Status)
|
|
}
|