Enhance MySubaru API integration with improved error handling and new utility functions
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 25s
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 25s
- Refactor Response struct's parse method to return detailed error messages based on API error codes. - Introduce UnixTime type for handling Unix timestamps in JSON marshaling and unmarshaling. - Add email masking utility function to obfuscate email addresses for privacy. - Implement containsValueInStruct function to check for substring presence in struct fields. - Create comprehensive unit tests for UnixTime, email masking, and struct value checking. - Update vehicle service request method documentation for clarity.
This commit is contained in:
179
client.go
179
client.go
@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
@ -17,14 +18,15 @@ import (
|
||||
type Client struct {
|
||||
credentials config.Credentials
|
||||
httpClient *resty.Client
|
||||
country string // USA | CA
|
||||
updateInterval int // 7200
|
||||
fetchInterval int // 360
|
||||
country string // USA | CA
|
||||
contactMethods dataMap // List of contact methods for 2FA
|
||||
currentVin string
|
||||
listOfVins []string
|
||||
isAuthenticated bool
|
||||
isRegistered bool
|
||||
isAlive bool
|
||||
updateInterval int // 7200
|
||||
fetchInterval int // 360
|
||||
logger *slog.Logger
|
||||
sync.RWMutex
|
||||
}
|
||||
@ -49,45 +51,73 @@ func New(config *config.Config) (*Client, error) {
|
||||
"X-Requested-With": MOBILE_APP[client.country],
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Accept": "*/*"})
|
||||
"Accept": "*/*"},
|
||||
)
|
||||
|
||||
client.httpClient = httpClient
|
||||
resp, err := client.auth()
|
||||
if err != nil {
|
||||
|
||||
if ok, err := client.auth(); !ok {
|
||||
client.logger.Error("error while executing auth request", "request", "auth", "error", err.Error())
|
||||
return nil, errors.New("error while executing auth request: " + err.Error())
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// auth authenticates the client with the MySubaru API using the provided credentials.
|
||||
func (c *Client) auth() (bool, error) {
|
||||
params := map[string]string{
|
||||
"env": "cloudprod",
|
||||
"deviceType": "android",
|
||||
"loginUsername": c.credentials.Username,
|
||||
"password": c.credentials.Password,
|
||||
"deviceId": c.credentials.DeviceID,
|
||||
"passwordToken": "",
|
||||
"selectedVin": "",
|
||||
"pushToken": ""}
|
||||
reqURL := MOBILE_API_VERSION + apiURLs["API_LOGIN"]
|
||||
resp, err := c.execute(POST, reqURL, params, false)
|
||||
if err != nil {
|
||||
c.logger.Error("error while executing auth request", "request", "auth", "error", err.Error())
|
||||
return false, errors.New("error while executing auth request: " + err.Error())
|
||||
}
|
||||
c.logger.Debug("http request output", "request", "auth", "body", resp)
|
||||
|
||||
var sd SessionData
|
||||
err = json.Unmarshal(resp.Data, &sd)
|
||||
if err != nil {
|
||||
client.logger.Error("error while parsing json", "request", "auth", "error", err.Error())
|
||||
c.logger.Error("error while parsing json", "request", "auth", "error", err.Error())
|
||||
}
|
||||
// client.logger.Debug("unmarshaled json data", "request", "auth", "type", "sessionData", "body", sd)
|
||||
|
||||
if sd.DeviceRegistered && sd.RegisteredDevicePermanent {
|
||||
// client.logger.Debug("client authentication successful")
|
||||
client.isAuthenticated = true
|
||||
client.isRegistered = true
|
||||
}
|
||||
// TODO: Work on registerDevice()
|
||||
// } else {
|
||||
// // client.logger.Debug("client authentication successful, but devices is not registered")
|
||||
// client.registerDevice()
|
||||
// }
|
||||
if !sd.DeviceRegistered {
|
||||
err := c.getContactMethods()
|
||||
if err != nil {
|
||||
c.logger.Error("error while getting contact methods", "request", "auth", "error", err.Error())
|
||||
return false, errors.New("error while getting contact methods: " + err.Error())
|
||||
}
|
||||
|
||||
c.logger.Error("device is not registered", "request", "auth", "deviceId", c.credentials.DeviceID)
|
||||
return false, errors.New("device is not registered: " + c.credentials.DeviceID)
|
||||
}
|
||||
|
||||
if sd.DeviceRegistered && sd.RegisteredDevicePermanent {
|
||||
c.isAuthenticated = true
|
||||
c.isRegistered = true
|
||||
c.isAlive = true
|
||||
}
|
||||
c.logger.Debug("MySubaru API client authenticated")
|
||||
|
||||
// client.logger.Debug("parsing cars assigned to the account", "quantity", len(sd.Vehicles))
|
||||
if len(sd.Vehicles) > 0 {
|
||||
for _, vehicle := range sd.Vehicles {
|
||||
// client.logger.Debug("parsing car", "vin", vehicle.Vin)
|
||||
client.listOfVins = append(client.listOfVins, vehicle.Vin)
|
||||
c.listOfVins = append(c.listOfVins, vehicle.Vin)
|
||||
}
|
||||
client.currentVin = client.listOfVins[0]
|
||||
c.currentVin = c.listOfVins[0]
|
||||
} else {
|
||||
client.logger.Error("there are no cars assigned to the account")
|
||||
return nil, err
|
||||
c.logger.Error("there are no vehicles associated with the account", "request", "auth", "error", "no vehicles found")
|
||||
return false, err
|
||||
}
|
||||
return client, nil
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// SelectVehicle selects a vehicle by its VIN. If no VIN is provided, it uses the current VIN.
|
||||
@ -142,17 +172,17 @@ func (c *Client) GetVehicleByVin(vin string) (*Vehicle, error) {
|
||||
reqURL := MOBILE_API_VERSION + apiURLs["API_SELECT_VEHICLE"]
|
||||
resp, err := c.execute(GET, reqURL, params, false)
|
||||
if err != nil {
|
||||
c.logger.Error("error while executing GetVehicleByVIN request", "request", "GetVehicleByVIN", "error", err.Error())
|
||||
return nil, errors.New("error while executing GetVehicleByVIN request: " + err.Error())
|
||||
c.logger.Error("error while executing GetVehicleByVin request", "request", "GetVehicleByVin", "error", err.Error())
|
||||
return nil, errors.New("error while executing GetVehicleByVin request: " + err.Error())
|
||||
}
|
||||
// c.logger.Debug("http request output", "request", "GetVehicleByVIN", "body", resp)
|
||||
// c.logger.Debug("http request output", "request", "GetVehicleByVin", "body", resp)
|
||||
|
||||
var vd VehicleData
|
||||
err = json.Unmarshal(resp.Data, &vd)
|
||||
if err != nil {
|
||||
c.logger.Error("error while parsing json", "request", "GetVehicleByVIN", "error", err.Error())
|
||||
c.logger.Error("error while parsing json", "request", "GetVehicleByVin", "error", err.Error())
|
||||
}
|
||||
// c.logger.Debug("http request output", "request", "GetVehicleByVIN", "body", resp)
|
||||
// c.logger.Debug("http request output", "request", "GetVehicleByVin", "body", resp)
|
||||
|
||||
vehicle = &Vehicle{
|
||||
Vin: vin,
|
||||
@ -198,6 +228,7 @@ func (c *Client) GetVehicleByVin(vin string) (*Vehicle, error) {
|
||||
}
|
||||
|
||||
// RefreshVehicles refreshes the list of vehicles associated with the client's account.
|
||||
// {"success":true,"dataName":"sessionData","data":{"sessionChanged":true,"vehicleInactivated":false,"account":{"marketId":1,"createdDate":1476984644000,"firstName":"Tatiana","lastName":"Savin","zipCode":"07974","accountKey":765268,"lastLoginDate":1751835464000,"zipCode5":"07974"},"resetPassword":false,"deviceId":"JddMBQXvAkgutSmEP6uFsThbq4QgEBBQ","sessionId":"0E154A99FA3D014D866840123E5E666A","deviceRegistered":true,"passwordToken":null,"vehicles":[{"customer":{"sessionCustomer":{"firstName":"Tatiana","lastName":"Savin","title":"","suffix":"","email":"tanya@savin.nyc","address":"29a Marion Ave","address2":"","city":"New Providence","state":"NJ","zip":"07974-1906","cellularPhone":"","workPhone":"","homePhone":"4013050505","countryCode":"USA","relationshipType":null,"gender":"","dealerCode":null,"oemCustId":"CRM-41PLM-5TYE","createMysAccount":null,"sourceSystemCode":"mys","vehicles":[{"vin":"4S4BSETC4H3265676","siebelVehicleRelationship":"Previous Owner","primary":false,"oemCustId":"1-8K7OBOJ","status":""},{"vin":"4S4BSETC4H3265676","siebelVehicleRelationship":"Previous TM Subscriber","primary":false,"oemCustId":"1-8JY3UVS","status":"Inactive"},{"vin":"4S4BTGND8L3137058","siebelVehicleRelationship":"Previous TM Subscriber","primary":false,"oemCustId":"CRM-44UFUA14-V","status":"Draft"},{"vin":"4S4BTGPD0P3199198","siebelVehicleRelationship":"TM Subscriber","primary":true,"oemCustId":"CRM-41PLM-5TYE","status":"Active"}],"phone":"","zip5Digits":"07974","primaryPersonalCountry":"USA"},"email":"","firstName":"Tatiana","lastName":"Savin","zip":"07974-1906","oemCustId":"CRM-41PLM-5TYE","phone":""},"vehicleName":"Subaru Outback TXT","stolenVehicle":false,"vin":"4S4BTGPD0P3199198","modelYear":null,"modelCode":null,"engineSize":null,"nickname":"Subaru Outback TXT","vehicleKey":8211380,"active":true,"licensePlate":"","licensePlateState":"","email":"tanya@savin.nyc","firstName":"Tatiana","lastName":"Savin","subscriptionFeatures":["REMOTE","SAFETY","Retail3"],"accessLevel":-1,"zip":"07974-1906","oemCustId":"CRM-41PLM-5TYE","vehicleMileage":null,"phone":"","timeZone":"America/New_York","features":["ABS_MIL","ACCS","AHBL_MIL","ATF_MIL","AWD_MIL","BSD","BSDRCT_MIL","CEL_MIL","CP1_5HHU","EBD_MIL","EOL_MIL","EPAS_MIL","EPB_MIL","ESS_MIL","EYESIGHT","ISS_MIL","MOONSTAT","OPL_MIL","PANPM-TUIRWAOC","PWAAADWWAP","RAB_MIL","RCC","REARBRK","RES","RESCC","RES_HVAC_HFS","RES_HVAC_VFS","RHSF","RPOI","RPOIA","RTGU","RVFS","SRH_MIL","SRS_MIL","SXM360L","T23DCM","TEL_MIL","TIF_35","TIR_33","TLD","TPMS_MIL","VALET","VDC_MIL","WASH_MIL","WDWSTAT","g3"],"userOemCustId":"CRM-41PLM-5TYE","subscriptionStatus":"ACTIVE","authorizedVehicle":false,"preferredDealer":null,"cachedStateCode":"NJ","modelName":null,"subscriptionPlans":[],"crmRightToRepair":false,"needMileagePrompt":false,"phev":null,"extDescrip":null,"sunsetUpgraded":true,"intDescrip":null,"transCode":null,"provisioned":true,"remoteServicePinExist":true,"needEmergencyContactPrompt":false,"vehicleGeoPosition":null,"show3gSunsetBanner":false,"vehicleBranded":false}],"rightToRepairEnabled":true,"rightToRepairStartYear":2022,"rightToRepairStates":"MA","enableXtime":true,"termsAndConditionsAccepted":true,"digitalGlobeConnectId":"0572e32b-2fcf-4bc8-abe0-1e3da8767132","digitalGlobeImageTileService":"https://earthwatch.digitalglobe.com/earthservice/tmsaccess/tms/1.0.0/DigitalGlobe:ImageryTileService@EPSG:3857@png/{z}/{x}/{y}.png?connectId=0572e32b-2fcf-4bc8-abe0-1e3da8767132","digitalGlobeTransparentTileService":"https://earthwatch.digitalglobe.com/earthservice/tmsaccess/tms/1.0.0/Digitalglobe:OSMTransparentTMSTileService@EPSG:3857@png/{z}/{x}/{-y}.png/?connectId=0572e32b-2fcf-4bc8-abe0-1e3da8767132","tomtomKey":"DHH9SwEQ4MW55Hj2TfqMeldbsDjTdgAs","currentVehicleIndex":0,"handoffToken":"$2a$08$EvONydxMhrhVgFsv4OgX3O8QaOu3naCFYex2Crqhl27cPQwJYXera$1751837730624","satelliteViewEnabled":true,"registeredDevicePermanent":true}}
|
||||
func (c *Client) RefreshVehicles() error {
|
||||
params := map[string]string{}
|
||||
reqURL := MOBILE_API_VERSION + apiURLs["API_REFRESH_VEHICLES"]
|
||||
@ -219,9 +250,24 @@ func (c *Client) RefreshVehicles() error {
|
||||
}
|
||||
|
||||
// RequestAuthCode requests an authentication code for two-factor authentication (2FA).
|
||||
func (c *Client) RequestAuthCode() error {
|
||||
// (?!^).(?=.*@)
|
||||
// (?!^): This is a negative lookbehind assertion. It ensures that the matched character is not at the beginning of the string.
|
||||
// .: This matches any single character (except newline, by default).
|
||||
// (?=.*@): This is a positive lookahead assertion. It ensures that the matched character is followed by any characters (.*) and then an "@" symbol. This targets the username part of the email address.
|
||||
func (c *Client) RequestAuthCode(email string) error {
|
||||
email, err := emailMasking(email)
|
||||
if err != nil {
|
||||
c.logger.Error("error while hiding email", "request", "RequestAuthCode", "error", err.Error())
|
||||
return errors.New("error while hiding email: " + err.Error())
|
||||
}
|
||||
|
||||
if !containsValueInStruct(c.contactMethods, email) {
|
||||
c.logger.Error("email is not in the list of contact methods", "request", "RequestAuthCode", "email", email)
|
||||
return errors.New("email is not in the list of contact methods: " + email)
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"contactMethod": "0",
|
||||
"contactMethod": email,
|
||||
"languagePreference": "EN"}
|
||||
reqUrl := MOBILE_API_VERSION + apiURLs["API_2FA_SEND_VERIFICATION"]
|
||||
resp, err := c.execute(POST, reqUrl, params, false)
|
||||
@ -236,6 +282,12 @@ func (c *Client) RequestAuthCode() error {
|
||||
|
||||
// SubmitAuthCode submits the authentication code received from the RequestAuthCode method.
|
||||
func (c *Client) SubmitAuthCode(code string, permanent bool) error {
|
||||
regex := regexp.MustCompile(`^\d{6}$`)
|
||||
if !regex.MatchString(code) {
|
||||
c.logger.Error("invalid verification code format", "request", "SubmitAuthCode", "code", code)
|
||||
return errors.New("invalid verification code format, must be 6 digits")
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"deviceId": c.credentials.DeviceID,
|
||||
"deviceName": c.credentials.DeviceName,
|
||||
@ -250,34 +302,44 @@ func (c *Client) SubmitAuthCode(code string, permanent bool) error {
|
||||
c.logger.Error("error while executing SubmitAuthCode request", "request", "SubmitAuthCode", "error", err.Error())
|
||||
return errors.New("error while executing SubmitAuthCode request: " + err.Error())
|
||||
}
|
||||
c.logger.Debug("http request output", "request", "SubmitAuthCode", "body", resp)
|
||||
|
||||
// Device registration does not always immediately take effect
|
||||
time.Sleep(time.Second * 3)
|
||||
c.logger.Debug("http request output", "request", "SubmitAuthCode", "body", resp)
|
||||
|
||||
// Reauthenticate after submitting the code
|
||||
if ok, err := c.auth(); !ok {
|
||||
c.logger.Error("error while executing auth request", "request", "auth", "error", err.Error())
|
||||
return errors.New("error while executing auth request: " + err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getContactMethods retrieves the available contact methods for two-factor authentication (2FA).
|
||||
func (c *Client) GetContactMethods() error {
|
||||
// {"success":true,"dataName":"dataMap","data":{"userName":"a**x@savin.nyc","email":"t***a@savin.nyc"}}
|
||||
func (c *Client) getContactMethods() error {
|
||||
params := map[string]string{}
|
||||
reqUrl := MOBILE_API_VERSION + apiURLs["API_2FA_CONTACT"]
|
||||
resp, err := c.execute(POST, reqUrl, params, false)
|
||||
if err != nil {
|
||||
c.logger.Error("error while executing GetContactMethods request", "request", "GetContactMethods", "error", err.Error())
|
||||
return errors.New("error while executing GetContactMethods request: " + err.Error())
|
||||
c.logger.Error("error while executing getContactMethods request", "request", "getContactMethods", "error", err.Error())
|
||||
return errors.New("error while executing getContactMethods request: " + err.Error())
|
||||
}
|
||||
c.logger.Debug("http request output", "request", "GetContactMethods", "body", resp)
|
||||
c.logger.Debug("http request output", "request", "getContactMethods", "body", resp)
|
||||
|
||||
var dm dataMap
|
||||
err = json.Unmarshal(resp.Data, &dm)
|
||||
if err != nil {
|
||||
c.logger.Error("error while parsing json", "request", "getContactMethods", "error", err.Error())
|
||||
return errors.New("error while parsing json while getting contact methods: " + err.Error())
|
||||
}
|
||||
c.contactMethods = dm
|
||||
c.logger.Debug("contact methods successfully retrieved", "request", "getContactMethods", "methods", dm)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// func isPINRequired() {}
|
||||
// func getEVStatus() {}
|
||||
// func getRemoteOptionsStatus() {}
|
||||
// func getRemoteStartStatus() {}
|
||||
// func getSafetyStatus() {}
|
||||
// func getSubscriptionStatus() {}
|
||||
|
||||
// IsAlive checks if the Client instance is alive
|
||||
func (c *Client) IsAlive() bool {
|
||||
return c.isAlive
|
||||
@ -335,6 +397,8 @@ func (c *Client) execute(method string, url string, params map[string]string, j
|
||||
}
|
||||
c.logger.Debug("parsed http request output", "data", string(resBytes))
|
||||
|
||||
c.httpClient.SetCookies(resp.Cookies())
|
||||
|
||||
if r, ok := c.parseResponse(resBytes); ok {
|
||||
c.isAlive = true
|
||||
c.Unlock()
|
||||
@ -361,28 +425,6 @@ func (c *Client) execute(method string, url string, params map[string]string, j
|
||||
return nil, errors.New("request is not successfull, HTTP code: " + resp.Status())
|
||||
}
|
||||
|
||||
// auth authenticates the client with the MySubaru API using the provided credentials.
|
||||
func (c *Client) auth() (*Response, error) {
|
||||
params := map[string]string{
|
||||
"env": "cloudprod",
|
||||
"deviceType": "android",
|
||||
"loginUsername": c.credentials.Username,
|
||||
"password": c.credentials.Password,
|
||||
"deviceId": c.credentials.DeviceID,
|
||||
"passwordToken": "",
|
||||
"selectedVin": "",
|
||||
"pushToken": ""}
|
||||
reqURL := MOBILE_API_VERSION + apiURLs["API_LOGIN"]
|
||||
resp, err := c.execute(POST, reqURL, params, false)
|
||||
c.logger.Debug("AUTH HTTP OUTPUT", "body", resp)
|
||||
if err != nil {
|
||||
c.logger.Error("error while executing auth request", "request", "auth", "error", err.Error())
|
||||
return nil, errors.New("error while executing auth request: " + err.Error())
|
||||
}
|
||||
c.logger.Debug("AUTH HTTP OUTPUT", "body", resp)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// parseResponse parses the JSON response from the MySubaru API into a Response struct.
|
||||
func (c *Client) parseResponse(b []byte) (Response, bool) {
|
||||
var r Response
|
||||
@ -407,6 +449,13 @@ func (c *Client) ValidateSession() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// func isPINRequired() {}
|
||||
// func getEVStatus() {}
|
||||
// func getRemoteOptionsStatus() {}
|
||||
// func getRemoteStartStatus() {}
|
||||
// func getSafetyStatus() {}
|
||||
// func getSubscriptionStatus() {}
|
||||
|
||||
// validateSession .
|
||||
// TODO: add session validation process and add it to the proper functions
|
||||
// func (c *Client) validateSession() bool {
|
||||
|
Reference in New Issue
Block a user