Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Maciej Leśniewski committed Apr 15, 2022
1 parent df98759 commit cca1a21
Show file tree
Hide file tree
Showing 5 changed files with 350 additions and 1 deletion.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
.vscode/
inpost-air
inpost-air.config.json

# Binaries for programs and plugins
*.exe
*.exe~
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
# inpost-air
# inpost-air
Get air sesnor data from Paczkomat.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/leshniak/inpost-air

go 1.18
263 changes: 263 additions & 0 deletions inpost_api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
package main

import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"strings"
"time"
)

type InPostAPI struct {
configFilePath string
authToken string
refreshToken string
client *http.Client
baseURL *url.URL
}

type APIError struct {
Status int
Error string
Description string
}

type Pollutant struct {
Value float32
Percent float32
}

type Point struct {
Name string
AirSensor bool
AirSensorData struct {
AirQuality string
Weather struct {
Temperature float32
Pressure float32
Humidity float32
}
Pollutants struct {
Pm10 Pollutant
Pm25 Pollutant
}
UpdatedUntil time.Time
}
}

type Config struct {
RefreshToken string `json:"refreshToken"`
AuthToken string `json:"authToken"`
}

func NewInPostAPI(configFilePath string) *InPostAPI {
inpost := new(InPostAPI)
inpost.client = &http.Client{Timeout: 10 * time.Second}
inpost.baseURL = &url.URL{
Scheme: "https",
Host: "api-inmobile-pl.easypack24.net",
}
inpost.configFilePath = configFilePath
inpost.ReadConfig()

return inpost
}

func (inpost *InPostAPI) GetPoint(pointId string) (*Point, error) {
if !inpost.isAuthTokenValid() && inpost.refreshToken != "" {
err := inpost.Authenticate()
if err != nil {
return nil, err
}
}

endpoint := url.URL{Path: fmt.Sprintf("/v2/points/%s", pointId)}
response, body := inpost.request("GET", &endpoint, nil)

if response.StatusCode != http.StatusOK {
apiErr := APIError{}
json.Unmarshal(body, &apiErr)

if apiErr.Error == "tokenExpiredException" {
err := inpost.Authenticate()
if err != nil {
return nil, err
}

return inpost.GetPoint(pointId)
}

return nil, errors.New(fmt.Sprintf("[%d] %s", response.StatusCode, apiErr.Error))
}

data := new(Point)
json.Unmarshal(body, data)

return data, nil
}

func (inpost *InPostAPI) Authenticate() error {
if inpost.refreshToken == "" {
return errors.New("Please log in again (--login).")
}

type RequestPayload struct {
RefreshToken string `json:"refreshToken"`
PhoneOS string `json:"phoneOS"`
}

type Response struct {
AuthToken string
}

endpoint := url.URL{Path: "/v1/authenticate"}
payload := RequestPayload{inpost.refreshToken, "Apple"}
jsonData, _ := json.Marshal(payload)
response, body := inpost.request("POST", &endpoint, bytes.NewBuffer(jsonData))

if response.StatusCode != http.StatusOK {
apiErr := APIError{}
json.Unmarshal(body, &apiErr)

return errors.New(fmt.Sprintf("[%d] %s", response.StatusCode, apiErr.Error))
}

data := Response{}
json.Unmarshal(body, &data)
inpost.authToken = data.AuthToken
inpost.SaveConfig()

return nil
}

func (inpost *InPostAPI) SendSMSCode(phoneNumber string) error {
type RequestPayload struct {
PhoneNumber string `json:"phoneNumber"`
}

endpoint := url.URL{Path: "/v1/sendSMSCode"}
payload := RequestPayload{phoneNumber}
jsonData, _ := json.Marshal(payload)
response, body := inpost.request("POST", &endpoint, bytes.NewBuffer(jsonData))

if response.StatusCode != http.StatusOK {
apiErr := APIError{}
json.Unmarshal(body, &apiErr)

return errors.New(fmt.Sprintf("[%d] %s", response.StatusCode, apiErr.Error))
}

return nil
}

func (inpost *InPostAPI) ConfirmSMSCode(phoneNumber string, smsCode string) error {
type RequestPayload struct {
PhoneNumber string `json:"phoneNumber"`
SmsCode string `json:"smsCode"`
PhoneOS string `json:"phoneOS"`
}

type Response struct {
RefreshToken string
AuthToken string
}

endpoint := url.URL{Path: "/v1/confirmSMSCode"}
payload := RequestPayload{phoneNumber, smsCode, "Apple"}
jsonData, _ := json.Marshal(payload)
response, body := inpost.request("POST", &endpoint, bytes.NewBuffer(jsonData))

if response.StatusCode != http.StatusOK {
apiErr := APIError{}
json.Unmarshal(body, &apiErr)

return errors.New(fmt.Sprintf("[%d] %s", response.StatusCode, apiErr.Error))
}

data := Response{}
json.Unmarshal(body, &data)
inpost.refreshToken = data.RefreshToken
inpost.authToken = data.AuthToken
inpost.SaveConfig()

return nil
}

