aioamazondevices 0.6.0__py3-none-any.whl → 0.7.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  """aioamazondevices library."""
2
2
 
3
- __version__ = "0.6.0"
3
+ __version__ = "0.7.1"
4
4
 
5
5
 
6
6
  from .api import AmazonDevice, AmazonEchoApi
aioamazondevices/api.py CHANGED
@@ -2,29 +2,37 @@
2
2
 
3
3
  import base64
4
4
  import hashlib
5
+ import mimetypes
5
6
  import secrets
6
7
  import uuid
7
8
  from dataclasses import dataclass
9
+ from datetime import UTC, datetime, timedelta
10
+ from http import HTTPStatus
8
11
  from pathlib import Path
9
12
  from typing import Any
10
- from urllib.parse import urlencode
13
+ from urllib.parse import parse_qs, urlencode
11
14
 
12
15
  import orjson
13
16
  from bs4 import BeautifulSoup, Tag
14
- from httpx import AsyncClient, Response
17
+ from httpx import URL, AsyncClient, Response
15
18
 
16
19
  from .const import (
17
20
  _LOGGER,
18
21
  AMAZON_APP_BUNDLE_ID,
19
22
  AMAZON_APP_ID,
23
+ AMAZON_APP_NAME,
20
24
  AMAZON_APP_VERSION,
21
- AMAZON_SERIAL_NUMBER,
22
- AMAZON_SOFTWARE_VERSION,
25
+ AMAZON_CLIENT_OS,
26
+ AMAZON_DEVICE_SOFTWARE_VERSION,
27
+ AMAZON_DEVICE_TYPE,
28
+ DEFAULT_ASSOC_HANDLE,
23
29
  DEFAULT_HEADERS,
24
30
  DOMAIN_BY_COUNTRY,
31
+ HTML_EXTENSION,
32
+ JSON_EXTENSION,
25
33
  URI_QUERIES,
26
34
  )
27
- from .exceptions import CannotAuthenticate
35
+ from .exceptions import CannotAuthenticate, CannotRegisterDevice
28
36
 
29
37
 
30
38
  @dataclass
@@ -57,9 +65,10 @@ class AmazonEchoApi:
57
65
  locale = DOMAIN_BY_COUNTRY.get(country_code)
58
66
  domain = locale["domain"] if locale else country_code
59
67
 
60
- assoc_handle = "amzn_dp_project_dee_ios"
61
- if not locale:
62
- assoc_handle += f"_{country_code}"
68
+ if locale and (assoc := locale.get("openid.assoc_handle")):
69
+ assoc_handle = assoc
70
+ else:
71
+ assoc_handle = f"{DEFAULT_ASSOC_HANDLE}_{country_code}"
63
72
  self._assoc_handle = assoc_handle
64
73
 
65
74
  self._login_email = login_email
@@ -69,6 +78,7 @@ class AmazonEchoApi:
69
78
  self._cookies = self._build_init_cookies()
70
79
  self._headers = DEFAULT_HEADERS
71
80
  self._save_html = save_html
81
+ self._serial = uuid.uuid4().hex.upper()
72
82
 
73
83
  self.session: AsyncClient
74
84
 
