@camstack/addon-import-alexa 0.2.148 → 0.2.150
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.
- package/CHANGELOG.md +9 -0
- package/assets/alexa-echo.LICENSE.md +13 -0
- package/assets/alexa-echo.svg +1 -0
- package/dist/MaskShapeCanvas-B6q5RDLi.mjs +13807 -0
- package/dist/MotionZonesSettings-Dxo0EAzN.mjs +309 -0
- package/dist/PrivacyMaskSettings-DwWxpZrj.mjs +368 -0
- package/dist/SceneMonitorEditor-CC0dk5TR.mjs +846 -0
- package/dist/_stub.js +39566 -0
- package/dist/_virtual_mf-localSharedImportMap___mfe_internal__addon_import_alexa_page-CcQjQJmj.mjs +156 -0
- package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare___mf_0_camstack_mf_1_sdk__loadShare__.js-BmoaT3cr.mjs +25 -0
- package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Msberbw_.mjs +26 -0
- package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-Bct5qZia.mjs +25 -0
- package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare___mf_0_trpc_mf_1_client__loadShare__.js-BHqOw8oh.mjs +25 -0
- package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare___mf_0_trpc_mf_1_react_mf_2_query__loadShare__.js-B3qxfxBT.mjs +26 -0
- package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare__react__loadShare__.js-DwXXYAlk.mjs +70 -0
- package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-CUiVYNMY.mjs +26 -0
- package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare__react_mf_2_dom__loadShare__.js-CIPmAyPi.mjs +25 -0
- package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare__react_mf_2_dom_mf_1_client__loadShare__.js-tXm1x5EZ.mjs +25 -0
- package/dist/addon-import-alexa.css +3 -0
- package/dist/addon.js +2708 -149
- package/dist/addon.mjs +2724 -168
- package/dist/hostInit-CRUwUtSm.mjs +129 -0
- package/dist/index.js +1 -0
- package/dist/index.mjs +2 -2
- package/dist/player-overlays-DNpsEqAe.mjs +51 -0
- package/dist/remoteEntry.js +2 -0
- package/dist/remoteEntry.ssr.js +33 -0
- package/dist/responsive-B82v3AbM.mjs +125 -0
- package/dist/rolldown-runtime-HEgqtunE.mjs +20 -0
- package/dist/scene-monitor-copy-CPnjwzw6.mjs +227 -0
- package/dist/square-YTl_L1_3.mjs +45 -0
- package/dist/trash-2-DCHugLVt.mjs +25 -0
- package/dist/ui-text-B5YO4hoJ.mjs +34 -0
- package/dist/virtualExposes-lZT06jeg.mjs +2 -0
- package/dist/virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_import_alexa_page__remoteEntry_js-DMdgQOgz.mjs +2857 -0
- package/dist/virtual_mf-exposes-ssr___mfe_internal__addon_import_alexa_page__remoteEntry_js-CViESMdd.mjs +10 -0
- package/dist/virtual_mf-exposes___mfe_internal__addon_import_alexa_page__remoteEntry_js-C3qJCjtV.mjs +27 -0
- package/i18n/en.json +8 -1
- package/i18n/it.json +8 -1
- package/package.json +26 -4
- package/python/NOTICE +4 -0
- package/python/alexa_gateway.py +226 -0
- package/python/alexa_protocol.py +115 -0
- package/python/alexa_sequences.py +74 -0
- package/python/alexa_worker.py +50 -0
- package/python/requirements.txt +3 -0
- package/python/test_alexa_gateway.py +141 -0
- package/python/test_alexa_protocol.py +177 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""The gateway signs in as a broker's account, with alexapy replaced by a fake module.
|
|
2
|
+
|
|
3
|
+
These tests must not contact Amazon. They check what the gateway hands to alexapy
|
|
4
|
+
(the broker's token, the registered app name, where the cookie jar lives) and what
|
|
5
|
+
it does when Amazon refuses.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
import types
|
|
14
|
+
import unittest
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
18
|
+
|
|
19
|
+
from alexa_gateway import AlexaPyGateway, bare_domain, normalize_device, session_output
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def install_fake_alexapy(exchange_ok: bool = True, stored_cookies: dict | None = None):
|
|
23
|
+
"""Put a fake `alexapy` (and `alexapy.alexalogin`) in sys.modules; return the call log."""
|
|
24
|
+
calls: dict = {"exchange_app_names": [], "logins": [], "saved": 0}
|
|
25
|
+
module = types.ModuleType("alexapy.alexalogin")
|
|
26
|
+
module.APP_NAME = "Alexa Media Player"
|
|
27
|
+
|
|
28
|
+
class FakeLogin:
|
|
29
|
+
def __init__(self, url, email, password, outputpath, debug=False, oauth=None, **_):
|
|
30
|
+
calls["url"] = url
|
|
31
|
+
calls["email"] = email
|
|
32
|
+
calls["password"] = password
|
|
33
|
+
calls["oauth"] = dict(oauth or {})
|
|
34
|
+
calls["jar_path"] = outputpath(".storage/alexa_media.x.pickle")
|
|
35
|
+
self.status = {"login_successful": True}
|
|
36
|
+
self.refresh_token = (oauth or {}).get("refresh_token")
|
|
37
|
+
|
|
38
|
+
async def exchange_token_for_cookies(self):
|
|
39
|
+
calls["exchange_app_names"].append(module.APP_NAME)
|
|
40
|
+
calls["exchanged_token"] = self.refresh_token
|
|
41
|
+
return exchange_ok
|
|
42
|
+
|
|
43
|
+
async def save_cookiefile(self):
|
|
44
|
+
calls["saved"] += 1
|
|
45
|
+
Path(calls["jar_path"]).write_bytes(b"jar")
|
|
46
|
+
|
|
47
|
+
async def load_cookie(self):
|
|
48
|
+
return {"session-id": "1"} if (exchange_ok or stored_cookies) else {}
|
|
49
|
+
|
|
50
|
+
async def login(self, cookies=None, data=None):
|
|
51
|
+
calls["logins"].append(cookies)
|
|
52
|
+
|
|
53
|
+
package = types.ModuleType("alexapy")
|
|
54
|
+
package.AlexaLogin = FakeLogin
|
|
55
|
+
package.AlexaAPI = object
|
|
56
|
+
package.alexalogin = module
|
|
57
|
+
sys.modules["alexapy"] = package
|
|
58
|
+
sys.modules["alexapy.alexalogin"] = module
|
|
59
|
+
return calls, module
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def remove_fake_alexapy() -> None:
|
|
63
|
+
sys.modules.pop("alexapy", None)
|
|
64
|
+
sys.modules.pop("alexapy.alexalogin", None)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def run(coro):
|
|
68
|
+
return asyncio.run(coro)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class GatewayTests(unittest.TestCase):
|
|
72
|
+
def tearDown(self) -> None:
|
|
73
|
+
remove_fake_alexapy()
|
|
74
|
+
|
|
75
|
+
def test_exchanges_the_brokers_token_under_its_registered_name_and_restores_the_default(self) -> None:
|
|
76
|
+
calls, module = install_fake_alexapy()
|
|
77
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
78
|
+
session_path = str(Path(directory) / "session")
|
|
79
|
+
result = run(
|
|
80
|
+
AlexaPyGateway().login_broker(
|
|
81
|
+
"alexa_001", "broker-refresh-token", "Registered App", "amazon.it", "EU", session_path
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
self.assertEqual(result, {"status": "ok"})
|
|
85
|
+
self.assertEqual(Path(session_path).read_bytes(), b"jar")
|
|
86
|
+
self.assertEqual(calls["exchanged_token"], "broker-refresh-token")
|
|
87
|
+
self.assertEqual(calls["exchange_app_names"], ["Registered App"])
|
|
88
|
+
self.assertEqual(module.APP_NAME, "Alexa Media Player")
|
|
89
|
+
self.assertEqual(calls["url"], "amazon.it")
|
|
90
|
+
self.assertEqual(calls["password"], "")
|
|
91
|
+
self.assertEqual(calls["email"], "alexa_001")
|
|
92
|
+
self.assertNotIn("@", calls["email"])
|
|
93
|
+
self.assertEqual(len(calls["logins"]), 1)
|
|
94
|
+
|
|
95
|
+
def test_an_account_two_brokers_share_is_still_two_labels(self) -> None:
|
|
96
|
+
first, _ = install_fake_alexapy()
|
|
97
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
98
|
+
run(AlexaPyGateway().login_broker("alexa_001", "t1", "", "amazon.it", "EU", str(Path(directory) / "s1")))
|
|
99
|
+
second, _ = install_fake_alexapy()
|
|
100
|
+
run(AlexaPyGateway().login_broker("alexa_002", "t2", "", "amazon.com", "NA", str(Path(directory) / "s2")))
|
|
101
|
+
self.assertEqual(first["email"], "alexa_001")
|
|
102
|
+
self.assertEqual(second["email"], "alexa_002")
|
|
103
|
+
self.assertEqual(second["exchanged_token"], "t2")
|
|
104
|
+
|
|
105
|
+
def test_a_token_amazon_refuses_is_a_named_failure_and_never_logs_in(self) -> None:
|
|
106
|
+
calls, module = install_fake_alexapy(exchange_ok=False)
|
|
107
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
108
|
+
result = run(
|
|
109
|
+
AlexaPyGateway().login_broker("alexa_001", "bad", "X", "amazon.com", "NA", str(Path(directory) / "s"))
|
|
110
|
+
)
|
|
111
|
+
self.assertEqual(result["status"], "failed")
|
|
112
|
+
self.assertNotIn("bad", result["error"])
|
|
113
|
+
self.assertEqual(calls["logins"], [])
|
|
114
|
+
self.assertEqual(module.APP_NAME, "Alexa Media Player")
|
|
115
|
+
|
|
116
|
+
def test_with_no_token_only_the_stored_jar_is_tried(self) -> None:
|
|
117
|
+
calls, _ = install_fake_alexapy(stored_cookies={"session-id": "1"})
|
|
118
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
119
|
+
result = run(
|
|
120
|
+
AlexaPyGateway().login_broker("alexa_001", "", "", "amazon.it", "EU", str(Path(directory) / "s"))
|
|
121
|
+
)
|
|
122
|
+
self.assertEqual(result, {"status": "ok"})
|
|
123
|
+
self.assertEqual(calls["exchange_app_names"], [])
|
|
124
|
+
|
|
125
|
+
def test_helpers(self) -> None:
|
|
126
|
+
self.assertEqual(bare_domain("https://alexa.amazon.co.uk/"), "amazon.co.uk")
|
|
127
|
+
self.assertEqual(bare_domain("www.amazon.de"), "amazon.de")
|
|
128
|
+
self.assertEqual(bare_domain(""), "amazon.com")
|
|
129
|
+
output = session_output("/tmp/x/session")
|
|
130
|
+
self.assertEqual(output(".storage/alexa_media.a.pickle"), "/tmp/x/session")
|
|
131
|
+
self.assertEqual(output("alexa_media.a.pickle"), "/tmp/x/alexa_media.a.pickle")
|
|
132
|
+
|
|
133
|
+
def test_a_room_is_passed_on_only_when_amazon_names_one(self) -> None:
|
|
134
|
+
with_room = normalize_device({"serialNumber": "s", "accountName": "Kitchen", "roomName": "Living room"})
|
|
135
|
+
without = normalize_device({"serialNumber": "s", "accountName": "Kitchen"})
|
|
136
|
+
self.assertEqual(with_room["room"], "Living room")
|
|
137
|
+
self.assertNotIn("room", without)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == "__main__":
|
|
141
|
+
unittest.main()
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Protocol handlers with alexapy mocked. These tests must not contact Amazon."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
import unittest
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
13
|
+
|
|
14
|
+
from alexa_protocol import Session
|
|
15
|
+
from alexa_sequences import build_announcement, build_speak_sequence
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FakeGateway:
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
self.refresh_token = ""
|
|
21
|
+
self.region = "EU"
|
|
22
|
+
self.login_status = "ok"
|
|
23
|
+
self.devices_payload = {
|
|
24
|
+
"devices": [
|
|
25
|
+
{
|
|
26
|
+
"id": "echo-1",
|
|
27
|
+
"name": "Kitchen",
|
|
28
|
+
"family": "ECHO",
|
|
29
|
+
"online": True,
|
|
30
|
+
"kind": "device",
|
|
31
|
+
"dnd": False,
|
|
32
|
+
"deviceType": "A3S5BH2HU6VAYF",
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
self.spoken: list[dict] = []
|
|
37
|
+
self.announced: list[dict] = []
|
|
38
|
+
|
|
39
|
+
async def login_broker(
|
|
40
|
+
self,
|
|
41
|
+
label: str,
|
|
42
|
+
refresh_token: str,
|
|
43
|
+
device_app_name: str,
|
|
44
|
+
domain: str,
|
|
45
|
+
region: str,
|
|
46
|
+
session_path: str,
|
|
47
|
+
) -> dict:
|
|
48
|
+
self.refresh_token = refresh_token
|
|
49
|
+
self.label = label
|
|
50
|
+
self.device_app_name = device_app_name
|
|
51
|
+
self.domain = domain
|
|
52
|
+
self.region = region
|
|
53
|
+
self.session_path = session_path
|
|
54
|
+
result = {"status": self.login_status}
|
|
55
|
+
if self.login_status == "failed":
|
|
56
|
+
result["error"] = "login failed"
|
|
57
|
+
return result
|
|
58
|
+
|
|
59
|
+
async def devices(self) -> dict:
|
|
60
|
+
return self.devices_payload
|
|
61
|
+
|
|
62
|
+
async def speak(self, sequence: dict, customer_id: str) -> None:
|
|
63
|
+
self.spoken.append({"sequence": sequence, "customerId": customer_id})
|
|
64
|
+
|
|
65
|
+
async def announce(self, announcement: dict, customer_id: str) -> None:
|
|
66
|
+
self.announced.append(announcement)
|
|
67
|
+
|
|
68
|
+
async def session_status(self) -> dict:
|
|
69
|
+
return {"loggedIn": True, "ageSec": 12, "lastSuccessAt": 100}
|
|
70
|
+
|
|
71
|
+
def target_for(self, serial: str):
|
|
72
|
+
if serial != "echo-1":
|
|
73
|
+
return None
|
|
74
|
+
return {"id": "echo-1", "deviceType": "A3S5BH2HU6VAYF", "customerId": "cust"}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def run(session: Session, message: dict) -> dict:
|
|
78
|
+
return asyncio.run(session.handle(message))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ProtocolTests(unittest.TestCase):
|
|
82
|
+
def test_login_reads_refresh_token_from_file_not_from_json(self) -> None:
|
|
83
|
+
gateway = FakeGateway()
|
|
84
|
+
session = Session(gateway)
|
|
85
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
86
|
+
token_path = Path(directory) / "refresh-token"
|
|
87
|
+
token_path.write_text("super-secret-token\n", encoding="utf-8")
|
|
88
|
+
session_path = Path(directory) / "session"
|
|
89
|
+
session_path.write_bytes(b"")
|
|
90
|
+
body = {
|
|
91
|
+
"id": "1",
|
|
92
|
+
"op": "login_broker",
|
|
93
|
+
"label": "alexa_001",
|
|
94
|
+
"refreshTokenPath": str(token_path),
|
|
95
|
+
"deviceAppName": "Registered App",
|
|
96
|
+
"domain": "amazon.it",
|
|
97
|
+
"sessionPath": str(session_path),
|
|
98
|
+
"region": "EU",
|
|
99
|
+
}
|
|
100
|
+
encoded = json.dumps(body)
|
|
101
|
+
self.assertNotIn("super-secret-token", encoded)
|
|
102
|
+
result = run(session, body)
|
|
103
|
+
self.assertTrue(result["ok"])
|
|
104
|
+
self.assertEqual(result["result"]["status"], "ok")
|
|
105
|
+
self.assertEqual(gateway.refresh_token, "super-secret-token")
|
|
106
|
+
self.assertEqual(gateway.label, "alexa_001")
|
|
107
|
+
self.assertEqual(gateway.device_app_name, "Registered App")
|
|
108
|
+
self.assertEqual(gateway.domain, "amazon.it")
|
|
109
|
+
|
|
110
|
+
def test_login_without_a_token_path_passes_an_empty_token(self) -> None:
|
|
111
|
+
gateway = FakeGateway()
|
|
112
|
+
session = Session(gateway)
|
|
113
|
+
result = run(
|
|
114
|
+
session,
|
|
115
|
+
{"id": "1", "op": "login_broker", "label": "alexa_001", "refreshTokenPath": ""},
|
|
116
|
+
)
|
|
117
|
+
self.assertTrue(result["ok"])
|
|
118
|
+
self.assertEqual(gateway.refresh_token, "")
|
|
119
|
+
|
|
120
|
+
def test_the_old_password_login_is_gone(self) -> None:
|
|
121
|
+
session = Session(FakeGateway())
|
|
122
|
+
for op in ("login_start", "login_otp"):
|
|
123
|
+
result = run(session, {"id": "1", "op": op})
|
|
124
|
+
self.assertFalse(result["ok"])
|
|
125
|
+
self.assertIn("unknown op", result["error"])
|
|
126
|
+
|
|
127
|
+
def test_devices_after_login(self) -> None:
|
|
128
|
+
session = Session(FakeGateway())
|
|
129
|
+
devices = run(session, {"id": "3", "op": "devices"})
|
|
130
|
+
self.assertEqual(devices["result"]["devices"][0]["id"], "echo-1")
|
|
131
|
+
self.assertEqual(devices["result"]["devices"][0]["family"], "ECHO")
|
|
132
|
+
self.assertTrue(devices["result"]["devices"][0]["online"])
|
|
133
|
+
|
|
134
|
+
def test_speak_and_announce_use_sequence_builders(self) -> None:
|
|
135
|
+
gateway = FakeGateway()
|
|
136
|
+
session = Session(gateway)
|
|
137
|
+
spoken = run(
|
|
138
|
+
session,
|
|
139
|
+
{"id": "4", "op": "speak", "targets": ["echo-1"], "text": "Person at the door", "volume": 30},
|
|
140
|
+
)
|
|
141
|
+
self.assertTrue(spoken["result"]["outcomes"][0]["ok"])
|
|
142
|
+
sequence = gateway.spoken[0]["sequence"]
|
|
143
|
+
self.assertEqual(sequence["startNode"]["type"], "Alexa.DeviceControls.Volume")
|
|
144
|
+
self.assertEqual(sequence["startNode"]["next"]["type"], "Alexa.Speak")
|
|
145
|
+
self.assertEqual(
|
|
146
|
+
sequence["startNode"]["next"]["operationPayload"]["textToSpeak"],
|
|
147
|
+
"Person at the door",
|
|
148
|
+
)
|
|
149
|
+
announced = run(
|
|
150
|
+
session,
|
|
151
|
+
{"id": "5", "op": "announce", "targets": ["echo-1"], "text": "Hello", "volume": 30},
|
|
152
|
+
)
|
|
153
|
+
self.assertTrue(announced["result"]["outcomes"][0]["ok"])
|
|
154
|
+
self.assertEqual(gateway.announced[0]["content"][0]["speak"]["value"], "Hello")
|
|
155
|
+
|
|
156
|
+
def test_unknown_op_and_bad_json_shape(self) -> None:
|
|
157
|
+
session = Session(FakeGateway())
|
|
158
|
+
unknown = run(session, {"id": "9", "op": "nope"})
|
|
159
|
+
self.assertFalse(unknown["ok"])
|
|
160
|
+
self.assertIn("unknown op", unknown["error"])
|
|
161
|
+
|
|
162
|
+
def test_session_status(self) -> None:
|
|
163
|
+
session = Session(FakeGateway())
|
|
164
|
+
status = run(session, {"id": "6", "op": "session_status"})
|
|
165
|
+
self.assertEqual(status["result"]["ageSec"], 12)
|
|
166
|
+
self.assertTrue(status["result"]["loggedIn"])
|
|
167
|
+
|
|
168
|
+
def test_sequence_helpers_do_not_embed_markup(self) -> None:
|
|
169
|
+
target = {"id": "echo-1", "deviceType": "T", "customerId": "c"}
|
|
170
|
+
sequence = build_speak_sequence(target, "plain", 10, "en-GB")
|
|
171
|
+
self.assertNotIn("<", json.dumps(sequence))
|
|
172
|
+
announcement = build_announcement(target, "plain", "en-GB")
|
|
173
|
+
self.assertEqual(announcement["content"][0]["speak"]["type"], "text")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
if __name__ == "__main__":
|
|
177
|
+
unittest.main()
|