func (inpost *InPostAPI) SaveConfig() {
text, _ := json.MarshalIndent(Config{inpost.refreshToken, inpost.authToken}, "", " ")
err := ioutil.WriteFile(inpost.configFilePath, text, 0644)
if err != nil {
log.Fatalf("Couldn't save config file: %+v", err)
}
}

func (inpost *InPostAPI) ReadConfig() {
text, _ := ioutil.ReadFile(inpost.configFilePath)
config := Config{}
json.Unmarshal(text, &config)
inpost.refreshToken = config.RefreshToken
inpost.authToken = config.AuthToken
}

func (inpost *InPostAPI) request(method string, apiURL *url.URL, requestBody io.Reader) (*http.Response, []byte) {
resolvedURL := inpost.baseURL.ResolveReference(apiURL)
req, err := http.NewRequest(method, resolvedURL.String(), requestBody)
if err != nil {
log.Fatalf("Error occurred: %+v", err)
}

req.Header.Set("User-Agent", "InPost-Mobile/3.7.2-release (iOS 15.1.1; iPhone14,2; pl)")
req.Header.Add("Accept-Language", "en-US")

if method == "POST" || method == "PUT" {
req.Header.Add("Content-Type", "application/json")
}

if inpost.authToken != "" {
req.Header.Add("Authorization", inpost.authToken)
}

response, err := inpost.client.Do(req)
if err != nil {
log.Fatalf("Error sending request to API endpoint: %+v", err)
}

defer response.Body.Close()

responseBody, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Fatalf("Couldn't parse response body: %+v", err)
}

return response, responseBody
}

func (inpost *InPostAPI) isAuthTokenValid() bool {
type TokenPayload struct {
Exp int64
}

if !strings.HasPrefix(inpost.authToken, "Bearer ") {
return false
}

jwt := strings.Replace(inpost.authToken, "Bearer ", "", 1)
encodedTokenPayload := strings.Split(jwt, ".")[1]
jsonData, _ := base64.RawStdEncoding.DecodeString(encodedTokenPayload)
decodedTokenPayload := TokenPayload{}
err := json.Unmarshal(jsonData, &decodedTokenPayload)
if err != nil {
log.Fatalf("Couldn't validate auth token.")
}

tokenExpirationDate := time.Unix(decodedTokenPayload.Exp, 0)

return time.Now().Before(tokenExpirationDate)
}
78 changes: 78 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package main

import (
"fmt"
"log"
"math"
"os"
"path"
"path/filepath"
"strings"
"time"
)

func containsString(list []string, str string) bool {
for _, value := range list {
if value == str {
return true
}
}
return false
}

func main() {
exec, err := os.Executable()
if err != nil {
panic(err)
}

programName := filepath.Base(exec)
programNameWithoutExt := strings.TrimSuffix(programName, filepath.Ext(programName))
configPath := path.Join(filepath.Dir(exec), fmt.Sprintf("%s.config.json", programNameWithoutExt))
inpost := NewInPostAPI(configPath)
args := os.Args[1:]

if len(args) == 0 {
fmt.Printf("Usage: %s [--login] POINT_ID\n", programName)
return
}

if containsString(args, "--login") {
number := 0
fmt.Print("Phone number: ")
fmt.Scanf("%d", &number)
inpost.SendSMSCode(fmt.Sprintf("%d", number))

smsCode := 0
fmt.Print("SMS code: ")
fmt.Scanf("%d", &smsCode)
inpost.ConfirmSMSCode(fmt.Sprintf("%d", number), fmt.Sprintf("%d", smsCode))

fmt.Println("Logged in.")
return
}

pointId := strings.ToUpper(args[0])
point, err := inpost.GetPoint(pointId)
if err != nil {
log.Fatalf("Couldn't get air sensor data for %s: %+v", pointId, err)
}

if !point.AirSensor {
fmt.Printf("%s has no air sensor.\n", pointId)
return
}

fmt.Printf("Point name........: %s\n", point.Name)
fmt.Printf("Temperature.......: %.1f °C\n", point.AirSensorData.Weather.Temperature)
fmt.Printf("Pressure..........: %d hPa\n", int(math.Round(float64(point.AirSensorData.Weather.Pressure))))
fmt.Printf("Humidity..........: %d%%\n", int(math.Round(float64(point.AirSensorData.Weather.Humidity))))
fmt.Printf("Dust PM 10........: %.1f μg/m³ (%d%%)\n",
point.AirSensorData.Pollutants.Pm10.Value,
int(math.Round(float64(point.AirSensorData.Pollutants.Pm10.Percent))))
fmt.Printf("Dust PM 2.5.......: %.1f μg/m³ (%d%%)\n",
point.AirSensorData.Pollutants.Pm25.Value,
int(math.Round(float64(point.AirSensorData.Pollutants.Pm25.Percent))))
fmt.Printf("Quality...........: %s\n", strings.ToLower(strings.ReplaceAll(point.AirSensorData.AirQuality, "_", " ")))
fmt.Printf("Last updated......: %s\n", point.AirSensorData.UpdatedUntil.Local().Format(time.Stamp))
}

0 comments on commit cca1a21

Please sign in to comment.