// Package plicflix troca o token do plicflix pelo jogador, no SEU servidor Go. // Só biblioteca padrão: copie este arquivo para o seu projeto (docs/SDK.md). // // p := plicflix.Novo(os.Getenv("PLICFLIX_CHAVE"), "") // "" = API oficial // r, err := p.Trocar(ctx, token) // r.Jogador.ID, r.Jogador.Nome // // O token vale 5 minutos ou UMA troca. // // "Entrar com Plicflix" fora do app (OAuth 2.0 + PKCE — /docs/OAUTH): // // pk := plicflix.NovoPKCE() // guarde pk.Verificador e pk.Estado num cookie curto // http.Redirect(w, r, plicflix.URLDeEntrada("meu-jogo", volta, pk), http.StatusFound) // // na volta: confira r.URL.Query().Get("state") == estado e // e, err := p.TrocarCodigo(ctx, "meu-jogo", r.URL.Query().Get("code"), volta, verificador) // e.Jogador.ID package plicflix import ( "bytes" "context" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/json" "errors" "fmt" "net/http" "net/url" "os" "strings" "time" ) // APIPadrao é o endereço da API do plicflix. const APIPadrao = "https://api.plicflix.com" // ErrToken: o token não vale (inválido, vencido ou já trocado). var ErrToken = errors.New("plicflix: token inválido, vencido ou já usado") // Troca é a resposta: tudo o que o plicflix entrega ao seu jogo. type Troca struct { Jogador struct { ID string `json:"id"` // "pf_…": fixo para esse jogador no seu jogo Nome string `json:"nome"` } `json:"jogador"` Jogo string `json:"jogo"` Partida string `json:"partida"` Assinante bool `json:"assinante"` Contexto struct { Ref string `json:"ref"` Idioma string `json:"idioma"` Plataforma string `json:"plataforma"` } `json:"contexto"` } // Cliente da troca. type Cliente struct { chave, api string http *http.Client } // Novo cliente com a chave do parceiro (pk_…). func Novo(chave, api string) *Cliente { if api == "" { api = APIPadrao } return &Cliente{chave: chave, api: strings.TrimRight(api, "/"), http: &http.Client{Timeout: 8 * time.Second}} } // DoAmbiente: o cliente com PLICFLIX_CHAVE e PLICFLIX_API (vazio: a API de produção). func DoAmbiente() *Cliente { return Novo(os.Getenv("PLICFLIX_CHAVE"), os.Getenv("PLICFLIX_API")) } // Trocar o token pelo jogador. func (c *Cliente) Trocar(ctx context.Context, token string) (Troca, error) { var t Troca if !strings.HasPrefix(token, "pft_") { return t, ErrToken } corpo, _ := json.Marshal(map[string]string{"token": token}) req, _ := http.NewRequestWithContext(ctx, http.MethodPost, c.api+"/v1/ingresso/trocar", bytes.NewReader(corpo)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+c.chave) resp, err := c.http.Do(req) if err != nil { return t, err } defer resp.Body.Close() if resp.StatusCode == http.StatusUnauthorized { return t, ErrToken } if resp.StatusCode != http.StatusOK { return t, fmt.Errorf("plicflix: troca respondeu %d", resp.StatusCode) } return t, json.NewDecoder(resp.Body).Decode(&t) } // ---------------------------------------------------------------- "Entrar com Plicflix" (OAuth 2.0 + PKCE) // AppPadrao: onde fica a página de autorizar. const AppPadrao = "https://plicflix.com" // ErrCodigo: o código da volta não vale (inválido, vencido, usado, verificador ou volta errados). var ErrCodigo = errors.New("plicflix: código inválido, vencido ou já usado") // PKCE: o par S256 e o state. type PKCE struct{ Verificador, Desafio, Estado string } func aleatorio(n int) string { b := make([]byte, n) _, _ = rand.Read(b) return base64.RawURLEncoding.EncodeToString(b) } // NovoPKCE: um par novo. func NovoPKCE() PKCE { v := aleatorio(48) h := sha256.Sum256([]byte(v)) return PKCE{Verificador: v, Desafio: base64.RawURLEncoding.EncodeToString(h[:]), Estado: aleatorio(16)} } // URLDeEntrada: para onde mandar o navegador (cliente = o slug do jogo). func URLDeEntrada(cliente, volta string, p PKCE) string { app := os.Getenv("PLICFLIX_APP") if app == "" { app = AppPadrao } q := url.Values{"response_type": {"code"}, "client_id": {cliente}, "redirect_uri": {volta}, "state": {p.Estado}, "code_challenge": {p.Desafio}, "code_challenge_method": {"S256"}, "scope": {"perfil"}} return strings.TrimRight(app, "/") + "/oauth/authorize?" + q.Encode() } // Entrada: a resposta do /v1/oauth/token. type Entrada struct { Jogador struct { ID string `json:"id"` // o mesmo "pf_…" da entrada pelo app Nome string `json:"nome"` } `json:"jogador"` Jogo string `json:"jogo"` Assinante bool `json:"assinante"` AccessToken string `json:"access_token"` ExpiresIn int `json:"expires_in"` } // TrocarCodigo: o código da volta pelo jogador (com a chave do estúdio). func (c *Cliente) TrocarCodigo(ctx context.Context, cliente, codigo, volta, verificador string) (Entrada, error) { var e Entrada if !strings.HasPrefix(codigo, "pfc_") { return e, ErrCodigo } corpo := url.Values{"grant_type": {"authorization_code"}, "code": {codigo}, "redirect_uri": {volta}, "client_id": {cliente}, "code_verifier": {verificador}} req, _ := http.NewRequestWithContext(ctx, http.MethodPost, c.api+"/v1/oauth/token", strings.NewReader(corpo.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") if c.chave != "" { req.Header.Set("Authorization", "Bearer "+c.chave) } resp, err := c.http.Do(req) if err != nil { return e, err } defer resp.Body.Close() if resp.StatusCode == http.StatusBadRequest { return e, ErrCodigo } if resp.StatusCode != http.StatusOK { return e, fmt.Errorf("plicflix: token respondeu %d", resp.StatusCode) } return e, json.NewDecoder(resp.Body).Decode(&e) }