Gravatar services.
--- /dev/null
+"""
+The MIT License
+
+Copyright (c) 2007 Leah Culver
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+Example consumer. This is not recommended for production.
+Instead, you'll want to create your own subclass of OAuthClient
+or find one that works with your web framework.
+"""
+
+import httplib
+import time
+import oauth.oauth as oauth
+
+# settings for the local test consumer
+SERVER = 'localhost'
+PORT = 8080
+
+# fake urls for the test server (matches ones in server.py)
+REQUEST_TOKEN_URL = 'https://photos.example.net/request_token'
+ACCESS_TOKEN_URL = 'https://photos.example.net/access_token'
+AUTHORIZATION_URL = 'https://photos.example.net/authorize'
+CALLBACK_URL = 'http://printer.example.com/request_token_ready'
+RESOURCE_URL = 'http://photos.example.net/photos'
+
+# key and secret granted by the service provider for this consumer application - same as the MockOAuthDataStore
+CONSUMER_KEY = 'key'
+CONSUMER_SECRET = 'secret'
+
+# example client using httplib with headers
+class SimpleOAuthClient(oauth.OAuthClient):
+
+ def __init__(self, server, port=httplib.HTTP_PORT, request_token_url='', access_token_url='', authorization_url=''):
+ self.server = server
+ self.port = port
+ self.request_token_url = request_token_url
+ self.access_token_url = access_token_url
+ self.authorization_url = authorization_url
+ self.connection = httplib.HTTPConnection("%s:%d" % (self.server, self.port))
+
+ def fetch_request_token(self, oauth_request):
+ # via headers
+ # -> OAuthToken
+ self.connection.request(oauth_request.http_method, self.request_token_url, headers=oauth_request.to_header())
+ response = self.connection.getresponse()
+ return oauth.OAuthToken.from_string(response.read())
+
+ def fetch_access_token(self, oauth_request):
+ # via headers
+ # -> OAuthToken
+ self.connection.request(oauth_request.http_method, self.access_token_url, headers=oauth_request.to_header())
+ response = self.connection.getresponse()
+ return oauth.OAuthToken.from_string(response.read())
+
+ def authorize_token(self, oauth_request):
+ # via url
+ # -> typically just some okay response
+ self.connection.request(oauth_request.http_method, oauth_request.to_url())
+ response = self.connection.getresponse()
+ return response.read()
+
+ def access_resource(self, oauth_request):
+ # via post body
+ # -> some protected resources
+ headers = {'Content-Type' :'application/x-www-form-urlencoded'}
+ self.connection.request('POST', RESOURCE_URL, body=oauth_request.to_postdata(), headers=headers)
+ response = self.connection.getresponse()
+ return response.read()
+
+def run_example():
+
+ # setup
+ print '** OAuth Python Library Example **'
+ client = SimpleOAuthClient(SERVER, PORT, REQUEST_TOKEN_URL, ACCESS_TOKEN_URL, AUTHORIZATION_URL)
+ consumer = oauth.OAuthConsumer(CONSUMER_KEY, CONSUMER_SECRET)
+ signature_method_plaintext = oauth.OAuthSignatureMethod_PLAINTEXT()
+ signature_method_hmac_sha1 = oauth.OAuthSignatureMethod_HMAC_SHA1()
+ pause()
+
+ # get request token
+ print '* Obtain a request token ...'
+ pause()
+ oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, callback=CALLBACK_URL, http_url=client.request_token_url)
+ oauth_request.sign_request(signature_method_plaintext, consumer, None)
+ print 'REQUEST (via headers)'
+ print 'parameters: %s' % str(oauth_request.parameters)
+ pause()
+ token = client.fetch_request_token(oauth_request)
+ print 'GOT'
+ print 'key: %s' % str(token.key)
+ print 'secret: %s' % str(token.secret)
+ print 'callback confirmed? %s' % str(token.callback_confirmed)
+ pause()
+
+ print '* Authorize the request token ...'
+ pause()
+ oauth_request = oauth.OAuthRequest.from_token_and_callback(token=token, http_url=client.authorization_url)
+ print 'REQUEST (via url query string)'
+ print 'parameters: %s' % str(oauth_request.parameters)
+ pause()
+ # this will actually occur only on some callback
+ response = client.authorize_token(oauth_request)
+ print 'GOT'
+ print response
+ # sad way to get the verifier
+ import urlparse, cgi
+ query = urlparse.urlparse(response)[4]
+ params = cgi.parse_qs(query, keep_blank_values=False)
+ verifier = params['oauth_verifier'][0]
+ print 'verifier: %s' % verifier
+ pause()
+
+ # get access token
+ print '* Obtain an access token ...'
+ pause()
+ oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token=token, verifier=verifier, http_url=client.access_token_url)
+ oauth_request.sign_request(signature_method_plaintext, consumer, token)
+ print 'REQUEST (via headers)'
+ print 'parameters: %s' % str(oauth_request.parameters)
+ pause()
+ token = client.fetch_access_token(oauth_request)
+ print 'GOT'
+ print 'key: %s' % str(token.key)
+ print 'secret: %s' % str(token.secret)
+ pause()
+
+ # access some protected resources
+ print '* Access protected resources ...'
+ pause()
+ parameters = {'file': 'vacation.jpg', 'size': 'original'} # resource specific params
+ oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token=token, http_method='POST', http_url=RESOURCE_URL, parameters=parameters)
+ oauth_request.sign_request(signature_method_hmac_sha1, consumer, token)
+ print 'REQUEST (via post body)'
+ print 'parameters: %s' % str(oauth_request.parameters)
+ pause()
+ params = client.access_resource(oauth_request)
+ print 'GOT'
+ print 'non-oauth parameters: %s' % params
+ pause()
+
+def pause():
+ print ''
+ time.sleep(1)
+
+if __name__ == '__main__':
+ run_example()
+ print 'Done.'
\ No newline at end of file
--- /dev/null
+"""
+The MIT License
+
+Copyright (c) 2007 Leah Culver
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
+import urllib
+
+import oauth.oauth as oauth
+
+# fake urls for the test server
+REQUEST_TOKEN_URL = 'https://photos.example.net/request_token'
+ACCESS_TOKEN_URL = 'https://photos.example.net/access_token'
+AUTHORIZATION_URL = 'https://photos.example.net/authorize'
+CALLBACK_URL = 'http://printer.example.com/request_token_ready'
+RESOURCE_URL = 'http://photos.example.net/photos'
+REALM = 'http://photos.example.net/'
+VERIFIER = 'verifier'
+
+# example store for one of each thing
+class MockOAuthDataStore(oauth.OAuthDataStore):
+
+ def __init__(self):
+ self.consumer = oauth.OAuthConsumer('key', 'secret')
+ self.request_token = oauth.OAuthToken('requestkey', 'requestsecret')
+ self.access_token = oauth.OAuthToken('accesskey', 'accesssecret')
+ self.nonce = 'nonce'
+ self.verifier = VERIFIER
+
+ def lookup_consumer(self, key):
+ if key == self.consumer.key:
+ return self.consumer
+ return None
+
+ def lookup_token(self, token_type, token):
+ token_attrib = getattr(self, '%s_token' % token_type)
+ if token == token_attrib.key:
+ ## HACK
+ token_attrib.set_callback(CALLBACK_URL)
+ return token_attrib
+ return None
+
+ def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
+ if oauth_token and oauth_consumer.key == self.consumer.key and (oauth_token.key == self.request_token.key or oauth_token.key == self.access_token.key) and nonce == self.nonce:
+ return self.nonce
+ return None
+
+ def fetch_request_token(self, oauth_consumer, oauth_callback):
+ if oauth_consumer.key == self.consumer.key:
+ if oauth_callback:
+ # want to check here if callback is sensible
+ # for mock store, we assume it is
+ self.request_token.set_callback(oauth_callback)
+ return self.request_token
+ return None
+
+ def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier):
+ if oauth_consumer.key == self.consumer.key and oauth_token.key == self.request_token.key and oauth_verifier == self.verifier:
+ # want to check here if token is authorized
+ # for mock store, we assume it is
+ return self.access_token
+ return None
+
+ def authorize_request_token(self, oauth_token, user):
+ if oauth_token.key == self.request_token.key:
+ # authorize the request token in the store
+ # for mock store, do nothing
+ return self.request_token
+ return None
+
+class RequestHandler(BaseHTTPRequestHandler):
+
+ def __init__(self, *args, **kwargs):
+ self.oauth_server = oauth.OAuthServer(MockOAuthDataStore())
+ self.oauth_server.add_signature_method(oauth.OAuthSignatureMethod_PLAINTEXT())
+ self.oauth_server.add_signature_method(oauth.OAuthSignatureMethod_HMAC_SHA1())
+ BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
+
+ # example way to send an oauth error
+ def send_oauth_error(self, err=None):
+ # send a 401 error
+ self.send_error(401, str(err.message))
+ # return the authenticate header
+ header = oauth.build_authenticate_header(realm=REALM)
+ for k, v in header.iteritems():
+ self.send_header(k, v)
+
+ def do_GET(self):
+
+ # debug info
+ #print self.command, self.path, self.headers
+
+ # get the post data (if any)
+ postdata = None
+ if self.command == 'POST':
+ try:
+ length = int(self.headers.getheader('content-length'))
+ postdata = self.rfile.read(length)
+ except:
+ pass
+
+ # construct the oauth request from the request parameters
+ oauth_request = oauth.OAuthRequest.from_request(self.command, self.path, headers=self.headers, query_string=postdata)
+
+ # request token
+ if self.path.startswith(REQUEST_TOKEN_URL):
+ try:
+ # create a request token
+ token = self.oauth_server.fetch_request_token(oauth_request)
+ # send okay response
+ self.send_response(200, 'OK')
+ self.end_headers()
+ # return the token
+ self.wfile.write(token.to_string())
+ except oauth.OAuthError, err:
+ self.send_oauth_error(err)
+ return
+
+ # user authorization
+ if self.path.startswith(AUTHORIZATION_URL):
+ try:
+ # get the request token
+ token = self.oauth_server.fetch_request_token(oauth_request)
+ # authorize the token (kind of does nothing for now)
+ token = self.oauth_server.authorize_token(token, None)
+ token.set_verifier(VERIFIER)
+ # send okay response
+ self.send_response(200, 'OK')
+ self.end_headers()
+ # return the callback url (to show server has it)
+ self.wfile.write(token.get_callback_url())
+ except oauth.OAuthError, err:
+ self.send_oauth_error(err)
+ return
+
+ # access token
+ if self.path.startswith(ACCESS_TOKEN_URL):
+ try:
+ # create an access token
+ token = self.oauth_server.fetch_access_token(oauth_request)
+ # send okay response
+ self.send_response(200, 'OK')
+ self.end_headers()
+ # return the token
+ self.wfile.write(token.to_string())
+ except oauth.OAuthError, err:
+ self.send_oauth_error(err)
+ return
+
+ # protected resources
+ if self.path.startswith(RESOURCE_URL):
+ try:
+ # verify the request has been oauth authorized
+ consumer, token, params = self.oauth_server.verify_request(oauth_request)
+ # send okay response
+ self.send_response(200, 'OK')
+ self.end_headers()
+ # return the extra parameters - just for something to return
+ self.wfile.write(str(params))
+ except oauth.OAuthError, err:
+ self.send_oauth_error(err)
+ return
+
+ def do_POST(self):
+ return self.do_GET()
+
+def main():
+ try:
+ server = HTTPServer(('', 8080), RequestHandler)
+ print 'Test server running...'
+ server.serve_forever()
+ except KeyboardInterrupt:
+ server.socket.close()
+
+if __name__ == '__main__':
+ main()
\ No newline at end of file
--- /dev/null
+"""
+The MIT License
+
+Copyright (c) 2007 Leah Culver
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+import cgi
+import urllib
+import time
+import random
+import urlparse
+import hmac
+import binascii
+
+
+VERSION = '1.0' # Hi Blaine!
+HTTP_METHOD = 'GET'
+SIGNATURE_METHOD = 'PLAINTEXT'
+
+
+class OAuthError(RuntimeError):
+ """Generic exception class."""
+ def __init__(self, message='OAuth error occured.'):
+ self.message = message
+
+def build_authenticate_header(realm=''):
+ """Optional WWW-Authenticate header (401 error)"""
+ return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
+
+def escape(s):
+ """Escape a URL including any /."""
+ return urllib.quote(s, safe='~')
+
+def _utf8_str(s):
+ """Convert unicode to utf-8."""
+ if isinstance(s, unicode):
+ return s.encode("utf-8")
+ else:
+ return str(s)
+
+def generate_timestamp():
+ """Get seconds since epoch (UTC)."""
+ return int(time.time())
+
+def generate_nonce(length=8):
+ """Generate pseudorandom number."""
+ return ''.join([str(random.randint(0, 9)) for i in range(length)])
+
+def generate_verifier(length=8):
+ """Generate pseudorandom number."""
+ return ''.join([str(random.randint(0, 9)) for i in range(length)])
+
+
+class OAuthConsumer(object):
+ """Consumer of OAuth authentication.
+
+ OAuthConsumer is a data type that represents the identity of the Consumer
+ via its shared secret with the Service Provider.
+
+ """
+ key = None
+ secret = None
+
+ def __init__(self, key, secret):
+ self.key = key
+ self.secret = secret
+
+
+class OAuthToken(object):
+ """OAuthToken is a data type that represents an End User via either an access
+ or request token.
+
+ key -- the token
+ secret -- the token secret
+
+ """
+ key = None
+ secret = None
+ callback = None
+ callback_confirmed = None
+ verifier = None
+
+ def __init__(self, key, secret):
+ self.key = key
+ self.secret = secret
+
+ def set_callback(self, callback):
+ self.callback = callback
+ self.callback_confirmed = 'true'
+
+ def set_verifier(self, verifier=None):
+ if verifier is not None:
+ self.verifier = verifier
+ else:
+ self.verifier = generate_verifier()
+
+ def get_callback_url(self):
+ if self.callback and self.verifier:
+ # Append the oauth_verifier.
+ parts = urlparse.urlparse(self.callback)
+ scheme, netloc, path, params, query, fragment = parts[:6]
+ if query:
+ query = '%s&oauth_verifier=%s' % (query, self.verifier)
+ else:
+ query = 'oauth_verifier=%s' % self.verifier
+ return urlparse.urlunparse((scheme, netloc, path, params,
+ query, fragment))
+ return self.callback
+
+ def to_string(self):
+ data = {
+ 'oauth_token': self.key,
+ 'oauth_token_secret': self.secret,
+ }
+ if self.callback_confirmed is not None:
+ data['oauth_callback_confirmed'] = self.callback_confirmed
+ return urllib.urlencode(data)
+
+ def from_string(s):
+ """ Returns a token from something like:
+ oauth_token_secret=xxx&oauth_token=xxx
+ """
+ params = cgi.parse_qs(s, keep_blank_values=False)
+ key = params['oauth_token'][0]
+ secret = params['oauth_token_secret'][0]
+ token = OAuthToken(key, secret)
+ try:
+ token.callback_confirmed = params['oauth_callback_confirmed'][0]
+ except KeyError:
+ pass # 1.0, no callback confirmed.
+ return token
+ from_string = staticmethod(from_string)
+
+ def __str__(self):
+ return self.to_string()
+
+
+class OAuthRequest(object):
+ """OAuthRequest represents the request and can be serialized.
+
+ OAuth parameters:
+ - oauth_consumer_key
+ - oauth_token
+ - oauth_signature_method
+ - oauth_signature
+ - oauth_timestamp
+ - oauth_nonce
+ - oauth_version
+ - oauth_verifier
+ ... any additional parameters, as defined by the Service Provider.
+ """
+ parameters = None # OAuth parameters.
+ http_method = HTTP_METHOD
+ http_url = None
+ version = VERSION
+
+ def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None):
+ self.http_method = http_method
+ self.http_url = http_url
+ self.parameters = parameters or {}
+
+ def set_parameter(self, parameter, value):
+ self.parameters[parameter] = value
+
+ def get_parameter(self, parameter):
+ try:
+ return self.parameters[parameter]
+ except:
+ raise OAuthError('Parameter not found: %s' % parameter)
+
+ def _get_timestamp_nonce(self):
+ return self.get_parameter('oauth_timestamp'), self.get_parameter(
+ 'oauth_nonce')
+
+ def get_nonoauth_parameters(self):
+ """Get any non-OAuth parameters."""
+ parameters = {}
+ for k, v in self.parameters.iteritems():
+ # Ignore oauth parameters.
+ if k.find('oauth_') < 0:
+ parameters[k] = v
+ return parameters
+
+ def to_header(self, realm=''):
+ """Serialize as a header for an HTTPAuth request."""
+ auth_header = 'OAuth realm="%s"' % realm
+ # Add the oauth parameters.
+ if self.parameters:
+ for k, v in self.parameters.iteritems():
+ if k[:6] == 'oauth_':
+ auth_header += ', %s="%s"' % (k, escape(str(v)))
+ return {'Authorization': auth_header}
+
+ def to_postdata(self):
+ """Serialize as post data for a POST request."""
+ return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) \
+ for k, v in self.parameters.iteritems()])
+
+ def to_url(self):
+ """Serialize as a URL for a GET request."""
+ return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata())
+
+ def get_normalized_parameters(self):
+ """Return a string that contains the parameters that must be signed."""
+ params = self.parameters
+ try:
+ # Exclude the signature if it exists.
+ del params['oauth_signature']
+ except:
+ pass
+ # Escape key values before sorting.
+ key_values = [(escape(_utf8_str(k)), escape(_utf8_str(v))) \
+ for k,v in params.items()]
+ # Sort lexicographically, first after key, then after value.
+ key_values.sort()
+ # Combine key value pairs into a string.
+ return '&'.join(['%s=%s' % (k, v) for k, v in key_values])
+
+ def get_normalized_http_method(self):
+ """Uppercases the http method."""
+ return self.http_method.upper()
+
+ def get_normalized_http_url(self):
+ """Parses the URL and rebuilds it to be scheme://host/path."""
+ parts = urlparse.urlparse(self.http_url)
+ scheme, netloc, path = parts[:3]
+ # Exclude default port numbers.
+ if scheme == 'http' and netloc[-3:] == ':80':
+ netloc = netloc[:-3]
+ elif scheme == 'https' and netloc[-4:] == ':443':
+ netloc = netloc[:-4]
+ return '%s://%s%s' % (scheme, netloc, path)
+
+ def sign_request(self, signature_method, consumer, token):
+ """Set the signature parameter to the result of build_signature."""
+ # Set the signature method.
+ self.set_parameter('oauth_signature_method',
+ signature_method.get_name())
+ # Set the signature.
+ self.set_parameter('oauth_signature',
+ self.build_signature(signature_method, consumer, token))
+
+ def build_signature(self, signature_method, consumer, token):
+ """Calls the build signature method within the signature method."""
+ return signature_method.build_signature(self, consumer, token)
+
+ def from_request(http_method, http_url, headers=None, parameters=None,
+ query_string=None):
+ """Combines multiple parameter sources."""
+ if parameters is None:
+ parameters = {}
+
+ # Headers
+ if headers and 'Authorization' in headers:
+ auth_header = headers['Authorization']
+ # Check that the authorization header is OAuth.
+ if auth_header[:6] == 'OAuth ':
+ auth_header = auth_header[6:]
+ try:
+ # Get the parameters from the header.
+ header_params = OAuthRequest._split_header(auth_header)
+ parameters.update(header_params)
+ except:
+ raise OAuthError('Unable to parse OAuth parameters from '
+ 'Authorization header.')
+
+ # GET or POST query string.
+ if query_string:
+ query_params = OAuthRequest._split_url_string(query_string)
+ parameters.update(query_params)
+
+ # URL parameters.
+ param_str = urlparse.urlparse(http_url)[4] # query
+ url_params = OAuthRequest._split_url_string(param_str)
+ parameters.update(url_params)
+
+ if parameters:
+ return OAuthRequest(http_method, http_url, parameters)
+
+ return None
+ from_request = staticmethod(from_request)
+
+ def from_consumer_and_token(oauth_consumer, token=None,
+ callback=None, verifier=None, http_method=HTTP_METHOD,
+ http_url=None, parameters=None):
+ if not parameters:
+ parameters = {}
+
+ defaults = {
+ 'oauth_consumer_key': oauth_consumer.key,
+ 'oauth_timestamp': generate_timestamp(),
+ 'oauth_nonce': generate_nonce(),
+ 'oauth_version': OAuthRequest.version,
+ }
+
+ defaults.update(parameters)
+ parameters = defaults
+
+ if token:
+ parameters['oauth_token'] = token.key
+ if token.callback:
+ parameters['oauth_callback'] = token.callback
+ # 1.0a support for verifier.
+ if verifier:
+ parameters['oauth_verifier'] = verifier
+ elif callback:
+ # 1.0a support for callback in the request token request.
+ parameters['oauth_callback'] = callback
+
+ return OAuthRequest(http_method, http_url, parameters)
+ from_consumer_and_token = staticmethod(from_consumer_and_token)
+
+ def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD,
+ http_url=None, parameters=None):
+ if not parameters:
+ parameters = {}
+
+ parameters['oauth_token'] = token.key
+
+ if callback:
+ parameters['oauth_callback'] = callback
+
+ return OAuthRequest(http_method, http_url, parameters)
+ from_token_and_callback = staticmethod(from_token_and_callback)
+
+ def _split_header(header):
+ """Turn Authorization: header into parameters."""
+ params = {}
+ parts = header.split(',')
+ for param in parts:
+ # Ignore realm parameter.
+ if param.find('realm') > -1:
+ continue
+ # Remove whitespace.
+ param = param.strip()
+ # Split key-value.
+ param_parts = param.split('=', 1)
+ # Remove quotes and unescape the value.
+ params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
+ return params
+ _split_header = staticmethod(_split_header)
+
+ def _split_url_string(param_str):
+ """Turn URL string into parameters."""
+ parameters = cgi.parse_qs(param_str, keep_blank_values=False)
+ for k, v in parameters.iteritems():
+ parameters[k] = urllib.unquote(v[0])
+ return parameters
+ _split_url_string = staticmethod(_split_url_string)
+
+class OAuthServer(object):
+ """A worker to check the validity of a request against a data store."""
+ timestamp_threshold = 300 # In seconds, five minutes.
+ version = VERSION
+ signature_methods = None
+ data_store = None
+
+ def __init__(self, data_store=None, signature_methods=None):
+ self.data_store = data_store
+ self.signature_methods = signature_methods or {}
+
+ def set_data_store(self, data_store):
+ self.data_store = data_store
+
+ def get_data_store(self):
+ return self.data_store
+
+ def add_signature_method(self, signature_method):
+ self.signature_methods[signature_method.get_name()] = signature_method
+ return self.signature_methods
+
+ def fetch_request_token(self, oauth_request):
+ """Processes a request_token request and returns the
+ request token on success.
+ """
+ try:
+ # Get the request token for authorization.
+ token = self._get_token(oauth_request, 'request')
+ except OAuthError:
+ # No token required for the initial token request.
+ version = self._get_version(oauth_request)
+ consumer = self._get_consumer(oauth_request)
+ try:
+ callback = self.get_callback(oauth_request)
+ except OAuthError:
+ callback = None # 1.0, no callback specified.
+ self._check_signature(oauth_request, consumer, None)
+ # Fetch a new token.
+ token = self.data_store.fetch_request_token(consumer, callback)
+ return token
+
+ def fetch_access_token(self, oauth_request):
+ """Processes an access_token request and returns the
+ access token on success.
+ """
+ version = self._get_version(oauth_request)
+ consumer = self._get_consumer(oauth_request)
+ try:
+ verifier = self._get_verifier(oauth_request)
+ except OAuthError:
+ verifier = None
+ # Get the request token.
+ token = self._get_token(oauth_request, 'request')
+ self._check_signature(oauth_request, consumer, token)
+ new_token = self.data_store.fetch_access_token(consumer, token, verifier)
+ return new_token
+
+ def verify_request(self, oauth_request):
+ """Verifies an api call and checks all the parameters."""
+ # -> consumer and token
+ version = self._get_version(oauth_request)
+ consumer = self._get_consumer(oauth_request)
+ # Get the access token.
+ token = self._get_token(oauth_request, 'access')
+ self._check_signature(oauth_request, consumer, token)
+ parameters = oauth_request.get_nonoauth_parameters()
+ return consumer, token, parameters
+
+ def authorize_token(self, token, user):
+ """Authorize a request token."""
+ return self.data_store.authorize_request_token(token, user)
+
+ def get_callback(self, oauth_request):
+ """Get the callback URL."""
+ return oauth_request.get_parameter('oauth_callback')
+
+ def build_authenticate_header(self, realm=''):
+ """Optional support for the authenticate header."""
+ return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
+
+ def _get_version(self, oauth_request):
+ """Verify the correct version request for this server."""
+ try:
+ version = oauth_request.get_parameter('oauth_version')
+ except:
+ version = VERSION
+ if version and version != self.version:
+ raise OAuthError('OAuth version %s not supported.' % str(version))
+ return version
+
+ def _get_signature_method(self, oauth_request):
+ """Figure out the signature with some defaults."""
+ try:
+ signature_method = oauth_request.get_parameter(
+ 'oauth_signature_method')
+ except:
+ signature_method = SIGNATURE_METHOD
+ try:
+ # Get the signature method object.
+ signature_method = self.signature_methods[signature_method]
+ except:
+ signature_method_names = ', '.join(self.signature_methods.keys())
+ raise OAuthError('Signature method %s not supported try one of the '
+ 'following: %s' % (signature_method, signature_method_names))
+
+ return signature_method
+
+ def _get_consumer(self, oauth_request):
+ consumer_key = oauth_request.get_parameter('oauth_consumer_key')
+ consumer = self.data_store.lookup_consumer(consumer_key)
+ if not consumer:
+ raise OAuthError('Invalid consumer.')
+ return consumer
+
+ def _get_token(self, oauth_request, token_type='access'):
+ """Try to find the token for the provided request token key."""
+ token_field = oauth_request.get_parameter('oauth_token')
+ token = self.data_store.lookup_token(token_type, token_field)
+ if not token:
+ raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
+ return token
+
+ def _get_verifier(self, oauth_request):
+ return oauth_request.get_parameter('oauth_verifier')
+
+ def _check_signature(self, oauth_request, consumer, token):
+ timestamp, nonce = oauth_request._get_timestamp_nonce()
+ self._check_timestamp(timestamp)
+ self._check_nonce(consumer, token, nonce)
+ signature_method = self._get_signature_method(oauth_request)
+ try:
+ signature = oauth_request.get_parameter('oauth_signature')
+ except:
+ raise OAuthError('Missing signature.')
+ # Validate the signature.
+ valid_sig = signature_method.check_signature(oauth_request, consumer,
+ token, signature)
+ if not valid_sig:
+ key, base = signature_method.build_signature_base_string(
+ oauth_request, consumer, token)
+ raise OAuthError('Invalid signature. Expected signature base '
+ 'string: %s' % base)
+ built = signature_method.build_signature(oauth_request, consumer, token)
+
+ def _check_timestamp(self, timestamp):
+ """Verify that timestamp is recentish."""
+ timestamp = int(timestamp)
+ now = int(time.time())
+ lapsed = abs(now - timestamp)
+ if lapsed > self.timestamp_threshold:
+ raise OAuthError('Expired timestamp: given %d and now %s has a '
+ 'greater difference than threshold %d' %
+ (timestamp, now, self.timestamp_threshold))
+
+ def _check_nonce(self, consumer, token, nonce):
+ """Verify that the nonce is uniqueish."""
+ nonce = self.data_store.lookup_nonce(consumer, token, nonce)
+ if nonce:
+ raise OAuthError('Nonce already used: %s' % str(nonce))
+
+
+class OAuthClient(object):
+ """OAuthClient is a worker to attempt to execute a request."""
+ consumer = None
+ token = None
+
+ def __init__(self, oauth_consumer, oauth_token):
+ self.consumer = oauth_consumer
+ self.token = oauth_token
+
+ def get_consumer(self):
+ return self.consumer
+
+ def get_token(self):
+ return self.token
+
+ def fetch_request_token(self, oauth_request):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+ def fetch_access_token(self, oauth_request):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+ def access_resource(self, oauth_request):
+ """-> Some protected resource."""
+ raise NotImplementedError
+
+
+class OAuthDataStore(object):
+ """A database abstraction used to lookup consumers and tokens."""
+
+ def lookup_consumer(self, key):
+ """-> OAuthConsumer."""
+ raise NotImplementedError
+
+ def lookup_token(self, oauth_consumer, token_type, token_token):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+ def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+ def fetch_request_token(self, oauth_consumer, oauth_callback):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+ def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+ def authorize_request_token(self, oauth_token, user):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+
+class OAuthSignatureMethod(object):
+ """A strategy class that implements a signature method."""
+ def get_name(self):
+ """-> str."""
+ raise NotImplementedError
+
+ def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token):
+ """-> str key, str raw."""
+ raise NotImplementedError
+
+ def build_signature(self, oauth_request, oauth_consumer, oauth_token):
+ """-> str."""
+ raise NotImplementedError
+
+ def check_signature(self, oauth_request, consumer, token, signature):
+ built = self.build_signature(oauth_request, consumer, token)
+ return built == signature
+
+
+class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
+
+ def get_name(self):
+ return 'HMAC-SHA1'
+
+ def build_signature_base_string(self, oauth_request, consumer, token):
+ sig = (
+ escape(oauth_request.get_normalized_http_method()),
+ escape(oauth_request.get_normalized_http_url()),
+ escape(oauth_request.get_normalized_parameters()),
+ )
+
+ key = '%s&' % escape(consumer.secret)
+ if token:
+ key += escape(token.secret)
+ raw = '&'.join(sig)
+ return key, raw
+
+ def build_signature(self, oauth_request, consumer, token):
+ """Builds the base signature string."""
+ key, raw = self.build_signature_base_string(oauth_request, consumer,
+ token)
+
+ # HMAC object.
+ try:
+ import hashlib # 2.5
+ hashed = hmac.new(key, raw, hashlib.sha1)
+ except:
+ import sha # Deprecated
+ hashed = hmac.new(key, raw, sha)
+
+ # Calculate the digest base 64.
+ return binascii.b2a_base64(hashed.digest())[:-1]
+
+
+class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
+
+ def get_name(self):
+ return 'PLAINTEXT'
+
+ def build_signature_base_string(self, oauth_request, consumer, token):
+ """Concatenates the consumer key and secret."""
+ sig = '%s&' % escape(consumer.secret)
+ if token:
+ sig = sig + escape(token.secret)
+ return sig, sig
+
+ def build_signature(self, oauth_request, consumer, token):
+ key, raw = self.build_signature_base_string(oauth_request, consumer,
+ token)
+ return key
\ No newline at end of file
photo is wider than it is tall, it will be cropped with a bias towards
the top of the photo."""
- f = urllib.urlopen(url)
- data = ''
- while True:
- read_data = f.read()
- data += read_data
- if not read_data:
- break
-
- im = Image.open(StringIO.StringIO(data))
- (w, h) = im.size
- if (h > w):
- print "Shrinking photo for %s as it's %d x %d" % (self._contact.get_name(), w, h)
- im = ImageOps.fit(im, (w, w), Image.NEAREST, 0, (0, 0.1))
-
- print "Updating photo for %s" % (self._contact.get_name())
- f = StringIO.StringIO()
- im.save(f, "JPEG")
- image_data = f.getvalue()
- photo = EContactPhoto()
- photo.type = 0
- photo.data = EContactPhoto_data()
- photo.data.inlined = EContactPhoto_inlined()
- photo.data.inlined.mime_type = cast(create_string_buffer("image/jpeg"), c_char_p)
- photo.data.inlined.length = len(image_data)
- photo.data.inlined.data = cast(create_string_buffer(image_data), c_void_p)
- ebook.e_contact_set(hash(self._contact), E_CONTACT_PHOTO, addressof(photo))
- return True
+ try:
+ f = urllib.urlopen(url)
+ data = ''
+ while True:
+ read_data = f.read()
+ data += read_data
+ if not read_data:
+ break
+
+ im = Image.open(StringIO.StringIO(data))
+ (w, h) = im.size
+ if (h > w):
+ ##print u"Shrinking photo for %s as it's %d x %d" % (self._contact.get_name(), w, h)
+ im = ImageOps.fit(im, (w, w), Image.NEAREST, 0, (0, 0.1))
+
+ ## print u"Updating photo for %s" % (self._contact.get_name())
+ f = StringIO.StringIO()
+ im.save(f, "JPEG")
+ image_data = f.getvalue()
+ photo = EContactPhoto()
+ photo.type = 0
+ photo.data = EContactPhoto_data()
+ photo.data.inlined = EContactPhoto_inlined()
+ photo.data.inlined.mime_type = cast(create_string_buffer("image/jpeg"), c_char_p)
+ photo.data.inlined.length = len(image_data)
+ photo.data.inlined.data = cast(create_string_buffer(image_data), c_void_p)
+ ebook.e_contact_set(hash(self._contact), E_CONTACT_PHOTO, addressof(photo))
+ return True
+ except:
+ print "FAILED to get photo from URL %s" % url
+ return False
+
+
+ # -----------------------------------------------------------------------
+ def has_photo(self):
+ # FIXME
+ return False
# -----------------------------------------------------------------------
birthday.year = year
birthday.month = month
birthday.day = day
- print "Setting birthday for [%s] to %d-%d-%d" % (self._contact.get_name(), year, month, day)
+ print u"Setting birthday for [%s] to %d-%d-%d" % (self._contact.get_name(), year, month, day)
ebook.e_contact_set(hash(self._contact), E_CONTACT_BIRTHDAY_DATE, addressof(birthday))
return True
# -----------------------------------------------------------------------
+ def set_nickname(self, nickname):
+ """Set the nickname for this contact to the given nickname."""
+
+ # FIXME does this work?
+ self._contact.set_property('nickname', nickname)
+ #ebook.e_contact_set(hash(self._contact), E_NICKNAME, addressof(nickname))
+
+
+ # -----------------------------------------------------------------------
def get_emails(self):
"""Return the email addresses associated with this contact."""
while ai.has_next():
attr = ai.next(as_a = EVCardAttribute)
if not attr:
- raise Exception("Unexpected null attribute for [" + self._contact.get_name() + "] with emails " + emails)
+ raise Exception(u"Unexpected null attribute for [" + self._contact.get_name() + "] with emails " + emails)
emails.append(string_at(attr.value().next()))
return emails
while ai.has_next():
attr = ai.next(as_a = EVCardAttribute)
if not attr:
- raise Exception("Unexpected null attribute for [" + self._contact.get_name() + "] with URLs " + urls)
+ raise Exception(u"Unexpected null attribute for [" + self._contact.get_name() + "] with URLs " + urls)
urls.append(string_at(attr.value().next()))
return urls
url_attr.name = 'URL'
val = GList()
- print "Setting URL for [%s] to [%s]" % (self._contact.get_name(), url)
+ ##print u"Setting URL for [%s] to [%s]" % (self._contact.get_name(), url)
val.set(create_string_buffer(url))
ai.set(addressof(url_attr))
url_attr.values = cast(addressof(val), POINTER(GList))
import gnome.gconf
import org.maemo.hermes.engine.service
-from facebook import Facebook
+from facebook import Facebook,FacebookError
from org.maemo.hermes.engine.names import canonical
+from org.maemo.hermes.engine.friend import Friend
class Service(org.maemo.hermes.engine.service.Service):
"""Facebook backend for Hermes.
self._friends = None
self._friends_by_url = {}
+ self._friends_by_contact = {}
self._should_create = set()
+
+ self.get_friends()
+
+ # -----------------------------------------------------------------------
+ def get_name(self):
+ return "Facebook"
+
# -----------------------------------------------------------------------
def _do_fb_login(self):
pass
self._do_fb_login()
+
+ def if_defined(data, key, callback):
+ if key in data and data[key]:
+ callback(data[key])
+
self._friends = {}
attrs = ['uid', 'name', 'pic_big', 'birthday_date', 'profile_url', 'first_name', 'last_name', 'website']
- for friend in self.fb.users.getInfo(self.fb.friends.get(), attrs):
- print "#"
- friend['key']= canonical(friend['name'])
- self._friends[friend['key']] = friend
-
- if 'profile_url' not in friend:
- friend['profile_url'] = "http://www.facebook.com/profile.php?id=" + str(friend ['uid'])
+ for data in self.fb.users.getInfo(self.fb.friends.get(), attrs):
+ key = canonical(data['name'])
+ friend = Friend(data['name'])
+ self._friends[key] = friend
- self._friends_by_url[friend['profile_url']] = friend
- friend['pic'] = friend[attrs[2]]
- friend['account'] = 'Facebook'
+ if 'profile_url' not in data:
+ data['profile_url'] = "http://www.facebook.com/profile.php?id=" + str(data['uid'])
- if 'website' in friend and friend['website']:
- friend['homepage'] = friend['website']
-
- if 'birthday_date' in friend and friend['birthday_date']:
- self._should_create.add(friend['profile_url'])
+ self._friends_by_url[data['profile_url']] = friend
+
+
+ if_defined(data, 'website', friend.add_url)
+ if_defined(data, 'profile_url', friend.add_url)
+ if_defined(data, 'birthday_date', friend.set_birthday_date)
+
+ # exception from the rule:
+# if_defined(data, 'pic_big', friend.set_photo_url)
+ friend.set_photo_url(data[attrs[2]])
+
+ if friend.has_birthday_date():
+ self._should_create.add(friend)
return self._friends.values()
+
+ # -----------------------------------------------------------------------
+ def pre_process_contact(self, contact):
+
+ if not self._friends:
+ self.get_friends()
+
+ for url in contact.get_urls():
+ if url in self._friends_by_url:
+ matched_friend = self._friends_by_url[url]
+ self._friends_by_contact[contact] = matched_friend
+ self._should_create.discard(matched_friend)
+
# -----------------------------------------------------------------------
- def process_contact(self, contact, overwrite = False, friend = None):
- """Called for each contact in the address book. Any friends linked to
- from the contact should have their matching updated. The backend should
- enrich the contact with any meta-data it can; and return 'True' if any
- information was updated. If 'friend' is provided, the information should
- be retrieved from friend. This will be one of the entries from
- 'get_friends' when manual mapping is being done."""
+ def process_contact(self, contact, friend):
if not self._friends:
self.get_friends()
-
+
+ if self._friends_by_contact.has_key(contact):
+ friend.update(self._friends_by_contact[contact])
+ return
+
matched_friend = None
+ # we might get a hit if the friend has setup a URL with another service
for url in contact.get_urls():
if url in self._friends_by_url:
matched_friend = self._friends_by_url[url]
+ self._friends_by_contact[contact] = matched_friend
break
if not matched_friend:
for id in contact.get_identifiers():
if id in self._friends:
matched_friend = self._friends[id]
+ self._friends_by_contact[contact] = matched_friend
break
if matched_friend:
- print contact.get_name(), " -> ", matched_friend['profile_url']
- self._should_create.discard(matched_friend['profile_url'])
- else:
- print contact.get_name(), " no match to Facebook with ", contact.get_identifiers()
+ print contact.get_name(), " -> ", matched_friend
+ self._should_create.discard(matched_friend)
+
enrichment. If any contacts are updated at this stage, 'updated' should
be added to."""
- if self._autocreate:
- for url in self._should_create:
- print "Need to autocreate:", self._friends_by_url[url]
+ if True or self._autocreate: # FIXME - remove True or
+ for friend in self._should_create:
+ print "Need to autocreate: %s (%s)" % (friend._attributes, friend._multi_attributes)
--- /dev/null
+class Friend():
+
+ def __init__(self, name):
+ self._attributes = { "fn": name }
+ self._multi_attributes = {}
+
+ def __unicode__(self):
+ return self.__repr__()
+
+ def __repr__(self):
+ return "Friend %s" % self._attributes['fn']
+
+ # public accessors -----------------
+
+ def add_url(self, url):
+ self._add('url', url)
+
+ def is_empty(self):
+ for a in self._attributes:
+ return False
+ for a in self._multi_attributes:
+ return False
+ return True
+
+ def has_birthday_date(self):
+ return self._has('bday')
+
+ def set_name(self, name):
+ self._set('fn', name)
+
+ def set_nickname(self, nickname):
+ self._set('nickname', nickname)
+
+ def set_birthday_date(self, date):
+ self._set('bday', date)
+
+ def set_photo_url(self, url):
+ self._set('photo-url', url)
+
+ def update(self, other_friend):
+ """
+ Overwrites any attributes in this friend, with attributes from other_friend
+ """
+
+ self._attributes.update(other_friend._attributes)
+
+ for key in other_friend._multi_attributes.keys():
+ for value in other_friend._multi_attributes[key]:
+ self._add(key, value)
+
+ def update_contact(self, contact, overwrite=False):
+ """
+ Updates the contact with information from this object,
+ without overwriting unless overwrite is set to True.
+ """
+
+ # FIXME: currently we overwrite everything
+ self._if_defined('photo-url', contact.set_photo)
+ self._if_defined('nickname', contact.set_nickname)
+ if self._multi_attributes.has_key('url'):
+ for url in self._multi_attributes['url']:
+ contact.add_url(url)
+
+ def fixme(arg):
+ pass
+ #print "FIXME - birthday date needs to be parsed/fixed %s before calling contact.set_birthday" % arg
+ self._if_defined('bday', fixme)
+
+ # private helpers -----------------------
+ #
+ def _if_defined(self, key, callback):
+ if self._attributes.has_key(key):
+ callback(self._attributes[key])
+
+ def _set(self, key, value):
+ if value is not None:
+# print "%s SET %s to %s" % (self, key, value)
+ self._attributes[key] = value
+
+ def _add(self, key, value):
+ if value is not None:
+ if not self._multi_attributes.has_key(key):
+ self._multi_attributes[key] = []
+# print "%s ADD %s to %s" % (self, key, value)
+ self._multi_attributes[key].append(value)
+
+ def _has(self, key):
+ return self._attributes.has_key(key) or self._multi_attributes.has_key(key)
+
--- /dev/null
+import gnome.gconf
+import org.maemo.hermes.engine.provider
+import org.maemo.hermes.engine.gravatar.service
+
+class Provider(org.maemo.hermes.engine.provider.Provider):
+ """Gravatar provider for Hermes.
+
+ Copyright (c) Fredrik Wendt <fredrik@wendt.se> 2010.
+ Released under the Artistic Licence."""
+
+
+ # -----------------------------------------------------------------------
+ def __init__(self):
+ self._gconf = gnome.gconf.client_get_default()
+
+
+ # -----------------------------------------------------------------------
+ def get_name(self):
+ """Return the display name of this service. An icon, of with the lower-case,
+ all-alphabetic version of this name is expected to be provided."""
+
+ return 'Gravatar'
+
+
+ # -----------------------------------------------------------------------
+ def has_preferences(self):
+ """Whether or not this provider has any preferences. If it does not,
+ open_preferences must NOT be called; as the behaviour is undetermined."""
+
+ return False
+
+
+ # -----------------------------------------------------------------------
+ def open_preferences(self, parent):
+ """Open the preferences for this provider as a child of the 'parent' widget."""
+
+ print "Err, open preferences. OK. Err, right. Hmm."
+
+
+ # -----------------------------------------------------------------------
+ def service(self, gui_callback):
+ """Return the service backend"""
+
+ return org.maemo.hermes.engine.gravatar.service.Service()
--- /dev/null
+import urllib, hashlib, xmlrpclib
+import gnome.gconf
+import org.maemo.hermes.engine.service
+
+from org.maemo.hermes.engine.names import canonical
+
+class Service(org.maemo.hermes.engine.service.Service):
+ """Gravatar backend for Hermes.
+
+ Copyright (c) Fredrik Wendt <fredrik@wendt.se> 2010.
+ Released under the Artistic Licence."""
+
+ _image_url_base = "http://www.gravatar.com/avatar.php?"
+
+ # -----------------------------------------------------------------------
+ def __init__(self):
+ """Initialise the Gravatar service"""
+
+ self._gc = gnome.gconf.client_get_default()
+ self._address_to_contact = {}
+ self._hash_to_address= {}
+ self._hash_has_gravatar = {}
+ self._empty_cache = True
+
+ self._api_email = self._gc.get_string('/apps/maemo/hermes/gravatar_api_email')
+ self._api_key = self._gc.get_string('/apps/maemo/hermes/gravatar_api_key')
+ self._api_email = 'maemohermes@wendt.se'
+ self._api_key = 'b14ec179822b' # FIXME
+ if self._api_key is None or self._api_email is None:
+ raise Exception('No Gravatar application keys found. Installation error.')
+ self._api_url = 'https://secure.gravatar.com/xmlrpc?user=' + hashlib.md5(self._api_email).hexdigest()
+
+
+ # -----------------------------------------------------------------------
+ def get_name(self):
+ return "Gravatar"
+
+
+ # -----------------------------------------------------------------------
+ def get_friends(self):
+ """Returns 'None' since manual mapping is not supported."""
+
+ return None
+
+
+ # -----------------------------------------------------------------------
+ def pre_process_contact(self, contact):
+ """Extracts addresses from the contact."""
+ for address in contact.get_emails():
+ self._address_to_contact[address] = contact
+
+
+ # -----------------------------------------------------------------------
+ def process_contact(self, contact, friend):
+ """On first call (with a contact missing a photo), go get gravatar existense data from the service."""
+
+ if not self._has_photo(contact):
+ if self._empty_cache:
+ self._lookup_addresses()
+ for address in contact.get_emails():
+ hash = self._get_hash_for_address(address)
+ if (self._hash_has_gravatar[hash]):
+ friend.set_photo_url(self._get_url_for_email_hash(hash))
+
+
+ # -----------------------------------------------------------------------
+ def finalise(self, updated, overwrite=True):
+ """We could make a new pass over all updated contacts, in the event that another service
+ added a new e-mail address to a contact (still missing a photo) that has a gravatar setup."""
+
+ pass
+ # It's plausible that info@company.com addresses would be present for several
+ # contacts, but unlikely that there'd be a gravatar set up for such generic
+ # addresses - the point of gravatar is to make it more personal ...
+
+ # we only look at contacts that has no photo (unless
+# for contact in self._contacts:
+# if not overwrite or not self._has_photo(contact):
+# for address in contact.get_emails():
+# self._address_to_contact[address] = contact
+# self._lookup_addresses()
+# for hash in self._hash_has_gravatar:
+# if self._hash_has_gravatar[hash]:
+# address = self._hash_to_address[hash]
+# contact = self._address_to_contact[address]
+# if not self._has_photo(contact):
+# url = self._get_url_for_email_hash(hash)
+# contact.set_photo(url)
+# updated.add(contact)
+
+ # -----------------------------------------------------------------------
+ # FIXME
+ def _has_photo(self, contact):
+ return False
+
+
+ # -----------------------------------------------------------------------
+ def _lookup_addresses(self):
+ """Constructs hashes for address_to_contact, makes call to the Gravatar.com service and updates
+ self._hash_has_gravatar"""
+
+ args = { "apikey" : self._api_key}
+ args["hashes"] = list(self._construct_hashes(self._address_to_contact.keys()))
+ service = xmlrpclib.ServerProxy(self._api_url)
+ self._hash_has_gravatar = service.grav.exists(args)
+ self._empty_cache = False
+
+
+ # -----------------------------------------------------------------------
+ def _get_url_for_email_hash(self, hash, default="404", size="128"):
+ """Assembles the URL to a gravatar, based on a hash of an e-mail address"""
+
+ return self._image_url_base + urllib.urlencode({'gravatar_id':hash, 'd':default, 'size':size})
+
+
+ # -----------------------------------------------------------------------
+ def _construct_hashes(self, addresses):
+ """Creates hashes for all addresses specified, returning a set of hashes"""
+
+ result = set()
+ for address in addresses:
+ hash = self._get_hash_for_address(address)
+ self._hash_to_address[hash] = address
+ result.add(hash)
+
+ return result
+
+ # -----------------------------------------------------------------------
+ def _get_hash_for_address(self, address):
+ return hashlib.md5(address.lower()).hexdigest()
\ No newline at end of file
--- /dev/null
+import org.maemo.hermes.engine.provider
+import org.maemo.hermes.engine.linkedin.service
+
+class Provider(org.maemo.hermes.engine.provider.Provider):
+ """LinkedIn provider for Hermes.
+
+ Copyright (c) Andrew Flegg <andrew@bleb.org> 2010.
+ Released under the Artistic Licence."""
+
+ # -----------------------------------------------------------------------
+ def get_name(self):
+ """Return the display name of this service. An icon, of with the lower-case,
+ all-alphabetic version of this name is expected to be provided."""
+
+ return 'LinkedIn'
+
+
+ # -----------------------------------------------------------------------
+ def has_preferences(self):
+ """Whether or not this provider has any preferences. If it does not,
+ open_preferences must NOT be called; as the behaviour is undetermined."""
+
+ return True
+
+
+ # -----------------------------------------------------------------------
+ def open_preferences(self, parent):
+ """Open the preferences for this provider as a child of the 'parent' widget."""
+
+ print "Err, open preferences. OK. Err, right. Hmm."
+ # FIXME: do auth:
+ # get request token
+ # get auth token
+ # open browser to have user allow data access
+ # user inputs the 5 char
+
+
+ # -----------------------------------------------------------------------
+ def service(self, gui_callback):
+ """Return the service backend."""
+
+ return org.maemo.hermes.engine.linkedin.service.Service()
--- /dev/null
+import httplib
+import gnome.gconf
+from oauth import oauth
+from xml.dom.minidom import parseString
+
+import org.maemo.hermes.engine.service
+from org.maemo.hermes.engine.friend import Friend
+
+from org.maemo.hermes.engine.names import canonical
+
+# httplib.HTTPSConnection.debuglevel = 1
+
+class Service(org.maemo.hermes.engine.service.Service):
+ """LinkedIn backend for Hermes.
+
+ This sets up two gconf paths to contain LinkedIn OAuth keys:
+ /apps/maemo/hermes/linkedin_oauth
+ /apps/maemo/hermes/linkedin_verifier
+
+ Copyright (c) Fredrik Wendt <fredrik@wendt.se> 2010.
+ Released under the Artistic Licence."""
+
+
+ LI_SERVER = "api.linkedin.com"
+ LI_API_URL = "https://api.linkedin.com"
+ LI_CONN_API_URL = LI_API_URL + "/v1/people/~/connections"
+
+ REQUEST_TOKEN_URL = LI_API_URL + "/uas/oauth/requestToken"
+ AUTHORIZE_URL = LI_API_URL + "/uas/oauth/authorize"
+ ACCESS_TOKEN_URL = LI_API_URL + "/uas/oauth/accessToken"
+
+
+ # -----------------------------------------------------------------------
+ def __init__(self, autocreate=False, gui_callback=None):
+ """Initialize the LinkedIn service, finding LinkedIn API keys in gconf and
+ having a gui_callback available."""
+
+ self._gc = gnome.gconf.client_get_default()
+ self._gui = gui_callback
+ self._autocreate = autocreate
+
+ # -- Check the environment is going to work...
+ #
+ if (self._gc.get_string('/desktop/gnome/url-handlers/http/command') == 'epiphany %s'):
+ raise Exception('Browser in gconf invalid (see NB#136012). Installation error.')
+
+ api_key = self._gc.get_string('/apps/maemo/hermes/linkedin_api_key')
+ secret_key = self._gc.get_string('/apps/maemo/hermes/linkedin_key_secret')
+
+ # FIXME: remove this
+ api_key = '1et4G-VtmtqNfY7gF8PHtxMOf0KNWl9ericlTEtdKJeoA4ubk4wEQwf8lSL8AnYE'
+ secret_key = 'uk--OtmWcxER-Yh6Py5p0VeLPNlDJSMaXj1xfHILoFzrK7fM9eepNo5RbwGdkRo_'
+
+ if api_key is None or secret_key is None:
+ raise Exception('No LinkedIn application keys found. Installation error.')
+
+ self.api_key = api_key
+ self.secret_key = secret_key
+
+ # FIXME: move this
+ token_str = "oauth_token_secret=60f817af-6437-4015-962f-cc3aefee0264&oauth_token=f89c2b7b-1c12-4f83-a469-838e78901716"
+ self.access_token = oauth.OAuthToken.from_string(token_str)
+
+ self.consumer = oauth.OAuthConsumer(api_key, secret_key)
+ self.sig_method = oauth.OAuthSignatureMethod_HMAC_SHA1()
+
+ self._friends = None
+ self._friends_by_url = {}
+ self._friends_by_contact = {}
+
+ self.get_friends()
+
+
+ # -----------------------------------------------------------------------
+ def get_name(self):
+ return "LinkedIn"
+
+
+ # -----------------------------------------------------------------------
+ def get_friends(self):
+ """Return a list of friends from this service, or 'None' if manual mapping
+ is not supported."""
+
+ if self._friends:
+ return self._friends.values()
+
+ # FIXME: Check the available OAuth session is still valid...
+
+ # get data
+ self._friends = {}
+
+ xml = self._make_api_request(self.LI_CONN_API_URL)
+ dom = parseString(xml)
+
+ self._parse_dom(dom)
+
+ print "get_friends found", len(self._friends)
+ return self._friends.values()
+
+
+
+ # -----------------------------------------------------------------------
+ def pre_process_contact(self, contact):
+ """Makes sure that if the contact has been mapped to a friend before,
+ remove the friend from "new friends" list."""
+
+ for url in contact.get_urls():
+ if url in self._friends_by_url:
+ matched_friend = self._friends_by_url[url]
+ print "Contact is known as a friend on this service (by URL) %s - %s" % (url, matched_friend)
+ self._friends_by_contact[contact] = matched_friend
+
+
+ # -----------------------------------------------------------------------
+ def process_contact(self, contact, friend):
+ """Updates friend if the contact can be mapped to a friend on LinkedIn,
+ either by previous mapping or by identifiers."""
+
+ if self._friends_by_contact.has_key(contact):
+ friend.update(self._friends_by_contact[contact])
+ else:
+ self._match_contact_by_identifiers(contact, friend)
+
+
+ # -----------------------------------------------------------------------
+ def finalise(self, updated):
+ if self._autocreate or True:
+ for f in self._friends.values():
+ if f not in self._friends_by_contact.values():
+ # FIXME: create friends as contact
+ print "Could/should create contact here for %s" % f
+
+
+ # -----------------------------------------------------------------------
+ def _get_access_token(self, token, verifier):
+ """user provides the verifier, which was displayed in the browser window"""
+
+ oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=token, verifier=verifier, http_url=self.ACCESS_TOKEN_URL)
+ oauth_request.sign_request(self.sig_method, self.consumer, token)
+
+ connection = httplib.HTTPSConnection(self.LI_SERVER)
+ connection.request(oauth_request.http_method, self.ACCESS_TOKEN_URL, headers=oauth_request.to_header())
+ response = connection.getresponse()
+ return oauth.OAuthToken.from_string(response.read())
+
+
+ # -----------------------------------------------------------------------
+ def _get_authorize_url(self, token):
+ """The URL that the user should browse to, in order to authorize the application to acess data"""
+
+ oauth_request = oauth.OAuthRequest.from_token_and_callback(token=token, http_url=self.AUTHORIZE_URL)
+ return oauth_request.to_url()
+
+
+ # -----------------------------------------------------------------------
+ def _get_request_token(self, callback):
+ """Get a request token from LinkedIn"""
+
+ oauth_consumer_key = self.api_key
+ oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, callback=callback, http_url=self.REQUEST_TOKEN_URL)
+ oauth_request.sign_request(self.sig_method, self.consumer, None)
+
+ connection = httplib.HTTPSConnection(self.LI_SERVER)
+ connection.request(oauth_request.http_method, self.REQUEST_TOKEN_URL, headers=oauth_request.to_header())
+ response = self.connection.getresponse().read()
+
+ token = oauth.OAuthToken.from_string(response)
+ return token
+
+
+ # -----------------------------------------------------------------------
+ def _make_api_request(self, url):
+ print "_make_api_request", url
+ oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.access_token, http_url=url)
+ oauth_request.sign_request(self.sig_method, self.consumer, self.access_token)
+ connection = httplib.HTTPSConnection(self.LI_SERVER)
+ try:
+ connection.request(oauth_request.http_method, url, headers=oauth_request.to_header())
+ return connection.getresponse().read()
+ except:
+ raise Exception("Failed to contact LinkedIn at " + url)
+
+
+ # -----------------------------------------------------------------------
+ def _do_li_login(self):
+ """Perform authentication against LinkedIn and store the result in gconf
+ for later use. Uses the 'need_auth' and 'block_for_auth' methods on
+ the callback class. The former allows a message to warn the user
+ about what is about to happen to be shown; the second is to wait
+ for the user to confirm they have logged in."""
+ # FIXME
+
+
+ # -----------------------------------------------------------------------
+ def _match_contact_by_identifiers(self, contact, friend):
+ for id in contact.get_identifiers():
+ if id in self._friends:
+ matched_friend = self._friends[id]
+ if matched_friend in self._friends_by_contact.values():
+ print "Avoiding assigning same friend to two contacts: %s " % matched_friend
+ else:
+ self._friends_by_contact[contact] = matched_friend
+ friend.update(matched_friend)
+
+
+ # -----------------------------------------------------------------------
+ def _parse_dom(self, dom):
+ print "parse_dom", dom
+ def get_first_tag(node, tagName):
+ tags = node.getElementsByTagName(tagName)
+ if tags and len(tags) > 0:
+ return tags[0]
+
+ def extract(node, tagName):
+ tag = get_first_tag(node, tagName)
+ if tag:
+ return tag.firstChild.nodeValue
+
+ def extract_public_url(node):
+ tag = get_first_tag(node, 'site-standard-profile-request')
+ if tag:
+ url = extract(tag, 'url')
+ return url.replace("&", "&")
+
+ # FIXME: look for <error>
+ people = dom.getElementsByTagName('person')
+ for p in people:
+ # try:
+ if True:
+ fn = extract(p, 'first-name')
+ ln = extract(p, 'last-name')
+ photo_url = extract(p, 'picture-url')
+ id = extract(p, 'id')
+ public_url = extract_public_url(p)
+
+ name = fn + " " + ln
+ friend = Friend(name)
+ friend.add_url(public_url)
+ if photo_url: friend.set_photo_url(photo_url)
+
+ key = canonical(name)
+ self._friends[key] = friend
+ self._friends_by_url[public_url] = friend
+# except:
+# continue
Copyright (c) Andrew Flegg <andrew@bleb.org> 2010.
Released under the Artistic Licence."""
+
+ def __init__(self):
+ """Should make initial calls to the service and retrieve a list of friends."""
# -----------------------------------------------------------------------
- def get_friends(self):
- """Return a list of friends from this service, or 'None' if manual mapping
- is not supported."""
+ def get_name(self):
+ """Should return the same name as the provider class - debugging only - REMOVE/FIXME"""
+
+ return None
+
+
+ # -----------------------------------------------------------------------
+ def pre_process_contact(self, contact):
+ """If the contact have an URL (or similar) that proves an existing matching
+ to a friend on this service, then this should be remembered to avoid
+ name collission/mapping the same friend to other contacts with the same
+ name."""
return None
# -----------------------------------------------------------------------
- def process_contact(self, contact, overwrite = False, friend = None):
+ def process_contact(self, contact, friend):
"""Called for each contact in the address book. Any friends linked to
- from the contact should have their matching updated. The backend should
- enrich the contact with any meta-data it can; and return 'True' if any
- information was updated. If 'friend' is provided, the information should
- be retrieved from friend. This will be one of the entries from
- 'get_friends' when manual mapping is being done."""
+ from the contact should have their matching updated. The back end should
+ enrich the friend passed with any meta-data it can."""
pass
# -----------------------------------------------------------------------
+ def get_unmatched_friends(self):
+ """Return a list of friends not matched to a contact, or 'None' if manual mapping
+ is not supported."""
+
+ return None
+
+
+ # -----------------------------------------------------------------------
def finalise(self, updated, overwrite = False):
"""Once all contacts have been processed, allows for any tidy-up/additional
enrichment. If any contacts are updated at this stage, 'updated' should
import twitter
from org.maemo.hermes.engine.names import canonical
+from org.maemo.hermes.engine.friend import Friend
class Service(org.maemo.hermes.engine.service.Service):
"""Twitter backend for Hermes.
self._friends = None
self._friends_by_url = {}
+ self._friends_by_contact = {}
# -----------------------------------------------------------------------
+ def get_name(self):
+ return "Twitter"
+
+
+ # -----------------------------------------------------------------------
def get_friends(self):
"""Return a list of friends from this service, or 'None' if manual mapping
is not supported."""
for tweeter in api.GetFriends():
key = canonical(tweeter.name)
url = 'http://twitter.com/%s' % (tweeter.screen_name)
- friend = {'name': tweeter.name, 'pic': tweeter.profile_image_url,
- 'birthday_date': None, 'profile_url': url,
- 'homepage': tweeter.url, 'account': 'twitter'}
- if friend['pic'].find('/default_profile') > -1:
- friend['pic'] = None
+ friend = Friend(tweeter.name)
+ friend.set_nickname(tweeter.screen_name)
+ friend.add_url(url)
+ friend.add_url(tweeter.url)
+ if '/default_profile' not in tweeter.profile_image_url:
+ friend.set_photo_url(tweeter.profile_image_url)
self._friends[key] = friend
self._friends_by_url[url] = friend
# -----------------------------------------------------------------------
- def process_contact(self, contact, overwrite = False, friend = None):
+ def pre_process_contact(self, contact):
+ if not self._friends:
+ self.get_friends()
+
+ for url in contact.get_urls():
+ if url in self._friends_by_url:
+ matched_friend = self._friends_by_url[url]
+ self._friends_by_contact[contact] = matched_friend
+
+
+ # -----------------------------------------------------------------------
+ def process_contact(self, contact, friend):
"""Called for each contact in the address book. Any friends linked to
from the contact should have their matching updated. The backend should
enrich the contact with any meta-data it can; and return 'True' if any
if not self._friends:
self.get_friends()
-
- matched_friend = None
- for url in contact.get_urls():
- if url in self._friends_by_url:
- matched_friend = self._friends_by_url[url]
- break
-
- if not matched_friend:
- for id in contact.get_identifiers():
- if id in self._friends:
- matched_friend = self._friends[id]
- break
+
+ if self._friends_by_contact.has_key(contact):
+ friend.update(self._friends_by_contact[contact])
+ return
+
+ for id in contact.get_identifiers():
+ if id in self._friends:
+ matched_friend = self._friends[id]
+ print contact.get_name(), " -> ", matched_friend
+ if matched_friend in self._friends_by_contact.values():
+ print "COLLISSION avoided for %s" % friend
+ else:
+ print "Match found %s" % friend
+ self._friends_by_contact[contact] = matched_friend
+ friend.update(matched_friend)
- if matched_friend:
- print contact.get_name(), " -> ", matched_friend['profile_url']
- else:
- print contact.get_name(), " no match to Twitter with ", contact.get_identifiers()
-
# -----------------------------------------------------------------------
import evolution
from org.maemo.hermes.engine.contact import Contact
+from org.maemo.hermes.engine.friend import Friend
from org.maemo.hermes.gui.console import ConsoleUICallback
import org.maemo.hermes.engine.facebook.provider
import org.maemo.hermes.engine.twitter.provider
+import org.maemo.hermes.engine.gravatar.provider
+import org.maemo.hermes.engine.linkedin.provider
providers = [
org.maemo.hermes.engine.facebook.provider.Provider(),
- org.maemo.hermes.engine.twitter.provider.Provider()
+ org.maemo.hermes.engine.twitter.provider.Provider(),
+ org.maemo.hermes.engine.gravatar.provider.Provider(),
+ org.maemo.hermes.engine.linkedin.provider.Provider(),
]
ui = ConsoleUICallback()
+
+# assemble services, least important (prioritized) first
+print "+++ Initialising Services"
services = []
for provider in providers:
print "Using %s" % (provider.get_name())
services.append(provider.service(ui))
+contacts = []
+updated_contacts = set()
+unmatched_contacts = set()
+overwrite = False
+
+print "\n+++ Pre processing"
addresses = evolution.ebook.open_addressbook('default')
for econtact in addresses.get_all_contacts():
contact = Contact(addresses, econtact)
- print "+++ %s" % (contact.get_name())
+ contacts.append(contact)
+ for service in services:
+ print " Pre processing %s with %s" % (contact, service.get_name())
+ service.pre_process_contact(contact)
+
+print "\n+++ Processing"
+for contact in contacts:
+ friend = Friend(contact.get_name())
for service in services:
- print service.process_contact(contact)
+ print " Processing %s with %s" % (friend, service.get_name())
+ service.process_contact(contact, friend)
+ if friend.is_empty():
+ print "No service matched %s\n" % friend
+ unmatched_contacts.add(contact)
+ else:
+ print " Updating contact data for %s\n" % friend
+ friend.update_contact(contact, overwrite)
+ updated_contacts.add(contact)
+print "\n+++ Finalising"
+for service in services:
+ print " Finalising service %s" % service.get_name()
+ service.finalise(updated_contacts)
--- /dev/null
+given
+ clean addressbook
+ friend "Adam" on linkedin
+ friend "Adam" on twitter
+when
+ full run
+then
+ addressbook should contain one newly created contact, with data from both services
+
+
+given
+ clean addressbook
+ 100 friends on linkedin
+ 100 friends on twitter, out of which 50 are the same people (have the same name) as on linkedin
+when
+ full run
+then
+ addressbook should contain 150 contacts (not 200)
+
+
+# this will probably not be too uncommon if a person really has an empty address book and runs this the first
+# time. We don't want that person to end up with many duplicates
+
+
+given
+ contact "Fredrik Wendt"
+ friend "Fredrik L. Wendt" on facebook
+when
+ full processing the contact
+then
+ the contact should be matched with the friend
+
+
+given
+ clean addressbook
+ friend "twitter" on twitter
+ friend "linkedin" on linkedin
+ friend "facebook" on facebook
+ friend "plaxo" on plaxo
+when
+ full run
+then
+ addressbook should contain 4 contacts
+
+# process_contact should not rely on pre_process_contact being called
+
+
+
+given
+ clean addressbook
+ friend "adam" on twitter, with homepage set to "http://facebook/user/adam"
+ friend "adam" on facebook (with ID url "http://facebook/user/adam")
+when
+ twitter is processed
+ and facebook is processed
+then
+ addressbook should contain one contact with two website URLs (twitter and facebook)
+
+
\ No newline at end of file