From: Andrew Flegg Date: Sun, 19 Dec 2010 13:04:49 +0000 (+0000) Subject: Add additional debugging to try and identify cause of MB#11103 X-Git-Tag: 0.8.8~7 X-Git-Url: http://git.maemo.org/git/?p=hermes;a=commitdiff_plain;h=c6ae92c6931a647da2b53ade25ac862d925b4edc Add additional debugging to try and identify cause of MB#11103 --- diff --git a/package/src/main.py b/package/src/main.py index 38bf6a9..1e8f5d8 100644 --- a/package/src/main.py +++ b/package/src/main.py @@ -6,6 +6,8 @@ import org.maemo.hermes.engine.twitter.provider import org.maemo.hermes.engine.gravatar.provider import org.maemo.hermes.engine.linkedin.provider from org.maemo.hermes.gui.gtkui import HermesGUI +import httplib, urllib +httplib.HTTPConnection.debuglevel = 1 # -- Check environment... # diff --git a/package/src/org/maemo/hermes/engine/facebook/provider.py b/package/src/org/maemo/hermes/engine/facebook/provider.py index 21d24f6..375db39 100644 --- a/package/src/org/maemo/hermes/engine/facebook/provider.py +++ b/package/src/org/maemo/hermes/engine/facebook/provider.py @@ -1,4 +1,4 @@ -from facebook import Facebook, FacebookError +from pythonfacebook import Facebook, FacebookError import gnome.gconf import gtk, hildon import org.maemo.hermes.engine.provider diff --git a/package/src/pythonfacebook.py b/package/src/pythonfacebook.py new file mode 100644 index 0000000..c46dfc7 --- /dev/null +++ b/package/src/pythonfacebook.py @@ -0,0 +1,1282 @@ +#! /usr/bin/env python +# +# pyfacebook - Python bindings for the Facebook API +# +# Copyright (c) 2008, Samuel Cormier-Iijima +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author nor the names of its contributors may +# be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" +Python bindings for the Facebook API (pyfacebook - http://code.google.com/p/pyfacebook) + +PyFacebook is a client library that wraps the Facebook API. + +For more information, see + +Home Page: http://code.google.com/p/pyfacebook +Developer Wiki: http://wiki.developers.facebook.com/index.php/Python +Facebook IRC Channel: #facebook on irc.freenode.net + +PyFacebook can use simplejson if it is installed, which +is much faster than XML and also uses less bandwith. Go to +http://undefined.org/python/#simplejson to download it, or do +apt-get install python-simplejson on a Debian-like system. +""" + +import md5 +import sys +import time +import struct +import urllib +import urllib2 +import httplib +import hashlib +import binascii +import urlparse +import mimetypes + +# try to use simplejson first, otherwise fallback to XML +RESPONSE_FORMAT = 'JSON' +try: + import simplejson +except ImportError: + try: + import json as simplejson + except ImportError: + try: + from django.utils import simplejson + except ImportError: + try: + import jsonlib as simplejson + simplejson.loads + except (ImportError, AttributeError): + from xml.dom import minidom + RESPONSE_FORMAT = 'XML' + +# support Google App Engine. GAE does not have a working urllib.urlopen. +try: + from google.appengine.api import urlfetch + + def urlread(url, data=None, headers=None): + if data is not None: + if headers is None: + headers = {"Content-type": "application/x-www-form-urlencoded"} + method = urlfetch.POST + else: + if headers is None: + headers = {} + method = urlfetch.GET + + result = urlfetch.fetch(url, method=method, + payload=data, headers=headers) + + if result.status_code == 200: + return result.content + else: + raise urllib2.URLError("fetch error url=%s, code=%d" % (url, result.status_code)) + +except ImportError: + def urlread(url, data=None): + res = urllib2.urlopen(url, data=data) + return res.read() + +__all__ = ['Facebook'] + +VERSION = '0.1' + +FACEBOOK_URL = 'http://api.facebook.com/restserver.php' +FACEBOOK_SECURE_URL = 'https://api.facebook.com/restserver.php' + +class json(object): pass + +# simple IDL for the Facebook API +METHODS = { + 'application': { + 'getPublicInfo': [ + ('application_id', int, ['optional']), + ('application_api_key', str, ['optional']), + ('application_canvas_name ', str,['optional']), + ], + }, + + # admin methods + 'admin': { + 'getAllocation': [ + ('integration_point_name', str, []), + ], + }, + + # feed methods + 'feed': { + 'publishStoryToUser': [ + ('title', str, []), + ('body', str, ['optional']), + ('image_1', str, ['optional']), + ('image_1_link', str, ['optional']), + ('image_2', str, ['optional']), + ('image_2_link', str, ['optional']), + ('image_3', str, ['optional']), + ('image_3_link', str, ['optional']), + ('image_4', str, ['optional']), + ('image_4_link', str, ['optional']), + ('priority', int, ['optional']), + ], + + 'publishActionOfUser': [ + ('title', str, []), + ('body', str, ['optional']), + ('image_1', str, ['optional']), + ('image_1_link', str, ['optional']), + ('image_2', str, ['optional']), + ('image_2_link', str, ['optional']), + ('image_3', str, ['optional']), + ('image_3_link', str, ['optional']), + ('image_4', str, ['optional']), + ('image_4_link', str, ['optional']), + ('priority', int, ['optional']), + ], + + 'publishTemplatizedAction': [ + ('title_template', str, []), + ('page_actor_id', int, ['optional']), + ('title_data', json, ['optional']), + ('body_template', str, ['optional']), + ('body_data', json, ['optional']), + ('body_general', str, ['optional']), + ('image_1', str, ['optional']), + ('image_1_link', str, ['optional']), + ('image_2', str, ['optional']), + ('image_2_link', str, ['optional']), + ('image_3', str, ['optional']), + ('image_3_link', str, ['optional']), + ('image_4', str, ['optional']), + ('image_4_link', str, ['optional']), + ('target_ids', list, ['optional']), + ], + + 'registerTemplateBundle': [ + ('one_line_story_templates', json, []), + ('short_story_templates', json, ['optional']), + ('full_story_template', json, ['optional']), + ('action_links', json, ['optional']), + ], + + 'deactivateTemplateBundleByID': [ + ('template_bundle_id', int, []), + ], + + 'getRegisteredTemplateBundles': [], + + 'getRegisteredTemplateBundleByID': [ + ('template_bundle_id', str, []), + ], + + 'publishUserAction': [ + ('template_bundle_id', int, []), + ('template_data', json, ['optional']), + ('target_ids', list, ['optional']), + ('body_general', str, ['optional']), + ], + }, + + # fql methods + 'fql': { + 'query': [ + ('query', str, []), + ], + }, + + # friends methods + 'friends': { + 'areFriends': [ + ('uids1', list, []), + ('uids2', list, []), + ], + + 'get': [ + ('flid', int, ['optional']), + ], + + 'getLists': [], + + 'getAppUsers': [], + }, + + # notifications methods + 'notifications': { + 'get': [], + + 'send': [ + ('to_ids', list, []), + ('notification', str, []), + ('email', str, ['optional']), + ('type', str, ['optional']), + ], + + 'sendRequest': [ + ('to_ids', list, []), + ('type', str, []), + ('content', str, []), + ('image', str, []), + ('invite', bool, []), + ], + + 'sendEmail': [ + ('recipients', list, []), + ('subject', str, []), + ('text', str, ['optional']), + ('fbml', str, ['optional']), + ] + }, + + # profile methods + 'profile': { + 'setFBML': [ + ('markup', str, ['optional']), + ('uid', int, ['optional']), + ('profile', str, ['optional']), + ('profile_action', str, ['optional']), + ('mobile_fbml', str, ['optional']), + ('profile_main', str, ['optional']), + ], + + 'getFBML': [ + ('uid', int, ['optional']), + ('type', int, ['optional']), + ], + + 'setInfo': [ + ('title', str, []), + ('type', int, []), + ('info_fields', json, []), + ('uid', int, []), + ], + + 'getInfo': [ + ('uid', int, []), + ], + + 'setInfoOptions': [ + ('field', str, []), + ('options', json, []), + ], + + 'getInfoOptions': [ + ('field', str, []), + ], + }, + + # users methods + 'users': { + 'getInfo': [ + ('uids', list, []), + ('fields', list, [('default', ['name'])]), + ], + + 'getStandardInfo': [ + ('uids', list, []), + ('fields', list, [('default', ['uid'])]), + ], + + 'getLoggedInUser': [], + + 'isAppAdded': [], + + 'hasAppPermission': [ + ('ext_perm', str, []), + ('uid', int, ['optional']), + ], + + 'setStatus': [ + ('status', str, []), + ('clear', bool, []), + ('status_includes_verb', bool, ['optional']), + ('uid', int, ['optional']), + ], + }, + + # events methods + 'events': { + 'get': [ + ('uid', int, ['optional']), + ('eids', list, ['optional']), + ('start_time', int, ['optional']), + ('end_time', int, ['optional']), + ('rsvp_status', str, ['optional']), + ], + + 'getMembers': [ + ('eid', int, []), + ], + + 'create': [ + ('event_info', json, []), + ], + }, + + # update methods + 'update': { + 'decodeIDs': [ + ('ids', list, []), + ], + }, + + # groups methods + 'groups': { + 'get': [ + ('uid', int, ['optional']), + ('gids', list, ['optional']), + ], + + 'getMembers': [ + ('gid', int, []), + ], + }, + + # marketplace methods + 'marketplace': { + 'createListing': [ + ('listing_id', int, []), + ('show_on_profile', bool, []), + ('listing_attrs', str, []), + ], + + 'getCategories': [], + + 'getListings': [ + ('listing_ids', list, []), + ('uids', list, []), + ], + + 'getSubCategories': [ + ('category', str, []), + ], + + 'removeListing': [ + ('listing_id', int, []), + ('status', str, []), + ], + + 'search': [ + ('category', str, ['optional']), + ('subcategory', str, ['optional']), + ('query', str, ['optional']), + ], + }, + + # pages methods + 'pages': { + 'getInfo': [ + ('page_ids', list, ['optional']), + ('uid', int, ['optional']), + ], + + 'isAdmin': [ + ('page_id', int, []), + ], + + 'isAppAdded': [ + ('page_id', int, []), + ], + + 'isFan': [ + ('page_id', int, []), + ('uid', int, []), + ], + }, + + # photos methods + 'photos': { + 'addTag': [ + ('pid', int, []), + ('tag_uid', int, [('default', 0)]), + ('tag_text', str, [('default', '')]), + ('x', float, [('default', 50)]), + ('y', float, [('default', 50)]), + ('tags', str, ['optional']), + ], + + 'createAlbum': [ + ('name', str, []), + ('location', str, ['optional']), + ('description', str, ['optional']), + ], + + 'get': [ + ('subj_id', int, ['optional']), + ('aid', int, ['optional']), + ('pids', list, ['optional']), + ], + + 'getAlbums': [ + ('uid', int, ['optional']), + ('aids', list, ['optional']), + ], + + 'getTags': [ + ('pids', list, []), + ], + }, + + # fbml methods + 'fbml': { + 'refreshImgSrc': [ + ('url', str, []), + ], + + 'refreshRefUrl': [ + ('url', str, []), + ], + + 'setRefHandle': [ + ('handle', str, []), + ('fbml', str, []), + ], + }, + + # SMS Methods + 'sms' : { + 'canSend' : [ + ('uid', int, []), + ], + + 'send' : [ + ('uid', int, []), + ('message', str, []), + ('session_id', int, []), + ('req_session', bool, []), + ], + }, + + 'data': { + 'getCookies': [ + ('uid', int, []), + ('string', str, []), + ], + + 'setCookie': [ + ('uid', int, []), + ('name', str, []), + ('value', str, []), + ('expires', int, ['optional']), + ('path', str, ['optional']), + ], + }, + + # connect methods + 'connect': { + 'registerUsers': [ + ('accounts', json, []), + ], + + 'unregisterUsers': [ + ('email_hashes', json, []), + ], + + 'getUnconnectedFriendsCount': [ + ], + }, +} + +class Proxy(object): + """Represents a "namespace" of Facebook API calls.""" + + def __init__(self, client, name): + self._client = client + self._name = name + + def __call__(self, method=None, args=None, add_session_args=True): + # for Django templates + if method is None: + return self + + if add_session_args: + self._client._add_session_args(args) + + return self._client('%s.%s' % (self._name, method), args) + + +# generate the Facebook proxies +def __generate_proxies(): + for namespace in METHODS: + methods = {} + + for method in METHODS[namespace]: + params = ['self'] + body = ['args = {}'] + + for param_name, param_type, param_options in METHODS[namespace][method]: + param = param_name + + for option in param_options: + if isinstance(option, tuple) and option[0] == 'default': + if param_type == list: + param = '%s=None' % param_name + body.append('if %s is None: %s = %s' % (param_name, param_name, repr(option[1]))) + else: + param = '%s=%s' % (param_name, repr(option[1])) + + if param_type == json: + # we only jsonify the argument if it's a list or a dict, for compatibility + body.append('if isinstance(%s, list) or isinstance(%s, dict): %s = simplejson.dumps(%s)' % ((param_name,) * 4)) + + if 'optional' in param_options: + param = '%s=None' % param_name + body.append('if %s is not None: args[\'%s\'] = %s' % (param_name, param_name, param_name)) + else: + body.append('args[\'%s\'] = %s' % (param_name, param_name)) + + params.append(param) + + # simple docstring to refer them to Facebook API docs + body.insert(0, '"""Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=%s.%s"""' % (namespace, method)) + + body.insert(0, 'def %s(%s):' % (method, ', '.join(params))) + + body.append('return self(\'%s\', args)' % method) + + exec('\n '.join(body)) + + methods[method] = eval(method) + + proxy = type('%sProxy' % namespace.title(), (Proxy, ), methods) + + globals()[proxy.__name__] = proxy + + +__generate_proxies() + + +class FacebookError(Exception): + """Exception class for errors received from Facebook.""" + + def __init__(self, code, msg, args=None): + self.code = code + self.msg = msg + self.args = args + + def __str__(self): + return 'Error %s: %s' % (self.code, self.msg) + + +class AuthProxy(Proxy): + """Special proxy for facebook.auth.""" + + def getSession(self): + """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=auth.getSession""" + args = {} + try: + args['auth_token'] = self._client.auth_token + except AttributeError: + raise RuntimeError('Client does not have auth_token set.') + result = self._client('%s.getSession' % self._name, args) + self._client.session_key = result['session_key'] + self._client.uid = result['uid'] + self._client.secret = result.get('secret') + self._client.session_key_expires = result['expires'] + return result + + def createToken(self): + """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=auth.createToken""" + token = self._client('%s.createToken' % self._name) + self._client.auth_token = token + return token + + +class FriendsProxy(FriendsProxy): + """Special proxy for facebook.friends.""" + + def get(self, **kwargs): + """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=friends.get""" + if not kwargs.get('flid') and self._client._friends: + return self._client._friends + return super(FriendsProxy, self).get(**kwargs) + + +class PhotosProxy(PhotosProxy): + """Special proxy for facebook.photos.""" + + def upload(self, image, aid=None, caption=None, size=(604, 1024), filename=None): + """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=photos.upload + + size -- an optional size (width, height) to resize the image to before uploading. Resizes by default + to Facebook's maximum display width of 604. + """ + args = {} + + if aid is not None: + args['aid'] = aid + + if caption is not None: + args['caption'] = caption + + args = self._client._build_post_args('facebook.photos.upload', self._client._add_session_args(args)) + + try: + import cStringIO as StringIO + except ImportError: + import StringIO + + # check for a filename specified...if the user is passing binary data in + # image then a filename will be specified + if filename is None: + try: + import Image + except ImportError: + data = StringIO.StringIO(open(image, 'rb').read()) + else: + img = Image.open(image) + if size: + img.thumbnail(size, Image.ANTIALIAS) + data = StringIO.StringIO() + img.save(data, img.format) + else: + # there was a filename specified, which indicates that image was not + # the path to an image file but rather the binary data of a file + data = StringIO.StringIO(image) + image = filename + + content_type, body = self.__encode_multipart_formdata(list(args.iteritems()), [(image, data)]) + urlinfo = urlparse.urlsplit(self._client.facebook_url) + try: + h = httplib.HTTP(urlinfo[1]) + h.putrequest('POST', urlinfo[2]) + h.putheader('Content-Type', content_type) + h.putheader('Content-Length', str(len(body))) + h.putheader('MIME-Version', '1.0') + h.putheader('User-Agent', 'PyFacebook Client Library') + h.endheaders() + h.send(body) + + reply = h.getreply() + + if reply[0] != 200: + raise Exception('Error uploading photo: Facebook returned HTTP %s (%s)' % (reply[0], reply[1])) + + response = h.file.read() + except: + # sending the photo failed, perhaps we are using GAE + try: + from google.appengine.api import urlfetch + + try: + response = urlread(url=self._client.facebook_url,data=body,headers={'POST':urlinfo[2],'Content-Type':content_type,'MIME-Version':'1.0'}) + except urllib2.URLError: + raise Exception('Error uploading photo: Facebook returned %s' % (response)) + except ImportError: + # could not import from google.appengine.api, so we are not running in GAE + raise Exception('Error uploading photo.') + + return self._client._parse_response(response, 'facebook.photos.upload') + + + def __encode_multipart_formdata(self, fields, files): + """Encodes a multipart/form-data message to upload an image.""" + boundary = '-------tHISiStheMulTIFoRMbOUNDaRY' + crlf = '\r\n' + l = [] + + for (key, value) in fields: + l.append('--' + boundary) + l.append('Content-Disposition: form-data; name="%s"' % str(key)) + l.append('') + l.append(str(value)) + for (filename, value) in files: + l.append('--' + boundary) + l.append('Content-Disposition: form-data; filename="%s"' % (str(filename), )) + l.append('Content-Type: %s' % self.__get_content_type(filename)) + l.append('') + l.append(value.getvalue()) + l.append('--' + boundary + '--') + l.append('') + body = crlf.join(l) + content_type = 'multipart/form-data; boundary=%s' % boundary + return content_type, body + + + def __get_content_type(self, filename): + """Returns a guess at the MIME type of the file from the filename.""" + return str(mimetypes.guess_type(filename)[0]) or 'application/octet-stream' + + +class Facebook(object): + """ + Provides access to the Facebook API. + + Instance Variables: + + added + True if the user has added this application. + + api_key + Your API key, as set in the constructor. + + app_name + Your application's name, i.e. the APP_NAME in http://apps.facebook.com/APP_NAME/ if + this is for an internal web application. Optional, but useful for automatic redirects + to canvas pages. + + auth_token + The auth token that Facebook gives you, either with facebook.auth.createToken, + or through a GET parameter. + + callback_path + The path of the callback set in the Facebook app settings. If your callback is set + to http://www.example.com/facebook/callback/, this should be '/facebook/callback/'. + Optional, but useful for automatic redirects back to the same page after login. + + desktop + True if this is a desktop app, False otherwise. Used for determining how to + authenticate. + + facebook_url + The url to use for Facebook requests. + + facebook_secure_url + The url to use for secure Facebook requests. + + in_canvas + True if the current request is for a canvas page. + + internal + True if this Facebook object is for an internal application (one that can be added on Facebook) + + page_id + Set to the page_id of the current page (if any) + + secret + Secret that is used after getSession for desktop apps. + + secret_key + Your application's secret key, as set in the constructor. + + session_key + The current session key. Set automatically by auth.getSession, but can be set + manually for doing infinite sessions. + + session_key_expires + The UNIX time of when this session key expires, or 0 if it never expires. + + uid + After a session is created, you can get the user's UID with this variable. Set + automatically by auth.getSession. + + ---------------------------------------------------------------------- + + """ + + def __init__(self, api_key, secret_key, auth_token=None, app_name=None, callback_path=None, internal=None, proxy=None, facebook_url=None, facebook_secure_url=None): + """ + Initializes a new Facebook object which provides wrappers for the Facebook API. + + If this is a desktop application, the next couple of steps you might want to take are: + + facebook.auth.createToken() # create an auth token + facebook.login() # show a browser window + wait_login() # somehow wait for the user to log in + facebook.auth.getSession() # get a session key + + For web apps, if you are passed an auth_token from Facebook, pass that in as a named parameter. + Then call: + + facebook.auth.getSession() + + """ + self.api_key = api_key + self.secret_key = secret_key + self.session_key = None + self.session_key_expires = None + self.auth_token = auth_token + self.secret = None + self.uid = None + self.page_id = None + self.in_canvas = False + self.added = False + self.app_name = app_name + self.callback_path = callback_path + self.internal = internal + self._friends = None + self.proxy = proxy + if facebook_url is None: + self.facebook_url = FACEBOOK_URL + else: + self.facebook_url = facebook_url + if facebook_secure_url is None: + self.facebook_secure_url = FACEBOOK_SECURE_URL + else: + self.facebook_secure_url = facebook_secure_url + + for namespace in METHODS: + self.__dict__[namespace] = eval('%sProxy(self, \'%s\')' % (namespace.title(), 'facebook.%s' % namespace)) + + self.auth = AuthProxy(self, 'facebook.auth') + + + def _hash_args(self, args, secret=None): + """Hashes arguments by joining key=value pairs, appending a secret, and then taking the MD5 hex digest.""" + # @author: houyr + # fix for UnicodeEncodeError + hasher = md5.new(''.join(['%s=%s' % (isinstance(x, unicode) and x.encode("utf-8") or x, isinstance(args[x], unicode) and args[x].encode("utf-8") or args[x]) for x in sorted(args.keys())])) + if secret: + hasher.update(secret) + elif self.secret: + hasher.update(self.secret) + else: + hasher.update(self.secret_key) + return hasher.hexdigest() + + + def _parse_response_item(self, node): + """Parses an XML response node from Facebook.""" + if node.nodeType == node.DOCUMENT_NODE and \ + node.childNodes[0].hasAttributes() and \ + node.childNodes[0].hasAttribute('list') and \ + node.childNodes[0].getAttribute('list') == "true": + return {node.childNodes[0].nodeName: self._parse_response_list(node.childNodes[0])} + elif node.nodeType == node.ELEMENT_NODE and \ + node.hasAttributes() and \ + node.hasAttribute('list') and \ + node.getAttribute('list')=="true": + return self._parse_response_list(node) + elif len(filter(lambda x: x.nodeType == x.ELEMENT_NODE, node.childNodes)) > 0: + return self._parse_response_dict(node) + else: + return ''.join(node.data for node in node.childNodes if node.nodeType == node.TEXT_NODE) + + + def _parse_response_dict(self, node): + """Parses an XML dictionary response node from Facebook.""" + result = {} + for item in filter(lambda x: x.nodeType == x.ELEMENT_NODE, node.childNodes): + result[item.nodeName] = self._parse_response_item(item) + if node.nodeType == node.ELEMENT_NODE and node.hasAttributes(): + if node.hasAttribute('id'): + result['id'] = node.getAttribute('id') + return result + + + def _parse_response_list(self, node): + """Parses an XML list response node from Facebook.""" + result = [] + for item in filter(lambda x: x.nodeType == x.ELEMENT_NODE, node.childNodes): + result.append(self._parse_response_item(item)) + return result + + + def _check_error(self, response): + """Checks if the given Facebook response is an error, and then raises the appropriate exception.""" + if type(response) is dict and response.has_key('error_code'): + raise FacebookError(response['error_code'], response['error_msg'], response['request_args']) + + + def _build_post_args(self, method, args=None): + """Adds to args parameters that are necessary for every call to the API.""" + if args is None: + args = {} + + for arg in args.items(): + if type(arg[1]) == list: + args[arg[0]] = ','.join(str(a) for a in arg[1]) + elif type(arg[1]) == unicode: + args[arg[0]] = arg[1].encode("UTF-8") + elif type(arg[1]) == bool: + args[arg[0]] = str(arg[1]).lower() + + args['method'] = method + args['api_key'] = self.api_key + args['v'] = '1.0' + args['format'] = RESPONSE_FORMAT + args['sig'] = self._hash_args(args) + + return args + + + def _add_session_args(self, args=None): + """Adds 'session_key' and 'call_id' to args, which are used for API calls that need sessions.""" + if args is None: + args = {} + + if not self.session_key: + return args + #some calls don't need a session anymore. this might be better done in the markup + #raise RuntimeError('Session key not set. Make sure auth.getSession has been called.') + + args['session_key'] = self.session_key + args['call_id'] = str(int(time.time() * 1000)) + + return args + + + def _parse_response(self, response, method, format=None): + """Parses the response according to the given (optional) format, which should be either 'JSON' or 'XML'.""" + if not format: + format = RESPONSE_FORMAT + + if format == 'JSON': + result = simplejson.loads(response) + + self._check_error(result) + elif format == 'XML': + dom = minidom.parseString(response) + result = self._parse_response_item(dom) + dom.unlink() + + if 'error_response' in result: + self._check_error(result['error_response']) + + result = result[method[9:].replace('.', '_') + '_response'] + else: + raise RuntimeError('Invalid format specified.') + + return result + + + def hash_email(self, email): + """ + Hash an email address in a format suitable for Facebook Connect. + + """ + email = email.lower().strip() + return "%s_%s" % ( + struct.unpack("I", struct.pack("i", binascii.crc32(email)))[0], + hashlib.md5(email).hexdigest(), + ) + + + def unicode_urlencode(self, params): + """ + @author: houyr + A unicode aware version of urllib.urlencode. + """ + if isinstance(params, dict): + params = params.items() + return urllib.urlencode([(k, isinstance(v, unicode) and v.encode('utf-8') or v) + for k, v in params]) + + + def __call__(self, method=None, args=None, secure=False): + """Make a call to Facebook's REST server.""" + # for Django templates, if this object is called without any arguments + # return the object itself + if method is None: + return self + + # @author: houyr + # fix for bug of UnicodeEncodeError + post_data = self.unicode_urlencode(self._build_post_args(method, args)) + print post_data + + if self.proxy: + proxy_handler = urllib2.ProxyHandler(self.proxy) + opener = urllib2.build_opener(proxy_handler) + if secure: + response = opener.open(self.facebook_secure_url, post_data).read() + else: + response = opener.open(self.facebook_url, post_data).read() + else: + if secure: + response = urlread(self.facebook_secure_url, post_data) + else: + response = urlread(self.facebook_url, post_data) + + print response + return self._parse_response(response, method) + + + # URL helpers + def get_url(self, page, **args): + """ + Returns one of the Facebook URLs (www.facebook.com/SOMEPAGE.php). + Named arguments are passed as GET query string parameters. + + """ + print 'page, args:', page, args + return 'http://www.facebook.com/%s.php?%s' % (page, urllib.urlencode(args)) + + + def get_app_url(self, path=''): + """ + Returns the URL for this app's canvas page, according to app_name. + + """ + return 'http://apps.facebook.com/%s/%s' % (self.app_name, path) + + + def get_add_url(self, next=None): + """ + Returns the URL that the user should be redirected to in order to add the application. + + """ + args = {'api_key': self.api_key, 'v': '1.0'} + + if next is not None: + args['next'] = next + + return self.get_url('install', **args) + + + def get_authorize_url(self, next=None, next_cancel=None): + """ + Returns the URL that the user should be redirected to in order to + authorize certain actions for application. + + """ + args = {'api_key': self.api_key, 'v': '1.0'} + + if next is not None: + args['next'] = next + + if next_cancel is not None: + args['next_cancel'] = next_cancel + + return self.get_url('authorize', **args) + + + def get_login_url(self, next=None, popup=False, canvas=False): + """ + Returns the URL that the user should be redirected to in order to login. + + next -- the URL that Facebook should redirect to after login + + """ + args = {'api_key': self.api_key, 'v': '1.0'} + + if next is not None: + args['next'] = next + + if canvas is True: + args['canvas'] = 1 + + if popup is True: + args['popup'] = 1 + + if self.auth_token is not None: + args['auth_token'] = self.auth_token + + return self.get_url('login', **args) + + + def login(self, popup=False): + """Open a web browser telling the user to login to Facebook.""" + import webbrowser + webbrowser.open(self.get_login_url(popup=popup)) + + + def get_ext_perm_url(self, ext_perm, next=None, popup=False): + """ + Returns the URL that the user should be redirected to in order to grant an extended permission. + + ext_perm -- the name of the extended permission to request + next -- the URL that Facebook should redirect to after login + + """ + args = {'ext_perm': ext_perm, 'api_key': self.api_key, 'v': '1.0'} + + if next is not None: + args['next'] = next + + if popup is True: + args['popup'] = 1 + + return self.get_url('authorize', **args) + + + def request_extended_permission(self, ext_perm, popup=False): + """Open a web browser telling the user to grant an extended permission.""" + import webbrowser + webbrowser.open(self.get_ext_perm_url(ext_perm, popup=popup)) + + + def check_session(self, request): + """ + Checks the given Django HttpRequest for Facebook parameters such as + POST variables or an auth token. If the session is valid, returns True + and this object can now be used to access the Facebook API. Otherwise, + it returns False, and the application should take the appropriate action + (either log the user in or have him add the application). + + """ + self.in_canvas = (request.POST.get('fb_sig_in_canvas') == '1') + + if self.session_key and (self.uid or self.page_id): + return True + + if request.method == 'POST': + params = self.validate_signature(request.POST) + else: + if 'installed' in request.GET: + self.added = True + + if 'fb_page_id' in request.GET: + self.page_id = request.GET['fb_page_id'] + + if 'auth_token' in request.GET: + self.auth_token = request.GET['auth_token'] + + try: + self.auth.getSession() + except FacebookError, e: + self.auth_token = None + return False + + return True + + params = self.validate_signature(request.GET) + + if not params: + # first check if we are in django - to check cookies + if hasattr(request, 'COOKIES'): + params = self.validate_cookie_signature(request.COOKIES) + else: + # if not, then we might be on GoogleAppEngine, check their request object cookies + if hasattr(request,'cookies'): + params = self.validate_cookie_signature(request.cookies) + + if not params: + return False + + if params.get('in_canvas') == '1': + self.in_canvas = True + + if params.get('added') == '1': + self.added = True + + if params.get('expires'): + self.session_key_expires = int(params['expires']) + + if 'friends' in params: + if params['friends']: + self._friends = params['friends'].split(',') + else: + self._friends = [] + + if 'session_key' in params: + self.session_key = params['session_key'] + if 'user' in params: + self.uid = params['user'] + elif 'page_id' in params: + self.page_id = params['page_id'] + else: + return False + elif 'profile_session_key' in params: + self.session_key = params['profile_session_key'] + if 'profile_user' in params: + self.uid = params['profile_user'] + else: + return False + else: + return False + + return True + + + def validate_signature(self, post, prefix='fb_sig', timeout=None): + """ + Validate parameters passed to an internal Facebook app from Facebook. + + """ + args = post.copy() + + if prefix not in args: + return None + + del args[prefix] + + if timeout and '%s_time' % prefix in post and time.time() - float(post['%s_time' % prefix]) > timeout: + return None + + args = dict([(key[len(prefix + '_'):], value) for key, value in args.items() if key.startswith(prefix)]) + + hash = self._hash_args(args) + + if hash == post[prefix]: + return args + else: + return None + + def validate_cookie_signature(self, cookies): + """ + Validate parameters passed by cookies, namely facebookconnect or js api. + """ + if not self.api_key in cookies.keys(): + return None + + sigkeys = [] + params = dict() + for k in sorted(cookies.keys()): + if k.startswith(self.api_key+"_"): + sigkeys.append(k) + params[k.replace(self.api_key+"_","")] = cookies[k] + + + vals = ''.join(['%s=%s' % (x.replace(self.api_key+"_",""), cookies[x]) for x in sigkeys]) + hasher = md5.new(vals) + + hasher.update(self.secret_key) + digest = hasher.hexdigest() + if digest == cookies[self.api_key]: + return params + else: + return False + + + + +if __name__ == '__main__': + # sample desktop application + + api_key = '' + secret_key = '' + + facebook = Facebook(api_key, secret_key) + + facebook.auth.createToken() + + # Show login window + # Set popup=True if you want login without navigational elements + facebook.login() + + # Login to the window, then press enter + print 'After logging in, press enter...' + raw_input() + + facebook.auth.getSession() + print 'Session Key: ', facebook.session_key + print 'Your UID: ', facebook.uid + + info = facebook.users.getInfo([facebook.uid], ['name', 'birthday', 'affiliations', 'sex'])[0] + + print 'Your Name: ', info['name'] + print 'Your Birthday: ', info['birthday'] + print 'Your Gender: ', info['sex'] + + friends = facebook.friends.get() + friends = facebook.users.getInfo(friends[0:5], ['name', 'birthday', 'relationship_status']) + + for friend in friends: + print friend['name'], 'has a birthday on', friend['birthday'], 'and is', friend['relationship_status'] + + arefriends = facebook.friends.areFriends([friends[0]['uid']], [friends[1]['uid']]) + + photos = facebook.photos.getAlbums(facebook.uid)