Files
tiennm99bot/internal/modules/misc/gacha_api_client.go
T
tiennm99 858eaa066b feat(misc): add /gacha Genshin-style wish picker
Options take an optional *4 or *5 rarity tag; untagged options are 3 stars.
A pick rolls a tier at Genshin base rates over the tiers present, then an
option uniformly within it, so untagged input matches /random. The wish
animation renders as MP4 on the wheelofnames service at /api/gacha, with a
text fallback when the renderer is unset or fails.
2026-10-01 13:02:46 +07:00

81 lines
2.1 KiB
Go

package misc
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/url"
"strings"
)
const (
gachaRemoteFPS = 24
gachaRemoteWidth = 640
gachaRemoteHeight = 360
gachaRemoteDuration = 7
)
type gachaAPIRequest struct {
Label string `json:"label"`
Rarity int `json:"rarity"`
FPS int `json:"fps"`
Width int `json:"width"`
}
// gachaAPIEndpoint derives the wish renderer from the configured wheel
// endpoint: the same service serves /api/gacha next to /api/gif, so the last
// path segment is swapped and no second URL needs configuring.
func gachaAPIEndpoint(rawURL string) (*url.URL, error) {
endpoint, err := wheelAPIEndpoint(rawURL)
if err != nil {
return nil, err
}
base := *endpoint
base.Path = strings.TrimSuffix(base.Path, "/")
base.RawPath = ""
return base.ResolveReference(&url.URL{Path: "gacha"}), nil
}
// RenderGacha returns the MP4 wish animation revealing label at rarity stars.
func (c wheelAPIClient) RenderGacha(ctx context.Context, label string, rarity int) ([]byte, error) {
endpoint, err := gachaAPIEndpoint(c.URL)
if err != nil {
return nil, err
}
if strings.TrimSpace(label) == "" {
return nil, fmt.Errorf("gacha api label empty")
}
if rarity < gachaMinRarity || rarity > gachaMaxRarity {
return nil, fmt.Errorf("gacha api rarity %d out of range", rarity)
}
body, err := json.Marshal(gachaAPIRequest{
Label: label,
Rarity: rarity,
FPS: gachaRemoteFPS,
Width: gachaRemoteWidth,
})
if err != nil {
return nil, fmt.Errorf("gacha api request encode failed: %w", err)
}
return c.post(ctx, endpoint, body, "video/mp4", isMP4)
}
// isMP4 checks for the ISO base media "ftyp" box that opens every MP4.
func isMP4(data []byte) bool {
return len(data) >= 8 && bytes.Equal(data[4:8], []byte("ftyp"))
}
func renderGachaAnimation(ctx context.Context, label string, rarity int) (wheelAnimation, error) {
data, err := newWheelAPIClientFromEnv().RenderGacha(ctx, label, rarity)
if err != nil {
return wheelAnimation{}, err
}
return wheelAnimation{
Data: data,
Duration: gachaRemoteDuration,
Width: gachaRemoteWidth,
Height: gachaRemoteHeight,
}, nil
}