package mysubaru import ( "encoding/json" "fmt" "io" "log/slog" "sync" "time" "git.savin.nyc/alex/mysubaru/config" "github.com/Jeffail/gabs/v2" "resty.dev/v3" ) // Client . type Client struct { credentials config.Credentials httpClient *resty.Client country string // USA | CA updateInterval int // 7200 fetchInterval int // 360 currentVin string listOfVins []string isAuthenticated bool isRegistered bool logger *slog.Logger sync.RWMutex } // auth . func (c *Client) auth() []byte { 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 := c.execute(reqURL, POST, params, "", false) c.logger.Debug("AUTH HTTP OUTPUT", "body", string([]byte(resp))) return resp } // GET // https://www.mysubaru.com/profile/verifyDeviceName.json?clientId=2574212&deviceName=Alex%20Google%20Pixel%204%20XL // RESP: true/false // POST // https://www.mysubaru.com/profile/editDeviceName.json?clientId=2574212&deviceName=Alex%20Google%20Pixel%204%20XL // clientId: 2574212 // deviceName: Alex Google Pixel 4 XL // RESP: true/false // GET // https://www.mysubaru.com/listMyDevices.json // {"success":true,"dataName":"authorizedDevices","data":[{"telematicsClientDeviceKey":2574212,"deviceType":"android","deviceName":"Alex Google Pixel 4 XL","createdDate":"2019-11-29T20:32:21.000+0000","modifiedDate":"2020-06-08T17:48:22.000+0000"},{"telematicsClientDeviceKey":4847533,"deviceName":"Home Assistant: Added 2021-03-03","createdDate":"2021-03-03T20:53:44.000+0000","modifiedDate":"2021-03-03T20:53:47.000+0000"},{"telematicsClientDeviceKey":7222995,"deviceType":"android","deviceName":"Alex Google Pixel 6 Pro","createdDate":"2021-10-28T15:27:36.000+0000","modifiedDate":"2021-10-28T15:27:58.000+0000"},{"telematicsClientDeviceKey":8207130,"deviceName":"Mac/iOS Chrome","createdDate":"2021-12-21T21:19:40.000+0000","modifiedDate":"2021-12-21T21:19:40.000+0000"}]} // {"success":true,"dataName":"authorizedDevices","data":[{"telematicsClientDeviceKey":2574212,"deviceType":"android","deviceName":"Alex Google Pixel 4 XL","createdDate":"2019-11-29T20:32:21.000+0000","modifiedDate":"2020-06-08T17:48:22.000+0000"},{"telematicsClientDeviceKey":4847533,"deviceName":"Home Assistant: Added 2021-03-03","createdDate":"2021-03-03T20:53:44.000+0000","modifiedDate":"2021-03-03T20:53:47.000+0000"},{"telematicsClientDeviceKey":7222995,"deviceType":"android","deviceName":"Alex Google Pixel 6 Pro","createdDate":"2021-10-28T15:27:36.000+0000","modifiedDate":"2021-10-28T15:27:58.000+0000"},{"telematicsClientDeviceKey":8210723,"deviceName":"Hassio Golang Integration","createdDate":"2021-12-22T01:38:43.000+0000","modifiedDate":"2021-12-22T01:38:43.000+0000"},{"telematicsClientDeviceKey":8207130,"deviceName":"Mac/iOS Chrome","createdDate":"2021-12-21T21:19:40.000+0000","modifiedDate":"2021-12-21T21:19:40.000+0000"}]} // registerDevice . func (c *Client) registerDevice() bool { // c.httpClient. // SetBaseURL(WEB_API_SERVER[c.country]). // R(). // SetFormData(map[string]string{ // "username": c.credentials.username, // "password": c.credentials.password, // "deviceId": c.credentials.deviceId, // }). // Post(apiURLs["WEB_API_LOGIN"]) params := map[string]string{ "username": c.credentials.Username, "password": c.credentials.Password, "deviceId": c.credentials.DeviceID} reqURL := WEB_API_SERVER[c.country] + apiURLs["WEB_API_LOGIN"] c.execute(reqURL, POST, params, "", true) // Authorizing device via web API // c.httpClient. // SetBaseURL(WEB_API_SERVER[c.country]). // R(). // SetQueryParams(map[string]string{ // "deviceId": c.credentials.deviceId, // }). // Get(apiURLs["WEB_API_AUTHORIZE_DEVICE"]) params = map[string]string{ "deviceId": c.credentials.DeviceID} reqURL = WEB_API_SERVER[c.country] + apiURLs["WEB_API_AUTHORIZE_DEVICE"] c.execute(reqURL, GET, params, "", false) return c.setDeviceName() } // setDeviceName . func (c *Client) setDeviceName() bool { params := map[string]string{ "deviceId": c.credentials.DeviceID, "deviceName": c.credentials.DeviceName} reqURL := WEB_API_SERVER[c.country] + apiURLs["WEB_API_NAME_DEVICE"] c.execute(reqURL, GET, params, "", false) return true } // listDevices . func (c *Client) listDevices() { // Accept: application/json, text/javascript, */*; q=0.01 // Accept-Encoding: gzip, deflate, br // Accept-Language: en-US,en;q=0.9,ru;q=0.8 // Connection: keep-alive // Cookie: ORA_OTD_JROUTE=ozLwELf5jS-NHQ2CKZorOFfRgb8uo6lL; soa-visitor=12212021VHWnkqERZYThWe87TLUhr2Db; AMCVS_94001C8B532957140A490D4D%40AdobeOrg=1; mys-referringCodes=7~direct~; s_cc=true; AMCVS_subarucom%40AdobeOrg=1; style=null; s_pv=login.html; AMCV_subarucom%40AdobeOrg=-1124106680%7CMCIDTS%7C18988%7CMCMID%7C81535064704660726005836131001032500276%7CMCAID%7CNONE%7CMCOPTOUT-1640567559s%7CNONE%7CvVersion%7C5.2.0; AMCV_94001C8B532957140A490D4D%40AdobeOrg=-1124106680%7CMCIDTS%7C18988%7CMCMID%7C76913534164341455390435376071204508177%7CMCAID%7CNONE%7CMCOPTOUT-1640567559s%7CNONE%7CvVersion%7C5.2.0; s_sq=subarumysubarucwpprod%3D%2526c.%2526a.%2526activitymap.%2526page%253Dlogin.html%2526link%253DLog%252520In%2526region%253DloginForm%2526pageIDType%253D1%2526.activitymap%2526.a%2526.c%2526pid%253Dlogin.html%2526pidt%253D1%2526oid%253DLog%252520In%2526oidt%253D3%2526ot%253DSUBMIT; JSESSIONID=9685CFEB7888A0E6E25239D559E3B580; X-Oracle-BMC-LBS-Route=89e3283ece707e8a0ba4850e1a622122e039fd3d27da03a11a2ff120e313e9b656c62fd8a7c42ae8061a49ad6e1caf63a49d7befe4ad2a0194b0aeca // Host: www.mysubaru.com // Referer: https://www.mysubaru.com/profile/authorizedDevices.html // User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36 // X-Requested-With: XMLHttpRequest resp, err := c.httpClient. SetBaseURL(WEB_API_SERVER[c.country]). R(). EnableTrace(). Get(apiURLs["WEB_API_LIST_DEVICES"]) // Explore response object fmt.Println("Response Info:") fmt.Println(" Error :", err) fmt.Println(" Status Code:", resp.StatusCode()) fmt.Println(" Status :", resp.Status()) fmt.Println(" Proto :", resp.Proto()) fmt.Println(" Time :", resp.Duration()) fmt.Println(" Received At:", resp.ReceivedAt()) // fmt.Println(" Body :\n", resp) fmt.Println() // Explore trace info fmt.Println("Request Trace Info:") ti := resp.Request.TraceInfo() fmt.Println(" DNSLookup :", ti.DNSLookup) fmt.Println(" ConnTime :", ti.ConnTime) fmt.Println(" TCPConnTime :", ti.TCPConnTime) fmt.Println(" TLSHandshake :", ti.TLSHandshake) fmt.Println(" ServerTime :", ti.ServerTime) fmt.Println(" ResponseTime :", ti.ResponseTime) fmt.Println(" TotalTime :", ti.TotalTime) fmt.Println(" IsConnReused :", ti.IsConnReused) fmt.Println(" IsConnWasIdle :", ti.IsConnWasIdle) fmt.Println(" ConnIdleTime :", ti.ConnIdleTime) fmt.Println(" RequestAttempt:", ti.RequestAttempt) fmt.Println(" RemoteAddr :", ti.RemoteAddr) // c.logger.Debug("LIST DEVICES OUTPUT", "body", string([]byte(resp.Body()))) // c.httpClient.SetBaseURL(WEB_API_SERVER[c.country]).SetCookies(c.cookies) // reqURL := apiURLs["WEB_API_LIST_DEVICES"] // resp := c.execute(reqURL, GET, map[string]string{}, "", false) // if isResponseSuccessfull(resp) { // logger.Debugf("LIST DEVICES OUTPUT >> %v\n", string(resp)) // } } // validateSession . func (c *Client) validateSession() bool { // { // "success": true, // "errorCode": null, // "dataName": null, // "data": null // } reqURL := MOBILE_API_VERSION + apiURLs["API_VALIDATE_SESSION"] resp := c.execute(reqURL, GET, map[string]string{}, "", false) if !c.isResponseSuccessfull(resp) { return false } // result = False // js_resp = await self.__open(API_VALIDATE_SESSION, GET) // _LOGGER.debug(pprint.pformat(js_resp)) // if js_resp["success"]: // if vin != self._current_vin: // # API call for VIN that is not the current remote context. // _LOGGER.debug("Switching Subaru API vehicle context to: %s", vin) // if await self._select_vehicle(vin): // result = True // else: // result = True // if result is False: // await self._authenticate(vin) // # New session cookie. Must call selectVehicle.json before any other API call. // if await self._select_vehicle(vin): // result = True c.logger.Debug("session validation", "body", string([]byte(resp))) return true } // New function creates a New MySubaru client func New(config *config.Config) (*Client, error) { client := &Client{ credentials: config.MySubaru.Credentials, country: config.MySubaru.Region, updateInterval: 7200, fetchInterval: 360, logger: config.Logger, } httpClient := resty.New() httpClient. SetBaseURL(MOBILE_API_SERVER[client.country]). SetHeaders(map[string]string{ "User-Agent": "Mozilla/5.0 (Linux; Android 10; Android SDK built for x86 Build/QSR1.191030.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/74.0.3729.185 Mobile Safari/537.36", "Origin": "file://", "X-Requested-With": MOBILE_APP[client.country], "Accept-Language": "en-US,en;q=0.9", "Accept-Encoding": "gzip, deflate", "Accept": "*/*"}) client.httpClient = httpClient resp := client.auth() respParsed, err := gabs.ParseJSON(resp) if err != nil { panic(err) } if client.isResponseSuccessfull(resp) { client.logger.Debug("Client authentication successful", "isRegistered", respParsed.Path("data.deviceRegistered").Data().(bool)) client.isAuthenticated = true client.isRegistered = respParsed.Path("data.deviceRegistered").Data().(bool) } else { error, _ := respParsed.Path("errorCode").Data().(string) switch { case error == apiErrors["ERROR_INVALID_ACCOUNT"]: client.logger.Debug("Invalid account") case error == apiErrors["ERROR_INVALID_CREDENTIALS"]: client.logger.Debug("Client authentication failed") case error == apiErrors["ERROR_PASSWORD_WARNING"]: client.logger.Debug("Multiple Password Failures.") default: client.logger.Debug("Uknown error") } } if !client.isRegistered { client.registerDevice() } // TODO fmt.Printf("Parcing cars: %d\n", len(respParsed.Path("data.vehicles").Children())) for _, vehicle := range respParsed.Path("data.vehicles").Children() { fmt.Printf("CAR: %s\n", vehicle.Path("vin").Data().(string)) client.listOfVins = append(client.listOfVins, vehicle.Path("vin").Data().(string)) } client.currentVin = respParsed.Path("data.vehicles.0.vin").Data().(string) return client, nil } func (c *Client) SelectVehicle(vin string) VehicleData { // API > json > dataName > vehicle if vin == "" { vin = c.currentVin } vinCheck(vin) params := map[string]string{ "vin": vin, "_": timestamp()} reqURL := MOBILE_API_VERSION + apiURLs["API_SELECT_VEHICLE"] resp := c.execute(reqURL, GET, params, "", false) var vData VehicleData respParsed, err := gabs.ParseJSON([]byte(resp)) if err != nil { panic(err) } vdString := respParsed.Path("data").String() json.Unmarshal([]byte(vdString), &vData) // resp := c.execute(reqURL, GET, params, "", false) // logger.Debugf("SELECT VEHICLE OUTPUT >> %v\n", string([]byte(resp))) // ERRORS // {"success":false,"errorCode":"vehicleNotInAccount","dataName":null,"data":null} // """Select active vehicle for accounts with multiple VINs.""" // params = {"vin": vin, "_": int(time.time())} // js_resp = await self.get(API_SELECT_VEHICLE, params=params) // _LOGGER.debug(pprint.pformat(js_resp)) // if js_resp.get("success"): // self._current_vin = vin // _LOGGER.debug("Current vehicle: vin=%s", js_resp["data"]["vin"]) // return js_resp["data"] // if not js_resp.get("success") and js_resp.get("errorCode") == "VEHICLESETUPERROR": // # Occasionally happens every few hours. Resetting the session seems to deal with it. // _LOGGER.warning("VEHICLESETUPERROR received. Resetting session.") // self.reset_session() // return False // _LOGGER.debug("Failed to switch vehicle errorCode=%s", js_resp.get("errorCode")) // # Something else is probably wrong with the backend server context - try resetting // self.reset_session() // raise SubaruException("Failed to switch vehicle %s - resetting session." % js_resp.get("errorCode")) return vData } // GetVehicles . func (c *Client) GetVehicles() []*Vehicle { var vehicles []*Vehicle for _, vin := range c.listOfVins { params := map[string]string{ "vin": vin, "_": timestamp()} reqURL := MOBILE_API_VERSION + apiURLs["API_SELECT_VEHICLE"] resp := c.execute(reqURL, GET, params, "", false) respParsed, err := gabs.ParseJSON([]byte(resp)) if err != nil { c.logger.Error("error which parsing json", "request", "GetVehicles", "error", err.Error()) } vData := VehicleData{} vdString := respParsed.Path("data").String() json.Unmarshal([]byte(vdString), &vData) // fmt.Printf("VEHICLE STRING: %+v\n\n", vdString) // fmt.Printf("VEHICLE DATA: %+v\n\n", vData) vehicle := &Vehicle{ Vin: vin, CarName: vData.VehicleName, CarNickname: vData.Nickname, ModelName: vData.ModelName, ModelYear: vData.ModelYear, ModelCode: vData.ModelCode, ExtDescrip: vData.ExtDescrip, IntDescrip: vData.IntDescrip, TransCode: vData.TransCode, EngineSize: vData.EngineSize, VehicleKey: vData.VehicleKey, LicensePlate: vData.LicensePlate, LicensePlateState: vData.LicensePlateState, Features: vData.Features, SubscriptionFeatures: vData.SubscriptionFeatures, client: c, } vehicle.GetVehicleStatus() vehicle.GetVehicleCondition() vehicle.GetClimatePresets() vehicle.GetClimateUserPresets() vehicles = append(vehicles, vehicle) } return vehicles } // GetVehicleByVIN . func (c *Client) GetVehicleByVIN(vin string) *Vehicle { var vehicle *Vehicle if contains(c.listOfVins, vin) { params := map[string]string{ "vin": vin, "_": timestamp()} reqURL := MOBILE_API_VERSION + apiURLs["API_SELECT_VEHICLE"] resp := c.execute(reqURL, GET, params, "", false) respParsed, err := gabs.ParseJSON([]byte(resp)) if err != nil { c.logger.Error("error which parsing json", "request", "GetVehicleByVIN", "error", err.Error()) } vData := VehicleData{} vdString := respParsed.Path("data").String() json.Unmarshal([]byte(vdString), &vData) // fmt.Printf("VEHICLE DATA: %+v\n\n", vData) vehicle = &Vehicle{ Vin: vin, CarName: vData.VehicleName, CarNickname: vData.Nickname, ModelName: vData.ModelName, ModelYear: vData.ModelYear, ModelCode: vData.ModelCode, ExtDescrip: vData.ExtDescrip, IntDescrip: vData.IntDescrip, TransCode: vData.TransCode, EngineSize: vData.EngineSize, VehicleKey: vData.VehicleKey, LicensePlate: vData.LicensePlate, LicensePlateState: vData.LicensePlateState, Features: vData.Features, SubscriptionFeatures: vData.SubscriptionFeatures, client: c, } vehicle.GetVehicleStatus() vehicle.GetVehicleCondition() vehicle.GetClimatePresets() vehicle.GetClimateUserPresets() } return vehicle } // GetVehicleStatus . func (c *Client) GetVehicleStatus() { // { // "dataName": null, // "errorCode": null, // "success": true, // "data": { // "avgFuelConsumptionLitersPer100Kilometers": 12.5, // "avgFuelConsumptionMpg": 18.8, // "distanceToEmptyFuelKilometers": 563, // "distanceToEmptyFuelKilometers10s": 560, // "distanceToEmptyFuelMiles": 349.83, // "distanceToEmptyFuelMiles10s": 350, // "evDistanceToEmptyByStateKilometers": null, // "evDistanceToEmptyByStateMiles": null, // "evDistanceToEmptyKilometers": null, // "evDistanceToEmptyMiles": null, // "evStateOfChargePercent": null, // "eventDate": 1640494569000, // "eventDateStr": "2021-12-26T04:56+0000", // "latitude": 40.700192, // "longitude": -74.401377, // "odometerValue": 24065, // "odometerValueKilometers": 38721, // "positionHeadingDegree": "150", // "tirePressureFrontLeft": "2413", // "tirePressureFrontLeftPsi": "35", // "tirePressureFrontRight": "2413", // "tirePressureFrontRightPsi": "35", // "tirePressureRearLeft": "2551", // "tirePressureRearLeftPsi": "37", // "tirePressureRearRight": "2482", // "tirePressureRearRightPsi": "36", // "vehicleStateType": "IGNITION_OFF", // "vhsId": 923920223 // } // } reqURL := MOBILE_API_VERSION + apiURLs["API_VEHICLE_STATUS"] resp := c.execute(reqURL, GET, map[string]string{}, "", false) respParsed, err := gabs.ParseJSON(resp) if err != nil { c.logger.Error("error which parsing json", "request", "GetVehicleStatus", "error", err.Error()) } c.logger.Debug("GET VEHICLE STATUS OUTPUT", "body", respParsed) success, ok := respParsed.Path("success").Data().(bool) // value == string, ok == false if !ok { // TODO: Work with errorCode panic(success) } } // GetVehicleStatus . func (c *Client) GetClimateSettings() { // { // "success": true, // "errorCode": null, // "dataName": null, // "data": { // "climateZoneFrontTemp": "70", // "runTimeMinutes": "10", // "climateZoneFrontAirMode": "WINDOW", // "heatedSeatFrontLeft": "LOW_HEAT", // "heatedSeatFrontRight": "LOW_HEAT", // "heatedRearWindowActive": "true", // "climateZoneFrontAirVolume": "6", // "outerAirCirculation": "outsideAir", // "airConditionOn": "false", // "startConfiguration": "START_ENGINE_ALLOW_KEY_IN_IGNITION" // } // } reqURL := MOBILE_API_VERSION + apiURLs["API_G2_FETCH_CLIMATE_SETTINGS"] resp := c.execute(reqURL, GET, map[string]string{}, "", false) respParsed, err := gabs.ParseJSON(resp) if err != nil { c.logger.Error("error which parsing json", "request", "GetClimateSettings", "error", err.Error()) } c.logger.Debug("CLIMATE SETTINGS OUTPUT", "response", respParsed) // ONLY FOR THAT REQUEST BECAUSE OF API SENDS BACK ESCAPING DATA IN DATA FIELD data, ok := respParsed.Path("data").Data().(string) // rawIn := json.RawMessage(in) // bytes, err := rawIn.MarshalJSON() // if err != nil { // panic(err) // } // value == string, ok == false if !ok { // TODO: Work with errorCode panic(data) } c.logger.Debug("CLIMATE SETTINGS OUTPUT", "body", data) } // func isPINRequired() {} // func getVehicles() {} // func getEVStatus() {} // func getRemoteOptionsStatus() {} // func getRemoteStartStatus() {} // func getSafetyStatus() {} // func getSubscriptionStatus() {} // func getAPIGen() {} // func getClimateData() {} // func saveClimateSettings() {} // func getVehicleName() {} // func fetch() {} // Exec method executes a Client instance with the API URL func (c *Client) execute(requestUrl string, method string, params map[string]string, pollingUrl string, json bool) []byte { defer timeTrack("[TIMETRK] Executing Get Request") var resp *resty.Response // GET Requests if method == "GET" { resp, _ = c.httpClient. // SetBaseURL(MOBILE_API_SERVER[c.country]). R(). SetQueryParams(params). Get(requestUrl) } // POST Requests if method == "POST" { if json { // POST > JSON Body resp, _ = c.httpClient.R(). SetBody(params). Post(requestUrl) } else { // POST > Form Data resp, _ = c.httpClient.R(). SetFormData(params). Post(requestUrl) } } resBytes, _ := io.ReadAll(resp.Body) // if err != nil { // fmt.Println(err) // return // } respParsed, err := gabs.ParseJSON(resBytes) if err != nil { c.logger.Error("error which parsing json", "request", "execute", "method", method, "url", requestUrl, "error", err.Error()) } c.logger.Debug("HTTP OUTPUT", "body", string(resBytes)) _, ok := respParsed.Path("success").Data().(bool) // value == string, ok == false if !ok { // TODO: Work with errorCode // panic(success) fmt.Printf("ERROR: %+v", string(resBytes)) } if pollingUrl != "" { serviceRequestId, _ := respParsed.Path("data.serviceRequestId").Data().(string) time.Sleep(3 * time.Second) attempts := 20 poolingLoop: for attempts > 0 { resp, _ = c.httpClient. SetBaseURL(MOBILE_API_SERVER[c.country]). R(). SetQueryParams(map[string]string{ "serviceRequestId": serviceRequestId, }). Get(pollingUrl) resBytes, _ := io.ReadAll(resp.Body) c.logger.Debug("POLLING HTTP OUTPUT", "body", string(resBytes)) // {"success":false,"errorCode":"404-soa-unableToParseResponseBody","dataName":"errorResponse","data":{"errorLabel":"404-soa-unableToParseResponseBody","errorDescription":null}} respParsed, err := gabs.ParseJSON(resBytes) if err != nil { panic(err) } success, ok := respParsed.Path("success").Data().(bool) if !ok { panic(success) } if success { status, _ := respParsed.Path("data.remoteServiceState").Data().(string) switch { case status == "finished": c.logger.Debug("Remote service request completed successfully", "request id", serviceRequestId) break poolingLoop case status == "started": c.logger.Debug("Subaru API reports remote service request is in progress", "request id", serviceRequestId) } } else { c.logger.Debug("Backend session expired, please try again") break poolingLoop } attempts-- time.Sleep(3 * time.Second) } } // fmt.Printf("[DEBUG] HTTP OUTPUT >> %v\n", string([]byte(resp.Body()))) return resBytes } // isResponseSuccessfull . func (c *Client) isResponseSuccessfull(resp []byte) bool { respParsed, err := gabs.ParseJSON(resp) if err != nil { c.logger.Debug("error while parsing json response", "error", err) } success, ok := respParsed.Path("success").Data().(bool) if !ok { c.logger.Debug("response is not successful", "error", resp) } // ERRORS FROM CLIENT CREATION AFTER AUTH // error, _ := respParsed.Path("errorCode").Data().(string) // switch { // case error == apiErrors["ERROR_INVALID_ACCOUNT"]: // fmt.Println("Invalid account") // case error == apiErrors["ERROR_INVALID_CREDENTIALS"]: // {"success":false,"errorCode":"InvalidCredentials","dataName":"remoteServiceStatus","data":{"serviceRequestId":null,"success":false,"cancelled":false,"remoteServiceType":null,"remoteServiceState":null,"subState":null,"errorCode":null,"result":null,"updateTime":null,"vin":null,"errorDescription":"The credentials supplied are invalid, tries left 2"}} // fmt.Println("Client authentication failed") // case error == apiErrors["ERROR_PASSWORD_WARNING"]: // fmt.Println("Multiple Password Failures.") // default: // fmt.Println("Uknown error") // } return success }