pyicloud
pyicloud copied to clipboard
Add Find My Friends service
Background
I just wanted to see where my friends are at without leaving my terminal.
Changes
- Adds
FindFriendsService
service - Adds a sanity unit test for
api.friends.location
Test Plan
- [x] Passes unit tests
- [x] Returns live location data when run locally on my machine
Merge Readiness
- [x] Passes pylint and black lint
- [x] Does not break any other service or tests
- [ ] PR receives at least on approval
Addresses
Fixes #123 Fixes #231
Examples
friends.locations
[{u'id': u'REDACTED_1',
u'location': {u'address': {u'administrativeArea': u'California',
u'country': u'United States',
u'countryCode': u'US',
u'formattedAddressLines': [u'2267-2269 15th St',
u'San Francisco, CA 94114',
u'United States'],
u'locality': u'San Francisco',
u'stateCode': u'CA',
u'streetAddress': u'2267-2269 15th St',
u'streetName': u'15th St'},
u'altitude': 0.0,
u'batteryStatus': None,
u'floorLevel': 0,
u'horizontalAccuracy': 65.0,
u'isInaccurate': False,
u'labels': [],
u'latitude': 37.76551176615568,
u'locSource': None,
u'locationId': u'56bad872-xxxx-xxxx-xxxx-acc5631035f9',
u'locationTimestamp': 0,
u'longitude': -122.43462385313705,
u'tempLangForAddrAndPremises': None,
u'timestamp': 1509180784641,
u'verticalAccuracy': 0.0},
u'locationStatus': None,
u'status': None}]
friends.contact_details
[{u'contactId': u'442A82F6-XXXX-XXXX-XXXX-636CEBECD50D',
u'emails': [u'[email protected]'],
u'firstName': u'Nicole',
u'id': u''REDACTED_2',
u'lastName': u'Smith',
u'middleName': u'',
u'phones': [u'1234567891'],
u'photoUrl': u''},
{u'contactId': u'6BB8D6AB-XXXX-XXX-XXXX-90C24FAD9E0C',
u'emails': [],
u'firstName': u'John',
u'id': u'REDACTED_1',
u'lastName': u'Doe',
u'middleName': u'',
u'phones': [u'+1234567890'],
u'photoUrl': u''}]
Would love to see this implemented. What work needs to be done to move it forward?
It was working when I opened the PR, and is still working as of today. Awaiting review.
friends.locations
[{u'id': u'REDACTED_1',
u'location': {u'address': {u'administrativeArea': u'California',
u'country': u'United States',
u'countryCode': u'US',
u'formattedAddressLines': [u'2273 15th St',
u'San Francisco, CA 94114',
u'United States'],
u'locality': u'San Francisco',
u'stateCode': u'CA',
u'streetAddress': u'2273 15th St',
u'streetName': u'15th St'},
u'altitude': 0.0,
u'batteryStatus': None,
u'floorLevel': 0,
u'horizontalAccuracy': 65.0,
u'isInaccurate': False,
u'labels': [],
u'latitude': 37.765503552515035,
u'locSource': None,
u'locationId': u'c9353bd4-3e2c-4638-b90f-ce2ed639173a',
u'locationTimestamp': 0,
u'longitude': -122.43470419238015,
u'tempLangForAddrAndPremises': None,
u'timestamp': 1537193778195,
u'verticalAccuracy': 0.0},
u'locationStatus': None,
u'status': None}]
friends.followers
[{u'expires': 0,
u'expiresByGroupId': {u'kFMFGroupIdOneToOne': 0},
u'id': u'REDACTED_ID',
u'invitationAcceptedByEmail': u'+REDACTED_PHONE',
u'invitationAcceptedHandles': [u'+REDACTED_PHONE'],
u'invitationFromEmail': u'+REDACTED_PHONE',
u'invitationFromHandles': [u'+REDACTED_PHONE'],
u'invitationId': u'73a5a2ef-6622-4f7b-ac62-5a3ce075f74d',
u'invitationSentToEmail': u'+REDACTED_PHONE',
u'offerId': u'73a5a2ef-6622-4f7b-ac62-5a3ce075f74d',
u'onlyInEvent': False,
u'personId': u'REDACTED_ID',
u'personIdHash': u'3dd0fce0fe3719de9925b06b0d35fda8480951d518b4cfe21356b48b39950c34',
u'source': u'OFFER_HANDLE',
u'updateTimestamp': 1501972046422},
{u'expires': 0,
u'expiresByGroupId': {u'kFMFGroupIdOneToOne': 0},
u'id': u'REDACTED_ID',
u'invitationAcceptedByEmail': u'+REDACTED_PHONE',
u'invitationAcceptedHandles': [u'+REDACTED_PHONE'],
u'invitationFromEmail': u'+REDACTED_PHONE',
u'invitationFromHandles': [u'REDACTED_EMAIL', u'+REDACTED_PHONE'],
u'invitationId': u'db97e243-bee1-4ecb-a690-59741837ae53',
u'invitationSentToEmail': u'+REDACTED_PHONE',
u'offerId': u'',
u'onlyInEvent': False,
u'personId': u'REDACTED_ID',
u'personIdHash': u'2fc2b746184a33a999c70aea6fcbf1b09bdad96463dc0ce421a97e38f2b409c3',
u'source': u'APP_OFFER',
u'updateTimestamp': 1517795995836}]
#265 will close this PR because most recent and adds a README.
Missing FmF service properties were asked to be added.
I'll add you as code author, as you did all the job :wink:
Correct me if I am wrong, but I think the location
property needs to have a data refresh added to get the latest location available when the request is made. Otherwise, it will/may return the location already in the self.data
variable that might be old.
@property
def locations(self):
"""Get friends locations"""
return self.data.get("locations")
vs.
@property
def locations(self):
"""Get friends locations"""
self._data = self.refresh_data()
return self.data.get('locations')
@gcobb321 - The intent is to separate concerns. Access of the data vs updates.
Consumers of this API can call refresh_data()
before property access after the first call for the latest data.
For example, locations = api.friends.locations
will fetch all the data once under the hood. If a consumer wanted to then return a chosen friend's details, they might want to use the data from api.friends.details
which is included in the full payload and thus should not trigger a second network call.
I don't have any real consumers of this API besides myself, so this was the design that best supported my use case -- as I am calling this function only once through each incoming HTTP request while running this library on my own RBPI.
Understood and makes sense.
My thinking is friends, contacts, etc are more static while the location data changes frequently. When I do a location request in iCloud3, I want to have the latest location possible, expecially when I'm doing a 15-second polling while getting close to home. Most of the time it may take 4-6 calls to get something less than 1-minute old. An approach to eliminate the additional call to the refresh_data
function might be to have another refreshed_location
property (or some other name) that refreshed the data and then returned the location.
current_locations = api.friends.refreshed_locations
Just a thought.
Hi @zzeleznick ! Sorry for almost bypassing you, I thought the PR was staled.
I was actually thinking of personally rebasing and fixing to get into the CI, then merge for the next release 1.0.0. But then an updated PR came 😅
I should asked if you can do the job before.
So, as we are back on stage now : May you apply my review from #265 ?
And ask me for a review when you are done 😉 !
Thanks a lot for your job 😌
I think we absolutely should separate data fetch from getting the data.
- It is making I/O in a property
- The client should update the data when needed because it controls the fetching interval
- The client may access multiple times the property over a loop so we should not fetch every time ...
Separating the FindMyiPhone fetch made the lib drastically reduce API fetches.
Before 1 fetch + 2 per device
After 1 fetch
See PR #227 (that I have also to update with new CI)
@Quentame - I believe I had added the requested changes into my PR including updating with the latest master. Feel free to comment on the changed files. I am currently updating the API responses with fresh data.
@gcobb321 - Your ask makes sense -- the question is whether there are API consumers that want the refreshed behavior by default or as an extra step. Based on the gitter chat and lack of activity, I think there are only 2 API consumers of this FMF service at this point. The difference is adding one extra line.
Suggested Usage
# At app start
locations = api.friends.locations
print("Initial locations: {}".format(locations))
# Poll method
last = time.time() # unix seconds
def refresh_location(poll_ms = 10):
now = time.time()
delta_ms = (now - last) * 1000
if delta_ms < poll_ms:
return
try:
api.friends.refresh_data()
locations = api.friends.locations
except:
pass # handle errors
else:
last = now # update last called timestamp
# Print last know locations
print("Locations: {}".format(locations))
while True:
refresh_location()
# FEAT: add some retry or backoff logic
Personally, I'd like to use (glom)[https://glom.readthedocs.io/en/latest/tutorial.html] to extract nested properties from the response payload, and am fine with changing the API was keeping it in the same style as the previous services.
Thanks for considering it. I look forward to the update and will modify iCloud3 when completed to return to the standard pyicloud.py package.
@Quentame - passes the checks and ready for review.
Any progress on this PR or is it dead?
For anyone who may find this PR, the following lines need to be changed for the new Apple Find My Friends:
From:
service_root = self._get_webservice_url("fmf")
To:
service_root = self._get_webservice_url("findme")
I believe find my friends
has been officially yanked from the iCloud web UI. Can someone confirm?
In which case, I'll look into Google Location Sharing... so I can somehow pull location from my iPhone.
I’m not having any problems with Find my Friends using the iCloud3 Home Assistant custom component
———————— Gary Cobb On Dec 31, 2022 at 10:02 PM -0500, zefoo @.***>, wrote:
I believe find my friends has been officially yanked from the iCloud web UI. Can someone confirm? In which case, I'll look into Google Location Sharing... so I can somehow pull location from my iPhone. — Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you were mentioned.Message ID: @.***>
@gcobb321 Is this still working for you? For me, I'm getting an pyicloud.exceptions.PyiCloudAPIResponseException: (501)
when trying to query the friend endpoint.
@gcobb321 At first I thought this was due to 2fa, but after fixing that, it still does not work:
api.requires_2fa: False
api.requires_2sa: False
api.is_trusted_session: True
Traceback (most recent call last):
File "/root/pyicloud/test.py", line 52, in <module>
print(api.friends.locations)
^^^^^^^^^^^^^^^^^^^^^
File "/root/pyicloud/pyicloud/services/findmyfriends.py", line 116, in locations
return self.data.get("locations", [])
^^^^^^^^^
File "/root/pyicloud/pyicloud/services/findmyfriends.py", line 75, in data
self.refresh_client()
File "/root/pyicloud/pyicloud/services/findmyfriends.py", line 46, in refresh_client
req = self.session.post(self._friend_endpoint, data=mock_payload, params=params)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/root/pyicloud/virtualenv/lib/python3.11/site-packages/requests/sessions.py", line 637, in post
return self.request("POST", url, data=data, json=json, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/root/pyicloud/pyicloud/base.py", line 135, in request
self._raise_error(response.status_code, response.reason)
File "/root/pyicloud/pyicloud/base.py", line 191, in _raise_error
raise api_error
pyicloud.exceptions.PyiCloudAPIResponseException: (501)
Apple removed the Find my Friends endpoint web address in May. It hasn’t worked for me since then. I’ve looked around for a solution but have not found one. I’ll let you know if I find anything.
@gcobb321 ah, I see. Thanks for the quick reply!
while find my friends is not available in the web client, the app does not use certificate pinning so it's easy enough to check it
POST /fmipservice/friends/..../minCallback/refreshClient HTTP/1.1
Host: p156-fmfmobile.icloud.com
Would a PR based on the endpoints used by the app still be considered @gcobb321 ?
I will check it out. I’ve commented out most of the FmF code in iCloud3 but can read that support if it works. One thing though, the p156- prefix is the Apple server supporting your device. Mine is p123 so there may be other complications. But worth looking at.
Thanks
thanks @gcobb321! Let me know if any help required, I wouldn’t mind trying to come up with a PR. My main question was whether that was a plausible reverse channel (the app) given its slightly trickier to maintain than web app
@fopina Out of curiosity, how did you find those endpoints? Did they come from the app?
Yes, using Find My app and opening friends tab
@fopina yes, but how did you get the URL from there? Did you install a known root certificate on the device and use wireshark or some other method? 😀
Yes, using any mitm proxy (mitmproxy, burp) and installing its CA on device, like for analyzing any other app traffic. Hence my comment on no pinning, as that would make it less trivial
@fopina For what it's worth, I'd very much appreciate a PR that gets this working again! :)
@fopina I tried your endpoint using several combinations of urls your fmfmobile.icloud.com and received a 404 response for all of them. Obviously, I haven't set it up correctly or there are some other pieces of info missing. I'm not sure what goes in the '/.../' in your example.
They are shown below, along with the response. Can you take a look at them give me some more info on what you found and how it should be incorporated into the FmF requests.
POST, https://p123-fmfmobile.icloud.com:443/fmipservice/client/fmfWeb/initClient,
{"clientContext": {"appVersion": "1.0", "contextApp": "com.icloud.web.fmf", "mapkitAvailable": true,
"productType": "fmfWeb", "tileServer": "Apple", "userInactivityTimeInMS": 537,
"windowInFocus": false,"windowVisible": true}, "dataContext": null, "serverContext": null}'}
_______ PYICLOUD_IC3 ICLOUD RESPONSE-HEADER (UNFILTERED)
ResponseCode-404 , ResponseOK-False,
Headers-{'Server': 'AppleHttpServer/b866cf47a603','Date': 'Mon,20 May 2024 13:55:35 GMT',
'Content-Type': 'text/plain', 'Content-Length': '29','Connection':'keep-alive'
,'Strict-Transport-Security': 'max-age=31536000; includeSubDomains;',
'x-apple-user-partition': '123',
'via': 'xrail:st53p00ic-qujn15050702.me.com:8301:24R158:grp60,
631194250daa17e24277dea86cf30319:7b6a49527e4cf7fe9736c2dcc14ba2b4:usmia1',
'X-Apple-Request-UUID': 'faaa1188-30de-40de-8f5a-8f8cf9bfbffa',
'access-control-expose-headers': 'X-Apple-Request-UUID,Via',
'X-Apple-Edge-Response-Time': '18'}
POST, https://p123-fmfmobile.icloud.com:443/fmipservice/client/fmfWeb/
initClient/minCallback/refreshClient HTTP/1.1,
{"clientContext": {"appVersion": "1.0","contextApp": "com.icloud.web.fmf", "mapkitAvailable": true,
"productType": "fmfWeb","tileServer": "Apple", "userInactivityTimeInMS": 537, "windowInFocus": false,
"windowVisible": true}, "dataContext": null, "serverContext": null}'}
________ PYICLOUD_IC3 ICLOUD RESPONSE-HEADER (UNFILTERED)
ResponseCode-404 , ResponseOK-False,
Headers-{'Server': 'AppleHttpServer/b866cf47a603','Date': 'Mon, 20 May 2024 13:59:34 GMT',
'Content-Type': 'text/plain', 'Content-Length': '29','Connection': 'keep-alive',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains;',
'x-apple-user-partition': '123',
'via': xrail:st53p00ic-qujn14040702.me.com:8301:24R158:grp60,631194250daa17e24277dea86cf30319:d50ce690f7d430c202fc8d883faa41c2:usmia1',
'X-Apple-Request-UUID': '3e38c6d1-b4a0-404f-b5f5-2de48d9fa6ce',
'access-control-expose-headers': 'X-Apple-Request-UUID,Via',
'X-Apple-Edge-Response-Time': '19'}
POST, https://p123-fmfmobile.icloud.com:443/fmipservice/minCallback/refreshClient HTTP/1.1,
{"clientContext": {"appVersion": "1.0", "contextApp": "com.icloud.web.fmf", "mapkitAvailable": true,
"productType": "fmfWeb", "tileServer": "Apple", "userInactivityTimeInMS": 537, "windowInFocus": false,
"windowVisible": true}, "dataContext": null, "serverContext": null}'}
________ PYICLOUD_IC3 ICLOUD RESPONSE-HEADER (UNFILTERED)
ResponseCode-404 , ResponseOK-False,
Headers-{'Server': 'AppleHttpServer/b866cf47a603', 'Date': 'Mon,20 May 2024 14:06:04 GMT',
'Content-Type': 'text/plain', 'Content-Length': '29','Connection': 'keep-alive',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains;',
'x-apple-user-partition': '123',
'via': 'xrail:st53p00ic-qujn13071302.me.com:8301:24R158:grp60,631194250daa17e24277dea86cf30319:ec5b3487662fbfbe954244b9ed4f38e7:usmia1',
'X-Apple-Request-UUID': '8cf8d097-22e7-4227-ba70-036430d03f8b',
'access-control-expose-headers': 'X-Apple-Request-UUID,Via',
'X-Apple-Edge-Response-Time': '24'}
It was something that looked like a token so I removed it, as I'm not familiar with the API 😀
It was mostly to show the endpoint exists and possible to see reverse it using the app, it wasn't meant to be "all details required", sorry.
I'll try to look into it a bit myself and push a PR, ideally.
In the meantime, I've moved friends to family and it does work nicely, thanks for that!
@fopina Here is the POST for FamShr that returns device and location data as an example of what is being used for FamShr requests.
You can see your iCloud3 traffic by setting the log level to RawData-Unfiltered
on the Configure > Format Parameters page and restarting HA.
POST, https://p123-fmipweb.icloud.com:443/fmipservice/client/web/refreshClient,
{"clientContext": {"fmly": true, "shouldLocate": true, "selectedDevice": "all", "deviceListVersion": 1}}'}
________ PYICLOUD_IC3 ICLOUD RESPONSE-HEADER (UNFILTERED)
{'raw': 'ResponseCode-200 '}