trackchecker

module
v0.0.0-...-277a339 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 19, 2025 License: MIT

README

TrackChecker

deploy action

TrackChecker is an application for tracking parcels from various postal and courier services.

This application demonstrates working with GRPC, NATS, Kafka, RabbitMQ, Swagger, Docker, Docker Swarm, GitHub Actions, and other technologies in Go.

As an example, it also implements a mini-parser engine that accepts configurations for parsing simple websites/APIs returning XML, JSON, or HTML.

Learn more in the How It Works section
Table of Contents

Technologies Used


How to View in Action


The application is deployed to a server using GitHub Actions. Swagger documentation is available at trackchecker.1trackapp.com/docs

How to Run Locally

docker-compose up

If you need to modify /api/swagger.yaml and rebuild the HTTP server, install go-swagger and run the command to generate code:

swagger generate server --exclude-main -f ./api/swagger.yaml -t ./internal/app/restapi --exclude-main

Demo Client

The main application must be running before proceeding.

To run the demo, start the demo client with the following command:
go run ./cmd/client/main.go

Important! Many foreign websites restrict requests from Russian IP addresses. Therefore, if you run the demo client from a Russian IP, some results may be partially empty or canceled due to timeouts.

Demo Client Results


LE704280574SE found at sweden-post: 2023-12-06T17:06:00Z, The shipment item has been dropped off by sender
LE704280574SE found at global-track-trace: 2023-12-07T16:22:00Z, Departure from outward office of exchange, SGSINN
RK166520145LV found at global-track-trace: 2024-01-19T11:28:00Z, Departure from outward office of exchange, LVRIXF
EH036261918US found at global-track-trace: 2024-01-21T00:43:00Z, Departure from outward office of exchange, USLAXA
EH036261918US found at usps: 2024-01-31T16:22:00Z, Delivered
LE704180823SE found at global-track-trace: 2023-12-05T17:19:00Z, Departure from outward office of exchange, SGSINN
LE704180823SE found at sweden-post: 2023-12-04T17:00:00Z, The shipment item has been dropped off by sender
...

Russian Post timed out because it blocked my IP address for excessive requests. :)

Sample POST Request

___
curl --location 'http://localhost:7777/track' --header 'Content-Type: application/json' --data '{
    "tracking_numbers": [
        "LH256986182AU",
        "UD656337373MY"
    ]
}'

Sample Response

___
{
  "tracking_numbers": [
    {
      "code": "LH256986182AU",
      "uuid": "50047d80-1880-4071-953f-b3bec70c3a91"
    },
    {
      "code": "UD656337373MY",
      "uuid": "50047d80-1880-4071-953f-b3bec70c3a91"
    }
  ],
  "tracking_id": "4a89c077-20b7-447c-86b7-dc93582af6b2"
}

Sample GET Request for Results


curl --location 'http://localhost:7777/track?id=4a89c077-20b7-447c-86b7-dc93582af6b2'

Sample Response for Results

___
{
  "data": [
    {
      "code": "UD656337373MY",
      "id": "50047d80-1880-4071-953f-b3bec70c3a91",
      "results": [
        {
          "execute_time": 0.017348659,
          "result": {
            "CountryTo": "NA",
            "CountryFrom": "MY",
            "events": [
              {
                "status": "Departure from outward office of exchange",
                "date": "2024-01-03T14:18:00Z",
                "place": "MYKULC"
              }
            ]
          },
          "spider": "global-track-trace",
          "tracking_number": "UD656337373MY"
        },
        {
          "execute_time": 0.843198383,
          "result": {
            "events": [
              {
                "status": "Item Sent to Namibia",
                "date": "2023-12-29T10:21:31Z",
                "place": "In Transit"
              },
              {
                "status": "Item Posted Over The Counter to Namibia",
                "date": "2023-12-29T09:45:49Z",
                "place": "In Transit"
              },
              {
                "status": "Dispatch PreAlert to Namibia",
                "date": "2023-12-26T15:02:53Z",
                "place": "In Transit"
              }
            ]
          },
          "spider": "malaysia-post",
          "tracking_number": "UD656337373MY"
        }
      ],
      "status": "finish",
      "uuid": "ffd8c81b-9804-4622-9e21-48f15ae69e55"
    }
  ],
  "status": true
}

How It Works


  1. TrackChecker receives a request with a list of tracking numbers as an array of strings.
  2. Each tracking code is sent to the queue as a separate message. The current version uses NATS (JetStream) as the message broker.
  3. Another part of the application retrieves one tracking code at a time from the queue.
  4. The tracking code is checked with each parser matching a regular expression.
  5. Results are stored in HSET Redis.
  6. The client queries the results after some time.

