From ff6cbabe4bc3c53c93d138047463ad5042d3f8e1 Mon Sep 17 00:00:00 2001 From: Amerigo Date: Sun, 1 Mar 2026 12:07:55 +0000 Subject: [PATCH] Support split-domain Alexa auth/runtime for EU accounts --- README.md | 38 +++- cmd/alexa/auth.go | 26 ++- cmd/alexa/root.go | 2 +- internal/api/client.go | 356 ++++++++++++++++++++++---------------- internal/config/config.go | 7 +- 5 files changed, 264 insertions(+), 165 deletions(-) diff --git a/README.md b/README.md index 6f0e858..e421870 100644 --- a/README.md +++ b/README.md @@ -39,11 +39,13 @@ alexacli auth This downloads [alexa-cookie-cli](https://github.com/adn77/alexa-cookie-cli) on first run, opens a browser at http://127.0.0.1:8080 for Amazon login, and automatically captures and saves your refresh token. -For non-US accounts: +For non-US accounts (recommended split-domain login flow): ```bash -alexacli auth --domain amazon.de -alexacli auth --domain amazon.co.uk +# Base auth domain (.com) + local marketplace country page (.it/.de/.co.uk...) +alexacli auth --domain amazon.com --country amazon.it +alexacli auth --domain amazon.com --country amazon.de +alexacli auth --domain amazon.com --country amazon.co.uk ``` ### Manual Token @@ -69,6 +71,21 @@ alexacli auth logout Configuration is stored in `~/.alexa-cli/config.json`. +### Config fields + +```json +{ + "refresh_token": "Atnr|...", + "amazon_domain": "amazon.com", + "amazon_local": "amazon.it", + "default_device": "YOUR_DEVICE_SERIAL" +} +``` + +- `amazon_domain`: auth/token domain (usually `amazon.com`) +- `amazon_local`: local marketplace/runtime domain (e.g. `amazon.it`, `amazon.de`) +- If `amazon_local` is omitted, the CLI falls back to `amazon_domain`. + ## Usage ### List Devices @@ -353,6 +370,12 @@ alexacli command "turn on lights" -d Kitchen --verbose Run `alexacli auth` to configure your refresh token. +For EU/IT accounts, prefer: + +```bash +alexacli auth --domain amazon.com --country amazon.it +``` + ### Device not found Use `alexacli devices` to see exact device names, then match them in your commands. Partial matching is supported. @@ -365,9 +388,12 @@ Try running the same command with `alexacli command` instead - this sends it as This CLI uses the same unofficial API that the Alexa mobile app uses. It: -1. Exchanges your refresh token for session cookies -2. Obtains a CSRF token from alexa.amazon.com -3. Sends commands to pitangui.amazon.com (US) or layla.amazon.com (EU) +1. Exchanges your refresh token for session cookies via Amazon auth (`api.amazon.com` with your `amazon_domain`) +2. Obtains CSRF tokens from Alexa web endpoints (`alexa.*`, with fallback hosts) +3. Sends command APIs to regional runtime endpoints: + - US/CA/BR/IN: `pitangui.*` + - EU (including IT): `layla.amazon.com` +4. For history/privacy APIs, uses `www.` with fallback to auth/global domains when needed (to handle temporary marketplace-side blocks) This approach is used by many popular projects including [alexa-remote-control](https://github.com/thorsten-gehrig/alexa-remote-control) and [Home Assistant's Alexa integration](https://github.com/alandtse/alexa_media_player). diff --git a/cmd/alexa/auth.go b/cmd/alexa/auth.go index 62aa1e1..0e36f1a 100644 --- a/cmd/alexa/auth.go +++ b/cmd/alexa/auth.go @@ -20,6 +20,7 @@ const cookieCLIVersion = "v5.0.1" func newAuthCmd(flags *rootFlags) *cobra.Command { var domain string + var country string cmd := &cobra.Command{ Use: "auth [refresh-token]", @@ -42,7 +43,7 @@ Or set the ALEXA_REFRESH_TOKEN environment variable.`, token = args[0] } else { // Try browser-based auth flow - t, err := runBrowserAuth(domain) + t, err := runBrowserAuth(domain, country) if err != nil { fmt.Fprintf(os.Stderr, "Browser auth failed: %v\n", err) fmt.Fprintf(os.Stderr, "Falling back to manual token entry.\n\n") @@ -64,7 +65,7 @@ Or set the ALEXA_REFRESH_TOKEN environment variable.`, } // Validate the token by trying to authenticate - client, err := api.NewClient(token, domain) + client, err := api.NewClientWithLocal(token, domain, country) if err != nil { return fmt.Errorf("authentication failed: %w", err) } @@ -79,6 +80,7 @@ Or set the ALEXA_REFRESH_TOKEN environment variable.`, cfg := &config.Config{ RefreshToken: token, AmazonDomain: domain, + AmazonLocal: country, } if err := config.Save(cfg); err != nil { @@ -90,7 +92,8 @@ Or set the ALEXA_REFRESH_TOKEN environment variable.`, }, } - cmd.Flags().StringVar(&domain, "domain", "amazon.com", "Amazon domain (amazon.com, amazon.de, amazon.co.uk, etc.)") + cmd.Flags().StringVar(&domain, "domain", "amazon.com", "Base Amazon domain for login/token exchange (usually amazon.com)") + cmd.Flags().StringVar(&country, "country", "amazon.it", "Marketplace country page for login (e.g. amazon.it, amazon.de)") cmd.AddCommand(newAuthStatusCmd(flags)) cmd.AddCommand(newAuthLogoutCmd(flags)) @@ -118,10 +121,10 @@ func newAuthStatusCmd(flags *rootFlags) *cobra.Command { masked = masked[:8] + "..." } - status := fmt.Sprintf("Domain: %s\nToken: %s", cfg.AmazonDomain, masked) + status := fmt.Sprintf("Domain: %s\nLocal: %s\nToken: %s", cfg.AmazonDomain, cfg.AmazonLocal, masked) if verify { - client, err := api.NewClient(cfg.RefreshToken, cfg.AmazonDomain) + client, err := api.NewClientWithLocal(cfg.RefreshToken, cfg.AmazonDomain, cfg.AmazonLocal) if err != nil { status += "\nStatus: invalid (authentication failed)" return out.Success(status) @@ -268,14 +271,21 @@ func localeFlags(domain string) []string { // runBrowserAuth launches alexa-cookie-cli to perform browser-based Amazon login. // Returns the captured refresh token. -func runBrowserAuth(domain string) (string, error) { +func runBrowserAuth(domain, country string) (string, error) { + if domain == "" { + domain = "amazon.com" + } + if country == "" { + country = domain + } + binPath, err := ensureCookieCLI() if err != nil { return "", err } - args := []string{"-b", domain, "-p", domain} - args = append(args, localeFlags(domain)...) + args := []string{"-b", domain, "-p", country} + args = append(args, localeFlags(country)...) args = append(args, "-q") fmt.Fprintf(os.Stderr, "Opening browser for Amazon login at http://127.0.0.1:8080 ...\n") diff --git a/cmd/alexa/root.go b/cmd/alexa/root.go index 48cbd88..c2cf4c2 100644 --- a/cmd/alexa/root.go +++ b/cmd/alexa/root.go @@ -68,7 +68,7 @@ func getClientWithFlags(flags *rootFlags) (*api.Client, error) { return nil, err } - client, err := api.NewClient(cfg.RefreshToken, cfg.AmazonDomain) + client, err := api.NewClientWithLocal(cfg.RefreshToken, cfg.AmazonDomain, cfg.AmazonLocal) if err != nil { return nil, err } diff --git a/internal/api/client.go b/internal/api/client.go index cda95c7..d9bd246 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -18,7 +18,8 @@ type Client struct { cookies string csrf string activityCSRF string // separate CSRF for activity/history endpoints - amazonDomain string // e.g., "amazon.com" + amazonDomain string // auth domain, e.g., "amazon.com" + amazonLocal string // local marketplace/runtime domain, e.g., "amazon.it" customerID string bearerToken string // Atna| token for AVS APIs conversationID string // Current conversation ID for Alexa+ @@ -37,13 +38,28 @@ func (c *Client) log(format string, args ...interface{}) { } } +func (c *Client) localDomain() string { + if c.amazonLocal != "" { + return c.amazonLocal + } + return c.amazonDomain +} + // NewClient creates a new Alexa API client func NewClient(refreshToken, amazonDomain string) (*Client, error) { + return NewClientWithLocal(refreshToken, amazonDomain, "") +} + +func NewClientWithLocal(refreshToken, amazonDomain, amazonLocal string) (*Client, error) { + if amazonLocal == "" { + amazonLocal = amazonDomain + } client := &Client{ httpClient: &http.Client{ Timeout: 30 * time.Second, }, amazonDomain: amazonDomain, + amazonLocal: amazonLocal, refreshToken: refreshToken, } @@ -125,31 +141,38 @@ func (c *Client) authenticate(refreshToken string) error { // fetchCSRF retrieves the CSRF token from Amazon func (c *Client) fetchCSRF() error { - // Use the language API endpoint which returns CSRF as a cookie - csrfURL := fmt.Sprintf("https://alexa.%s/api/language", c.amazonDomain) - - req, err := http.NewRequest("GET", csrfURL, nil) - if err != nil { - return err + tryURLs := []string{ + fmt.Sprintf("https://alexa.%s/api/language", c.localDomain()), + fmt.Sprintf("https://alexa.%s/api/language", c.amazonDomain), + "https://alexa.amazon.com/api/language", + "https://layla.amazon.com/api/language", + "https://pitangui.amazon.com/api/language", } - req.Header.Set("Cookie", c.cookies) - req.Header.Set("Accept", "application/json") + for _, csrfURL := range tryURLs { + req, err := http.NewRequest("GET", csrfURL, nil) + if err != nil { + continue + } + req.Header.Set("Cookie", c.cookies) + req.Header.Set("Accept", "application/json") - resp, err := c.httpClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() + resp, err := c.httpClient.Do(req) + if err != nil { + continue + } - // Extract csrf from response cookies - for _, cookie := range resp.Cookies() { - if cookie.Name == "csrf" { - c.csrf = cookie.Value - // Also add csrf to our cookie string for future requests - c.cookies = c.cookies + "; csrf=" + cookie.Value - return nil + for _, cookie := range resp.Cookies() { + if cookie.Name == "csrf" { + resp.Body.Close() + c.csrf = cookie.Value + if !strings.Contains(c.cookies, "csrf=") { + c.cookies = c.cookies + "; csrf=" + cookie.Value + } + return nil + } } + resp.Body.Close() } // Try to extract from existing cookies (may already be present) @@ -166,20 +189,12 @@ func (c *Client) fetchCSRF() error { // baseURL returns the Alexa API base URL func (c *Client) baseURL() string { // pitangui for US, layla for EU/UK - // The subdomain changes based on region, but the TLD matches the user's amazon domain - switch c.amazonDomain { + // Runtime is based on local marketplace domain + switch c.localDomain() { case "amazon.com": return "https://pitangui.amazon.com" - case "amazon.co.uk": - return "https://layla.amazon.co.uk" - case "amazon.de": - return "https://layla.amazon.de" - case "amazon.fr": - return "https://layla.amazon.fr" - case "amazon.it": - return "https://layla.amazon.it" - case "amazon.es": - return "https://layla.amazon.es" + case "amazon.co.uk", "amazon.de", "amazon.fr", "amazon.it", "amazon.es": + return "https://layla.amazon.com" case "amazon.co.jp": return "https://layla.amazon.co.jp" case "amazon.com.au": @@ -192,13 +207,13 @@ func (c *Client) baseURL() string { return "https://pitangui.amazon.in" default: // Fall back to layla with the user's domain - return fmt.Sprintf("https://layla.%s", c.amazonDomain) + return fmt.Sprintf("https://layla.%s", c.localDomain()) } } // alexaURL returns the alexa.amazon.com base URL func (c *Client) alexaURL() string { - return fmt.Sprintf("https://alexa.%s", c.amazonDomain) + return fmt.Sprintf("https://alexa.%s", c.localDomain()) } // request makes an authenticated request to the Alexa API (pitangui/layla) @@ -263,13 +278,13 @@ func (c *Client) doRequest(baseURL, method, endpoint string, body interface{}) ( // Device represents an Alexa device type Device struct { - AccountName string `json:"accountName"` - SerialNumber string `json:"serialNumber"` - DeviceType string `json:"deviceType"` - DeviceFamily string `json:"deviceFamily"` - DeviceOwnerCustomerID string `json:"deviceOwnerCustomerId"` - Online bool `json:"online"` - Capabilities []string `json:"capabilities"` + AccountName string `json:"accountName"` + SerialNumber string `json:"serialNumber"` + DeviceType string `json:"deviceType"` + DeviceFamily string `json:"deviceFamily"` + DeviceOwnerCustomerID string `json:"deviceOwnerCustomerId"` + Online bool `json:"online"` + Capabilities []string `json:"capabilities"` } // GetDevices returns all Alexa devices @@ -410,9 +425,9 @@ func (c *Client) ExecuteRoutine(device *Device, routineName string) error { // Execute the routine payload := map[string]interface{}{ - "behaviorId": targetRoutine.AutomationID, + "behaviorId": targetRoutine.AutomationID, "sequenceJson": targetRoutine.Sequence, - "status": "ENABLED", + "status": "ENABLED", } _, err = c.request("POST", "/api/behaviors/preview", payload) @@ -435,8 +450,8 @@ func (c *Client) GetRoutines() ([]Routine, error) { } var rawRoutines []struct { - AutomationID string `json:"automationId"` - Name string `json:"name"` + AutomationID string `json:"automationId"` + Name string `json:"name"` Sequence json.RawMessage `json:"sequence"` } @@ -458,12 +473,12 @@ func (c *Client) GetRoutines() ([]Routine, error) { // SmartHomeDevice represents a smart home device type SmartHomeDevice struct { - EntityID string `json:"entityId"` - ApplianceID string `json:"applianceId"` - Name string `json:"friendlyName"` - Description string `json:"friendlyDescription"` - Type string `json:"applianceTypes"` - Reachable bool `json:"isReachable"` + EntityID string `json:"entityId"` + ApplianceID string `json:"applianceId"` + Name string `json:"friendlyName"` + Description string `json:"friendlyDescription"` + Type string `json:"applianceTypes"` + Reachable bool `json:"isReachable"` } // GetSmartHomeDevices returns all smart home devices @@ -503,7 +518,7 @@ func (c *Client) ControlSmartHome(entityID string, action string, value interfac payload = map[string]interface{}{ "controlRequests": []map[string]interface{}{ { - "entityId": entityID, + "entityId": entityID, "entityType": "APPLIANCE", "parameters": map[string]interface{}{ "action": "turnOn", @@ -515,7 +530,7 @@ func (c *Client) ControlSmartHome(entityID string, action string, value interfac payload = map[string]interface{}{ "controlRequests": []map[string]interface{}{ { - "entityId": entityID, + "entityId": entityID, "entityType": "APPLIANCE", "parameters": map[string]interface{}{ "action": "turnOff", @@ -527,10 +542,10 @@ func (c *Client) ControlSmartHome(entityID string, action string, value interfac payload = map[string]interface{}{ "controlRequests": []map[string]interface{}{ { - "entityId": entityID, + "entityId": entityID, "entityType": "APPLIANCE", "parameters": map[string]interface{}{ - "action": "setBrightness", + "action": "setBrightness", "brightness": value, }, }, @@ -546,60 +561,76 @@ func (c *Client) ControlSmartHome(entityID string, action string, value interfac // fetchActivityCSRF retrieves the CSRF token needed for activity/history endpoints func (c *Client) fetchActivityCSRF() error { - activityURL := fmt.Sprintf("https://www.%s/alexa-privacy/apd/activity?ref=activityHistory", c.amazonDomain) - - req, err := http.NewRequest("GET", activityURL, nil) - if err != nil { - return err + tryURLs := []string{ + fmt.Sprintf("https://www.%s/alexa-privacy/apd/activity?ref=activityHistory", c.localDomain()), + fmt.Sprintf("https://www.%s/alexa-privacy/apd/activity?ref=activityHistory", c.amazonDomain), + "https://www.amazon.com/alexa-privacy/apd/activity?ref=activityHistory", } - req.Header.Set("Cookie", c.cookies) - req.Header.Set("Accept", "text/html,application/xhtml+xml") - req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36") + for _, activityURL := range tryURLs { + req, err := http.NewRequest("GET", activityURL, nil) + if err != nil { + continue + } - resp, err := c.httpClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() + req.Header.Set("Cookie", c.cookies) + req.Header.Set("Accept", "text/html,application/xhtml+xml") + req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36") - body, err := io.ReadAll(resp.Body) - if err != nil { - return err - } + resp, err := c.httpClient.Do(req) + if err != nil { + continue + } - bodyStr := string(body) + body, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + continue + } - // Try multiple patterns for the CSRF token - // Pattern 1: meta tag - re := regexp.MustCompile(`= 2 { - c.activityCSRF = matches[1] - return nil - } + // First, check response cookies for csrf directly + for _, ck := range resp.Cookies() { + if ck.Name == "csrf" && ck.Value != "" { + c.activityCSRF = ck.Value + return nil + } + } - // Pattern 2: data attribute - re2 := regexp.MustCompile(`data-csrf="([^"]+)"`) - matches = re2.FindStringSubmatch(bodyStr) - if len(matches) >= 2 { - c.activityCSRF = matches[1] - return nil - } + bodyStr := string(body) - // Pattern 3: JavaScript variable - re3 := regexp.MustCompile(`"csrfToken"\s*:\s*"([^"]+)"`) - matches = re3.FindStringSubmatch(bodyStr) - if len(matches) >= 2 { - c.activityCSRF = matches[1] - return nil + // Try multiple patterns for the CSRF token + re := regexp.MustCompile(`= 2 { + c.activityCSRF = matches[1] + return nil + } + + re2 := regexp.MustCompile(`data-csrf="([^"]+)"`) + matches = re2.FindStringSubmatch(bodyStr) + if len(matches) >= 2 { + c.activityCSRF = matches[1] + return nil + } + + re3 := regexp.MustCompile(`"csrfToken"\s*:\s*"([^"]+)"`) + matches = re3.FindStringSubmatch(bodyStr) + if len(matches) >= 2 { + c.activityCSRF = matches[1] + return nil + } + + re4 := regexp.MustCompile(`anti-csrftoken-a2z['":\s]+['"]([^'"]+)['"]`) + matches = re4.FindStringSubmatch(bodyStr) + if len(matches) >= 2 { + c.activityCSRF = matches[1] + return nil + } } - // Pattern 4: anti-csrf in any form - re4 := regexp.MustCompile(`anti-csrftoken-a2z['":\s]+['"]([^'"]+)['"]`) - matches = re4.FindStringSubmatch(bodyStr) - if len(matches) >= 2 { - c.activityCSRF = matches[1] + // Practical fallback: in many accounts this matches the required anti-csrftoken-a2z + if c.csrf != "" { + c.activityCSRF = c.csrf return nil } @@ -608,11 +639,11 @@ func (c *Client) fetchActivityCSRF() error { // HistoryRecord represents a voice history record type HistoryRecord struct { - RecordKey string `json:"recordKey"` - Timestamp int64 `json:"timestamp"` - Device string `json:"device"` - CustomerUtterance string `json:"customerUtterance"` // What you said (ASR) - AlexaResponse string `json:"alexaResponse"` // What Alexa said (TTS) + RecordKey string `json:"recordKey"` + Timestamp int64 `json:"timestamp"` + Device string `json:"device"` + CustomerUtterance string `json:"customerUtterance"` // What you said (ASR) + AlexaResponse string `json:"alexaResponse"` // What Alexa said (TTS) } // GetCustomerHistoryRecords retrieves recent voice activity history @@ -624,48 +655,75 @@ func (c *Client) GetCustomerHistoryRecords(startTime, endTime int64) ([]HistoryR } } - // Build URL with time range - historyURL := fmt.Sprintf( - "https://www.%s/alexa-privacy/apd/rvh/customer-history-records-v2/?startTime=%d&endTime=%d&pageType=VOICE_HISTORY", - c.amazonDomain, startTime, endTime, - ) + tryDomains := []string{c.localDomain(), c.amazonDomain, "amazon.com"} + seen := map[string]bool{} + var respBody []byte + var lastErr error + var done bool - body := bytes.NewReader([]byte(`{"previousRequestToken": null}`)) - req, err := http.NewRequest("POST", historyURL, body) - if err != nil { - return nil, err - } + for _, d := range tryDomains { + if d == "" || seen[d] { + continue + } + seen[d] = true - req.Header.Set("Cookie", c.cookies) - req.Header.Set("csrf", c.csrf) - req.Header.Set("anti-csrftoken-a2z", c.activityCSRF) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "application/json, text/plain, */*") - req.Header.Set("Accept-Language", "en-US,en;q=0.9") - req.Header.Set("Origin", fmt.Sprintf("https://www.%s", c.amazonDomain)) - req.Header.Set("Referer", fmt.Sprintf("https://www.%s/alexa-privacy/apd/activity?ref=activityHistory", c.amazonDomain)) - req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") + historyURL := fmt.Sprintf( + "https://www.%s/alexa-privacy/apd/rvh/customer-history-records-v2/?startTime=%d&endTime=%d&pageType=VOICE_HISTORY", + d, startTime, endTime, + ) - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() + body := bytes.NewReader([]byte(`{"previousRequestToken": null}`)) + req, err := http.NewRequest("POST", historyURL, body) + if err != nil { + lastErr = err + continue + } - respBody, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err + req.Header.Set("Cookie", c.cookies) + req.Header.Set("csrf", c.csrf) + req.Header.Set("anti-csrftoken-a2z", c.activityCSRF) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json, text/plain, */*") + req.Header.Set("Accept-Language", "it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7") + req.Header.Set("Origin", fmt.Sprintf("https://www.%s", d)) + req.Header.Set("Referer", fmt.Sprintf("https://www.%s/alexa-privacy/apd/activity?ref=activityHistory", d)) + req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") + + resp, err := c.httpClient.Do(req) + if err != nil { + lastErr = err + continue + } + + b, readErr := io.ReadAll(resp.Body) + resp.Body.Close() + if readErr != nil { + lastErr = readErr + continue + } + + if resp.StatusCode >= 400 { + lastErr = fmt.Errorf("history API error %d: %s", resp.StatusCode, string(b)) + continue + } + + respBody = b + done = true + break } - if resp.StatusCode >= 400 { - return nil, fmt.Errorf("history API error %d: %s", resp.StatusCode, string(respBody)) + if !done { + if lastErr != nil { + return nil, lastErr + } + return nil, fmt.Errorf("history API failed on all candidate domains") } // Parse the response var result struct { CustomerHistoryRecords []struct { - RecordKey string `json:"recordKey"` - Timestamp int64 `json:"timestamp"` + RecordKey string `json:"recordKey"` + Timestamp int64 `json:"timestamp"` VoiceHistoryRecordItems []struct { RecordItemType string `json:"recordItemType"` TranscriptText string `json:"transcriptText"` @@ -751,7 +809,7 @@ func (c *Client) Ask(device *Device, question string, timeout time.Duration) (st // Check if the utterance matches our question (case-insensitive partial match) if record.CustomerUtterance != "" && - strings.Contains(strings.ToLower(record.CustomerUtterance), strings.ToLower(question[:min(len(question), 20)])) { + strings.Contains(strings.ToLower(record.CustomerUtterance), strings.ToLower(question[:min(len(question), 20)])) { if record.AlexaResponse != "" { return record.AlexaResponse, nil } @@ -774,9 +832,9 @@ func mustJSON(v interface{}) string { return string(data) } -// locale returns the appropriate locale for the user's Amazon domain +// locale returns the appropriate locale for the user's Amazon local domain func (c *Client) locale() string { - switch c.amazonDomain { + switch c.localDomain() { case "amazon.com": return "en-US" case "amazon.co.uk": @@ -811,7 +869,7 @@ func (c *Client) locale() string { // avsURL returns the AVS API base URL func (c *Client) avsURL() string { // AVS has regional endpoints - switch c.amazonDomain { + switch c.localDomain() { case "amazon.com", "amazon.ca", "amazon.com.br": return "https://avs-alexa-12-na.amazon.com" case "amazon.co.uk", "amazon.de", "amazon.fr", "amazon.it", "amazon.es", "amazon.in": @@ -1143,7 +1201,7 @@ func (c *Client) buildAVSContext() []map[string]interface{} { }, "payload": map[string]interface{}{ "dialog": map[string]interface{}{ - "interface": "SpeechSynthesizer", + "interface": "SpeechSynthesizer", "idleTimeInMilliseconds": 100000, }, }, @@ -1184,13 +1242,13 @@ func (c *Client) buildAVSContext() []map[string]interface{} { "name": "PlaybackState", }, "payload": map[string]interface{}{ - "state": "IDLE", - "shuffle": "NOT_SHUFFLED", - "repeat": "NOT_REPEATED", - "favorite": "NOT_RATED", + "state": "IDLE", + "shuffle": "NOT_SHUFFLED", + "repeat": "NOT_REPEATED", + "favorite": "NOT_RATED", "positionMilliseconds": 0, - "supportedOperations": []string{"Play", "Pause", "Previous", "Next"}, - "players": []interface{}{}, + "supportedOperations": []string{"Play", "Pause", "Previous", "Next"}, + "players": []interface{}{}, }, }, { @@ -1276,8 +1334,8 @@ func (c *Client) buildAVSContext() []map[string]interface{} { "height": 932, }, "scrollable": map[string]interface{}{ - "direction": "vertical", - "allowForward": false, + "direction": "vertical", + "allowForward": false, "allowBackward": true, }, "elements": []interface{}{}, diff --git a/internal/config/config.go b/internal/config/config.go index 6c96aa6..fb60de5 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -15,7 +15,8 @@ const ( // Config holds the Alexa CLI configuration type Config struct { RefreshToken string `json:"refresh_token"` - AmazonDomain string `json:"amazon_domain,omitempty"` // e.g., "amazon.com", "amazon.de" + AmazonDomain string `json:"amazon_domain,omitempty"` // auth domain, e.g. amazon.com + AmazonLocal string `json:"amazon_local,omitempty"` // local marketplace/runtime domain, e.g. amazon.it DeviceSerial string `json:"default_device,omitempty"` } @@ -53,6 +54,7 @@ func Load() (*Config, error) { return &Config{ RefreshToken: token, AmazonDomain: getEnvOrDefault("ALEXA_AMAZON_DOMAIN", "amazon.com"), + AmazonLocal: os.Getenv("ALEXA_AMAZON_LOCAL"), }, nil } @@ -82,6 +84,9 @@ func Load() (*Config, error) { if cfg.AmazonDomain == "" { cfg.AmazonDomain = "amazon.com" } + if cfg.AmazonLocal == "" { + cfg.AmazonLocal = cfg.AmazonDomain + } return &cfg, nil }