Initial commit - support for iPhone status/location data and calendar events.
This commit is contained in:
parent
9d38c46457
commit
8592d52324
7 changed files with 218 additions and 0 deletions
1
__init__.py
Normal file
1
__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
from base import PyiCloudService
|
107
base.py
Normal file
107
base.py
Normal file
|
@ -0,0 +1,107 @@
|
|||
import time
|
||||
import uuid
|
||||
import hashlib
|
||||
import json
|
||||
import requests
|
||||
|
||||
from exceptions import PyiCloudFailedLoginException
|
||||
from services import FindMyiPhoneService, CalendarService
|
||||
|
||||
|
||||
class PyiCloudService(object):
|
||||
"""
|
||||
A base authentication class for the iCloud service. Handles the
|
||||
validation and authentication required to access iCloud services.
|
||||
|
||||
Usage:
|
||||
from pyicloud import PyiCloudService
|
||||
pyicloud = PyiCloudService('username@apple.com', 'password')
|
||||
pyicloud.iphone.location()
|
||||
"""
|
||||
def __init__(self, apple_id, password):
|
||||
self.discovery = None
|
||||
self.client_id = str(uuid.uuid1()).upper()
|
||||
self.user = {'apple_id': apple_id, 'password': password}
|
||||
|
||||
self._home_endpoint = 'https://www.icloud.com'
|
||||
self._setup_endpoint = 'https://p12-setup.icloud.com/setup/ws/1'
|
||||
self._push_endpoint = 'https://p12-pushws.icloud.com'
|
||||
|
||||
self._base_login_url = '%s/login' % self._setup_endpoint
|
||||
self._base_validate_url = '%s/validate' % self._setup_endpoint
|
||||
self._base_system_url = '%s/system/version.json' % self._home_endpoint
|
||||
self._base_webauth_url = '%s/refreshWebAuth' % self._push_endpoint
|
||||
|
||||
self.session = requests.Session()
|
||||
self.session.verify = False
|
||||
self.session.headers.update({
|
||||
'host': 'setup.icloud.com',
|
||||
'origin': self._home_endpoint,
|
||||
'referer': '%s/' % self._home_endpoint,
|
||||
'User-Agent': 'Opera/9.52 (X11; Linux i686; U; en)'
|
||||
})
|
||||
|
||||
self.refresh_version()
|
||||
self.params = {
|
||||
'clientId': self.client_id,
|
||||
'clientBuildNumber': self.build_id
|
||||
}
|
||||
|
||||
self.authenticate()
|
||||
|
||||
def refresh_version(self):
|
||||
"""
|
||||
Retrieves the buildNumber from the /version endpoint.
|
||||
This is used by almost all request query strings.
|
||||
"""
|
||||
req = requests.get(self._base_system_url)
|
||||
self.build_id = req.json()['buildNumber']
|
||||
|
||||
def refresh_validate(self):
|
||||
"""
|
||||
Queries the /validate endpoint and fetches two key values we need:
|
||||
1. "dsInfo" is a nested object which contains the "dsid" integer.
|
||||
This object doesn't exist until *after* the login has taken place,
|
||||
the first request will compain about a X-APPLE-WEBAUTH-TOKEN cookie
|
||||
2. "instance" is an int which is used to build the "id" query string.
|
||||
This is, pseudo: sha1(email + "instance") to uppercase.
|
||||
"""
|
||||
req = self.session.get(self._base_validate_url, params=self.params)
|
||||
resp = req.json()
|
||||
if 'dsInfo' in resp:
|
||||
dsid = resp['dsInfo']['dsid']
|
||||
self.params.update({'dsid': dsid})
|
||||
instance = resp['instance']
|
||||
sha = hashlib.sha1(self.user.get('apple_id') + instance)
|
||||
self.params.update({'id': sha.hexdigest().upper()})
|
||||
|
||||
def authenticate(self):
|
||||
"""
|
||||
Handles the full authentication steps, validating,
|
||||
authenticating and then validating again.
|
||||
"""
|
||||
self.refresh_validate()
|
||||
|
||||
data = dict(self.user)
|
||||
data.update({'id': self.params['id'], 'extended_login': False})
|
||||
req = self.session.post(
|
||||
self._base_login_url,
|
||||
params=self.params,
|
||||
data=json.dumps(data)
|
||||
)
|
||||
|
||||
if not req.ok:
|
||||
msg = 'Invalid email/password combination.'
|
||||
raise PyiCloudFailedLoginException(msg)
|
||||
|
||||
self.refresh_validate()
|
||||
|
||||
self.discovery = req.json()
|
||||
|
||||
@property
|
||||
def iphone(self):
|
||||
return FindMyiPhoneService(self.session, self.params)
|
||||
|
||||
@property
|
||||
def calendar(self):
|
||||
return CalendarService(self.session, self.params)
|
8
exceptions.py
Normal file
8
exceptions.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
|
||||
|
||||
class PyiCloudNoDevicesException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PyiCloudFailedLoginException(Exception):
|
||||
pass
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
uuid
|
||||
requests
|
2
services/__init__.py
Normal file
2
services/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
from calendar import CalendarService
|
||||
from findmyiphone import FindMyiPhoneService
|
52
services/calendar.py
Normal file
52
services/calendar.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
from __future__ import absolute_import
|
||||
import os
|
||||
from datetime import datetime
|
||||
from calendar import monthrange
|
||||
|
||||
|
||||
class CalendarService(object):
|
||||
"""
|
||||
The 'Calendar' iCloud service, connects to iCloud and returns events.
|
||||
"""
|
||||
def __init__(self, session, params):
|
||||
self.session = session
|
||||
self.params = params
|
||||
self._calendar_endpoint = 'https://p12-calendarws.icloud.com/ca'
|
||||
self._calendar_refresh_url = '%s/events' % self._calendar_endpoint
|
||||
|
||||
def get_system_tz(self):
|
||||
"""
|
||||
Retrieves the system's timezone.
|
||||
From: http://stackoverflow.com/a/7841417
|
||||
"""
|
||||
return '/'.join(os.readlink('/etc/localtime').split('/')[-2:])
|
||||
|
||||
def refresh_client(self, from_dt=None, to_dt=None):
|
||||
"""
|
||||
Refreshes the CalendarService endpoint, ensuring that the
|
||||
event data is up-to-date. If no 'from_dt' or 'to_dt' datetimes
|
||||
have been given, the range becomes this month.
|
||||
"""
|
||||
today = datetime.today()
|
||||
first_day, last_day = monthrange(today.year, today.month)
|
||||
if not from_dt:
|
||||
from_dt = datetime(today.year, today.month, first_day)
|
||||
if not to_dt:
|
||||
to_dt = datetime(today.year, today.month, last_day)
|
||||
self.session.headers.update({'host': 'p12-calendarws.icloud.com'})
|
||||
params = dict(self.params)
|
||||
params.update({
|
||||
'lang': 'en-us',
|
||||
'usertz': self.get_system_tz(),
|
||||
'startDate': from_dt.strftime('%Y-%m-%d'),
|
||||
'endDate': to_dt.strftime('%Y-%m-%d')
|
||||
})
|
||||
req = self.session.get(self._calendar_refresh_url, params=params)
|
||||
self.response = req.json()
|
||||
|
||||
def events(self, from_dt=None, to_dt=None):
|
||||
"""
|
||||
Retrieves events for a given date range, by default, this month.
|
||||
"""
|
||||
self.refresh_client(from_dt, to_dt)
|
||||
return self.response['Event']
|
46
services/findmyiphone.py
Normal file
46
services/findmyiphone.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
from pyicloud.exceptions import PyiCloudNoDevicesException
|
||||
|
||||
|
||||
class FindMyiPhoneService(object):
|
||||
"""
|
||||
The 'Find my iPhone' iCloud service, connects to iCloud and returns
|
||||
phone data including the near-realtime latitude and longitude.
|
||||
"""
|
||||
def __init__(self, session, params):
|
||||
self.session = session
|
||||
self.params = params
|
||||
self._fmip_root = 'https://p12-fmipweb.icloud.com'
|
||||
self._fmip_endpoint = '%s/fmipservice/client/web' % self._fmip_root
|
||||
self._fmip_refresh_url = '%s/refreshClient' % self._fmip_endpoint
|
||||
|
||||
def refresh_client(self):
|
||||
"""
|
||||
Refreshes the FindMyiPhoneService endpoint,
|
||||
ensuring that the location data is up-to-date.
|
||||
"""
|
||||
self.session.headers.update({'host': 'p12-fmipweb.icloud.com'})
|
||||
req = self.session.post(self._fmip_refresh_url, params=self.params)
|
||||
self.response = req.json()
|
||||
if self.response['content']:
|
||||
# TODO: Support multiple devices.
|
||||
self.content = self.response['content'][0]
|
||||
else:
|
||||
raise PyiCloudNoDevicesException('You do not have any active devices.')
|
||||
self.user_info = self.response['userInfo']
|
||||
|
||||
def location(self):
|
||||
self.refresh_client()
|
||||
return self.content['location']
|
||||
|
||||
def status(self, additional=[]):
|
||||
"""
|
||||
The FindMyiPhoneService response is quite bloated, this method
|
||||
will return a subset of the more useful properties.
|
||||
"""
|
||||
self.refresh_client()
|
||||
fields = ['batteryLevel', 'deviceDisplayName', 'deviceStatus', 'name']
|
||||
fields += additional
|
||||
properties = {}
|
||||
for field in fields:
|
||||
properties[field] = self.content.get(field, 'Unknown')
|
||||
return properties
|
Loading…
Reference in a new issue