pyicloud icon indicating copy to clipboard operation
pyicloud copied to clipboard

Add Find My Friends service

Open zzeleznick opened this issue 7 years ago • 31 comments

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''}]

zzeleznick avatar Oct 28 '17 08:10 zzeleznick

Would love to see this implemented. What work needs to be done to move it forward?

troytc avatar Sep 07 '18 23:09 troytc

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}]

zzeleznick avatar Sep 17 '18 14:09 zzeleznick

#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:

Quentame avatar Apr 04 '20 09:04 Quentame

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 avatar Apr 04 '20 19:04 gcobb321

@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.

zzeleznick avatar Apr 04 '20 20:04 zzeleznick

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.

gcobb321 avatar Apr 04 '20 21:04 gcobb321

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 😌

Quentame avatar Apr 04 '20 21:04 Quentame

I think we absolutely should separate data fetch from getting the data.

  1. It is making I/O in a property
  2. The client should update the data when needed because it controls the fetching interval
  3. 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 avatar Apr 04 '20 21:04 Quentame

@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.

zzeleznick avatar Apr 04 '20 21:04 zzeleznick

Thanks for considering it. I look forward to the update and will modify iCloud3 when completed to return to the standard pyicloud.py package.

gcobb321 avatar Apr 04 '20 21:04 gcobb321

@Quentame - passes the checks and ready for review.

zzeleznick avatar Apr 04 '20 23:04 zzeleznick

Any progress on this PR or is it dead?

amunchet avatar Sep 15 '22 17:09 amunchet

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")

amunchet avatar Sep 15 '22 20:09 amunchet

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.

zefoo avatar Jan 01 '23 03:01 zefoo

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 avatar Jan 03 '23 00:01 gcobb321

@gcobb321 Is this still working for you? For me, I'm getting an pyicloud.exceptions.PyiCloudAPIResponseException: (501) when trying to query the friend endpoint.

Yannik avatar Aug 14 '23 11:08 Yannik

@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)

Yannik avatar Aug 14 '23 11:08 Yannik

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 avatar Aug 14 '23 12:08 gcobb321

@gcobb321 ah, I see. Thanks for the quick reply!

Yannik avatar Aug 14 '23 12:08 Yannik

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 ?

fopina avatar May 17 '24 10:05 fopina

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

gcobb321 avatar May 17 '24 12:05 gcobb321

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 avatar May 17 '24 12:05 fopina

@fopina Out of curiosity, how did you find those endpoints? Did they come from the app?

amunchet avatar May 17 '24 18:05 amunchet

Yes, using Find My app and opening friends tab

fopina avatar May 17 '24 21:05 fopina

@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? 😀

amunchet avatar May 17 '24 23:05 amunchet

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 avatar May 17 '24 23:05 fopina

@fopina For what it's worth, I'd very much appreciate a PR that gets this working again! :)

Yannik avatar May 19 '24 17:05 Yannik

@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'}

gcobb321 avatar May 20 '24 14:05 gcobb321

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 avatar May 20 '24 14:05 fopina

@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 '}

gcobb321 avatar May 20 '24 14:05 gcobb321