@@ -80,7 +90,7 @@ class AmazonEchoApi:
80
90
  map_md_dict = {
81
91
  "device_user_dictionary": [],
82
92
  "device_registration_data": {
83
- "software_version": AMAZON_SOFTWARE_VERSION,
93
+ "software_version": AMAZON_DEVICE_SOFTWARE_VERSION,
84
94
  },
85
95
  "app_identifier": {
86
96
  "app_version": AMAZON_APP_VERSION,
@@ -102,9 +112,9 @@ class AmazonEchoApi:
102
112
  m = hashlib.sha256(verifier)
103
113
  return base64.urlsafe_b64encode(m.digest()).rstrip(b"=")
104
114
 
105
- def _build_client_id(self, serial: str) -> str:
115
+ def _build_client_id(self) -> str:
106
116
  """Build client ID."""
107
- client_id = serial.encode() + AMAZON_SERIAL_NUMBER
117
+ client_id = self._serial.encode() + b"#" + AMAZON_DEVICE_TYPE.encode("utf-8")
108
118
  return client_id.hex()
109
119
 
110
120
  def _build_oauth_url(
@@ -161,6 +171,11 @@ class AmazonEchoApi:
161
171
  return method, url
162
172
  raise TypeError("Unable to extract form data from response.")
163
173
 
174
+ def _extract_code_from_url(self, url: URL) -> str:
175
+ """Extract the access token from url query after login."""
176
+ parsed_url = parse_qs(url.query.decode())
177
+ return parsed_url["openid.oa2.authorization_code"][0]
178
+
164
179
  def _client_session(self) -> None:
165
180
  """Create httpx ClientSession."""
166
181
  if not hasattr(self, "session") or self.session.is_closed:
@@ -185,31 +200,22 @@ class AmazonEchoApi:
185
200
  url,
186
201
  data=input_data,
187
202
  )
188
- content_type = resp.headers.get("Content-Type", "")
203
+ content_type: str = resp.headers.get("Content-Type", "")
189
204
  _LOGGER.debug("Response content type: %s", content_type)
190
205
 
191
- if "text/html" in content_type:
192
- await self._save_to_file(
193
- resp.text,
194
- url,
195
- )
196
- elif content_type == "application/json":
197
- await self._save_to_file(
198
- orjson.dumps(
199
- orjson.loads(resp.text),
200
- option=orjson.OPT_INDENT_2,
201
- ).decode("utf-8"),
202
- url,
203
- extension="json",
204
- )
206
+ await self._save_to_file(
207
+ resp.text,
208
+ url,
209
+ mimetypes.guess_extension(content_type.split(";")[0]) or ".raw",
210
+ )
205
211
 
206
212
  return BeautifulSoup(resp.content, "html.parser"), resp
207
213
 
208
214
  async def _save_to_file(
209
215
  self,
210
- html_code: str,
216
+ raw_data: str | dict,
211
217
  url: str,
212
- extension: str = "html",
218
+ extension: str = HTML_EXTENSION,
213
219
  output_path: str = "out",
214
220
  ) -> None:
215
221
  """Save response data to disk."""
@@ -219,19 +225,138 @@ class AmazonEchoApi:
219
225
  output_dir = Path(output_path)
220
226
  output_dir.mkdir(parents=True, exist_ok=True)
221
227
 
222
- url_split = url.split("/")
223
- filename = f"{url_split[3]}-{url_split[4].split('?')[0]}.{extension}"
224
- with Path.open(output_dir / filename, "w+") as file:
225
- file.write(html_code)
228
+ if url.startswith("http"):
229
+ url_split = url.split("/")
230
+ base_filename = f"{url_split[3]}-{url_split[4].split('?')[0]}"
231
+ else:
232
+ base_filename = url
233
+ fullpath = Path(output_dir, base_filename + extension)
234
+
235
+ if type(raw_data) is dict:
236
+ data = orjson.dumps(raw_data, option=orjson.OPT_INDENT_2).decode("utf-8")
237
+ elif extension == HTML_EXTENSION:
238
+ data = raw_data
239
+ else:
240
+ data = orjson.dumps(
241
+ orjson.loads(raw_data),
242
+ option=orjson.OPT_INDENT_2,
243
+ ).decode("utf-8")
244
+
245
+ i = 2
246
+ while fullpath.exists():
247
+ filename = f"{base_filename}_{i!s}{extension}"
248
+ fullpath = Path(output_dir, filename)
249
+ i += 1
250
+
251
+ _LOGGER.warning("Saving data to %s", fullpath)
252
+
253
+ with Path.open(fullpath, "w+") as file:
254
+ file.write(data)
226
255
  file.write("\n")
227
256
 
228
- async def login(self, otp_code: str) -> bool:
257
+ async def _register_device(
258
+ self,
259
+ data: dict[str, Any],
260
+ ) -> dict[str, Any]:
261
+ """Register a dummy Alexa device."""
262
+ authorization_code: str = data["authorization_code"]
263
+ code_verifier: bytes = data["code_verifier"]
264
+
265
+ body = {
266
+ "requested_extensions": ["device_info", "customer_info"],
267
+ "cookies": {"website_cookies": [], "domain": f".amazon.{self._domain}"},
268
+ "registration_data": {
269
+ "domain": "Device",
270
+ "app_version": AMAZON_APP_VERSION,
271
+ "device_type": AMAZON_DEVICE_TYPE,
272
+ "device_name": (
273
+ f"%FIRST_NAME%\u0027s%DUPE_STRATEGY_1ST%{AMAZON_APP_NAME}"
274
+ ),
275
+ "os_version": AMAZON_CLIENT_OS,
276
+ "device_serial": self._serial,
277
+ "device_model": "iPhone",
278
+ "app_name": AMAZON_APP_NAME,
279
+ "software_version": AMAZON_DEVICE_SOFTWARE_VERSION,
280
+ },
281
+ "auth_data": {
282
+ "client_id": self._build_client_id(),
283
+ "authorization_code": authorization_code,
284
+ "code_verifier": code_verifier.decode(),
285
+ "code_algorithm": "SHA-256",
286
+ "client_domain": "DeviceLegacy",
287
+ },
288
+ "user_context_map": {"frc": self._cookies["frc"]},
289
+ "requested_token_type": [
290
+ "bearer",
291
+ "mac_dms",
292
+ "website_cookies",
293
+ "store_authentication_cookie",
294
+ ],
295
+ }
296
+
297
+ headers = {"Content-Type": "application/json"}
298
+
299
+ register_url = f"https://api.amazon.{self._domain}/auth/register"
300
+ resp = await self.session.post(
301
+ register_url,
302
+ json=body,
303
+ headers=headers,
304
+ )
305
+ resp_json = resp.json()
306
+
307
+ if resp.status_code != HTTPStatus.OK:
308
+ _LOGGER.error(
309
+ "Cannot register device for %s: %s",
310
+ self._login_email,
311
+ resp_json["response"]["error"]["message"],
312
+ )
313
+ raise CannotRegisterDevice(resp_json)
314
+
315
+ await self._save_to_file(
316
+ resp.text,
317
+ url=register_url,
318
+ extension=JSON_EXTENSION,
319
+ )
320
+ success_response = resp_json["response"]["success"]
321
+
322
+ tokens = success_response["tokens"]
323
+ adp_token = tokens["mac_dms"]["adp_token"]
324
+ device_private_key = tokens["mac_dms"]["device_private_key"]
325
+ store_authentication_cookie = tokens["store_authentication_cookie"]
326
+ access_token = tokens["bearer"]["access_token"]
327
+ refresh_token = tokens["bearer"]["refresh_token"]
328
+ expires_s = int(tokens["bearer"]["expires_in"])
329
+ expires = (datetime.now(UTC) + timedelta(seconds=expires_s)).timestamp()
330
+
331
+ extensions = success_response["extensions"]
332
+ device_info = extensions["device_info"]
333
+ customer_info = extensions["customer_info"]
334
+
335
+ website_cookies = {}
336
+ for cookie in tokens["website_cookies"]:
337
+ website_cookies[cookie["Name"]] = cookie["Value"].replace(r'"', r"")
338
+
339
+ login_data = {
340
+ "adp_token": adp_token,
341
+ "device_private_key": device_private_key,
342
+ "access_token": access_token,
343
+ "refresh_token": refresh_token,
344
+ "expires": expires,
345
+ "website_cookies": website_cookies,
346
+ "store_authentication_cookie": store_authentication_cookie,
347
+ "device_info": device_info,
348
+ "customer_info": customer_info,
349
+ }
350
+ await self._save_to_file(login_data, "login_data", JSON_EXTENSION)
351
+ return login_data
352
+
353
+ async def login(self, otp_code: str) -> dict[str, Any]:
229
354
  """Login to Amazon."""
230
355
  _LOGGER.debug("Logging-in for %s [otp code %s]", self._login_email, otp_code)
231
356
  self._client_session()
232
357
 
233
358
  code_verifier = self._create_code_verifier()
234
- client_id = self._build_client_id(uuid.uuid4().hex.upper())
359
+ client_id = self._build_client_id()
235
360
 
236
361
  _LOGGER.debug("Build oauth URL")
237
362
  login_url = self._build_oauth_url(code_verifier, client_id)
@@ -279,7 +404,16 @@ class AmazonEchoApi:
279
404
  if authcode_url is None:
280
405
  raise CannotAuthenticate
281
406
 
282
- return True
407
+ device_login_data = {
408
+ "authorization_code": self._extract_code_from_url(authcode_url),
409
+ "code_verifier": code_verifier,
410
+ "domain": self._domain,
411
+ }
412
+
413
+ register_device = await self._register_device(device_login_data)
414
+
415
+ _LOGGER.info("Register device: %s", register_device)
416
+ return register_device
283
417
 
284
418
  async def close(self) -> None:
285
419
  """Close httpx session."""
aioamazondevices/const.py CHANGED
@@ -4,10 +4,12 @@ import logging
4
4
 
5
5
  _LOGGER = logging.getLogger(__package__)
6
6
 
7
+ DEFAULT_ASSOC_HANDLE = "amzn_dp_project_dee_ios"
8
+
7
9
  DOMAIN_BY_COUNTRY = {
8
10
  "us": {
9
11
  "domain": "com",
10
- "openid.assoc_handle": "amzn_dp_project_dee_ios",
12
+ "openid.assoc_handle": DEFAULT_ASSOC_HANDLE,
11
13
  },
12
14
  "uk": {
13
15
  "domain": "co.uk",
@@ -17,6 +19,7 @@ DOMAIN_BY_COUNTRY = {
17
19
  },
18
20
  "jp": {
19
21
  "domain": "co.jp",
22
+ "openid.assoc_handle": "jpflex",
20
23
  },
21
24
  "br": {
22
25
  "domain": "com.br",
@@ -40,8 +43,15 @@ URI_QUERIES = {
40
43
  "bluetooth": "/api/bluetooth",
41
44
  }
42
45
 
43
- AMAZON_APP_BUNDLE_ID = "com.audible.iphone"
46
+ # Amazon APP info
47
+ AMAZON_APP_BUNDLE_ID = "com.amazon.echo"
44
48
  AMAZON_APP_ID = "MAPiOSLib/6.0/ToHideRetailLink"
45
- AMAZON_APP_VERSION = "3.56.2"
46
- AMAZON_SOFTWARE_VERSION = "35602678"
47
- AMAZON_SERIAL_NUMBER = b"#A2CZJZGLK2JJVM"
49
+ AMAZON_APP_NAME = "AioAmazonDevices"
50
+ AMAZON_APP_VERSION = "2.2.556530.0"
51
+ AMAZON_DEVICE_SOFTWARE_VERSION = "35602678"
52
+ AMAZON_DEVICE_TYPE = "A2IVLV5VM2W81"
53
+ AMAZON_CLIENT_OS = "16.6"
54
+
55
+ # File extensions
56
+ HTML_EXTENSION = ".html"
57
+ JSON_EXTENSION = ".json"
@@ -17,3 +17,7 @@ class CannotAuthenticate(AmazonError):
17
17
 
18
18
  class CannotRetrieveData(AmazonError):
19
19
  """Exception raised when data retrieval fails."""
20
+
21
+
22
+ class CannotRegisterDevice(AmazonError):
23
+ """Exception raised when device registration fails."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: aioamazondevices
3
- Version: 0.6.0
3
+ Version: 0.7.1
4
4
  Summary: Python library to control Amazon devices
5
5
  Home-page: https://github.com/chemelli74/aioamazondevices
6
6
  License: Apache Software License 2.0
@@ -15,8 +15,10 @@ Classifier: Operating System :: OS Independent
15
15
  Classifier: Programming Language :: Python :: 3
16
16
  Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
18
19
  Classifier: Topic :: Software Development :: Libraries
19
20
  Requires-Dist: beautifulsoup4
21
+ Requires-Dist: colorlog
20
22
  Requires-Dist: httpx
21
23
  Requires-Dist: orjson
22
24
  Project-URL: Bug Tracker, https://github.com/chemelli74/aioamazondevices/issues
@@ -0,0 +1,9 @@
1
+ aioamazondevices/__init__.py,sha256=IxGu-Lv8_pSEPCQnjhVi-_VdJGmh9WI0NOELkRSSVjQ,276
2
+ aioamazondevices/api.py,sha256=3Gshun5XSEbD47aDs-hyQkZ_zVYuQ3OMwXIaZA13lmo,15902
3
+ aioamazondevices/const.py,sha256=CQFuM-4gFrzveQcBMsH6noI8PN69J4ogV-P46uYhcTQ,1330
4
+ aioamazondevices/exceptions.py,sha256=yQ9nL4UwBdHNXvdRj8TRemed6PXBmExP8lbHaAp04vY,546
5
+ aioamazondevices/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ aioamazondevices-0.7.1.dist-info/LICENSE,sha256=sS48k5sp9bFV-NSHDfAJuTZZ_-AP9ZDqUzQ9sffGlsg,11346
7
+ aioamazondevices-0.7.1.dist-info/METADATA,sha256=Eq-0torKLJNk79WSTqq2I4pnPqAM-VsQ3u8ibrVytUo,4755
8
+ aioamazondevices-0.7.1.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
9
+ aioamazondevices-0.7.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.0
2
+ Generator: poetry-core 1.9.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,9 +0,0 @@
1
- aioamazondevices/__init__.py,sha256=yo5hkSgxhpMQLUQO67XfINgQNxrncsWR32abuZzsals,276
2
- aioamazondevices/api.py,sha256=_KQtNmw7mOEMOxWmLl0MAQj_MLnTngL_zJzgxQUqfwg,10830
3
- aioamazondevices/const.py,sha256=bZaeO8AeJbDc5hdlbJ3cMwM9teTgYhExSR1oEpRFMLk,1089
4
- aioamazondevices/exceptions.py,sha256=tERMur_gry9TmU3UyzndJO_CLViISn4b8ClrRbryFy8,444
5
- aioamazondevices/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- aioamazondevices-0.6.0.dist-info/LICENSE,sha256=sS48k5sp9bFV-NSHDfAJuTZZ_-AP9ZDqUzQ9sffGlsg,11346
7
- aioamazondevices-0.6.0.dist-info/METADATA,sha256=kE5iT7Eid9foSh4dKTHaLVGw_0TezvDqCGDWZtH3Qes,4680
8
- aioamazondevices-0.6.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
9
- aioamazondevices-0.6.0.dist-info/RECORD,,