package google import ( scn "blackforestbytes.com/simplecloudnotifier" "context" "encoding/json" "errors" "fmt" "github.com/rs/zerolog/log" "io" "net/http" "strings" "time" ) // https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.products/get // https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.products#ProductPurchase type AndroidPublisher struct { client http.Client auth *GoogleOAuth2 baseURL string } func NewAndroidPublisherAPI(conf scn.Config) (AndroidPublisherClient, error) { pkey := strings.ReplaceAll(conf.GoogleAPIPrivateKey, "\\n", "\n") googauth, err := NewAuth(conf.GoogleAPITokenURI, conf.GoogleAPIPrivKeyID, conf.GoogleAPIClientMail, pkey) if err != nil { return nil, err } return &AndroidPublisher{ client: http.Client{Timeout: 5 * time.Second}, auth: googauth, baseURL: "https://androidpublisher.googleapis.com/androidpublisher", }, nil } type PurchaseType int //@enum:type const ( PurchaseTypeTest PurchaseType = 0 // i.e. purchased from a license testing account PurchaseTypePromo PurchaseType = 1 // i.e. purchased using a promo code PurchaseTypeRewarded PurchaseType = 2 // i.e. from watching a video ad instead of paying ) type ConsumptionState int //@enum:type const ( ConsumptionStateYetToBeConsumed ConsumptionState = 0 ConsumptionStateConsumed ConsumptionState = 1 ) type PurchaseState int //@enum:type const ( PurchaseStatePurchased PurchaseState = 0 PurchaseStateCanceled PurchaseState = 1 PurchaseStatePending PurchaseState = 2 ) type AcknowledgementState int //@enum:type const ( AcknowledgementStateYetToBeAcknowledged AcknowledgementState = 0 AcknowledgementStateAcknowledged AcknowledgementState = 1 ) type ProductPurchase struct { Kind string `json:"kind"` PurchaseTimeMillis string `json:"purchaseTimeMillis"` PurchaseState *PurchaseState `json:"purchaseState"` ConsumptionState ConsumptionState `json:"consumptionState"` DeveloperPayload string `json:"developerPayload"` OrderId string `json:"orderId"` PurchaseType *PurchaseType `json:"purchaseType"` AcknowledgementState AcknowledgementState `json:"acknowledgementState"` PurchaseToken *string `json:"purchaseToken"` ProductId *string `json:"productId"` Quantity *int `json:"quantity"` ObfuscatedExternalAccountId string `json:"obfuscatedExternalAccountId"` ObfuscatedExternalProfileId string `json:"obfuscatedExternalProfileId"` RegionCode string `json:"regionCode"` } type apiError struct { Code int `json:"code"` Message string `json:"message"` } func (ap AndroidPublisher) GetProductPurchase(ctx context.Context, packageName string, productId string, token string) (*ProductPurchase, error) { uri := fmt.Sprintf("%s/v3/applications/%s/purchases/products/%s/tokens/%s", ap.baseURL, packageName, productId, token) request, err := http.NewRequestWithContext(ctx, "GET", uri, nil) if err != nil { return nil, err } tok, err := ap.auth.Token(ctx) if err != nil { log.Err(err).Msg("Refreshing FB token failed") return nil, err } request.Header.Set("Authorization", "Bearer "+tok) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") response, err := ap.client.Do(request) if err != nil { return nil, err } defer func() { _ = response.Body.Close() }() respBodyBin, err := io.ReadAll(response.Body) if err != nil { return nil, err } if response.StatusCode == 400 { var errBody struct { Error apiError `json:"error"` } if err := json.Unmarshal(respBodyBin, &errBody); err != nil { return nil, err } if errBody.Error.Code == 400 { return nil, nil // probably token not found } } if response.StatusCode < 200 || response.StatusCode >= 300 { if bstr, err := io.ReadAll(response.Body); err == nil { return nil, errors.New(fmt.Sprintf("GetProducts-Request returned %d: %s", response.StatusCode, string(bstr))) } else { return nil, errors.New(fmt.Sprintf("GetProducts-Request returned %d", response.StatusCode)) } } var respBody ProductPurchase if err := json.Unmarshal(respBodyBin, &respBody); err != nil { return nil, err } if respBody.Kind != "androidpublisher#productPurchase" { return nil, errors.New(fmt.Sprintf("Invalid ProductPurchase.kind: '%s'", respBody.Kind)) } return &respBody, nil }