refractor
This commit is contained in:
2
.idea/pymd3_vue_location_sim.iml
generated
2
.idea/pymd3_vue_location_sim.iml
generated
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<module external.system.id="java-source" type="PYTHON_MODULE" version="4">
|
<module external.system.id="pyproject.toml" type="PYTHON_MODULE" version="4">
|
||||||
<component name="NewModuleRootManager">
|
<component name="NewModuleRootManager">
|
||||||
<content url="file://$MODULE_DIR$">
|
<content url="file://$MODULE_DIR$">
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
#LWP-Cookies-2.0
|
#LWP-Cookies-2.0
|
||||||
Set-Cookie3: dslang=US-EN; path="/"; domain=.apple.com; path_spec; secure; discard; HttpOnly=None; version=0
|
Set-Cookie3: dslang=US-EN; path="/"; domain=.apple.com; path_spec; secure; discard; HttpOnly=None; version=0
|
||||||
Set-Cookie3: site=USA; path="/"; domain=.apple.com; path_spec; secure; discard; HttpOnly=None; version=0
|
Set-Cookie3: site=USA; path="/"; domain=.apple.com; path_spec; secure; discard; HttpOnly=None; version=0
|
||||||
Set-Cookie3: acn01=VXsJ1FolypnjIirlSDusIR9ovu+CG7pXMkEM9wALeBAzs3+u; path="/"; domain=.apple.com; path_spec; secure; expires="2027-04-05 02:36:14Z"; HttpOnly=None; version=0
|
Set-Cookie3: acn01="NNc+TvoQfp2pQiuHIAAqUBtXR1S4XhqZJWcHCAARBs+41xk/"; path="/"; domain=.apple.com; path_spec; secure; expires="2027-04-14 09:01:23Z"; HttpOnly=None; version=0
|
||||||
Set-Cookie3: aasp=9968286574C1B31BE2158A19285C92934F9213370AF67665FD3DC55BCB20C2B39E8728B5D8B4952B5F8B8B09F4D1C6826058791E379DE064B11DD79D0F7F1F87A27809C8C1CF291EEC649A8495D06747AA26022E3C56113F2E43528B8F75ECA9A7FA5EADDBFA510B80AF6C0198C74B431BA8BA5E76EA6411; path="/"; domain=.idmsa.apple.com; path_spec; secure; discard; HttpOnly=None; version=0
|
Set-Cookie3: aasp=DE9DF160D458CB33DB61E598CA924CC24441BCA91527DC0B793F5576DC6E8DF8BDBF5D9424A9243220F7D62057DD673EE28B6F754666DDF1949F07205B0E0BC0066D0CEDEA32E34965B3EA5AC66D2F90BEAC5E289B72B338B05E5D57B8DDD402BBC24F218664008D4D70C62F90DF6411A599DFC6DE59BA38; path="/"; domain=.idmsa.apple.com; path_spec; secure; discard; HttpOnly=None; version=0
|
||||||
Set-Cookie3: DES580750186337023c50d1415a6e6ca44a2="HSARMTKNSRVXWFlajR2ecD1662phQjqU9vXxnL49ZjypuVYYXHDpA3wTiX6Mf2J4WDlIhZj52z81aDOuz+VC80bVhV41TSNN4ggoPjW8WnsQrjniTQYkgJycPQNnzhkK4hfe2AMrr/bhrJJm8sHHc+Oh1HUckN6T7T4c1bmf2Qg9tRwsdRDNyMMyFH/Ml/cQlWKj39/YHlY=SRVX"; path="/"; domain=.idmsa.apple.com; path_spec; secure; expires="2026-04-21 03:09:12Z"; HttpOnly=None; version=0
|
Set-Cookie3: DES580750186337023c50d1415a6e6ca44a2="HSARMTKNSRVXWFlajR2ecD1662phQjqU9vXxnL49ZjypuVYYXHDpA3wTiX6Mf2J4WDlIhZj52z81aDOuz+VC80bVhV41TSNN4ggoPjW8WnsQrjniTQYkgJycPQNnzhkK4hfe2AMrr/bhrJJm8sHHc+Oh1HUckN6T7T4c1bmf2Qg9tRwsdRDNyMMyFH/Ml/cQlWKj39/YHlY=SRVX"; path="/"; domain=.idmsa.apple.com; path_spec; secure; expires="2026-04-21 03:09:12Z"; HttpOnly=None; version=0
|
||||||
Set-Cookie3: X-APPLE-UNIQUE-CLIENT-ID="\"BA==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; discard; version=0
|
Set-Cookie3: X-APPLE-UNIQUE-CLIENT-ID="\"BA==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; discard; version=0
|
||||||
Set-Cookie3: X-APPLE-WEBAUTH-LOGIN="\"v=1:t=BA==BST_IAAAAAAABLwIAAAAAGnRzaIRDmdzLmljbG91ZC5hdXRovQCklMXNjS27UT7gDS-gYQ89116WefHaUfyFRHCo9vj0gIvottHkbhRRXrty62DAgu_MJaMZUjxNBFe0CwGCcMtgC4Lg6NaNaHBlBlsy-OAjkFv1sJqr_nax-L7L3P6u9vXYy9jjrvy5Bl47wGJmZgnMbee6RQ~~\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; discard; HttpOnly=None; version=0
|
Set-Cookie3: X-APPLE-WEBAUTH-LOGIN="\"v=1:t=BA==BST_IAAAAAAABLwIAAAAAGneOZkRDmdzLmljbG91ZC5hdXRovQCaguVTZqS1WtbK-2fXiMlSEneJRuCgMGur1kfGcotuhS8Kvhra-S8EJ_LDYfFgbF8KYm2Dwug7ev54fe7_F-ui_zK0AqM9w7i8HvwWtWOA6QeYJtzFKgl7BVt_c5I4FrExr468XbgzmKtoXQxDTJu_wOLbbA~~\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; discard; HttpOnly=None; version=0
|
||||||
Set-Cookie3: X-APPLE-WEBAUTH-VALIDATE="\"v=1:t=BA==BST_IAAAAAAABLwIAAAAAGnRzaIRDmdzLmljbG91ZC5hdXRovQCklMXNjS27UT7gDS-gYQ89116WefHaUfyFRHCo9vj0gIvottHkbhRRXrty62DAgu_MJaMZUjxNBFe0CwGCcMtgC4Lg6NaNaHBlBlsy-OAjkCBWl1oaHvC06H_6zbXfJv5b7ws2aI9JUCBGIrlYvdtyArGvxA~~\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; discard; version=0
|
Set-Cookie3: X-APPLE-WEBAUTH-VALIDATE="\"v=1:t=BA==BST_IAAAAAAABLwIAAAAAGneOZkRDmdzLmljbG91ZC5hdXRovQCaguVTZqS1WtbK-2fXiMlSEneJRuCgMGur1kfGcotuhS8Kvhra-S8EJ_LDYfFgbF8KYm2Dwug7ev54fe7_F-ui_zK0AqM9w7i8HvwWtWOA6amlkIysG_APwQqmrfcDi-L_p8u1i1A8tqFrDUUfJs21Y1v8oA~~\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; discard; version=0
|
||||||
Set-Cookie3: X-APPLE-WEBAUTH-USER="\"v=1:s=1:d=157320350\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-05 02:49:09Z"; HttpOnly=None; version=0
|
Set-Cookie3: X-APPLE-WEBAUTH-USER="\"v=1:s=1:d=157320350\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-14 12:57:00Z"; HttpOnly=None; version=0
|
||||||
Set-Cookie3: X_APPLE_WEB_KB-FHMLYL_TPMN_3A8D3KIPPI0C_EC="\"v=1:t=BA==BST_IAAAAAAABLwIAAAAAGm_OLQRDmdzLmljbG91ZC5hdXRovQD38nYoxQenHW9WggeFKkoDa8I8zeKoOshv6I4dsZQalR2itry1r6kUZe9d_BZan1W-oKlImTrYi_-Vt5Q4YEJWJITWeqN8QChxvbTXB0o8sQ-wAIzBL1J5sQIRBqMadrtP5U0wslkRg0u0AguK20CM4TGoGg~~\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-21 00:32:52Z"; HttpOnly=None; version=0
|
Set-Cookie3: X_APPLE_WEB_KB-FHMLYL_TPMN_3A8D3KIPPI0C_EC="\"v=1:t=BA==BST_IAAAAAAABLwIAAAAAGm_OLQRDmdzLmljbG91ZC5hdXRovQD38nYoxQenHW9WggeFKkoDa8I8zeKoOshv6I4dsZQalR2itry1r6kUZe9d_BZan1W-oKlImTrYi_-Vt5Q4YEJWJITWeqN8QChxvbTXB0o8sQ-wAIzBL1J5sQIRBqMadrtP5U0wslkRg0u0AguK20CM4TGoGg~~\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-21 00:32:52Z"; HttpOnly=None; version=0
|
||||||
Set-Cookie3: X-APPLE-DS-WEB-SESSION-TOKEN="\"AQGj4lLXV42fukxli9in3ovKoBpXDePY1U3TMHAECRIIVd+zcdKkZDdg0hx6yPg+bVVZDVKN5iqJGKZsbIGATe6G/ce1fuxw+aON29I3TuOIuWrLNQyy1Idjn6SzX8RWY5MlFmoM+oVFt4MqdtvYJgGSfGv1IAe8CfTWPyQhqlk4qGgczVCCx74ReWAz7mh0GQk3aFwre6RK3QpJV1sgFGLh5uu0Jw2ThzgGfsYlORVDtz/90OjWuGMO6fr8BeXAu5Oh3mgryYTM39Et/Pc/8GuW9beXXI1WLul1eyQ/rK3xd8iZcEwYSFeBYfjNRYFAS3raJkIC0DJkKd93baYUNhEGJHXNtb6uzrueFmQT+svVA8VXY79IG9noP0S+0Ie3oDs0rZVwz5dHZhIzCEepxFz+WtAuIrkUXOFk71wkytU+oAdA90qfPP/DzeXbKGFbHgiBkoxSxsKsGpikC3U0ne6bPWcJZIsLh1vlcpNutXJGkAqm4VXYBGWbaWqr2UJyDBxJQClRngaKR/VB9U+BFvK5NylHsU0gzv7U8goPnQBr62ZAEmD3TDTCxuqIbbgaRxYRJD4smOVemXktlhwV8FvcH1r/UonCGal9YedLDLzL9bxMPg9vZVKnaAMDKwsJstLDFzY4EB2IS1Ga/XJSfO85MFPe6SeNOU2v2how1RyjoTeKhvACva16kcmT/RpUFR9W8qruLadc8sGGGXZ/LrpwG76hZBBrHef1jrsZw2lagAhtT7p69Sr7V9WQgZKoRq8ixSllm5VX1TJqoHkizS6lOBCOeasVT8g=\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-05 02:49:09Z"; HttpOnly=None; version=0
|
Set-Cookie3: X-APPLE-DS-WEB-SESSION-TOKEN="\"AQFfzSjK/WZszw0Fsqj3EgUNWNY4t+SKa9A0tAZvx+6URTf4eVhwhvkiDp7K92hEHTDNkvBVpeLWCOciQyzVLPcDhnss5IUImi6rDXxU6diem5bTWiznw50LWVBDnC9GnFB1ER6z1jqvw6u/2pDoO7slxsylU2j5fDp6mPvMe2iz83UhRRDLqUV5RjHOZ3Y93rnGSskislOzvvd/wuWBL9aXdC22oeGCs8lALzT2LDTJE/fJfw4H5ttKDoh5sA5bPgO3UjBtvAOMurTwdlYHmyaZ5pPrRDKSl9sgcbzkxwwvbvCVUbbsLt7rUolhsJmY33Pu51A7ubiG6hzIEg8U/mAQH7A9Ill4D/HbpR0Q4/uW3Sw9BkdI2MopLsLLJudBW66IBfYSGY53Dzr33IXjyi8bn+f8drCoxv+Jqsut5Yn/MI8Ooc9vpkON2INj+MDPCbr9LP2dh2hjOjvyHWJvDIYuIa/1ols9fx7GAT37dp8iuJYq7UYUT4GVaayt5xRH/a+hE8IpM+6gMAVgXMYBYi40eAmvcCG1+Pd4NKTQGRio+Z53bfcpMq4IjV5ZpwmcfWHMsCs6UNuFzDUhz3Oh0O9zK1hubuxgP4NvpJWZTGeDxm3Ak4nphbELvoFeRzqzX+wHLIKfr8H/ruK8LCrLeJvt4QAExQH9N6pjBDot0M/WpG0aZ0ZbOubqhfmX5KftG2QKJwDfLouyLeWp07TvyAsWXkS+o5Bc2ararFO8R5+OHURIIx9O47UUHkA0a7C1iYhdbxf8MMhOjYt6J8buormR4LDZEp2q5Xk=\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-14 12:57:00Z"; HttpOnly=None; version=0
|
||||||
Set-Cookie3: X-APPLE-WEBAUTH-HSA-TRUST="\"28a33818a1dce9a0eecde38e7c8fcc6f080b70bc9feb505599fb2855903a4792_HSARMTKNSRVXWFlajR2ecD1662phQjqU9vXxnL49ZjypuVYYXHDpA3wTiX6Mf2J4WDlIhZj52z81aDOuz+VC80bVhV41TSNN4ggoPjW8WnsQrjniTQYkgJycPQNnzhkK4hfe2AMrr/bhrJJm8sHHc+Oh1HUckN6T7T4c1bmf2Qg9tRwsdRDNyMMyFH/Ml/cQlWKj39/YHlY=SRVX\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-07-04 02:49:06Z"; HttpOnly=None; version=0
|
Set-Cookie3: X-APPLE-WEBAUTH-HSA-TRUST="\"28a33818a1dce9a0eecde38e7c8fcc6f080b70bc9feb505599fb2855903a4792_HSARMTKNSRVXWFlajR2ecD1662phQjqU9vXxnL49ZjypuVYYXHDpA3wTiX6Mf2J4WDlIhZj52z81aDOuz+VC80bVhV41TSNN4ggoPjW8WnsQrjniTQYkgJycPQNnzhkK4hfe2AMrr/bhrJJm8sHHc+Oh1HUckN6T7T4c1bmf2Qg9tRwsdRDNyMMyFH/Ml/cQlWKj39/YHlY=SRVX\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-07-13 12:56:57Z"; HttpOnly=None; version=0
|
||||||
Set-Cookie3: X-APPLE-WEBAUTH-PCS-Events="\"S2V5QXBwbDoBAAAA8QQzAADzWe4VyeK8B8xw8IwuUAj3/m/Tnx3zxoa+V62xZNyypQ==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-05 02:49:09Z"; HttpOnly=None; version=0
|
Set-Cookie3: X-APPLE-WEBAUTH-PCS-Events="\"S2V5QXBwbDoBAAAA8QRQAABCqtxc6DiNgfM7sfluuTm75zY6t8HcSbX5byVn83L6Yg==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-14 12:57:00Z"; HttpOnly=None; version=0
|
||||||
Set-Cookie3: X-APPLE-WEBAUTH-PCS-Documents="\"S2V5QXBwbDoBAAAAAgQzAABmoTT+n0/L3H+DLCJo23ecPgEOXkHSr8swBUva2awlRw==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-05 02:49:09Z"; HttpOnly=None; version=0
|
Set-Cookie3: X-APPLE-WEBAUTH-PCS-Documents="\"S2V5QXBwbDoBAAAAAgRQAAC9HDQLy/UwkOt+Z+OPhwCJxnyB1byDPGL1UYWgHtJEgg==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-14 12:57:00Z"; HttpOnly=None; version=0
|
||||||
Set-Cookie3: X-APPLE-WEBAUTH-PCS-Photos="\"S2V5QXBwbDoBAAAAAwQzAACZIqX0p5ijwnU4kYUFAd6s41Ki8Ll00cb5+m/HALArxA==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-05 02:49:09Z"; HttpOnly=None; version=0
|
Set-Cookie3: X-APPLE-WEBAUTH-PCS-Photos="\"S2V5QXBwbDoBAAAAAwRQAACxKufqce7i/IW850g8JkJ73J0Wyxpjpfh5nAk7eioyvw==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-14 12:57:00Z"; HttpOnly=None; version=0
|
||||||
Set-Cookie3: X-APPLE-WEBAUTH-PCS-Cloudkit="\"S2V5QXBwbDoBAAAABAQzAABlviOyJnRiHxxWxB8ItTVk+oDDvc7OUX1eJkqX0bYdXA==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-05 02:49:09Z"; HttpOnly=None; version=0
|
Set-Cookie3: X-APPLE-WEBAUTH-PCS-Cloudkit="\"S2V5QXBwbDoBAAAABARQAABnmo+VJgPhrzid+Xl/01UA02nJiwkk9q0MBvy6QH758g==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-14 12:57:00Z"; HttpOnly=None; version=0
|
||||||
Set-Cookie3: X-APPLE-WEBAUTH-PCS-Safari="\"S2V5QXBwbDoBAAAAFgQzAACET30lpsAp5WwxJGxde2g5ajvsMowlnYgU2tfOoebjgA==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-05 02:49:09Z"; HttpOnly=None; version=0
|
Set-Cookie3: X-APPLE-WEBAUTH-PCS-Safari="\"S2V5QXBwbDoBAAAAFgRQAAC9tTzH5+GZyvSdFF1r+Zmvj/kJmQNWTYNLCmNG8tX6Cw==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-14 12:57:00Z"; HttpOnly=None; version=0
|
||||||
Set-Cookie3: X-APPLE-WEBAUTH-PCS-Mail="\"S2V5QXBwbDoBAAAABwQzAACaoZ2L80C9Pl1HRn65m+rka24bPiCumxBL/I07rSfkGQ==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-05 02:49:09Z"; HttpOnly=None; version=0
|
Set-Cookie3: X-APPLE-WEBAUTH-PCS-Mail="\"S2V5QXBwbDoBAAAABwRQAABywaR34zLT3fm0XcBUYCbETV16EkpVL6/O4hB9EHwDcQ==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-14 12:57:00Z"; HttpOnly=None; version=0
|
||||||
Set-Cookie3: X-APPLE-WEBAUTH-PCS-Notes="\"S2V5QXBwbDoBAAAACQQzAAA5vanrsd/XLqXzVPYQGtt68uOMqCgEcW8J8pisVkU8xw==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-05 02:49:09Z"; HttpOnly=None; version=0
|
Set-Cookie3: X-APPLE-WEBAUTH-PCS-Notes="\"S2V5QXBwbDoBAAAACQRQAADqrnrINYzUlz7QoxZO+ami+5QY9Qtys3wAuCPjM0x7wQ==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-14 12:57:00Z"; HttpOnly=None; version=0
|
||||||
Set-Cookie3: X-APPLE-WEBAUTH-PCS-News="\"S2V5QXBwbDoBAAAACwQzAADVWJymh0KvnbnDDHZBntjVHLnfcTr/Q0ES5n8RjkStNg==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-05 02:49:09Z"; HttpOnly=None; version=0
|
Set-Cookie3: X-APPLE-WEBAUTH-PCS-News="\"S2V5QXBwbDoBAAAACwRQAAAYQEni07N+B3hrogj/ZlPdWE3ibkkBCYJcDgadmwRzOw==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-14 12:57:00Z"; HttpOnly=None; version=0
|
||||||
Set-Cookie3: X-APPLE-WEBAUTH-PCS-Sharing="\"S2V5QXBwbDoBAAAADAQzAADpgr9x39VeWGIvzow7K8TzwvGLETRpkbcQH4Q8gKed2w==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-05 02:49:09Z"; HttpOnly=None; version=0
|
Set-Cookie3: X-APPLE-WEBAUTH-PCS-Sharing="\"S2V5QXBwbDoBAAAADARQAABGcfw9nr8NjONRvLxWcwgMoaBXj7xode3Sf7ajDnV2IQ==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-14 12:57:00Z"; HttpOnly=None; version=0
|
||||||
Set-Cookie3: X-APPLE-WEBAUTH-TOKEN="\"v=2:t=BA==BST_IAAAAAAABLwIAAAAAGnRztkRDmdzLmljbG91ZC5hdXRovQDHdf4C78ZKILT8BBTIOy2E0pByyyFr6yZauanzkyx8PQqrJBuXKi3q6Ms5vECA80g_yAB1WH-0ep0fExYB60bSXj-tV6WHYlFPew3HCW9LTJGs4uea0vlYHya2rKBlXbmkU3-WnL6eHN_U6MuYKO1zefaf8A~~\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-04-19 02:54:17Z"; HttpOnly=None; version=0
|
Set-Cookie3: X-APPLE-WEBAUTH-TOKEN="\"v=2:t=BA==BST_IAAAAAAABLwIAAAAAGneP2oRDmdzLmljbG91ZC5hdXRovQCgEswrYhaJx11ybTgoBMzcGPh9PUsXyVTJCpHvMijnTIkcF0JOFbgPAg8KKzEH9Rd9ebBJmHwfJP5LUVHG1YaUgB5xL9LX2AQYoGwnJcaj2mCD7LQxw4WCZSVJf9TauwpgZlhHpR15qHwnpiIC3Qg8Rue57g~~\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-04-28 13:21:46Z"; HttpOnly=None; version=0
|
||||||
Set-Cookie3: X-APPLE-WEBAUTH-FMIP="\"BA==BST_IAAAAAAABLwIAAAAAGnRztkRDmdzLmljbG91ZC5hdXRovQDHdf4C78ZKILT8BBTIOy2E0pByyyFr6yZauanzkyx8PQqrJBuXKi3q6Ms5vECA80g_yAB1WH-0ep0fExYB60bSXj-tV6WHYlFPew3HCW9LTFFwE029MTNgDRSz7RG3UgEKnrs8iiy5GqIVN5vkstidJ8ogxA~~\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; discard; HttpOnly=None; version=0
|
Set-Cookie3: X-APPLE-WEBAUTH-FMIP="\"BA==BST_IAAAAAAABLwIAAAAAGneP2oRDmdzLmljbG91ZC5hdXRovQCgEswrYhaJx11ybTgoBMzcGPh9PUsXyVTJCpHvMijnTIkcF0JOFbgPAg8KKzEH9Rd9ebBJmHwfJP5LUVHG1YaUgB5xL9LX2AQYoGwnJcaj2h-pjNdL9bRPA229-vtr8LU4Syp2u2FO2pDcJ6aasJg2FHdtQg~~\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; discard; HttpOnly=None; version=0
|
||||||
Set-Cookie3: xr_3n2093n1a="f16+Jo6W6qPJmRkZzBixaQfuGOyKO+kJ4iSEZyG94A=="; path="/"; domain=p144-fmipweb.icloud.com; path_spec; secure; discard; HttpOnly=None; version=0
|
Set-Cookie3: xr_3n2093n1a="B50g9gcKmoM+KF+j8hPX6wKXv22QcYsBKEONZTiNR4g="; path="/"; domain=p144-fmipweb.icloud.com; path_spec; secure; discard; HttpOnly=None; version=0
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"client_id": "a803c3ce-2586-11f1-a724-8f6777a1d2b5", "session_id": "9968286574C1B31BE2158A19285C92934F9213370AF67665FD3DC55BCB20C2B39E8728B5D8B4952B5F8B8B09F4D1C6826058791E379DE064B11DD79D0F7F1F87A27809C8C1CF291EEC649A8495D06747AA26022E3C56113F2E43528B8F75ECA9A7FA5EADDBFA510B80AF6C0198C74B431BA8BA5E76EA6411", "auth_attributes": "Xmk4qpyEj50xx+3lPjCdsfTj8wLQuUJ9ZlW4t+MjKa0jiHZ9YmQQJfUGKqW0Jl3jpyX0m4eYPaS7oSPeWR3jUx9jjrdLW3fuUjZrksYarxCZGX8/V1ArrgSzl8lsbD9MSNDxtBDEyJZSlnp4syp2/Tqs2jM/dE3641Pdyh4ScxQi9S1c4HtHnn0ryjT3XcH034UwQnGtnLnJS8FXtWAjFqPUv0I9C06BVPLkI7jw+I/efCr0sMYUsPgclZbSVuPU7+ioZPJeeV5xJm/dGQpoAAt4ED/Fik8=", "scnt": "AAAA-jk5NjgyODY1NzRDMUIzMUJFMjE1OEExOTI4NUM5MjkzNEY5MjEzMzcwQUY2NzY2NUZEM0RDNTVCQ0IyMEMyQjM5RTg3MjhCNUQ4QjQ5NTJCNUY4QjhCMDlGNEQxQzY4MjYwNTg3OTFFMzc5REUwNjRCMTFERDc5RDBGN0YxRjg3QTI3ODA5QzhDMUNGMjkxRUVDNjQ5QTg0OTVEMDY3NDdBQTI2MDIyRTNDNTYxMTNGMkU0MzUyOEI4Rjc1RUNBOUE3RkE1RUFEREJGQTUxMEI4MEFGNkMwMTk4Qzc0QjQzMUJBOEJBNUU3NkVBNjQxMXwyAAABnVuNNE9ZApXl4PMBXcfJlCl96p_1cCfBRhHOdT0BkG04tbOjDgF3V8vlM9CkAAt4ECMQx8n6Pfm37fS8OCpOsUoTL7pc1A-DXplPGubA4CDTzbZfkw", "account_country": "USA", "session_token": "YaPd8Iuy2R5+ZsjKRO52duQIvtCutVY0Mf8UTjNowXeyq6E8O+VBbYFMEh1oq/bQIlTmu0zYRc/79K6Qt8jTYpuQArQNhVDqCkUNpEgc0wyKA3L15xFP11t7/nDDlzL1U0hU2JEpiXcNpr8oXF0oJxDf+p9zRA+ryHL/jybhiiaVX48zUB6cYc8m++GA+Oid2r45sftOPRUkYpgIF2UwIq0pF9xat810EDnaDTVXYe2geQ2Vt9BNJiAJLWNA3WPfifMpAZzN5b2vHg8Nsh3e/Nm/+GQvjrtcBr5vT3RwlslOfWIqJbtoLL/np16f+szHM6e5ZOxejMdOifG5PxfPshr+qMzu/HRX7Ex8r6975KaiW98MBrvbVITsgsL/YmutmdCAFfATXYxu2JOfnOHhVPDANNRLh8N1ZTSJtGY3fXP3DxbNlCjmCA3JAMWMiUkhlxDcA4xPubDiiU5ZNJ6x1ckkAdjtQkOvjESt9N3ydMf89sz9WDqdps0ll1sYXINjLM1OagoBSxzE0y0szkX34YkCki0j3f25OH+utNCNIDZV9fWTEYiINOY1POSX07ZPbzw2AKfVqJP3DduVL4rzjXTIcGtTBkcr04c0Lz43knqcPkVQI1QNckxiFL1owcZEdlxoPKIOL8ojXRK7b6NAJyk+t5vLpDWrt97bHegGaJoe7swSYP75rSxkVdh8vAczd/Mir4OmB/uerwtEZq9KUiKWvC52mYYolF/XYazJKAVGx1FpywM8DL5mHlbcJzTUR2NBWRD/b0/m73Jxs32kLButuWtnKXiHk/OqC1JZfA8+LNdNaK6KtJcdEul3go7LCwuxQZ4o8o2QzN8SqgM9UUvcyMf/3LwDoZgRdPmKNRN6GV5a3J9I+WF+Jz98W0Klbn5PJa6y0Zc9aaUy1H6Ho6pdzYeLdMMhwJ5RaJqvgnHxDFYU2UQKiFt3w26gFANk1AALeBBE/iN7", "trust_eligible": "true", "grant_code": "c3254a0d3cdf4487fa423d13cca72142f.0.przwz.C6giZgZ9fLof99ebfJFxDA", "trust_token": "HSARMTKNSRVXWFlajR2ecD1662phQjqU9vXxnL49ZjypuVYYXHDpA3wTiX6Mf2J4WDlIhZj52z81aDOuz+VC80bVhV41TSNN4ggoPjW8WnsQrjniTQYkgJycPQNnzhkK4hfe2AMrr/bhrJJm8sHHc+Oh1HUckN6T7T4c1bmf2Qg9tRwsdRDNyMMyFH/Ml/cQlWKj39/YHlY=SRVX"}
|
{"client_id": "a803c3ce-2586-11f1-a724-8f6777a1d2b5", "session_id": "DE9DF160D458CB33DB61E598CA924CC24441BCA91527DC0B793F5576DC6E8DF8BDBF5D9424A9243220F7D62057DD673EE28B6F754666DDF1949F07205B0E0BC0066D0CEDEA32E34965B3EA5AC66D2F90BEAC5E289B72B338B05E5D57B8DDD402BBC24F218664008D4D70C62F90DF6411A599DFC6DE59BA38", "auth_attributes": "zahj2u9cli9LAxaDkM/LZ+cVDLo7WY7x9DXKo/GGGAb+VYFvMJT/jxRGAHy2AaUN3MbqtRLU0i7jA2oeXhQ1iMaUEntaTv9AWTsTfylbAnoJ1Vo+euKP22hW6V/IbTfClCFMHGxZul3ooBdwJwz+otrAo3VPF3yUptZPvP5+7JOegMRDA1awse9Ci0QtcOfI8p6tyj5aV1xjcpLbt29wdmV+Ny/Lm25iFpDKnf/XIgvT1MUXIpGot1NfW3rqWViq/M/pq8DK0i8/RuLDevFYABEGz8Qt3PE=", "scnt": "AAAA-kRFOURGMTYwRDQ1OENCMzNEQjYxRTU5OENBOTI0Q0MyNDQ0MUJDQTkxNTI3REMwQjc5M0Y1NTc2REM2RThERjhCREJGNUQ5NDI0QTkyNDMyMjBGN0Q2MjA1N0RENjczRUUyOEI2Rjc1NDY2NkRERjE5NDlGMDcyMDVCMEUwQkMwMDY2RDBDRURFQTMyRTM0OTY1QjNFQTVBQzY2RDJGOTBCRUFDNUUyODlCNzJCMzM4QjA1RTVENTdCOERERDQwMkJCQzI0RjIxODY2NDAwOEQ0RDcwQzYyRjkwREY2NDExQTU5OURGQzZERTU5QkEzOHwyAAABnYtHDOAIdGLN7q_QgI9AB0GH5OFan9jwoX6JhEYkfqH0K-ydWfBkwqF6Le3-ABEGz6vfU7IlwYBHKE9RVBG8MeYu5XjWFmHIzPcRjW3ImaknBIL7cA", "account_country": "USA", "session_token": "cD1YUhA3Q+BFVazN3KBXOKbVHASxhLLJyWAqVySi3kNzZY4W8ikOiA+sWjs3ROhJcgQh2aErAhxbB3MFVq0h1rdhwbfe6pDtiC1AO7gjuDjCjEJyfZXr4Z5YpeI0ETekbJnNtFrOvxQ5eMdBpXEf9GGOD8FSkBNkl1XYZWdBbsFPKrmGMPEBWcv07pD1XzkONFiwQs71dLPTEwbImkKMjYtTUckv61hjDV9+ktZrYRmRjEhclp3nXkB4a49d88pNVTt9+Dm2MGIuxPvjKogTmE6rLhhxQavVjJrc16zSayrXv30qNiKJfckwSCfcliqWEgICB/I56ZALxbAmvMBWOdB9YPYfvle81KfI1UGrOiXko3G7IzPAA/7voNZqStR0z8FiTiua3GxorxDmUFHezp1kpLgpa+yzNyBobbLP3vbSRC6083rN0XbXM/ad7Tmm2G8FDyzM4XOrE+P2X5mtQcXxlba7ygKq51nP4vM1YrfQYc6ZRgRBJK+vA21i3igPA8awj9zrg4KF5GvMGjwSZ6Tp3wG6MSws909+80bvdp5x22b88k/sjzk2MLNjborCRPz9xC9H/ZAeqjPp6F7S/AqwwEa6RQ34ykiZVLA5dv5bNFteEQZNVO6FmoDBecw3BscsBt6MAHA50mUjq6dvzeZxOOOlpGtgJyOWybyfK3VoevZ03utoYg12dTvLMwGaIyELPhOP6mJFDa2bJxmeDPwkkt+bwk3EVumRudUAhOcIbxXJ52Zt4HuvgzDE/6c+bg9Jbv29cw5njr2P+8VYJO4ghFqmnan+QLnFiwP7Ypacc+Jl0YbEB1bBa/paAnD0wlzYzqS8qi+GQpD5Ro4BFkbS7hMPIvZL1+n4xC3sepc3AAq+0IP6PerQiUn6ORK88JYuRnnMUuwEJcpDtzL/CONusB75h9R4KFIQZl9GykYQ3ZLj+3JerAgvzcFOR0D1OQARBs/I9osU", "trust_eligible": "true", "grant_code": "c610a05475fb544cab5128ff1b920b9a6.0.srzwz.U70T2OR3TxSYAjcY4DHZ9w", "trust_token": "HSARMTKNSRVXWFlajR2ecD1662phQjqU9vXxnL49ZjypuVYYXHDpA3wTiX6Mf2J4WDlIhZj52z81aDOuz+VC80bVhV41TSNN4ggoPjW8WnsQrjniTQYkgJycPQNnzhkK4hfe2AMrr/bhrJJm8sHHc+Oh1HUckN6T7T4c1bmf2Qg9tRwsdRDNyMMyFH/Ml/cQlWKj39/YHlY=SRVX"}
|
||||||
BIN
geocache.db
BIN
geocache.db
Binary file not shown.
@@ -1,44 +0,0 @@
|
|||||||
|
|
||||||
from typing import List
|
|
||||||
from sqlalchemy import select
|
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
|
|
||||||
from .db_models import Base, Location, Route, Waypoint
|
|
||||||
|
|
||||||
|
|
||||||
DATABASE_URL = "sqlite+aiosqlite:///.locations.db"
|
|
||||||
engine = create_async_engine(DATABASE_URL, echo=True)
|
|
||||||
async_session_local = async_sessionmaker(engine, expire_on_commit=False)
|
|
||||||
|
|
||||||
async def init_db() -> None:
|
|
||||||
async with engine.begin() as conn:
|
|
||||||
await conn.run_sync(Base.metadata.create_all)
|
|
||||||
|
|
||||||
async def create_location(name: str, address: str, latitude: float, longitude: float, is_favorite: bool = False):
|
|
||||||
async with async_session_local() as session:
|
|
||||||
new_location = Location(name=name, address=address, latitude=latitude, longitude=longitude, is_favorite=is_favorite)
|
|
||||||
session.add(new_location)
|
|
||||||
await session.commit()
|
|
||||||
return new_location
|
|
||||||
|
|
||||||
async def get_locations():
|
|
||||||
async with async_session_local() as session:
|
|
||||||
result = await session.execute(select(Location))
|
|
||||||
return result.scalars().all()
|
|
||||||
|
|
||||||
async def create_route(name: str, origin_id: int, destination_id: int, waypoints_data: List[dict]):
|
|
||||||
async with async_session_local() as session:
|
|
||||||
new_route = Route(name=name, origin_id=origin_id, destination_id=destination_id)
|
|
||||||
for wp in waypoints_data:
|
|
||||||
new_route.waypoints.append(Waypoint(order=wp['order'], description=wp['description']))
|
|
||||||
session.add(new_route)
|
|
||||||
await session.commit()
|
|
||||||
return new_route
|
|
||||||
|
|
||||||
async def get_routes():
|
|
||||||
async with async_session() as session:
|
|
||||||
# Use joinedload for efficient loading of relationships
|
|
||||||
from sqlalchemy.orm import joinedload
|
|
||||||
result = await session.execute(
|
|
||||||
select(Route).options(joinedload(Route.waypoints))
|
|
||||||
)
|
|
||||||
return result.scalars().unique().all()
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
|
|
||||||
from sqlalchemy import ForeignKey, String
|
|
||||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncAttrs
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
|
|
||||||
class Base(AsyncAttrs, DeclarativeBase):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class Location(Base):
|
|
||||||
__tablename__ = "locations"
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
name: Mapped[str] = mapped_column(String(100))
|
|
||||||
address: Mapped[str] = mapped_column(String(255))
|
|
||||||
latitude: Mapped[float]
|
|
||||||
longitude: Mapped[float]
|
|
||||||
is_favorite: Mapped[bool] = mapped_column(default=False)
|
|
||||||
|
|
||||||
routes: Mapped[List["Route"]] = relationship(back_populates="destination", cascade="all, delete-orphan")
|
|
||||||
|
|
||||||
|
|
||||||
class Route(Base):
|
|
||||||
__tablename__ = "routes"
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
name: Mapped[str] = mapped_column(String(100))
|
|
||||||
origin_id: Mapped[int] = mapped_column(ForeignKey("locations.id"))
|
|
||||||
destination_id: Mapped[int] = mapped_column(ForeignKey("locations.id"))
|
|
||||||
destination: Mapped["Location"] = relationship(back_populates="routes")
|
|
||||||
|
|
||||||
waypoints: Mapped[List["Waypoint"]] = relationship(back_populates="route", cascade="all, delete-orphan")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Waypoint(Base):
|
|
||||||
__tablename__ = "waypoint"
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
order: Mapped[int]
|
|
||||||
description: Mapped[str]
|
|
||||||
route_id: Mapped[int] = mapped_column(ForeignKey("route.id"))
|
|
||||||
route: Mapped["Route"] = relationship(back_populates="waypoints")
|
|
||||||
|
|
||||||
@@ -6,15 +6,21 @@ from geopy.adapters import AioHTTPAdapter
|
|||||||
from geopy.extra.rate_limiter import AsyncRateLimiter
|
from geopy.extra.rate_limiter import AsyncRateLimiter
|
||||||
|
|
||||||
logger = logging.getLogger("ios-api")
|
logger = logging.getLogger("ios-api")
|
||||||
|
CACHE_LOOKUP_SQL = "SELECT address FROM location_cache WHERE lat_lon = ?"
|
||||||
|
CACHE_UPSERT_SQL = "INSERT OR REPLACE INTO location_cache VALUES (?, ?)"
|
||||||
|
|
||||||
|
|
||||||
class AsyncReverseGeocoder:
|
class AsyncReverseGeocoder:
|
||||||
def __init__(self, db_path="geocache.db", user_agent="pymd3_vue_location_sim/0.1.0 (iam@williambr.uno)"):
|
def __init__(
|
||||||
|
self,
|
||||||
|
db_path: str = "geocache.db",
|
||||||
|
user_agent: str = "pymd3_vue_location_sim/0.1.0 (iam@williambr.uno)",
|
||||||
|
):
|
||||||
self.db_path = db_path
|
self.db_path = db_path
|
||||||
self.user_agent = user_agent
|
self.user_agent = user_agent
|
||||||
self._init_db()
|
self._init_db()
|
||||||
|
|
||||||
def _init_db(self):
|
def _init_db(self) -> None:
|
||||||
"""Initializes the SQLite database."""
|
"""Initializes the SQLite database."""
|
||||||
with sqlite3.connect(self.db_path) as conn:
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
@@ -26,19 +32,29 @@ class AsyncReverseGeocoder:
|
|||||||
''')
|
''')
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
async def get_address(self, lat, lon):
|
@staticmethod
|
||||||
"""Reverse geocode with caching."""
|
def _cache_key(lat: float, lon: float) -> str:
|
||||||
key = f"{lat:.5f},{lon:.5f}"
|
return f"{lat:.5f},{lon:.5f}"
|
||||||
logger.info("Checking location_cache for %s", key)
|
|
||||||
# Check Cache
|
def _read_cached_address(self, key: str) -> dict | None:
|
||||||
with sqlite3.connect(self.db_path) as conn:
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
cursor = conn.execute(
|
cursor = conn.execute(CACHE_LOOKUP_SQL, (key,))
|
||||||
"SELECT address FROM location_cache WHERE lat_lon = ?", (key,))
|
row = cursor.fetchone()
|
||||||
row = cursor.fetchone()
|
return json.loads(row[0]) if row else None
|
||||||
if row:
|
|
||||||
return json.loads(row[0])
|
def _store_cached_address(self, key: str, address_data: dict) -> None:
|
||||||
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
|
conn.execute(CACHE_UPSERT_SQL, (key, json.dumps(address_data)))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
async def get_address(self, lat: float, lon: float) -> dict | None:
|
||||||
|
"""Reverse geocode with SQLite cache."""
|
||||||
|
key = self._cache_key(lat, lon)
|
||||||
|
logger.info("Checking location_cache for %s", key)
|
||||||
|
cached_address = self._read_cached_address(key)
|
||||||
|
if cached_address is not None:
|
||||||
|
return cached_address
|
||||||
|
|
||||||
# Fetch New Data
|
|
||||||
async with Nominatim(
|
async with Nominatim(
|
||||||
user_agent=self.user_agent,
|
user_agent=self.user_agent,
|
||||||
adapter_factory=AioHTTPAdapter
|
adapter_factory=AioHTTPAdapter
|
||||||
@@ -49,15 +65,10 @@ class AsyncReverseGeocoder:
|
|||||||
location = await reverse(key)
|
location = await reverse(key)
|
||||||
if location:
|
if location:
|
||||||
logger.info("Nominatim response: %s", location)
|
logger.info("Nominatim response: %s", location)
|
||||||
address_data = location.raw['address']
|
address_data = location.raw.get("address", {})
|
||||||
# Save to Cache
|
self._store_cached_address(key, address_data)
|
||||||
conn.execute(
|
|
||||||
"INSERT OR REPLACE INTO location_cache VALUES (?, ?)",
|
|
||||||
(key, json.dumps(address_data))
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
return address_data
|
return address_data
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception:
|
||||||
print(f"Error: {e}")
|
logger.exception("Reverse geocoding failed for key=%s", key)
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import textwrap
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
|
||||||
import click
|
import click
|
||||||
@@ -16,10 +17,17 @@ from pyicloud.exceptions import (
|
|||||||
PyiCloudPasswordException,
|
PyiCloudPasswordException,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .models import iCloudReturnData
|
from .models import ICloudReturnData
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
logger = logging.getLogger("ios-api")
|
logger = logging.getLogger("ios-api")
|
||||||
|
COOKIE_DIRECTORY = "./cookies"
|
||||||
|
ENV_APPLE_ID = "APPLE_ID"
|
||||||
|
ENV_APPLE_PW = "APPLE_PW"
|
||||||
|
ENV_SELECTED_DEVICE_ID = "SELECTED_DEVICE_ID"
|
||||||
|
ENV_SELECTED_DEVICE_NAME = "SELECTED_DEVICE_NAME"
|
||||||
|
ENV_AUTH_INIT_TIMEOUT = "ICLOUD_AUTH_INIT_TIMEOUT_SECONDS"
|
||||||
|
BACKOFF_SCHEDULE = (15, 30, 60, 120, 300)
|
||||||
AUTH_EXCEPTIONS = (
|
AUTH_EXCEPTIONS = (
|
||||||
PyiCloudAuthRequiredException,
|
PyiCloudAuthRequiredException,
|
||||||
PyiCloudFailedLoginException,
|
PyiCloudFailedLoginException,
|
||||||
@@ -38,11 +46,11 @@ class FindMyMonitor:
|
|||||||
get_client_sids: Callable[[], list[str]] | None = None,
|
get_client_sids: Callable[[], list[str]] | None = None,
|
||||||
code_timeout_seconds: int = 180,
|
code_timeout_seconds: int = 180,
|
||||||
):
|
):
|
||||||
self.username = os.getenv("APPLE_ID")
|
self.username = os.getenv(ENV_APPLE_ID)
|
||||||
self.password = os.getenv("APPLE_PW")
|
self.password = os.getenv(ENV_APPLE_PW)
|
||||||
self.token_file = token_file
|
self.token_file = token_file
|
||||||
self.selected_device_id = os.getenv("SELECTED_DEVICE_ID")
|
self.selected_device_id = os.getenv(ENV_SELECTED_DEVICE_ID)
|
||||||
self.selected_device_name = os.getenv("SELECTED_DEVICE_NAME")
|
self.selected_device_name = os.getenv(ENV_SELECTED_DEVICE_NAME)
|
||||||
self.selected_device = None
|
self.selected_device = None
|
||||||
self.queue = queue
|
self.queue = queue
|
||||||
self.api = None
|
self.api = None
|
||||||
@@ -52,13 +60,33 @@ class FindMyMonitor:
|
|||||||
self.get_client_sids = get_client_sids
|
self.get_client_sids = get_client_sids
|
||||||
self.code_timeout_seconds = code_timeout_seconds
|
self.code_timeout_seconds = code_timeout_seconds
|
||||||
self.auth_init_timeout_seconds = int(
|
self.auth_init_timeout_seconds = int(
|
||||||
os.getenv("ICLOUD_AUTH_INIT_TIMEOUT_SECONDS", "0")
|
os.getenv(ENV_AUTH_INIT_TIMEOUT, "0")
|
||||||
)
|
)
|
||||||
self._logged_candidates = False
|
self._logged_candidates = False
|
||||||
self._no_location_streak = 0
|
self._no_location_streak = 0
|
||||||
self._auth_error_streak = 0
|
self._auth_error_streak = 0
|
||||||
self._fetch_lock = asyncio.Lock()
|
self._fetch_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
async def _create_api_session(self, has_token: bool) -> bool:
|
||||||
|
try:
|
||||||
|
init_args = [self.username]
|
||||||
|
if not has_token:
|
||||||
|
init_args.append(self.password)
|
||||||
|
init_task = asyncio.to_thread(
|
||||||
|
PyiCloudService, *init_args, cookie_directory=COOKIE_DIRECTORY
|
||||||
|
)
|
||||||
|
if self.auth_init_timeout_seconds > 0:
|
||||||
|
self.api = await asyncio.wait_for(
|
||||||
|
init_task, timeout=self.auth_init_timeout_seconds
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.api = await init_task
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
source = "cookies" if has_token else "credentials"
|
||||||
|
logger.exception("Failed to initialize iCloud session from %s", source)
|
||||||
|
return False
|
||||||
|
|
||||||
async def _request_code_from_vue(self, prompt: str) -> str | None:
|
async def _request_code_from_vue(self, prompt: str) -> str | None:
|
||||||
if self.sio is None or self.get_client_sids is None:
|
if self.sio is None or self.get_client_sids is None:
|
||||||
logger.warning("2FA request skipped: Socket.IO context not configured")
|
logger.warning("2FA request skipped: Socket.IO context not configured")
|
||||||
@@ -91,7 +119,7 @@ class FindMyMonitor:
|
|||||||
logger.warning("Invalid 2FA code payload from sid=%s", sid)
|
logger.warning("Invalid 2FA code payload from sid=%s", sid)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def authenticate(self):
|
async def authenticate(self) -> bool:
|
||||||
"""Authenticates with iCloud, handling 2FA and token storage."""
|
"""Authenticates with iCloud, handling 2FA and token storage."""
|
||||||
if not self.username:
|
if not self.username:
|
||||||
logger.warning("APPLE_ID is not configured; skipping iCloud monitor authentication")
|
logger.warning("APPLE_ID is not configured; skipping iCloud monitor authentication")
|
||||||
@@ -103,42 +131,15 @@ class FindMyMonitor:
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if os.path.exists(self.token_file):
|
logger.info(
|
||||||
print("Loading stored session...")
|
"Initializing iCloud session from %s",
|
||||||
try:
|
"stored cookies" if has_token else "credentials",
|
||||||
init_task = asyncio.to_thread(
|
|
||||||
PyiCloudService, self.username, cookie_directory="./cookies"
|
|
||||||
)
|
)
|
||||||
if self.auth_init_timeout_seconds > 0:
|
if not await self._create_api_session(has_token=has_token):
|
||||||
self.api = await asyncio.wait_for(
|
|
||||||
init_task, timeout=self.auth_init_timeout_seconds
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.api = await init_task
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception("Failed to initialize iCloud session from cookies: %s", e)
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
print("No stored session. Authenticating...")
|
|
||||||
try:
|
|
||||||
init_task = asyncio.to_thread(
|
|
||||||
PyiCloudService,
|
|
||||||
self.username,
|
|
||||||
self.password,
|
|
||||||
cookie_directory="./cookies",
|
|
||||||
)
|
|
||||||
if self.auth_init_timeout_seconds > 0:
|
|
||||||
self.api = await asyncio.wait_for(
|
|
||||||
init_task, timeout=self.auth_init_timeout_seconds
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.api = await init_task
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception("Failed to initialize iCloud session with credentials: %s", e)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self.api.requires_2fa:
|
if self.api.requires_2fa:
|
||||||
print("Two-factor authentication required.")
|
logger.info("Two-factor authentication required.")
|
||||||
code = await self._request_code_from_vue("Enter the 6-digit Apple verification code")
|
code = await self._request_code_from_vue("Enter the 6-digit Apple verification code")
|
||||||
if code is None:
|
if code is None:
|
||||||
if sys.stdin and sys.stdin.isatty():
|
if sys.stdin and sys.stdin.isatty():
|
||||||
@@ -154,29 +155,28 @@ class FindMyMonitor:
|
|||||||
"2FA required but no interactive terminal or Vue responder is available; deferring authentication"
|
"2FA required but no interactive terminal or Vue responder is available; deferring authentication"
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
# Verify the code
|
|
||||||
result = await asyncio.to_thread(self.api.validate_2fa_code, code)
|
result = await asyncio.to_thread(self.api.validate_2fa_code, code)
|
||||||
print(f"2FA validation result: {result}")
|
logger.info("2FA validation result: %s", result)
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
print("Failed to verify 2FA code")
|
logger.warning("Failed to verify 2FA code")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Trust the session
|
|
||||||
await asyncio.to_thread(self.api.trust_session)
|
await asyncio.to_thread(self.api.trust_session)
|
||||||
|
|
||||||
if self.api.requires_2sa:
|
if self.api.requires_2sa:
|
||||||
import textwrap
|
logger.info(textwrap.dedent("""
|
||||||
print(textwrap.dedent("""
|
|
||||||
Two-step authentication required.
|
Two-step authentication required.
|
||||||
Please select a device to receive a SMS verification code:
|
Please select a device to receive a SMS verification code:
|
||||||
"""))
|
"""))
|
||||||
# List available devices for 2SA
|
|
||||||
for i, device in enumerate(self.api.trusted_devices):
|
for i, device in enumerate(self.api.trusted_devices):
|
||||||
print(
|
logger.info(
|
||||||
f" {i + 1}: {device.get('deviceName', 'Unknown device')} ({device.get('phoneNumber', 'Unknown number')})")
|
" %s: %s (%s)",
|
||||||
|
i + 1,
|
||||||
|
device.get("deviceName", "Unknown device"),
|
||||||
|
device.get("phoneNumber", "Unknown number"),
|
||||||
|
)
|
||||||
|
|
||||||
# Prompt the user for their choice
|
|
||||||
if not (sys.stdin and sys.stdin.isatty()):
|
if not (sys.stdin and sys.stdin.isatty()):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"2SA required but no interactive terminal is available; deferring authentication"
|
"2SA required but no interactive terminal is available; deferring authentication"
|
||||||
@@ -185,19 +185,18 @@ class FindMyMonitor:
|
|||||||
device_index = await asyncio.to_thread(click.prompt, "Please select a device number", type=int) - 1
|
device_index = await asyncio.to_thread(click.prompt, "Please select a device number", type=int) - 1
|
||||||
device = self.api.trusted_devices[device_index]
|
device = self.api.trusted_devices[device_index]
|
||||||
if not await asyncio.to_thread(self.api.send_verification_code, device):
|
if not await asyncio.to_thread(self.api.send_verification_code, device):
|
||||||
print("Failed to send verification code")
|
logger.warning("Failed to send verification code")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Prompt the user to enter the verification code they received
|
|
||||||
code = await asyncio.to_thread(click.prompt, "Please enter verification code", type=int)
|
code = await asyncio.to_thread(click.prompt, "Please enter verification code", type=int)
|
||||||
if not await asyncio.to_thread(self.api.validate_verification_code, device, code):
|
if not await asyncio.to_thread(self.api.validate_verification_code, device, code):
|
||||||
print("Failed to verify verification code")
|
logger.warning("Failed to verify verification code")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
print("Successfully authenticated.")
|
logger.info("Successfully authenticated.")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def get_location(self):
|
async def get_location(self) -> ICloudReturnData | None:
|
||||||
"""Fetches the latest latitude and longitude."""
|
"""Fetches the latest latitude and longitude."""
|
||||||
if not self.api:
|
if not self.api:
|
||||||
await self.authenticate()
|
await self.authenticate()
|
||||||
@@ -259,7 +258,7 @@ class FindMyMonitor:
|
|||||||
"deviceStatus": status['deviceStatus'],
|
"deviceStatus": status['deviceStatus'],
|
||||||
"name": status['name']
|
"name": status['name']
|
||||||
}
|
}
|
||||||
response = iCloudReturnData(**data)
|
response = ICloudReturnData(**data)
|
||||||
return response
|
return response
|
||||||
logger.info("Location payload is None for device=%s", self.selected_device.name)
|
logger.info("Location payload is None for device=%s", self.selected_device.name)
|
||||||
return None
|
return None
|
||||||
@@ -279,14 +278,12 @@ class FindMyMonitor:
|
|||||||
self.running = False
|
self.running = False
|
||||||
|
|
||||||
def _no_location_backoff_seconds(self, base_interval: int) -> int:
|
def _no_location_backoff_seconds(self, base_interval: int) -> int:
|
||||||
schedule = [15, 30, 60, 120, 300]
|
idx = min(self._no_location_streak - 1, len(BACKOFF_SCHEDULE) - 1)
|
||||||
idx = min(self._no_location_streak - 1, len(schedule) - 1)
|
return max(base_interval, BACKOFF_SCHEDULE[idx])
|
||||||
return max(base_interval, schedule[idx])
|
|
||||||
|
|
||||||
def _auth_backoff_seconds(self, base_interval: int) -> int:
|
def _auth_backoff_seconds(self, base_interval: int) -> int:
|
||||||
schedule = [15, 30, 60, 120, 300]
|
idx = min(self._auth_error_streak - 1, len(BACKOFF_SCHEDULE) - 1)
|
||||||
idx = min(self._auth_error_streak - 1, len(schedule) - 1)
|
return max(base_interval, BACKOFF_SCHEDULE[idx])
|
||||||
return max(base_interval, schedule[idx])
|
|
||||||
|
|
||||||
async def refresh_location(self):
|
async def refresh_location(self):
|
||||||
"""Fetch one location update while serializing iCloud API access."""
|
"""Fetch one location update while serializing iCloud API access."""
|
||||||
@@ -308,7 +305,12 @@ class FindMyMonitor:
|
|||||||
if device_data is not None:
|
if device_data is not None:
|
||||||
self._no_location_streak = 0
|
self._no_location_streak = 0
|
||||||
self._auth_error_streak = 0
|
self._auth_error_streak = 0
|
||||||
print(f"{device_data.timeStamp} - Location: {device_data.latitude}, {device_data.longitude}")
|
logger.info(
|
||||||
|
"%s - Location: %s, %s",
|
||||||
|
device_data.timeStamp,
|
||||||
|
device_data.latitude,
|
||||||
|
device_data.longitude,
|
||||||
|
)
|
||||||
await self.queue.put(device_data)
|
await self.queue.put(device_data)
|
||||||
else:
|
else:
|
||||||
self._no_location_streak += 1
|
self._no_location_streak += 1
|
||||||
|
|||||||
@@ -1,213 +0,0 @@
|
|||||||
from pymobiledevice3.services.dvt.instruments.location_simulation_base import (
|
|
||||||
LocationSimulationBase,
|
|
||||||
)
|
|
||||||
from pymobiledevice3.services.dvt.instruments.location_simulation import (
|
|
||||||
LocationSimulation,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class LocationSimulationQueue(LocationSimulation):
|
|
||||||
def __init__(self, dvt, context: LocationSimulationState):
|
|
||||||
super().__init__(dvt)
|
|
||||||
self.context = context
|
|
||||||
|
|
||||||
async def play_queue(
|
|
||||||
self, disable_sleep: bool = False, timing_randomness_range: int = 0
|
|
||||||
) -> None:
|
|
||||||
while True:
|
|
||||||
if self.context.queue_state == "PAUSED":
|
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
continue
|
|
||||||
if self.context.queue_state == "SHUTDOWN":
|
|
||||||
break
|
|
||||||
loc_id = await self.context.queue.get()
|
|
||||||
if loc_id is None:
|
|
||||||
self.context.queue.task_done()
|
|
||||||
break
|
|
||||||
location_item = self.context.queue_data.get(loc_id)
|
|
||||||
if location_item is None:
|
|
||||||
logger.warning(
|
|
||||||
"Simulation queue item missing for loc_id=%s; skipping stale entry",
|
|
||||||
loc_id,
|
|
||||||
)
|
|
||||||
self.context.queue.task_done()
|
|
||||||
continue
|
|
||||||
new_latitude = location_item.get("latitude")
|
|
||||||
new_longitude = location_item.get("longitude")
|
|
||||||
new_delay = location_item.get("delay")
|
|
||||||
new_delay = 0 if new_delay is None else new_delay
|
|
||||||
new_start = location_item.get("start")
|
|
||||||
|
|
||||||
current_location_item = self.context.queue_data.get(self.context.loc_id)
|
|
||||||
current_latitude = (
|
|
||||||
current_location_item.get("latitude")
|
|
||||||
if isinstance(current_location_item, dict)
|
|
||||||
else self.context.latitude
|
|
||||||
)
|
|
||||||
current_longitude = (
|
|
||||||
current_location_item.get("longitude")
|
|
||||||
if isinstance(current_location_item, dict)
|
|
||||||
else self.context.longitude
|
|
||||||
)
|
|
||||||
current_start = (
|
|
||||||
current_location_item.get("start")
|
|
||||||
if isinstance(current_location_item, dict)
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.context.set_location_enabled:
|
|
||||||
if new_delay > 0 and not disable_sleep:
|
|
||||||
countdown_delay = int(round(float(new_delay)))
|
|
||||||
if timing_randomness_range > 0:
|
|
||||||
new_delay = new_delay + random.uniform(
|
|
||||||
-timing_randomness_range, timing_randomness_range
|
|
||||||
)
|
|
||||||
countdown_delay = int(round(float(new_delay)))
|
|
||||||
for i in range(max(0, countdown_delay), 0, -1):
|
|
||||||
self.context.next_move = i
|
|
||||||
await self.context.sio.emit(
|
|
||||||
"simulation_status",
|
|
||||||
{
|
|
||||||
"status": self.context.simulation_active,
|
|
||||||
"loc_id": self.context.loc_id,
|
|
||||||
"latitude": current_latitude,
|
|
||||||
"longitude": current_longitude,
|
|
||||||
"start": current_start,
|
|
||||||
"next_move": i,
|
|
||||||
},
|
|
||||||
namespace="/",
|
|
||||||
)
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
self.context.queue_data[loc_id]["start"] = self.context.queue_data[self.context.loc_id]["end"] = datetime.now(timezone.utc).isoformat()
|
|
||||||
await self.set(new_latitude, new_longitude)
|
|
||||||
self.context.loc_id = loc_id
|
|
||||||
self.context.latitude = new_latitude
|
|
||||||
self.context.longitude = new_longitude
|
|
||||||
await self.context.sio.emit(
|
|
||||||
"simulation_status",
|
|
||||||
{
|
|
||||||
"status": self.context.simulation_active,
|
|
||||||
"loc_id": self.context.loc_id,
|
|
||||||
"latitude": self.context.latitude,
|
|
||||||
"longitude": self.context.longitude,
|
|
||||||
"start": new_start,
|
|
||||||
"next_move": None,
|
|
||||||
},
|
|
||||||
namespace="/",
|
|
||||||
)
|
|
||||||
logger.info(
|
|
||||||
"Set simulated location to %s, %s after %ss delay",
|
|
||||||
new_latitude,
|
|
||||||
new_longitude,
|
|
||||||
new_delay,
|
|
||||||
)
|
|
||||||
self.context.queue.task_done()
|
|
||||||
|
|
||||||
|
|
||||||
class LocationSimulationTestQueue(LocationSimulationBase):
|
|
||||||
def __init__(self, context: LocationSimulationState):
|
|
||||||
super().__init__()
|
|
||||||
self.context = context
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
async def set(self, latitude: float, longitude: float) -> None:
|
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
logger.info("Simulated location set to %s, %s", latitude, longitude)
|
|
||||||
|
|
||||||
async def clear(self) -> None:
|
|
||||||
q = self.context.queue
|
|
||||||
self.context.set_location_enabled = False
|
|
||||||
self.context.queue_state = "SHUTDOWN"
|
|
||||||
while not q.empty():
|
|
||||||
try:
|
|
||||||
item = q.get_nowait()
|
|
||||||
q.task_done()
|
|
||||||
logger.info("Discarding item from queue: %s", item)
|
|
||||||
except asyncio.QueueEmpty:
|
|
||||||
break
|
|
||||||
await q.put(None)
|
|
||||||
|
|
||||||
if self.context.simulation_task is not None and not self.context.simulation_task.done():
|
|
||||||
try:
|
|
||||||
await asyncio.wait_for(self.context.simulation_task, timeout=5)
|
|
||||||
except TimeoutError:
|
|
||||||
self.context.simulation_task.cancel()
|
|
||||||
with suppress(asyncio.CancelledError):
|
|
||||||
await self.context.simulation_task
|
|
||||||
self.context.simulation_active = False
|
|
||||||
self.context.queue_state = "SHUTDOWN"
|
|
||||||
|
|
||||||
async def play_queue(
|
|
||||||
self, disable_sleep: bool = False, timing_randomness_range: int = 0
|
|
||||||
) -> None:
|
|
||||||
while True:
|
|
||||||
if self.context.queue_state == "PAUSED":
|
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
continue
|
|
||||||
if self.context.queue_state == "SHUTDOWN":
|
|
||||||
break
|
|
||||||
loc_id = await self.context.queue.get()
|
|
||||||
if loc_id is None:
|
|
||||||
self.context.queue.task_done()
|
|
||||||
break
|
|
||||||
location_item = self.context.queue_data.get(loc_id)
|
|
||||||
if location_item is None:
|
|
||||||
logger.warning(
|
|
||||||
"Test simulation queue item missing for loc_id=%s; skipping stale entry",
|
|
||||||
loc_id,
|
|
||||||
)
|
|
||||||
self.context.queue.task_done()
|
|
||||||
continue
|
|
||||||
latitude = location_item.get("latitude")
|
|
||||||
longitude = location_item.get("longitude")
|
|
||||||
delay = location_item.get("delay")
|
|
||||||
delay = 0 if delay is None else delay
|
|
||||||
start_time = location_item.get("start")
|
|
||||||
if self.context.set_location_enabled:
|
|
||||||
if delay > 0 and not disable_sleep:
|
|
||||||
countdown_delay = int(round(float(delay)))
|
|
||||||
if timing_randomness_range > 0:
|
|
||||||
delay = delay + random.uniform(
|
|
||||||
-timing_randomness_range, timing_randomness_range
|
|
||||||
)
|
|
||||||
countdown_delay = int(round(float(delay)))
|
|
||||||
for i in range(max(0, countdown_delay), 0, -1):
|
|
||||||
self.context.next_move = i
|
|
||||||
await self.context.sio.emit(
|
|
||||||
"simulation_status",
|
|
||||||
{
|
|
||||||
"status": self.context.simulation_active,
|
|
||||||
"loc_id": self.context.loc_id,
|
|
||||||
"latitude": self.context.latitude,
|
|
||||||
"longitude": self.context.longitude,
|
|
||||||
"next_move": i,
|
|
||||||
},
|
|
||||||
namespace="/",
|
|
||||||
)
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
await self.set(latitude, longitude)
|
|
||||||
self.context.latitude = latitude
|
|
||||||
self.context.longitude = longitude
|
|
||||||
await self.context.sio.emit(
|
|
||||||
"simulation_status",
|
|
||||||
{
|
|
||||||
"status": self.context.simulation_active,
|
|
||||||
"loc_id": self.context.loc_id,
|
|
||||||
"latitude": self.context.latitude,
|
|
||||||
"longitude": self.context.longitude,
|
|
||||||
"next_move": None,
|
|
||||||
},
|
|
||||||
namespace="/",
|
|
||||||
)
|
|
||||||
logger.info(
|
|
||||||
"Set simulated location to %s, %s after %ss delay",
|
|
||||||
latitude,
|
|
||||||
longitude,
|
|
||||||
delay,
|
|
||||||
)
|
|
||||||
self.context.queue.task_done()
|
|
||||||
@@ -1,59 +1,63 @@
|
|||||||
from typing import Optional, Dict
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
class SimulationStatusData(BaseModel):
|
|
||||||
|
class Coordinate(BaseModel):
|
||||||
latitude: float
|
latitude: float
|
||||||
longitude: float
|
longitude: float
|
||||||
|
|
||||||
|
|
||||||
|
class ScheduledCoordinate(Coordinate):
|
||||||
|
delay: int = 0
|
||||||
|
start: Optional[str] = None
|
||||||
|
end: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class SimulationStatusData(Coordinate):
|
||||||
start: float
|
start: float
|
||||||
end: Optional[float]
|
end: Optional[float] = None
|
||||||
next_move: Optional[float]
|
next_move: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
class SimulationStatus(BaseModel):
|
class SimulationStatus(BaseModel):
|
||||||
status: bool
|
status: bool
|
||||||
data: Optional[SimulationStatusData]
|
data: Optional[SimulationStatusData] = None
|
||||||
|
|
||||||
|
|
||||||
class SimulationRequestData(BaseModel):
|
class SimulationRequestData(ScheduledCoordinate):
|
||||||
latitude: float
|
pass
|
||||||
longitude: float
|
|
||||||
delay: int = 0
|
|
||||||
start: Optional[str] = None
|
|
||||||
end: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class SimulationRequest(BaseModel):
|
class SimulationRequest(BaseModel):
|
||||||
status: bool
|
status: bool
|
||||||
data: Optional[SimulationRequestData]
|
data: Optional[SimulationRequestData] = None
|
||||||
|
|
||||||
|
|
||||||
class SimulationRequestResponseData(BaseModel):
|
class SimulationRequestResponseData(ScheduledCoordinate):
|
||||||
loc_id: str
|
loc_id: str
|
||||||
latitude: float
|
|
||||||
longitude: float
|
|
||||||
delay: int = 0
|
|
||||||
start: Optional[str] = None
|
|
||||||
end: Optional[str] = None
|
|
||||||
|
|
||||||
class SimulationQueueList(BaseModel):
|
class SimulationQueueList(BaseModel):
|
||||||
data: Optional[SimulationRequestResponseData]
|
data: Optional[SimulationRequestResponseData] = None
|
||||||
|
|
||||||
|
|
||||||
class SimulationRequestResponse(BaseModel):
|
class SimulationRequestResponse(BaseModel):
|
||||||
status: bool
|
status: bool
|
||||||
data: Optional[SimulationRequestResponseData]
|
data: Optional[SimulationRequestResponseData] = None
|
||||||
|
|
||||||
|
|
||||||
class SimulationQueueDict(BaseModel):
|
class SimulationQueueDict(BaseModel):
|
||||||
location_id: Dict[str, SimulationRequestResponseData]
|
location_id: dict[str, SimulationRequestResponseData]
|
||||||
|
|
||||||
class iCloudLocationData(BaseModel):
|
|
||||||
latitude: float
|
class ICloudLocationData(Coordinate):
|
||||||
longitude: float
|
|
||||||
timestamp: str
|
timestamp: str
|
||||||
|
|
||||||
class iCloudReturnData(BaseModel):
|
|
||||||
latitude: float
|
class ICloudReturnData(Coordinate):
|
||||||
longitude: float
|
|
||||||
timeStamp: int
|
timeStamp: int
|
||||||
altitude: float
|
altitude: float
|
||||||
horizontalAccuracy: float
|
horizontalAccuracy: float
|
||||||
@@ -63,11 +67,16 @@ class iCloudReturnData(BaseModel):
|
|||||||
deviceStatus: int
|
deviceStatus: int
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
class LatLng(BaseModel):
|
|
||||||
latitude: float
|
class LatLng(Coordinate):
|
||||||
longitude: float
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ORSRequest(BaseModel):
|
class ORSRequest(BaseModel):
|
||||||
geometry_simplify: bool
|
geometry_simplify: bool
|
||||||
coordinates: List[List[float]]
|
coordinates: list[list[float]]
|
||||||
|
|
||||||
|
|
||||||
|
# Backward compatibility aliases for existing imports.
|
||||||
|
iCloudLocationData = ICloudLocationData
|
||||||
|
iCloudReturnData = ICloudReturnData
|
||||||
|
|||||||
@@ -1,379 +0,0 @@
|
|||||||
""" Tunnel Functions"""
|
|
||||||
|
|
||||||
@self._app.get("/start-tunnel")
|
|
||||||
async def start_tunnel(
|
|
||||||
udid: Optional[str] = self.context.udid,
|
|
||||||
ip: Optional[str] = None,
|
|
||||||
connection_type: Optional[str] = None,
|
|
||||||
) -> fastapi.Response:
|
|
||||||
udid_tunnels = [
|
|
||||||
t.tunnel
|
|
||||||
for t in self._tunneld_core.tunnel_tasks.values()
|
|
||||||
if t.udid == udid and t.tunnel is not None
|
|
||||||
]
|
|
||||||
if len(udid_tunnels) > 0:
|
|
||||||
self.context.udid = udid
|
|
||||||
data = {
|
|
||||||
"interface": udid_tunnels[0].interface,
|
|
||||||
"port": udid_tunnels[0].port,
|
|
||||||
"address": udid_tunnels[0].address,
|
|
||||||
}
|
|
||||||
return generate_http_response(data)
|
|
||||||
queue = asyncio.Queue()
|
|
||||||
created_task = False
|
|
||||||
try:
|
|
||||||
if not created_task and connection_type in ("usbmux", None):
|
|
||||||
task_identifier = f"usbmux-{udid}"
|
|
||||||
try:
|
|
||||||
async with await create_using_usbmux(udid) as lockdown:
|
|
||||||
service = await CoreDeviceTunnelProxy.create(lockdown)
|
|
||||||
task = asyncio.create_task(
|
|
||||||
self._tunneld_core.start_tunnel_task(
|
|
||||||
task_identifier,
|
|
||||||
service,
|
|
||||||
protocol=TunnelProtocol.TCP,
|
|
||||||
queue=queue,
|
|
||||||
),
|
|
||||||
name=f"start-tunnel-task-{task_identifier}",
|
|
||||||
)
|
|
||||||
self._tunneld_core.tunnel_tasks[task_identifier] = TunnelTask(
|
|
||||||
task=task, udid=udid
|
|
||||||
)
|
|
||||||
created_task = True
|
|
||||||
except ConnectionFailedError, InvalidServiceError, MuxException:
|
|
||||||
pass
|
|
||||||
if connection_type in ("usb", None):
|
|
||||||
for rsd in await get_rsds(udid=udid):
|
|
||||||
rsd_ip = rsd.service.address[0]
|
|
||||||
if ip is not None and rsd_ip != ip:
|
|
||||||
await rsd.close()
|
|
||||||
continue
|
|
||||||
task = asyncio.create_task(
|
|
||||||
self._tunneld_core.start_tunnel_task(
|
|
||||||
rsd_ip,
|
|
||||||
await create_core_device_tunnel_service_using_rsd(rsd),
|
|
||||||
queue=queue,
|
|
||||||
),
|
|
||||||
name=f"start-tunnel-usb-{rsd_ip}",
|
|
||||||
)
|
|
||||||
self._tunneld_core.tunnel_tasks[rsd_ip] = TunnelTask(
|
|
||||||
task=task, udid=rsd.udid
|
|
||||||
)
|
|
||||||
created_task = True
|
|
||||||
if not created_task and connection_type in ("wifi", None):
|
|
||||||
for remotepairing in await get_remote_pairing_tunnel_services(
|
|
||||||
udid=udid
|
|
||||||
):
|
|
||||||
remotepairing_ip = remotepairing.hostname
|
|
||||||
if ip is not None and remotepairing_ip != ip:
|
|
||||||
await remotepairing.close()
|
|
||||||
continue
|
|
||||||
task = asyncio.create_task(
|
|
||||||
self._tunneld_core.start_tunnel_task(
|
|
||||||
remotepairing_ip, remotepairing, queue=queue
|
|
||||||
),
|
|
||||||
name=f"start-tunnel-wifi-{remotepairing_ip}",
|
|
||||||
)
|
|
||||||
self._tunneld_core.tunnel_tasks[remotepairing_ip] = TunnelTask(
|
|
||||||
task=task, udid=remotepairing.remote_identifier
|
|
||||||
)
|
|
||||||
created_task = True
|
|
||||||
except Exception as e:
|
|
||||||
return fastapi.Response(
|
|
||||||
status_code=501,
|
|
||||||
content=json.dumps(
|
|
||||||
{
|
|
||||||
"error": {
|
|
||||||
"exception": e.__class__.__name__,
|
|
||||||
"traceback": traceback.format_exc(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if not created_task:
|
|
||||||
return fastapi.Response(
|
|
||||||
status_code=501, content=json.dumps({"error": "task not created"})
|
|
||||||
)
|
|
||||||
tunnel: Optional[TunnelResult] = await queue.get()
|
|
||||||
if tunnel is not None:
|
|
||||||
self.context.udid = udid
|
|
||||||
data = {
|
|
||||||
"interface": tunnel.interface,
|
|
||||||
"port": tunnel.port,
|
|
||||||
"address": tunnel.address,
|
|
||||||
}
|
|
||||||
return generate_http_response(data)
|
|
||||||
else:
|
|
||||||
return fastapi.Response(
|
|
||||||
status_code=404,
|
|
||||||
content=json.dumps(
|
|
||||||
{"error": "something went wrong during tunnel creation"}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
@self._app.get("/restart-tunneld")
|
|
||||||
async def restart() -> fastapi.Response:
|
|
||||||
"""Restart Tunneld"""
|
|
||||||
self._tunneld_core.clear()
|
|
||||||
await asyncio.sleep(2)
|
|
||||||
self._tunneld_core.start()
|
|
||||||
data = {
|
|
||||||
"operation": "restart-tunneld",
|
|
||||||
"data": True,
|
|
||||||
"message": "Restarting tunneld...",
|
|
||||||
}
|
|
||||||
return generate_http_response(data)
|
|
||||||
|
|
||||||
|
|
||||||
@self._app.get("/shutdown")
|
|
||||||
async def shutdown() -> fastapi.Response:
|
|
||||||
"""Shutdown Tunneld"""
|
|
||||||
os.kill(os.getpid(), signal.SIGINT)
|
|
||||||
data = {
|
|
||||||
"operation": "shutdown",
|
|
||||||
"data": True,
|
|
||||||
"message": "Server shutting down...",
|
|
||||||
}
|
|
||||||
return generate_http_response(data)
|
|
||||||
|
|
||||||
@self._app.get("/clear-tunnels")
|
|
||||||
async def clear_tunnels() -> fastapi.Response:
|
|
||||||
"""Clear all tunnels"""
|
|
||||||
self._tunneld_core.clear()
|
|
||||||
data = {
|
|
||||||
"operation": "clear_tunnels",
|
|
||||||
"data": True,
|
|
||||||
"message": "Cleared tunnels...",
|
|
||||||
}
|
|
||||||
return generate_http_response(data)
|
|
||||||
|
|
||||||
@self._app.get("/cancel")
|
|
||||||
async def cancel_tunnel(udid: str) -> fastapi.Response:
|
|
||||||
"""Cancel a tunnel"""
|
|
||||||
self._tunneld_core.cancel(udid=udid)
|
|
||||||
data = {
|
|
||||||
"operation": "cancel",
|
|
||||||
"udid": udid,
|
|
||||||
"data": True,
|
|
||||||
"message": f"tunnel {udid} Canceled ...",
|
|
||||||
}
|
|
||||||
return generate_http_response(data)
|
|
||||||
|
|
||||||
"""Simulation Functions"""
|
|
||||||
|
|
||||||
@self._app.get("/start-simulation")
|
|
||||||
async def app_start_simulation() -> fastapi.Response:
|
|
||||||
logger.info("Simulation Start Requested ")
|
|
||||||
if (
|
|
||||||
self.context.simulation_task is None
|
|
||||||
or self.context.simulation_task.done()
|
|
||||||
):
|
|
||||||
await start_icloud_monitor()
|
|
||||||
self.context.simulation_active = True
|
|
||||||
self.context.simulation_task = asyncio.create_task(
|
|
||||||
start_simulation_queue(),
|
|
||||||
name="location-simulation-worker",
|
|
||||||
)
|
|
||||||
data = {"status": "started", "message": "Simulation started"}
|
|
||||||
else:
|
|
||||||
data = {"status": "error", "message": "Simulation already running"}
|
|
||||||
return generate_http_response(data)
|
|
||||||
|
|
||||||
@self._app.get("/start-icloud-monitor")
|
|
||||||
async def app_start_icloud_monitor() -> fastapi.Response:
|
|
||||||
await start_icloud_monitor()
|
|
||||||
data = {
|
|
||||||
"status": "started",
|
|
||||||
"icloud_monitor_enabled": self.context.icloud_monitor_enabled,
|
|
||||||
"icloud_monitor_running": is_icloud_monitor_running(),
|
|
||||||
}
|
|
||||||
return generate_http_response(data)
|
|
||||||
|
|
||||||
@self._app.get("/stop-icloud-monitor")
|
|
||||||
async def app_stop_icloud_monitor() -> fastapi.Response:
|
|
||||||
await end_icloud_monitor()
|
|
||||||
data = {
|
|
||||||
"status": "stopped",
|
|
||||||
"icloud_monitor_enabled": self.context.icloud_monitor_enabled,
|
|
||||||
"icloud_monitor_running": is_icloud_monitor_running(),
|
|
||||||
}
|
|
||||||
return generate_http_response(data)
|
|
||||||
|
|
||||||
@self._app.get("/icloud-monitor-status")
|
|
||||||
async def app_icloud_monitor_status() -> fastapi.Response:
|
|
||||||
data = {
|
|
||||||
"status": "ok",
|
|
||||||
"icloud_monitor_enabled": self.context.icloud_monitor_enabled,
|
|
||||||
"icloud_monitor_running": is_icloud_monitor_running(),
|
|
||||||
}
|
|
||||||
return generate_http_response(data)
|
|
||||||
|
|
||||||
@self._app.post("/add-location")
|
|
||||||
async def app_add_location(data: SimulationRequestData) -> fastapi.Response:
|
|
||||||
"""Add a location to the simulation queue"""
|
|
||||||
logger.info("Request to add new location to queue")
|
|
||||||
|
|
||||||
loc_id = str(uuid.uuid4())
|
|
||||||
latitude = (
|
|
||||||
data.get("latitude")
|
|
||||||
if isinstance(data, dict)
|
|
||||||
else getattr(data, "latitude", None)
|
|
||||||
)
|
|
||||||
longitude = (
|
|
||||||
data.get("longitude")
|
|
||||||
if isinstance(data, dict)
|
|
||||||
else getattr(data, "longitude", None)
|
|
||||||
)
|
|
||||||
delay = (
|
|
||||||
data.get("delay", 0)
|
|
||||||
if isinstance(data, dict)
|
|
||||||
else getattr(data, "delay", 0)
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
delay = parse_delay_seconds(delay)
|
|
||||||
except ValueError as e:
|
|
||||||
return generate_http_response(
|
|
||||||
{"status": "error", "message": str(e)},
|
|
||||||
status_code=400,
|
|
||||||
)
|
|
||||||
if latitude is not None and longitude is not None:
|
|
||||||
logger.info(
|
|
||||||
"Adding location %s (%s, %s) with %s delay to the queue",
|
|
||||||
loc_id,
|
|
||||||
latitude,
|
|
||||||
longitude,
|
|
||||||
delay,
|
|
||||||
)
|
|
||||||
accrued_delay = 0
|
|
||||||
if self.context.queue_data:
|
|
||||||
accrued_delay = sum(
|
|
||||||
parse_delay_seconds(item.get("delay", 0))
|
|
||||||
for item in self.context.queue_data.values()
|
|
||||||
)
|
|
||||||
now_time = datetime.now(timezone.utc)
|
|
||||||
new_time = (
|
|
||||||
now_time
|
|
||||||
+ timedelta(seconds=accrued_delay)
|
|
||||||
+ timedelta(seconds=delay)
|
|
||||||
)
|
|
||||||
start_time = new_time.isoformat()
|
|
||||||
location_item = {
|
|
||||||
"loc_id": loc_id,
|
|
||||||
"latitude": latitude,
|
|
||||||
"longitude": longitude,
|
|
||||||
"delay": delay,
|
|
||||||
"start": start_time,
|
|
||||||
"status": "queued",
|
|
||||||
}
|
|
||||||
resp = {
|
|
||||||
"status": "added",
|
|
||||||
"message": f"Location {loc_id} added to the queue",
|
|
||||||
"item": location_item,
|
|
||||||
}
|
|
||||||
await self.context.queue.put(loc_id)
|
|
||||||
add_item(loc_id, location_item)
|
|
||||||
logger.info("Location %s added to the queue", loc_id)
|
|
||||||
else:
|
|
||||||
resp = {"status": "error", "message": "Invalid location data"}
|
|
||||||
return generate_http_response(resp)
|
|
||||||
|
|
||||||
@self._app.get("/clear-queue")
|
|
||||||
async def app_clear_queue() -> fastapi.Response:
|
|
||||||
"""Clear the simulation queue"""
|
|
||||||
logger.info("Simulation Start Requested ")
|
|
||||||
await empty_simulation_queue()
|
|
||||||
data = {"status": "cleared", "message": "Simulation cleared"}
|
|
||||||
return generate_http_response(data)
|
|
||||||
|
|
||||||
@self._app.get("/pause-queue")
|
|
||||||
async def app_pause_queue() -> fastapi.Response:
|
|
||||||
"""Pause the simulation queue"""
|
|
||||||
await pause_simulation_queue()
|
|
||||||
data = {"status": "paused", "message": "Simulation paused"}
|
|
||||||
return generate_http_response(data)
|
|
||||||
|
|
||||||
@self._app.get("/resume-queue")
|
|
||||||
async def app_resume_queue() -> fastapi.Response:
|
|
||||||
"""Resume the simulation queue"""
|
|
||||||
await resume_simulation_queue()
|
|
||||||
data = {"status": "resumed", "message": "Simulation resumed"}
|
|
||||||
return generate_http_response(data)
|
|
||||||
|
|
||||||
@self._app.get("/end-simulation")
|
|
||||||
async def app_end_simulation() -> fastapi.Response:
|
|
||||||
"""End the simulation queue"""
|
|
||||||
logger.info("End location simulation request")
|
|
||||||
end_task = asyncio.create_task(
|
|
||||||
end_simulation_queue(), name="end-simulation-worker"
|
|
||||||
)
|
|
||||||
result = await end_task
|
|
||||||
data = {"status": result, "message": "Simulation ended"}
|
|
||||||
return generate_http_response(data)
|
|
||||||
|
|
||||||
"""Status Functions"""
|
|
||||||
|
|
||||||
@self._app.get("/")
|
|
||||||
async def list_tunnels() -> dict[str, list[dict]]:
|
|
||||||
"""Retrieve the available tunnels and format them as {UUID: TUNNEL_ADDRESS}"""
|
|
||||||
tunnels = {}
|
|
||||||
for ip, active_tunnel in self._tunneld_core.tunnel_tasks.items():
|
|
||||||
if (active_tunnel.udid is None) or (active_tunnel.tunnel is None):
|
|
||||||
continue
|
|
||||||
if active_tunnel.udid not in tunnels:
|
|
||||||
tunnels[active_tunnel.udid] = []
|
|
||||||
tunnels[active_tunnel.udid].append(
|
|
||||||
{
|
|
||||||
"tunnel-address": active_tunnel.tunnel.address,
|
|
||||||
"tunnel-port": active_tunnel.tunnel.port,
|
|
||||||
"interface": ip,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return tunnels
|
|
||||||
|
|
||||||
|
|
||||||
@self._app.get("/device-info")
|
|
||||||
async def device_info():
|
|
||||||
"""Get device information"""
|
|
||||||
tunnels = {}
|
|
||||||
for ip, active_tunnel in self._tunneld_core.tunnel_tasks.items():
|
|
||||||
if (active_tunnel.udid is None) or (active_tunnel.tunnel is None):
|
|
||||||
continue
|
|
||||||
if active_tunnel.udid not in tunnels:
|
|
||||||
tunnels[active_tunnel.udid] = {}
|
|
||||||
try:
|
|
||||||
lockdown = await create_using_usbmux(
|
|
||||||
serial=active_tunnel.udid, autopair=False
|
|
||||||
)
|
|
||||||
tunnels[active_tunnel.udid] = iterate_multidim(lockdown.all_values)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(
|
|
||||||
f"Failed to create lockdown session for device {active_tunnel.udid}: {e}"
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
return tunnels
|
|
||||||
|
|
||||||
@self._app.get("/device-name")
|
|
||||||
async def rsd_info():
|
|
||||||
"""Get rsd information"""
|
|
||||||
device_name = await get_device_name()
|
|
||||||
return generate_http_response(device_name)
|
|
||||||
|
|
||||||
@self._app.get("/rsd-info")
|
|
||||||
async def rsd_info():
|
|
||||||
"""Get rsd information"""
|
|
||||||
rsd_info = {}
|
|
||||||
if self.context.tunnel is None:
|
|
||||||
await get_tun()
|
|
||||||
if self.context.tunnel is not None:
|
|
||||||
rsd_info = self.context.tunnel.peer_info
|
|
||||||
return generate_http_response(rsd_info)
|
|
||||||
|
|
||||||
@self._app.get("/hello")
|
|
||||||
async def hello() -> fastapi.Response:
|
|
||||||
data = {"message": "Hello, I'm alive"}
|
|
||||||
return generate_http_response(data)
|
|
||||||
|
|
||||||
@self._app.get("/context-status")
|
|
||||||
async def app_context_status() -> fastapi.Response:
|
|
||||||
data = get_status()
|
|
||||||
return generate_http_response(data)
|
|
||||||
@@ -1,425 +0,0 @@
|
|||||||
""" Socket.IO Functions"""
|
|
||||||
|
|
||||||
|
|
||||||
async def sio_send_status(sid):
|
|
||||||
"""Send Current Status"""
|
|
||||||
await self.context.sio.emit(
|
|
||||||
"status", jsonable_encoder(get_status()), namespace="/", to=sid
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
"""Socket.IO Connection Events"""
|
|
||||||
|
|
||||||
|
|
||||||
@self.context.sio.event
|
|
||||||
async def connect(sid, environ):
|
|
||||||
"""Client connection event handler."""
|
|
||||||
self.context.connected_clients.add(sid)
|
|
||||||
logger.info("Client connected: %s", sid)
|
|
||||||
await sio_send_status(sid)
|
|
||||||
return "%s connected" % sid
|
|
||||||
|
|
||||||
|
|
||||||
@self.context.sio.event
|
|
||||||
async def disconnect(sid):
|
|
||||||
"""Client disconnection event handler."""
|
|
||||||
self.context.connected_clients.discard(sid)
|
|
||||||
logger.info("Client disconnected: %s", sid)
|
|
||||||
|
|
||||||
|
|
||||||
""" Socket.IO Request Events """
|
|
||||||
|
|
||||||
|
|
||||||
@self.context.sio.event
|
|
||||||
async def request_update(sid):
|
|
||||||
status_update = jsonable_encoder(get_status())
|
|
||||||
logger.info("Update request from %s sending %s", sid, status_update)
|
|
||||||
return status_update
|
|
||||||
|
|
||||||
|
|
||||||
@self.context.sio.event
|
|
||||||
async def message(sid, data):
|
|
||||||
logger.info("Received message from %s: %s", sid, data)
|
|
||||||
return True, "Message received"
|
|
||||||
|
|
||||||
|
|
||||||
# await self.context.sio.emit("message", f"Received message from {sid}: {data}", namespace="/")
|
|
||||||
|
|
||||||
""" Device Control"""
|
|
||||||
|
|
||||||
|
|
||||||
@self.context.sio.event
|
|
||||||
async def device_control(sid, data):
|
|
||||||
"""Device Control"""
|
|
||||||
command = (
|
|
||||||
data.get("command")
|
|
||||||
if isinstance(data, dict)
|
|
||||||
else getattr(data, "command", None)
|
|
||||||
)
|
|
||||||
delay = (
|
|
||||||
data.get("delay")
|
|
||||||
if isinstance(data, dict)
|
|
||||||
else getattr(data, "delay", None)
|
|
||||||
)
|
|
||||||
if delay is None:
|
|
||||||
delay = 5
|
|
||||||
match command:
|
|
||||||
case "shutdown":
|
|
||||||
""" Shutdown the device"""
|
|
||||||
logger.info(
|
|
||||||
"Shutdown command received from %s with delay %s", sid, delay
|
|
||||||
)
|
|
||||||
await device_shutdown(delay)
|
|
||||||
return {
|
|
||||||
"command": "shutdown",
|
|
||||||
"status": "success",
|
|
||||||
"message": f"Device shutdown initiated with {delay} seconds delay",
|
|
||||||
}
|
|
||||||
|
|
||||||
case "reboot":
|
|
||||||
""" Reboot the device"""
|
|
||||||
logger.info(
|
|
||||||
"Reboot command received from %s with delay %s", sid, delay
|
|
||||||
)
|
|
||||||
await device_reboot(delay)
|
|
||||||
return {
|
|
||||||
"command": "reboot",
|
|
||||||
"status": "success",
|
|
||||||
"message": f"Device reboot initiated with {delay} seconds delay",
|
|
||||||
}
|
|
||||||
|
|
||||||
case _:
|
|
||||||
return {
|
|
||||||
"command": command,
|
|
||||||
"status": "error",
|
|
||||||
"message": f"Invalid command: {command}",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@self.context.sio.event
|
|
||||||
async def simulation_control(sid, data):
|
|
||||||
"""Simulation Control"""
|
|
||||||
command = (
|
|
||||||
data.get("command")
|
|
||||||
if isinstance(data, dict)
|
|
||||||
else getattr(data, "command", None)
|
|
||||||
)
|
|
||||||
logger.info(
|
|
||||||
"Simulation Control command: %s requested from %s", command, sid
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
match command:
|
|
||||||
case "add":
|
|
||||||
""" Add a location to the simulation queue"""
|
|
||||||
loc_id = str(uuid.uuid4())
|
|
||||||
latitude = (
|
|
||||||
data.get("latitude")
|
|
||||||
if isinstance(data, dict)
|
|
||||||
else getattr(data, "latitude", None)
|
|
||||||
)
|
|
||||||
longitude = (
|
|
||||||
data.get("longitude")
|
|
||||||
if isinstance(data, dict)
|
|
||||||
else getattr(data, "longitude", None)
|
|
||||||
)
|
|
||||||
delay = (
|
|
||||||
data.get("delay", 0)
|
|
||||||
if isinstance(data, dict)
|
|
||||||
else getattr(data, "delay", 0)
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
delay = parse_delay_seconds(delay)
|
|
||||||
except ValueError as e:
|
|
||||||
return {
|
|
||||||
"command": command,
|
|
||||||
"status": "error",
|
|
||||||
"message": str(e),
|
|
||||||
}
|
|
||||||
if latitude is not None and longitude is not None:
|
|
||||||
logger.info(
|
|
||||||
"Adding location %s (%s, %s) with %s delay to the queue",
|
|
||||||
loc_id,
|
|
||||||
latitude,
|
|
||||||
longitude,
|
|
||||||
delay,
|
|
||||||
)
|
|
||||||
accrued_delay = 0
|
|
||||||
if self.context.queue_data:
|
|
||||||
accrued_delay = sum(
|
|
||||||
parse_delay_seconds(item.get("delay", 0))
|
|
||||||
for item in self.context.queue_data.values()
|
|
||||||
)
|
|
||||||
now_time = datetime.now(timezone.utc)
|
|
||||||
new_time = (
|
|
||||||
now_time
|
|
||||||
+ timedelta(seconds=accrued_delay)
|
|
||||||
+ timedelta(seconds=delay)
|
|
||||||
)
|
|
||||||
start_time = new_time.isoformat()
|
|
||||||
coords = f"{latitude}, {longitude}"
|
|
||||||
rev_geocode = self.context.reverse_geocode(coords)
|
|
||||||
if rev_geocode:
|
|
||||||
address = rev_geocode.address
|
|
||||||
else:
|
|
||||||
address = f"{latitude}, {longitude}"
|
|
||||||
|
|
||||||
location_item = {
|
|
||||||
"loc_id": loc_id,
|
|
||||||
"latitude": latitude,
|
|
||||||
"longitude": longitude,
|
|
||||||
"delay": delay,
|
|
||||||
"start": start_time,
|
|
||||||
"address": address,
|
|
||||||
}
|
|
||||||
ack = {
|
|
||||||
"command": command,
|
|
||||||
"status": "added",
|
|
||||||
"message": f"Location {loc_id} added to the queue",
|
|
||||||
"item": location_item,
|
|
||||||
}
|
|
||||||
await self.context.queue.put(loc_id)
|
|
||||||
add_item(loc_id, location_item)
|
|
||||||
logger.info("Location %s added to the queue", loc_id)
|
|
||||||
return ack
|
|
||||||
else:
|
|
||||||
logger.warning(
|
|
||||||
"Invalid location data received from %s: %s", sid, data
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"command": command,
|
|
||||||
"status": "error",
|
|
||||||
"message": "Invalid location data",
|
|
||||||
"data": location_item,
|
|
||||||
}
|
|
||||||
case "clear":
|
|
||||||
""" Clear the simulation queue"""
|
|
||||||
await empty_simulation_queue()
|
|
||||||
return {
|
|
||||||
"command": command,
|
|
||||||
"status": "cleared",
|
|
||||||
"message": "Simulation cleared",
|
|
||||||
}
|
|
||||||
case "pause":
|
|
||||||
""" Pause the simulation queue"""
|
|
||||||
await pause_simulation_queue()
|
|
||||||
return {
|
|
||||||
"command": command,
|
|
||||||
"status": "paused",
|
|
||||||
"message": "Simulation paused",
|
|
||||||
}
|
|
||||||
case "resume":
|
|
||||||
""" Resume the simulation queue"""
|
|
||||||
await resume_simulation_queue()
|
|
||||||
return {
|
|
||||||
"command": command,
|
|
||||||
"status": "resumed",
|
|
||||||
"message": "Simulation resumed",
|
|
||||||
}
|
|
||||||
case "end":
|
|
||||||
""" End the simulation queue"""
|
|
||||||
logger.info("End location simulation request from %s", sid)
|
|
||||||
end_task = asyncio.create_task(
|
|
||||||
end_simulation_queue(), name="end-simulation-worker"
|
|
||||||
)
|
|
||||||
result = await end_task
|
|
||||||
simstatus = not result
|
|
||||||
return {
|
|
||||||
"command": command,
|
|
||||||
"status": simstatus,
|
|
||||||
"message": "Simulation ended",
|
|
||||||
}
|
|
||||||
case "start":
|
|
||||||
""" Start the simulation queue"""
|
|
||||||
logger.info("Start location simulation request from %s", sid)
|
|
||||||
if (
|
|
||||||
self.context.simulation_task is None
|
|
||||||
or self.context.simulation_task.done()
|
|
||||||
):
|
|
||||||
await start_icloud_monitor()
|
|
||||||
self.context.simulation_active = True
|
|
||||||
self.context.queue_state = "RUNNING"
|
|
||||||
self.context.simulation_task = asyncio.create_task(
|
|
||||||
start_simulation_queue(),
|
|
||||||
name="location-simulation-worker",
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"command": command,
|
|
||||||
"status": self.context.queue_state,
|
|
||||||
"message": "Simulation started",
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
return {
|
|
||||||
"command": command,
|
|
||||||
"status": "error",
|
|
||||||
"message": "Simulation already running",
|
|
||||||
}
|
|
||||||
case _:
|
|
||||||
logger.warning(
|
|
||||||
"Invalid command received from %s: %s", sid, command
|
|
||||||
)
|
|
||||||
return {"status": "error", "message": "Invalid command"}
|
|
||||||
finally:
|
|
||||||
await sio_send_status(sid)
|
|
||||||
|
|
||||||
|
|
||||||
@self.context.sio.event
|
|
||||||
async def icloud_monitor_control(sid, data):
|
|
||||||
command = (
|
|
||||||
data.get("command")
|
|
||||||
if isinstance(data, dict)
|
|
||||||
else getattr(data, "command", None)
|
|
||||||
)
|
|
||||||
logger.info(
|
|
||||||
"iCloud Monitor control command: %s requested from %s", command, sid
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
match command:
|
|
||||||
case "start":
|
|
||||||
await start_icloud_monitor()
|
|
||||||
return {
|
|
||||||
"command": command,
|
|
||||||
"status": "running",
|
|
||||||
"icloud_monitor_enabled": self.context.icloud_monitor_enabled,
|
|
||||||
"icloud_monitor_running": is_icloud_monitor_running(),
|
|
||||||
}
|
|
||||||
case "stop":
|
|
||||||
await end_icloud_monitor()
|
|
||||||
return {
|
|
||||||
"command": command,
|
|
||||||
"status": "stopped",
|
|
||||||
"icloud_monitor_enabled": self.context.icloud_monitor_enabled,
|
|
||||||
"icloud_monitor_running": is_icloud_monitor_running(),
|
|
||||||
}
|
|
||||||
case "status":
|
|
||||||
return {
|
|
||||||
"command": command,
|
|
||||||
"status": "ok",
|
|
||||||
"icloud_monitor_enabled": self.context.icloud_monitor_enabled,
|
|
||||||
"icloud_monitor_running": is_icloud_monitor_running(),
|
|
||||||
}
|
|
||||||
case _:
|
|
||||||
return {
|
|
||||||
"command": command,
|
|
||||||
"status": "error",
|
|
||||||
"message": "Invalid command",
|
|
||||||
}
|
|
||||||
finally:
|
|
||||||
await sio_send_status(sid)
|
|
||||||
|
|
||||||
|
|
||||||
""" Tunnel Control """
|
|
||||||
|
|
||||||
|
|
||||||
@self.context.sio.event
|
|
||||||
async def tunneld_control(sid, data):
|
|
||||||
command = (
|
|
||||||
data.get("command")
|
|
||||||
if isinstance(data, dict)
|
|
||||||
else getattr(data, "command", None)
|
|
||||||
)
|
|
||||||
logger.info("Tunneld Control command: %s requested from %s", command, sid)
|
|
||||||
match command:
|
|
||||||
case "start":
|
|
||||||
"""Start Tunneld"""
|
|
||||||
logger.info("Start tunneld request from %s: %s", sid, data)
|
|
||||||
try:
|
|
||||||
self._tunneld_core.start()
|
|
||||||
logger.info("Tunneld started successfully")
|
|
||||||
return {
|
|
||||||
"status": "running",
|
|
||||||
"message": "Tunneld started successfully",
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Error starting tunneld: %s", e)
|
|
||||||
return {
|
|
||||||
"command": command,
|
|
||||||
"status": "error",
|
|
||||||
"message": f"Error starting tunneld: {e}",
|
|
||||||
}
|
|
||||||
|
|
||||||
case "start-watcher":
|
|
||||||
""" Start Tunneld Watcher """
|
|
||||||
logger.info("Start tunneld watcher request from %s: %s", sid, data)
|
|
||||||
await start_tunnel_watcher()
|
|
||||||
return {
|
|
||||||
"status": "running",
|
|
||||||
"message": "Tunneld watcher started successfully",
|
|
||||||
}
|
|
||||||
|
|
||||||
case "end-watcher":
|
|
||||||
""" End Tunneld Watcher """
|
|
||||||
logger.info("End tunneld watcher request from %s: %s", sid, data)
|
|
||||||
await end_tunnel_watcher()
|
|
||||||
return {
|
|
||||||
"status": "stopped",
|
|
||||||
"message": "Tunneld watcher stopped successfully",
|
|
||||||
}
|
|
||||||
|
|
||||||
case "shutdown":
|
|
||||||
"""Shutdown Tunneld"""
|
|
||||||
logger.info("Shutdown tunneld request from %s: %s", sid, data)
|
|
||||||
try:
|
|
||||||
os.kill(os.getpid(), signal.SIGINT)
|
|
||||||
return {
|
|
||||||
"command": command,
|
|
||||||
"status": "Success",
|
|
||||||
"message": "Server shutting down...",
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Error shutting down tunneld: %s", e)
|
|
||||||
return {
|
|
||||||
"command": command,
|
|
||||||
"status": "error",
|
|
||||||
"message": f"Error shutting down tunneld: {e}",
|
|
||||||
}
|
|
||||||
|
|
||||||
case "clear":
|
|
||||||
"""Clear all tunnels"""
|
|
||||||
logger.info("Clearing tunnels...")
|
|
||||||
try:
|
|
||||||
self._tunneld_core.clear()
|
|
||||||
return {
|
|
||||||
"command": command,
|
|
||||||
"status": "Success",
|
|
||||||
"message": "Cleared tunnels...",
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Error clearing tunnels: %s", e)
|
|
||||||
return {
|
|
||||||
"command": command,
|
|
||||||
"status": "error",
|
|
||||||
"message": f"Error clearing tunnels: {e}",
|
|
||||||
}
|
|
||||||
|
|
||||||
case "cancel":
|
|
||||||
"""Cancel a tunnel"""
|
|
||||||
logger.info("Canceling tunnel request from %s: %s", sid, data)
|
|
||||||
try:
|
|
||||||
udid = (
|
|
||||||
data.get("udid")
|
|
||||||
if isinstance(data, dict)
|
|
||||||
else getattr(data, "udid", self.context.udid)
|
|
||||||
)
|
|
||||||
if udid is None:
|
|
||||||
udid = self.context.udid
|
|
||||||
self._tunneld_core.cancel(udid=udid)
|
|
||||||
return {
|
|
||||||
"command": command,
|
|
||||||
"status": "Success",
|
|
||||||
"udid": udid,
|
|
||||||
"message": f"tunnel {udid} Canceled ...",
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Error canceling tunnel: %s", e)
|
|
||||||
return {
|
|
||||||
"command": command,
|
|
||||||
"status": "error",
|
|
||||||
"message": f"Error canceling tunnel: {e}",
|
|
||||||
}
|
|
||||||
|
|
||||||
case _:
|
|
||||||
return {
|
|
||||||
"command": command,
|
|
||||||
"status": "error",
|
|
||||||
"message": f"Unknown operation: {command}",
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user