diff --git a/providers/cainiao/cainiao_provider.go b/providers/cainiao/cainiao_provider.go new file mode 100644 index 0000000..a5583ff --- /dev/null +++ b/providers/cainiao/cainiao_provider.go @@ -0,0 +1,105 @@ +package cainiao + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "time" + + "github.com/alufers/paczkobot/commondata" + "github.com/alufers/paczkobot/commonerrors" +) + +type CainiaoProvider struct{} + +func (pp *CainiaoProvider) GetName() string { + return "cainiao" +} + +func (pp *CainiaoProvider) MatchesNumber(trackingNumber string) bool { + return true +} + +func (pp *CainiaoProvider) Track(ctx context.Context, trackingNumber string) (*commondata.TrackingData, error) { + + req, err := http.NewRequest( + "GET", + "https://global.cainiao.com/global/detail.json?mailNos="+url.QueryEscape(trackingNumber)+"&lang=en-US&language=en-US", + nil, + ) + if err != nil { + return nil, fmt.Errorf("failed to create GET request: %w", err) + } + commondata.SetCommonHTTPHeaders(&req.Header) + httpResponse, err := http.DefaultClient.Do(req) + if err != nil { + return nil, commonerrors.NewNetworkError(pp.GetName(), req) + } + + if httpResponse.StatusCode != 200 { + return nil, commonerrors.NotFoundError + } + + var cainiaoResponse CainiaoResponse + err = json.NewDecoder(httpResponse.Body).Decode(&cainiaoResponse) + if err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + if len(cainiaoResponse.Module) == 0 { + return nil, commonerrors.NotFoundError + } + module := cainiaoResponse.Module[0] + if len(module.DetailList) == 0 { + return nil, commonerrors.NotFoundError + } + + td := &commondata.TrackingData{ + ProviderName: pp.GetName(), + ShipmentNumber: trackingNumber, + TrackingSteps: make([]*commondata.TrackingStep, 0), + Destination: module.DestCountry, + SentFrom: module.OriginCountry, + } + + for _, detail := range module.DetailList { + + date, _ := time.Parse("2006-01-02 15:04:05", detail.TimeStr) + step := &commondata.TrackingStep{ + Datetime: date, + Message: detail.Desc, + Location: detail.Group.NodeDesc, + CommonType: commondata.CommonTrackingStepType_UNKNOWN, + } + td.TrackingSteps = append(td.TrackingSteps, step) + } + + // attempt to augement the destination with the city + // requires a separate request, if it fails, it's not a big deal + cityReq, err := http.NewRequest( + "GET", + "https://global.cainiao.com/global/getCity.json?mailNo="+url.QueryEscape(trackingNumber)+"&lang=en-US&language=en-US", + nil, + ) + if err != nil { + return td, nil + } + commondata.SetCommonHTTPHeaders(&cityReq.Header) + cityResp, err := http.DefaultClient.Do(cityReq) + if err != nil { + return td, nil + } + if cityResp.StatusCode != 200 { + return td, nil + } + var cityResponse GetCityResponse + err = json.NewDecoder(cityResp.Body).Decode(&cityResponse) + if err != nil || !cityResponse.Success { + return td, nil + } + + td.Destination = cityResponse.Module + ", " + td.Destination + + return td, nil +} diff --git a/providers/cainiao/json_schema.go b/providers/cainiao/json_schema.go new file mode 100644 index 0000000..9356f2b --- /dev/null +++ b/providers/cainiao/json_schema.go @@ -0,0 +1,80 @@ +package cainiao + +type CainiaoResponse struct { + Module []Module `json:"module"` + Success bool `json:"success"` +} + +type GetCityResponse struct { + Module string `json:"module"` + Success bool `json:"success"` +} + +type ProgressPointList struct { + PointName string `json:"pointName"` + Light bool `json:"light,omitempty"` + Reload bool `json:"reload,omitempty"` +} +type ProcessInfo struct { + ProgressStatus string `json:"progressStatus"` + ProgressRate float64 `json:"progressRate"` + Type string `json:"type"` + ProgressPointList []ProgressPointList `json:"progressPointList"` +} +type GlobalEtaInfo struct { + EtaDesc string `json:"etaDesc"` + DeliveryMinTime int64 `json:"deliveryMinTime"` + DeliveryMaxTime int64 `json:"deliveryMaxTime"` +} +type Group struct { + NodeCode string `json:"nodeCode"` + NodeDesc string `json:"nodeDesc"` + CurrentIconURL string `json:"currentIconUrl"` + HistoryIconURL string `json:"historyIconUrl"` +} +type GlobalCombinedLogisticsTraceDTO struct { + Time int64 `json:"time"` + TimeStr string `json:"timeStr"` + Desc string `json:"desc"` + StanderdDesc string `json:"standerdDesc"` + DescTitle string `json:"descTitle"` + TimeZone string `json:"timeZone"` + ActionCode string `json:"actionCode"` + Group Group `json:"group"` +} +type LatestTrace struct { + Time int64 `json:"time"` + TimeStr string `json:"timeStr"` + Desc string `json:"desc"` + StanderdDesc string `json:"standerdDesc"` + DescTitle string `json:"descTitle"` + TimeZone string `json:"timeZone"` + ActionCode string `json:"actionCode"` + Group Group `json:"group"` +} +type DetailListItem struct { + Time int64 `json:"time"` + TimeStr string `json:"timeStr"` + Desc string `json:"desc"` + StanderdDesc string `json:"standerdDesc"` + DescTitle string `json:"descTitle"` + TimeZone string `json:"timeZone"` + ActionCode string `json:"actionCode"` + Group Group `json:"group,omitempty"` +} +type Module struct { + MailNo string `json:"mailNo"` + OriginCountry string `json:"originCountry"` + DestCountry string `json:"destCountry"` + MailType string `json:"mailType"` + MailTypeDesc string `json:"mailTypeDesc"` + Status string `json:"status"` + StatusDesc string `json:"statusDesc"` + MailNoSource string `json:"mailNoSource"` + ProcessInfo ProcessInfo `json:"processInfo"` + GlobalEtaInfo GlobalEtaInfo `json:"globalEtaInfo"` + GlobalCombinedLogisticsTraceDTO GlobalCombinedLogisticsTraceDTO `json:"globalCombinedLogisticsTraceDTO"` + LatestTrace LatestTrace `json:"latestTrace"` + DetailList []DetailListItem `json:"detailList"` + DaysNumber string `json:"daysNumber"` +} diff --git a/providers/caniao/caniao_provider.go b/providers/caniao/caniao_provider.go deleted file mode 100644 index 3849c74..0000000 --- a/providers/caniao/caniao_provider.go +++ /dev/null @@ -1,90 +0,0 @@ -package caniao - -import ( - "context" - "encoding/json" - "fmt" - "log" - "net/http" - "net/url" - "strings" - "time" - - "github.com/PuerkitoBio/goquery" - "github.com/alufers/paczkobot/commondata" - "github.com/alufers/paczkobot/commonerrors" -) - -type CaniaoProvider struct{} - -func (pp *CaniaoProvider) GetName() string { - return "caniao" -} - -func (pp *CaniaoProvider) MatchesNumber(trackingNumber string) bool { - return true -} - -func (pp *CaniaoProvider) Track(ctx context.Context, trackingNumber string) (*commondata.TrackingData, error) { - requestData := url.Values{} - requestData.Set("barcodes", trackingNumber) - - req, err := http.NewRequest( - "GET", - "https://global.cainiao.com/detail.htm?mailNoList="+url.QueryEscape(trackingNumber), - nil, - ) - if err != nil { - return nil, fmt.Errorf("failed to create GET request: %w", err) - } - commondata.SetCommonHTTPHeaders(&req.Header) - httpResponse, err := http.DefaultClient.Do(req) - if err != nil { - return nil, commonerrors.NewNetworkError(pp.GetName(), req) - } - defer httpResponse.Body.Close() - - if httpResponse.StatusCode != 200 { - return nil, fmt.Errorf("HTTP status code %v", httpResponse.StatusCode) - } - doc, err := goquery.NewDocumentFromReader(httpResponse.Body) - if err != nil { - return nil, fmt.Errorf("failed to read HTML response from caniao: %w", err) - } - dataTextarea := doc.Find("textarea#waybill_list_val_box") - if strings.TrimSpace(dataTextarea.Text()) == "" { - return nil, fmt.Errorf("textarea #waybill_list_val_box not found in response") - } - var trackingData CaniaoJSONRoot - if err := json.Unmarshal([]byte(dataTextarea.Text()), &trackingData); err != nil { - log.Printf("caniao malformed JSON: %v", dataTextarea.Text()) - return nil, fmt.Errorf("failed to parse JSON: %w", err) - } - if len(trackingData.Data) <= 0 { - return nil, fmt.Errorf("len(trackingData.Data) <= 0") - } - if trackingData.Data[0].ErrorCode == "RESULT_EMPTY" || trackingData.Data[0].ErrorCode == "ORDER_NOT_FOUND" { - return nil, commonerrors.NotFoundError - } - - if !trackingData.Data[0].Success { - return nil, fmt.Errorf("caniao error: trackingData.Data[0].ErrorCode = %v", trackingData.Data[0].ErrorCode) - } - td := &commondata.TrackingData{ - ShipmentNumber: trackingNumber, - ProviderName: pp.GetName(), - Destination: trackingData.Data[0].DestCountry, - TrackingSteps: []*commondata.TrackingStep{}, - } - for _, d := range trackingData.Data[0].Section2.DetailList { - t, _ := time.Parse("2006-01-02 15:04:05", d.Time) - - td.TrackingSteps = append(td.TrackingSteps, &commondata.TrackingStep{ - Datetime: t, - CommonType: commondata.CommonTrackingStepType_UNKNOWN, - Message: d.Desc, - }) - } - - return td, nil -} diff --git a/providers/caniao/json_schema.go b/providers/caniao/json_schema.go deleted file mode 100644 index 033df49..0000000 --- a/providers/caniao/json_schema.go +++ /dev/null @@ -1,48 +0,0 @@ -package caniao - -type CaniaoJSONRoot struct { - Data []Data `json:"data"` - Success bool `json:"success"` - TimeSeconds float64 `json:"timeSeconds"` -} -type LatestTrackingInfo struct { - Desc string `json:"desc"` - Status string `json:"status"` - Time string `json:"time"` - TimeZone string `json:"timeZone"` -} -type Section1 struct { - CountryName string `json:"countryName"` - DetailList []interface{} `json:"detailList"` -} -type DetailList struct { - Desc string `json:"desc"` - Status string `json:"status"` - Time string `json:"time"` - TimeZone string `json:"timeZone"` -} -type Section2 struct { - CountryName string `json:"countryName"` - DetailList []DetailList `json:"detailList"` -} -type Data struct { - AllowRetry bool `json:"allowRetry"` - BizType string `json:"bizType"` - CachedTime string `json:"cachedTime"` - DestCountry string `json:"destCountry"` - DestCpList []interface{} `json:"destCpList"` - HasRefreshBtn bool `json:"hasRefreshBtn"` - LatestTrackingInfo LatestTrackingInfo `json:"latestTrackingInfo"` - MailNo string `json:"mailNo"` - OriginCountry string `json:"originCountry"` - OriginCpList []interface{} `json:"originCpList"` - Section1 Section1 `json:"section1"` - Section2 Section2 `json:"section2"` - ShippingTime float64 `json:"shippingTime"` - ShowEstimateTime bool `json:"showEstimateTime"` - Status string `json:"status"` - StatusDesc string `json:"statusDesc"` - Success bool `json:"success"` - ErrorCode string `json:"errorCode"` - SyncQuery bool `json:"syncQuery"` -} diff --git a/providers/provider.go b/providers/provider.go index 58cea35..d9e3036 100644 --- a/providers/provider.go +++ b/providers/provider.go @@ -4,6 +4,7 @@ import ( "context" "github.com/alufers/paczkobot/commondata" + "github.com/alufers/paczkobot/providers/cainiao" "github.com/alufers/paczkobot/providers/deutsche_post" "github.com/alufers/paczkobot/providers/dhl" "github.com/alufers/paczkobot/providers/dpdcompl" @@ -23,7 +24,7 @@ var AllProviders = []Provider{ &inpost.InpostProvider{}, &pocztapolska.PocztaPolskaProvider{}, &postnl.PostnlProvider{}, - // &caniao.CaniaoProvider{}, + &cainiao.CainiaoProvider{}, &dpdcompl.DpdComPlProvider{}, &ups.UPSProvider{}, &dhl.DHLProvider{},