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.
- aioamazondevices/__init__.py +1 -1
- aioamazondevices/api.py +169 -35
- aioamazondevices/const.py +15 -5
- aioamazondevices/exceptions.py +4 -0
- {aioamazondevices-0.6.0.dist-info → aioamazondevices-0.7.1.dist-info}/METADATA +3 -1
- aioamazondevices-0.7.1.dist-info/RECORD +9 -0
- {aioamazondevices-0.6.0.dist-info → aioamazondevices-0.7.1.dist-info}/WHEEL +1 -1
- aioamazondevices-0.6.0.dist-info/RECORD +0 -9
- {aioamazondevices-0.6.0.dist-info → aioamazondevices-0.7.1.dist-info}/LICENSE +0 -0
aioamazondevices/__init__.py
CHANGED
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
|
-
|
22
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
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":
|
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
|
115
|
+
def _build_client_id(self) -> str:
|
106
116
|
"""Build client ID."""
|
107
|
-
client_id =
|
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
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
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
|
-
|
216
|
+
raw_data: str | dict,
|
211
217
|
url: str,
|
212
|
-
extension: str =
|
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
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
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
|
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(
|
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
|
-
|
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":
|
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
|
-
|
46
|
+
# Amazon APP info
|
47
|
+
AMAZON_APP_BUNDLE_ID = "com.amazon.echo"
|
44
48
|
AMAZON_APP_ID = "MAPiOSLib/6.0/ToHideRetailLink"
|
45
|
-
|
46
|
-
|
47
|
-
|
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"
|
aioamazondevices/exceptions.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: aioamazondevices
|
3
|
-
Version: 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,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,,
|
File without changes
|