Parsers


The parsers are structures that declaratively describe "how to parse," specifying a sequence of actions to perform HTTP requests and subsequently parse the document using:

Sample Declarative Description of USPS Parser

{
  "code":"usps",
  "masks": [
    "[A-Z]{2}[0-9]{9}US"
  ],
  "examples": [
    "EH036261918US"
  ],
  "tasks":[
    {
      "type":"request",
      "payload":"http://production.shippingapis.com/ShippingApi.dll?API=TrackV2&XML=%3CTrackFieldRequest%20USERID=%22707HGUPS0501%22%3E%3CTrackID%20ID=%22[track]%22/%3E%3C/TrackFieldRequest%3E",
      "params":{
        "method":"GET",
        "type":"xml"
      }
    },
    {
      "type":"query",
      "payload":"//TrackSummary|//TrackDetail",
      "field":{
        "path":"events",
        "type":"array",
        "element":{
          "type":"object",
          "object":[
            {
              "path":"status",
              "query":".//Event"
            },
            {
              "path":"date",
              "query":"concat(./EventDate,' ', ./EventTime)"
            },
            {
              "path":"place",
              "query":"concat(./EventCity,', ', ./EventState, ' ', ./EventZIPCode)"
            },
            {
              "path":"country",
              "query":"./EventCountry"
            }
          ]
        }
      }
    }
  ]
}

Sample Declarative Description of CDEK Parser (Go)

cdek := models.Spider{
		Scraper: scraper.Scraper{
			Code: "cdek",
			Tasks: []scraper.Task{
				{
					Type:    scraper.TaskTypeRequest,
					Payload: `https://mobile-apps.cdek.ru/api/v2/order/[track]`,
					Params: map[string]interface{}{
						"type":   scraper.JSONXpath,
						"method": "GET",
						"headers": map[string]string{
							"User-Agent":      "CDEK/2.5 (com.cdek.cdekapp; build:1; iOS 13.3.1) Alamofire/4.9.1",
							"Accept-Language": "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7",
							"X-User-Lang":     "ru",
						},
					},
				},
				{
					Type:    scraper.TaskTypeQuery,
					Payload: `concat(//office/city/name,', ', //office/address)`,
					Field: scraper.Field{
						Path: "AddressTo",
					},
				},
				{
					Type:    scraper.TaskTypeQuery,
					Payload: `//office/latitude`,
					Field: scraper.Field{
						Path: "delivery_office.latitude",
					},
				},
				{
					Type:    scraper.TaskTypeQuery,
					Payload: `//office/longitude`,
					Field: scraper.Field{
						Path: "delivery_office.longitude",
					},
				},
				{
					Type:    scraper.TaskTypeQuery,
					Payload: `//realWeight`,
					Field: scraper.Field{
						Path: "Weight",
					},
				},
				{
					Type:    scraper.TaskTypeQuery,
					Payload: `//additionalInfo//goods/*/name`,
					Field: scraper.Field{
						Path: "Goods",
						Type: scraper.FieldTypeArray,
					},
				},
				{
					Type:    scraper.TaskTypeQuery,
					Payload: `(//orderStatusGroups/* | //orderStatusGroups//statuses/*)`,
					Field: scraper.Field{
						Path: "events",
						Type: scraper.FieldTypeArray,
						Element: &scraper.Field{
							Type: scraper.FieldTypeObject,
							Object: []*scraper.Field{
								{
									Path:  "status",
									Query: "./title",
								},
								{
									Path:  "date",
									Query: "./date",
									Transformers: []transform.Transformer{
										{
											Type: transform.TypeReplaceRegexp,
											Params: map[string]string{
												"regexp": "(\\d+)\\.(\\d+)\\.(\\d+)",
												"new":    "$3-$2-$1",
											},
										},
										{
											Type: transform.TypeDate,
										},
									},
								},
								{
									Path:  "place",
									Query: "../../city",
								},
							},
						},
					},
				},
			},
		},
		RegexpMasks: []*regexp.Regexp{
			regexp.MustCompile(`^(\d{10})$`),
			regexp.MustCompile(`^CN[A-Z0-9]+RU[0-9]{1}$`),
		},
	}

Added parsers (postal services)


  • Russian post
  • USPS (USA post)
  • New Zealand post
  • South Korea
  • Malaysia post
  • DPD Poland
  • Global Track&Trace
  • Sweden Post
  • CDEK (Express courier)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL