feat(init): Start a bot for tesla
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Matthieu 'JP' DERASSE
2023-06-05 13:41:48 +00:00
commit 444d9b1563
20 changed files with 2105 additions and 0 deletions

95
api/client.go Normal file
View File

@ -0,0 +1,95 @@
package api
import (
"context"
"fmt"
"io"
"log"
"net/http"
"net/http/httputil"
)
// Client is the API client for tesla.
type Client struct {
baseUrl string
debug bool
HTTPClient *http.Client
}
// NewClient allow to instantiate an API Client for Tesla.
func NewClient() (*Client, error) {
client := Client{
baseUrl: baseUrl,
HTTPClient: http.DefaultClient,
}
return &client, nil
}
// SetDebug will enable or disable debug.
func (c *Client) SetDebug(debug bool) {
c.debug = debug
}
func (c *Client) get(ctx context.Context, path string) (*http.Response, error) {
return c.do(ctx, http.MethodGet, path, nil)
}
func (c *Client) customizeRequest(req *http.Request) *http.Request {
// Content-Type expected by the API
req.Header.Set("Content-Type", contentTypeHeader)
// add user-agent header
req.Header.Set("User-Agent", userAgentHeader)
return req
}
func (c *Client) do(ctx context.Context, method string, url string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequestWithContext(
ctx,
method,
fmt.Sprintf("%s%s", c.baseUrl, url),
body,
)
if err != nil {
return nil, fmt.Errorf("failed to build request: %w", err)
}
req = c.customizeRequest(req)
if c.debug {
dump, err := httputil.DumpRequestOut(req, true)
if err != nil {
return nil, err
}
log.Printf("\n----------- Request Debug -----------\n%s\n-------------------------------------\n", string(dump))
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return resp, fmt.Errorf("an error occurred while calling the API: %w", err)
}
if c.debug {
dump, err := httputil.DumpResponse(resp, true)
if err != nil {
return resp, err
}
log.Printf("\n----------- Response Debug ----------\n%s\n------------------------------------\n", string(dump))
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return resp, Error{
HTTPCode: resp.StatusCode,
HTTPMessage: resp.Status,
}
}
return resp, nil
}

8
api/config.go Normal file
View File

@ -0,0 +1,8 @@
package api
// Configuration Section.
const (
baseUrl = "https://www.tesla.com"
userAgentHeader = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36"
contentTypeHeader = "application/vnd.api+json"
)

18
api/error.go Normal file
View File

@ -0,0 +1,18 @@
package api
import "fmt"
// Error represent an API Error.
type Error struct {
HTTPCode int `json:"-"`
HTTPMessage string `json:"-"`
}
// Error statisfy the standard Error interface and return a human readeable message.
func (a Error) Error() string {
return fmt.Sprintf(
"API call failed with the following error: %s (HTTP status: %d)",
a.HTTPMessage,
a.HTTPCode,
)
}

51
api/sdk.go Normal file
View File

@ -0,0 +1,51 @@
package api
import (
"context"
"encoding/json"
"fmt"
"io"
"net/url"
)
// GetAvailabilities return car availabilities for the given filters.
func (c *Client) GetAvailabilities(ctx context.Context, params *AvailabilityParams) (*AvailabilitiesResponse, error) {
b, err := json.Marshal(params)
if err != nil {
return nil, fmt.Errorf("fail to marshal availability params. Error: %w", err)
}
queryParams := url.Values{
"query": {string(b)},
}
resp, err := c.get(
ctx,
fmt.Sprintf("/inventory/api/v1/inventory-results?%s", queryParams.Encode()),
)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer func() {
if err := resp.Body.Close(); err != nil {
fmt.Printf("Error closing body: %s", err)
}
}()
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("fail to read body. Error: %w", err)
}
availabilities := AvailabilitiesResponse{}
err = json.Unmarshal(body, &availabilities)
if err != nil {
return nil, fmt.Errorf("fail to unmarshal response. Error: %w", err)
}
return &availabilities, nil
}

307
api/struct.go Normal file
View File

@ -0,0 +1,307 @@
package api
import "time"
// Availability represent a available car with its characteristics.
type Availability struct {
InTransit bool `json:"InTransit,omitempty"`
AdlOpts any `json:"ADL_OPTS,omitempty"`
Autopilot []string `json:"AUTOPILOT,omitempty"`
AcquisitionSubType any `json:"AcquisitionSubType,omitempty"`
AcquisitionType any `json:"AcquisitionType,omitempty"`
ActualGAInDate string `json:"ActualGAInDate,omitempty"`
Battery any `json:"BATTERY,omitempty"`
CabinConfig []string `json:"CABIN_CONFIG,omitempty"`
CPORefurbishmentStatus any `json:"CPORefurbishmentStatus,omitempty"`
City string `json:"City,omitempty"`
CompositorViews struct {
FrontView string `json:"frontView,omitempty"`
SideView string `json:"sideView,omitempty"`
InteriorView string `json:"interiorView,omitempty"`
} `json:"CompositorViews,omitempty"`
CountryCode string `json:"CountryCode,omitempty"`
CountryCodes []string `json:"CountryCodes,omitempty"`
CountryHasVehicleAtLocation bool `json:"CountryHasVehicleAtLocation,omitempty"`
CountryOfOrigin string `json:"CountryOfOrigin,omitempty"`
CurrencyCode string `json:"CurrencyCode,omitempty"`
CurrencyCodes string `json:"CurrencyCodes,omitempty"`
Decor any `json:"DECOR,omitempty"`
Drive []string `json:"DRIVE,omitempty"`
DamageDisclosureStatus any `json:"DamageDisclosureStatus,omitempty"`
DestinationHandlingFee int `json:"DestinationHandlingFee,omitempty"`
Discount int `json:"Discount,omitempty"`
DisplayWarranty bool `json:"DisplayWarranty,omitempty"`
EtaToCurrent string `json:"EtaToCurrent,omitempty"`
FactoryCode string `json:"FactoryCode,omitempty"`
FactoryDepartureDate string `json:"FactoryDepartureDate,omitempty"`
FixedAssets bool `json:"FixedAssets,omitempty"`
FlexibleOptionsData []struct {
Code string `json:"code,omitempty"`
Description string `json:"description,omitempty"`
Group string `json:"group,omitempty"`
LongName string `json:"long_name,omitempty"`
Name string `json:"name,omitempty"`
Price int `json:"price,omitempty"`
} `json:"FlexibleOptionsData,omitempty"`
ForecastedFactoryGatedDate any `json:"ForecastedFactoryGatedDate,omitempty"`
Headliner any `json:"HEADLINER,omitempty"`
HasDamagePhotos bool `json:"HasDamagePhotos,omitempty"`
HasOptionCodeData bool `json:"HasOptionCodeData,omitempty"`
Hash string `json:"Hash,omitempty"`
Interior []string `json:"INTERIOR,omitempty"`
IncentivesDetails struct {
Current struct {
Fuel struct {
Data []struct {
Algorithm bool `json:"algorithm,omitempty"`
Amount any `json:"amount,omitempty"`
Description string `json:"description,omitempty"`
IncentiveType string `json:"incentiveType,omitempty"`
Market string `json:"market,omitempty"`
Period string `json:"period,omitempty"`
Variables struct {
Distance any `json:"distance,omitempty"`
FuelEfficiencyImperial any `json:"fuel_efficiency_imperial,omitempty"`
FuelEfficiencyMetric float64 `json:"fuel_efficiency_metric,omitempty"`
FuelPrice float64 `json:"fuel_price,omitempty"`
KwhConsumption float64 `json:"kwh_consumption,omitempty"`
KwhPrice float64 `json:"kwh_price,omitempty"`
Months int `json:"months,omitempty"`
TollSavings int `json:"toll_savings,omitempty"`
} `json:"variables,omitempty"`
Variant string `json:"variant,omitempty"`
} `json:"data,omitempty"`
Total int `json:"total,omitempty"`
} `json:"fuel,omitempty"`
} `json:"current,omitempty"`
Total struct {
Fuel any `json:"fuel,omitempty"`
IncludedInPurchasePrice int `json:"includedInPurchasePrice,omitempty"`
Monthly int `json:"monthly,omitempty"`
Once int `json:"once,omitempty"`
} `json:"total,omitempty"`
} `json:"IncentivesDetails,omitempty"`
InspectionDocumentGUID any `json:"InspectionDocumentGuid,omitempty"`
InventoryPrice int `json:"InventoryPrice,omitempty"`
IsAtLocation bool `json:"IsAtLocation,omitempty"`
IsChargingConnectorIncluded bool `json:"IsChargingConnectorIncluded,omitempty"`
IsDemo bool `json:"IsDemo,omitempty"`
IsFactoryGated bool `json:"IsFactoryGated,omitempty"`
IsInTransit bool `json:"IsInTransit,omitempty"`
IsLegacy bool `json:"IsLegacy,omitempty"`
IsPreProdWithDisclaimer bool `json:"IsPreProdWithDisclaimer,omitempty"`
IsTegra bool `json:"IsTegra,omitempty"`
Language string `json:"Language,omitempty"`
Languages []string `json:"Languages,omitempty"`
LexiconDefaultOptions []struct {
Code string `json:"code,omitempty"`
Description string `json:"description,omitempty"`
Group string `json:"group,omitempty"`
LongName string `json:"long_name,omitempty"`
Name string `json:"name,omitempty"`
} `json:"LexiconDefaultOptions,omitempty"`
ListingType string `json:"ListingType,omitempty"`
ListingTypes string `json:"ListingTypes,omitempty"`
MarketingInUseDate any `json:"MarketingInUseDate,omitempty"`
Model string `json:"Model,omitempty"`
Odometer int `json:"Odometer,omitempty"`
OdometerType string `json:"OdometerType,omitempty"`
OnConfiguratorPricePercentage int `json:"OnConfiguratorPricePercentage,omitempty"`
OptionCodeData []struct {
AccelerationUnitLong string `json:"acceleration_unit_long,omitempty"`
AccelerationUnitShort string `json:"acceleration_unit_short,omitempty"`
AccelerationValue string `json:"acceleration_value,omitempty"`
Code string `json:"code,omitempty"`
Group string `json:"group,omitempty"`
Price int `json:"price,omitempty"`
UnitLong string `json:"unit_long,omitempty"`
UnitShort string `json:"unit_short,omitempty"`
Value string `json:"value,omitempty"`
TopSpeedLabel string `json:"top_speed_label,omitempty"`
RangeLabelSource string `json:"range_label_source,omitempty"`
RangeSource string `json:"range_source,omitempty"`
RangeSourceInventoryNew string `json:"range_source_inventory_new,omitempty"`
Description string `json:"description,omitempty"`
LongName string `json:"long_name,omitempty"`
Name string `json:"name,omitempty"`
} `json:"OptionCodeData,omitempty"`
OptionCodeList string `json:"OptionCodeList,omitempty"`
OptionCodeListDisplayOnly any `json:"OptionCodeListDisplayOnly,omitempty"`
OptionCodePricing []struct {
Code string `json:"code,omitempty"`
Group string `json:"group,omitempty"`
Price int `json:"price,omitempty"`
} `json:"OptionCodePricing,omitempty"`
OrderFee struct {
Type string `json:"type,omitempty"`
Value int `json:"value,omitempty"`
} `json:"OrderFee,omitempty"`
OriginalDeliveryDate any `json:"OriginalDeliveryDate,omitempty"`
OriginalInCustomerGarageDate any `json:"OriginalInCustomerGarageDate,omitempty"`
Paint []string `json:"PAINT,omitempty"`
PlannedGADailyDate string `json:"PlannedGADailyDate,omitempty"`
Price int64 `json:"Price,omitempty"`
PurchasePrice int `json:"PurchasePrice,omitempty"`
Roof any `json:"ROOF,omitempty"`
RegistrationCount int `json:"RegistrationCount,omitempty"`
SteeringWheel any `json:"STEERING_WHEEL,omitempty"`
SalesMetro string `json:"SalesMetro,omitempty"`
StateProvince string `json:"StateProvince,omitempty"`
StateProvinceLongName string `json:"StateProvinceLongName,omitempty"`
Trim []string `json:"TRIM,omitempty"`
TaxScheme any `json:"TaxScheme,omitempty"`
ThirdPartyHistoryURL any `json:"ThirdPartyHistoryUrl,omitempty"`
TitleStatus string `json:"TitleStatus,omitempty"`
TitleSubtype []string `json:"TitleSubtype,omitempty"`
TotalPrice int `json:"TotalPrice,omitempty"`
TradeInType any `json:"TradeInType,omitempty"`
TransportFees struct {
ExemptVRL []any `json:"exemptVRL,omitempty"`
Fees []any `json:"fees,omitempty"`
MetroFees []any `json:"metro_fees,omitempty"`
UnfundedLocationFees []any `json:"unfunded_location_fees,omitempty"`
} `json:"TransportFees,omitempty"`
TrimCode string `json:"TrimCode,omitempty"`
TrimName string `json:"TrimName,omitempty"`
Trt int `json:"Trt,omitempty"`
TrtName string `json:"TrtName,omitempty"`
Vin string `json:"VIN,omitempty"`
VehicleHistory any `json:"VehicleHistory,omitempty"`
VehicleRegion string `json:"VehicleRegion,omitempty"`
VrlName string `json:"VrlName,omitempty"`
Wheels []string `json:"WHEELS,omitempty"`
WarrantyBatteryExpDate time.Time `json:"WarrantyBatteryExpDate,omitempty"`
WarrantyBatteryIsExpired bool `json:"WarrantyBatteryIsExpired,omitempty"`
WarrantyBatteryMile int `json:"WarrantyBatteryMile,omitempty"`
WarrantyBatteryYear int `json:"WarrantyBatteryYear,omitempty"`
WarrantyData struct {
UsedVehicleLimitedWarrantyMile int `json:"UsedVehicleLimitedWarrantyMile,omitempty"`
UsedVehicleLimitedWarrantyYear int `json:"UsedVehicleLimitedWarrantyYear,omitempty"`
WarrantyBatteryExpDate time.Time `json:"WarrantyBatteryExpDate,omitempty"`
WarrantyBatteryIsExpired bool `json:"WarrantyBatteryIsExpired,omitempty"`
WarrantyBatteryMile int `json:"WarrantyBatteryMile,omitempty"`
WarrantyBatteryYear int `json:"WarrantyBatteryYear,omitempty"`
WarrantyDriveUnitExpDate time.Time `json:"WarrantyDriveUnitExpDate,omitempty"`
WarrantyDriveUnitMile int `json:"WarrantyDriveUnitMile,omitempty"`
WarrantyDriveUnitYear int `json:"WarrantyDriveUnitYear,omitempty"`
WarrantyMile int `json:"WarrantyMile,omitempty"`
WarrantyVehicleExpDate time.Time `json:"WarrantyVehicleExpDate,omitempty"`
WarrantyVehicleIsExpired bool `json:"WarrantyVehicleIsExpired,omitempty"`
WarrantyYear int `json:"WarrantyYear,omitempty"`
} `json:"WarrantyData,omitempty"`
WarrantyDriveUnitExpDate time.Time `json:"WarrantyDriveUnitExpDate,omitempty"`
WarrantyDriveUnitMile int `json:"WarrantyDriveUnitMile,omitempty"`
WarrantyDriveUnitYear int `json:"WarrantyDriveUnitYear,omitempty"`
WarrantyMile int `json:"WarrantyMile,omitempty"`
WarrantyVehicleExpDate time.Time `json:"WarrantyVehicleExpDate,omitempty"`
WarrantyVehicleIsExpired bool `json:"WarrantyVehicleIsExpired,omitempty"`
WarrantyYear int `json:"WarrantyYear,omitempty"`
Year int `json:"Year,omitempty"`
AlternateCurrency []any `json:"AlternateCurrency,omitempty"`
UsedVehicleLimitedWarrantyMile int `json:"UsedVehicleLimitedWarrantyMile,omitempty"`
UsedVehicleLimitedWarrantyYear int `json:"UsedVehicleLimitedWarrantyYear,omitempty"`
OdometerTypeShort string `json:"OdometerTypeShort,omitempty"`
DeliveryDateDisplay bool `json:"DeliveryDateDisplay,omitempty"`
TransportationFee int `json:"TransportationFee,omitempty"`
VrlList []struct {
Vrl int `json:"vrl,omitempty"`
Lat int `json:"lat,omitempty"`
Lon int `json:"lon,omitempty"`
VrlLocks []any `json:"vrlLocks,omitempty"`
} `json:"vrlList,omitempty"`
OptionCodeSpecs struct {
CSpecs struct {
Code string `json:"code,omitempty"`
Name string `json:"name,omitempty"`
Options []struct {
Code string `json:"code,omitempty"`
Name string `json:"name,omitempty"`
LongName string `json:"long_name,omitempty"`
Description string `json:"description,omitempty"`
} `json:"options,omitempty"`
} `json:"C_SPECS,omitempty"`
CDesign struct {
Code string `json:"code,omitempty"`
Name string `json:"name,omitempty"`
Options []any `json:"options,omitempty"`
} `json:"C_DESIGN,omitempty"`
CCallouts struct {
Code string `json:"code,omitempty"`
Name string `json:"name,omitempty"`
Options []struct {
Code string `json:"code,omitempty"`
Name string `json:"name,omitempty"`
LongName string `json:"long_name,omitempty"`
Description string `json:"description,omitempty"`
Group string `json:"group,omitempty"`
List []string `json:"list,omitempty"`
Period string `json:"period,omitempty"`
} `json:"options,omitempty"`
} `json:"C_CALLOUTS,omitempty"`
COpts struct {
Code string `json:"code,omitempty"`
Name string `json:"name,omitempty"`
Options []struct {
Code string `json:"code,omitempty"`
Name string `json:"name,omitempty"`
LongName string `json:"long_name,omitempty"`
Description string `json:"description,omitempty"`
} `json:"options,omitempty"`
} `json:"C_OPTS,omitempty"`
} `json:"OptionCodeSpecs,omitempty"`
CompositorViewsCustom struct {
IsProductWithCustomViews bool `json:"isProductWithCustomViews,omitempty"`
ExternalZoom struct {
Order int `json:"order,omitempty"`
Search int `json:"search,omitempty"`
} `json:"externalZoom,omitempty"`
ExternalCrop struct {
Order string `json:"order,omitempty"`
Search string `json:"search,omitempty"`
} `json:"externalCrop,omitempty"`
} `json:"CompositorViewsCustom,omitempty"`
IsRangeStandard bool `json:"IsRangeStandard,omitempty"`
MetroName string `json:"MetroName,omitempty"`
GeoPoints [][]any `json:"geoPoints,omitempty"`
HasMarketingOptions bool `json:"HasMarketingOptions,omitempty"`
InTransitMetroName string `json:"InTransitMetroName,omitempty"`
InTransitSalesMetro string `json:"InTransitSalesMetro,omitempty"`
FirstRegistrationDate any `json:"FirstRegistrationDate,omitempty"`
}
// AvailabilitiesResponse contain the a list of car availability.
type AvailabilitiesResponse struct {
Results []Availability `json:"results,omitempty"`
TotalMatchesFound string `json:"total_matches_found,omitempty"`
}
// AvailabilityParams is the params accepted by the API.
type AvailabilityParams struct {
Query AvailabilityQueryParams `json:"query"`
Offset int `json:"offset"`
Count int `json:"count"`
OutsideOffset int `json:"outsideOffset"`
OutsideSearch bool `json:"outsideSearch"`
}
// AvailabilityQueryParams are the params to filter results.
type AvailabilityQueryParams struct {
Model string `json:"model"`
Condition string `json:"condition"`
Options OptionsParams `json:"options"`
Arrangeby string `json:"arrangeby"`
Order string `json:"order"`
Market string `json:"market"`
Language string `json:"language"`
SuperRegion string `json:"super_region"`
Lng float64 `json:"lng"`
Lat float64 `json:"lat"`
Zip string `json:"zip"`
Range int `json:"range"`
Region string `json:"region"`
}
// OptionsParams contain the car option.
type OptionsParams struct {
Trim []string `json:"TRIM"`
}