-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Maciej Leśniewski
committed
Apr 15, 2022
1 parent
df98759
commit cca1a21
Showing
5 changed files
with
350 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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~ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
# inpost-air | ||
# inpost-air | ||
Get air sesnor data from Paczkomat. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module github.com/leshniak/inpost-air | ||
|
||
go 1.18 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} |