Created
This commit is contained in:
335
woo.go
Normal file
335
woo.go
Normal file
@@ -0,0 +1,335 @@
|
||||
// package woogo is a Woo Commerce lib.
|
||||
//
|
||||
// Quick start:
|
||||
//
|
||||
// b, err := os.ReadFile("./config/config_test.json")
|
||||
// if err != nil {
|
||||
// panic(fmt.Sprintf("Read config error: %s", err.Error()))
|
||||
// }
|
||||
// var c config.Config
|
||||
// err = jsoniter.Unmarshal(b, &c)
|
||||
// if err != nil {
|
||||
// panic(fmt.Sprintf("Parse config file error: %s", err.Error()))
|
||||
// }
|
||||
//
|
||||
// wooClient = NewClient(c)
|
||||
// // Query an order
|
||||
// order, err := wooClient.Services.Order.One(1)
|
||||
// if err != nil {
|
||||
// fmt.Println(err)
|
||||
// } else {
|
||||
// fmt.Println(fmt.Sprintf("%#v", order))
|
||||
// }
|
||||
package woogo
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"git.cloudyne.io/go/hiscaler-gox/inx"
|
||||
"git.cloudyne.io/go/hiscaler-gox/stringx"
|
||||
"git.cloudyne.io/go/woogo/config"
|
||||
"github.com/go-resty/resty/v2"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/json-iterator/go/extra"
|
||||
)
|
||||
|
||||
const (
|
||||
Version = "1.0.3"
|
||||
UserAgent = "WooCommerce API Client-Golang/" + Version
|
||||
HashAlgorithm = "HMAC-SHA256"
|
||||
)
|
||||
|
||||
// https://woocommerce.github.io/woocommerce-rest-api-docs/?php#request-response-format
|
||||
const (
|
||||
BadRequestError = 400 // 错误的请求
|
||||
UnauthorizedError = 401 // 身份验证或权限错误
|
||||
NotFoundError = 404 // 访问资源不存在
|
||||
InternalServerError = 500 // 服务器内部错误
|
||||
MethodNotImplementedError = 501 // 方法未实现
|
||||
)
|
||||
|
||||
var ErrNotFound = errors.New("WooCommerce: not found")
|
||||
|
||||
func init() {
|
||||
extra.RegisterFuzzyDecoders()
|
||||
}
|
||||
|
||||
type WooCommerce struct {
|
||||
Debug bool // Is debug mode
|
||||
Logger *log.Logger // Log
|
||||
Services services // WooCommerce API services
|
||||
}
|
||||
|
||||
type service struct {
|
||||
debug bool // Is debug mode
|
||||
logger *log.Logger // Log
|
||||
httpClient *resty.Client // HTTP client
|
||||
}
|
||||
|
||||
type services struct {
|
||||
Coupon couponService
|
||||
Customer customerService
|
||||
Order orderService
|
||||
OrderNote orderNoteService
|
||||
OrderRefund orderRefundService
|
||||
Product productService
|
||||
ProductVariation productVariationService
|
||||
ProductAttribute productAttributeService
|
||||
ProductAttributeTerm productAttributeTermService
|
||||
ProductCategory productCategoryService
|
||||
ProductShippingClass productShippingClassService
|
||||
ProductTag productTagService
|
||||
ProductReview productReviewService
|
||||
Report reportService
|
||||
TaxRate taxRateService
|
||||
TaxClass taxClassService
|
||||
Webhook webhookService
|
||||
Setting settingService
|
||||
SettingOption settingOptionService
|
||||
PaymentGateway paymentGatewayService
|
||||
ShippingZone shippingZoneService
|
||||
ShippingZoneLocation shippingZoneLocationService
|
||||
ShippingZoneMethod shippingZoneMethodService
|
||||
ShippingMethod shippingMethodService
|
||||
SystemStatus systemStatusService
|
||||
SystemStatusTool systemStatusToolService
|
||||
Data dataService
|
||||
}
|
||||
|
||||
// OAuth signature
|
||||
func oauthSignature(config config.Config, method, endpoint string, params url.Values) string {
|
||||
sb := strings.Builder{}
|
||||
sb.WriteString(config.ConsumerSecret)
|
||||
if config.Version != "v1" && config.Version != "v2" {
|
||||
sb.WriteByte('&')
|
||||
}
|
||||
consumerSecret := sb.String()
|
||||
|
||||
sb.Reset()
|
||||
sb.WriteString(method)
|
||||
sb.WriteByte('&')
|
||||
sb.WriteString(url.QueryEscape(endpoint))
|
||||
sb.WriteByte('&')
|
||||
sb.WriteString(url.QueryEscape(params.Encode()))
|
||||
mac := hmac.New(sha256.New, stringx.ToBytes(consumerSecret))
|
||||
mac.Write(stringx.ToBytes(sb.String()))
|
||||
signatureBytes := mac.Sum(nil)
|
||||
return base64.StdEncoding.EncodeToString(signatureBytes)
|
||||
}
|
||||
|
||||
// NewClient Creates a new WooCommerce client
|
||||
//
|
||||
// You must give a config with NewClient method params.
|
||||
// After you can operate data use this client.
|
||||
func NewClient(config config.Config) *WooCommerce {
|
||||
logger := log.New(os.Stdout, "[ WooCommerce ] ", log.LstdFlags|log.Llongfile)
|
||||
wooClient := &WooCommerce{
|
||||
Debug: config.Debug,
|
||||
Logger: logger,
|
||||
}
|
||||
// Add default value
|
||||
if config.Version == "" {
|
||||
config.Version = "v3"
|
||||
} else {
|
||||
config.Version = strings.ToLower(config.Version)
|
||||
if !inx.StringIn(config.Version, "v1", "v2", "v3") {
|
||||
config.Version = "v3"
|
||||
}
|
||||
}
|
||||
if config.Timeout < 2 {
|
||||
config.Timeout = 2
|
||||
}
|
||||
|
||||
httpClient := resty.New().
|
||||
SetDebug(config.Debug).
|
||||
SetBaseURL(strings.TrimRight(config.URL, "/") + "/wp-json/wc/" + config.Version).
|
||||
SetHeaders(map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"User-Agent": UserAgent,
|
||||
}).
|
||||
SetAllowGetMethodPayload(true).
|
||||
SetTimeout(config.Timeout * time.Second).
|
||||
SetTransport(&http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: !config.VerifySSL},
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: config.Timeout * time.Second,
|
||||
}).DialContext,
|
||||
}).
|
||||
OnBeforeRequest(func(client *resty.Client, request *resty.Request) error {
|
||||
params := url.Values{}
|
||||
for k, vs := range request.QueryParam {
|
||||
var v string
|
||||
switch len(vs) {
|
||||
case 0:
|
||||
continue
|
||||
case 1:
|
||||
v = vs[0]
|
||||
default:
|
||||
// if is array params, must convert to string, example: status=1&status=2 replace to status=1,2
|
||||
v = strings.Join(vs, ",")
|
||||
}
|
||||
params.Set(k, v)
|
||||
}
|
||||
if strings.HasPrefix(config.URL, "https") {
|
||||
// basicAuth
|
||||
if config.AddAuthenticationToURL {
|
||||
params.Add("consumer_key", config.ConsumerKey)
|
||||
params.Add("consumer_secret", config.ConsumerSecret)
|
||||
} else {
|
||||
// Set to header
|
||||
client.SetAuthScheme("Basic").
|
||||
SetAuthToken(fmt.Sprintf("%s %s", config.ConsumerKey, config.ConsumerSecret))
|
||||
}
|
||||
} else {
|
||||
// oAuth
|
||||
params.Add("oauth_consumer_key", config.ConsumerKey)
|
||||
params.Add("oauth_timestamp", strconv.Itoa(int(time.Now().Unix())))
|
||||
nonce := make([]byte, 16)
|
||||
rand.Read(nonce)
|
||||
sha1Nonce := fmt.Sprintf("%x", sha1.Sum(nonce))
|
||||
params.Add("oauth_nonce", sha1Nonce)
|
||||
params.Add("oauth_signature_method", HashAlgorithm)
|
||||
params.Add("oauth_signature", oauthSignature(config, request.Method, client.BaseURL+request.URL, params))
|
||||
}
|
||||
request.QueryParam = params
|
||||
return nil
|
||||
}).
|
||||
OnAfterResponse(func(client *resty.Client, response *resty.Response) (err error) {
|
||||
if response.IsError() {
|
||||
r := struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}{}
|
||||
if err = jsoniter.Unmarshal(response.Body(), &r); err == nil {
|
||||
err = ErrorWrap(response.StatusCode(), r.Message)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
logger.Printf("OnAfterResponse error: %s", err.Error())
|
||||
}
|
||||
return
|
||||
})
|
||||
if config.Debug {
|
||||
httpClient.EnableTrace()
|
||||
}
|
||||
|
||||
jsoniter.RegisterTypeDecoderFunc("float64", func(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
|
||||
switch iter.WhatIsNext() {
|
||||
case jsoniter.StringValue:
|
||||
var t float64
|
||||
v := strings.TrimSpace(iter.ReadString())
|
||||
if v != "" {
|
||||
var err error
|
||||
if t, err = strconv.ParseFloat(v, 64); err != nil {
|
||||
iter.Error = err
|
||||
return
|
||||
}
|
||||
}
|
||||
*((*float64)(ptr)) = t
|
||||
default:
|
||||
*((*float64)(ptr)) = iter.ReadFloat64()
|
||||
}
|
||||
})
|
||||
httpClient.JSONMarshal = jsoniter.Marshal
|
||||
httpClient.JSONUnmarshal = jsoniter.Unmarshal
|
||||
xService := service{
|
||||
debug: config.Debug,
|
||||
logger: logger,
|
||||
httpClient: httpClient,
|
||||
}
|
||||
wooClient.Services = services{
|
||||
Coupon: (couponService)(xService),
|
||||
Customer: (customerService)(xService),
|
||||
Order: (orderService)(xService),
|
||||
OrderNote: (orderNoteService)(xService),
|
||||
OrderRefund: (orderRefundService)(xService),
|
||||
Product: (productService)(xService),
|
||||
ProductVariation: (productVariationService)(xService),
|
||||
ProductAttribute: (productAttributeService)(xService),
|
||||
ProductAttributeTerm: (productAttributeTermService)(xService),
|
||||
ProductCategory: (productCategoryService)(xService),
|
||||
ProductShippingClass: (productShippingClassService)(xService),
|
||||
ProductTag: (productTagService)(xService),
|
||||
ProductReview: (productReviewService)(xService),
|
||||
Report: (reportService)(xService),
|
||||
TaxRate: (taxRateService)(xService),
|
||||
TaxClass: (taxClassService)(xService),
|
||||
Webhook: (webhookService)(xService),
|
||||
Setting: (settingService)(xService),
|
||||
SettingOption: (settingOptionService)(xService),
|
||||
PaymentGateway: (paymentGatewayService)(xService),
|
||||
ShippingZone: (shippingZoneService)(xService),
|
||||
ShippingZoneLocation: (shippingZoneLocationService)(xService),
|
||||
ShippingZoneMethod: (shippingZoneMethodService)(xService),
|
||||
ShippingMethod: (shippingMethodService)(xService),
|
||||
SystemStatus: (systemStatusService)(xService),
|
||||
SystemStatusTool: (systemStatusToolService)(xService),
|
||||
Data: (dataService)(xService),
|
||||
}
|
||||
return wooClient
|
||||
}
|
||||
|
||||
// Parse response header, get total and total pages, and check it is last page
|
||||
func parseResponseTotal(currentPage int, resp *resty.Response) (total, totalPages int, isLastPage bool) {
|
||||
if currentPage == 0 {
|
||||
currentPage = 1
|
||||
}
|
||||
value := resp.Header().Get("X-Wp-Total")
|
||||
if value != "" {
|
||||
total, _ = strconv.Atoi(value)
|
||||
}
|
||||
|
||||
value = resp.Header().Get("X-Wp-Totalpages")
|
||||
if value != "" {
|
||||
totalPages, _ = strconv.Atoi(value)
|
||||
}
|
||||
isLastPage = currentPage >= totalPages
|
||||
return
|
||||
}
|
||||
|
||||
// ErrorWrap wrap an error, if status code is 200, return nil, otherwise return an error
|
||||
func ErrorWrap(code int, message string) error {
|
||||
if code == http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
|
||||
if code == NotFoundError {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
message = strings.TrimSpace(message)
|
||||
if message == "" {
|
||||
switch code {
|
||||
case BadRequestError:
|
||||
message = "Bad request"
|
||||
case UnauthorizedError:
|
||||
message = "Unauthorized operation, please confirm whether you have permission"
|
||||
case NotFoundError:
|
||||
message = "Resource not found"
|
||||
case InternalServerError:
|
||||
message = "Server internal error"
|
||||
case MethodNotImplementedError:
|
||||
message = "method not implemented"
|
||||
default:
|
||||
message = "Unknown error"
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%d: %s", code, message)
|
||||
}
|
||||
Reference in New Issue
Block a user