conduit r1829 - in trunk: . conduit/modules/FacebookModule/pyfacebook



Author: jstowers
Date: Fri Jan 23 00:40:21 2009
New Revision: 1829
URL: http://svn.gnome.org/viewvc/conduit?rev=1829&view=rev

Log:
2009-01-23  John Stowers  <john stowers gmail com>

	* conduit/modules/FacebookModule/pyfacebook/__init__.py:
	Update to latest upstream svn snapshot



Modified:
   trunk/ChangeLog
   trunk/conduit/modules/FacebookModule/pyfacebook/__init__.py

Modified: trunk/conduit/modules/FacebookModule/pyfacebook/__init__.py
==============================================================================
--- trunk/conduit/modules/FacebookModule/pyfacebook/__init__.py	(original)
+++ trunk/conduit/modules/FacebookModule/pyfacebook/__init__.py	Fri Jan 23 00:40:21 2009
@@ -12,14 +12,14 @@
 #     * 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 <organization> nor the
-#       names of its contributors may be used to endorse or promote products
-#       derived from this software without specific prior written permission.
+#     * 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 <copyright holder> ``AS IS'' AND ANY
+# 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 <copyright holder> BE LIABLE FOR ANY
+# 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
@@ -47,33 +47,45 @@
 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
-    RESPONSE_FORMAT = 'JSON'
+    import json as simplejson
 except ImportError:
     try:
-        from django.utils import simplejson
-        RESPONSE_FORMAT = 'JSON'
+        import simplejson
     except ImportError:
-        from xml.dom import minidom
-        RESPONSE_FORMAT = 'XML'
+        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):
+    def urlread(url, data=None, headers=None):
         if data is not None:
-            headers = {"Content-type": "application/x-www-form-urlencoded"}
+            if headers is None:
+                headers = {"Content-type": "application/x-www-form-urlencoded"}
             method = urlfetch.POST
         else:
-            headers = {}
+            if headers is None:
+                headers = {}
             method = urlfetch.GET
 
         result = urlfetch.fetch(url, method=method,
@@ -93,8 +105,6 @@
 
 VERSION = '0.1'
 
-# REST URLs
-# Change these to /bestserver.php to use the bestserver.
 FACEBOOK_URL = 'http://api.facebook.com/restserver.php'
 FACEBOOK_SECURE_URL = 'https://api.facebook.com/restserver.php'
 
@@ -102,6 +112,21 @@
 
 # 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': [
@@ -133,15 +158,11 @@
         ],
 
         'publishTemplatizedAction': [
-            # facebook expects title_data and body_data to be JSON
-            # simplejson.dumps({'place':'Florida'}) would do fine
-            # actor_id is now deprecated, use page_actor_id instead
-            ('actor_id', int, []),
-            ('page_actor_id', int, []),
             ('title_template', str, []),
-            ('title_data', str, ['optional']),
+            ('page_actor_id', int, ['optional']),
+            ('title_data', json, ['optional']),
             ('body_template', str, ['optional']),
-            ('body_data', str, ['optional']),
+            ('body_data', json, ['optional']),
             ('body_general', str, ['optional']),
             ('image_1', str, ['optional']),
             ('image_1_link', str, ['optional']),
@@ -153,6 +174,30 @@
             ('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
@@ -169,7 +214,11 @@
             ('uids2', list, []),
         ],
 
-        'get': [],
+        'get': [
+            ('flid', int, ['optional']),
+        ],
+
+        'getLists': [],
 
         'getAppUsers': [],
     },
@@ -182,6 +231,7 @@
             ('to_ids', list, []),
             ('notification', str, []),
             ('email', str, ['optional']),
+            ('type', str, ['optional']),
         ],
 
         'sendRequest': [
@@ -208,10 +258,32 @@
             ('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, []),
         ],
     },
 
@@ -222,17 +294,25 @@
             ('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']),
         ],
     },
 
@@ -249,6 +329,10 @@
         'getMembers': [
             ('eid', int, []),
         ],
+
+        'create': [
+            ('event_info', json, []),
+        ],
     },
 
     # update methods
@@ -371,6 +455,20 @@
         ],
     },
 
+    # SMS Methods
+    'sms' : {
+        'canSend' : [
+            ('uid', int, []),
+        ],
+
+        'send' : [
+            ('uid', int, []),
+            ('message', str, []),
+            ('session_id', int, []),
+            ('req_session', bool, []),
+        ],
+    },
+
     'data': {
         'getCookies': [
             ('uid', int, []),
@@ -385,8 +483,21 @@
             ('path', str, ['optional']),
         ],
     },
-}
 
+    # connect methods
+    'connect': {
+        'registerUsers': [
+            ('accounts', json, []),
+        ],
+
+        'unregisterUsers': [
+            ('email_hashes', json, []),
+        ],
+
+        'getUnconnectedFriendsCount': [
+        ],
+    },
+}
 
 class Proxy(object):
     """Represents a "namespace" of Facebook API calls."""
@@ -395,7 +506,11 @@
         self._client = client
         self._name = name
 
-    def __call__(self, method, args=None, add_session_args=True):
+    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)
 
@@ -422,6 +537,10 @@
                         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))
@@ -488,17 +607,17 @@
 class FriendsProxy(FriendsProxy):
     """Special proxy for facebook.friends."""
 
-    def get(self):
+    def get(self, **kwargs):
         """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=friends.get""";
-        if self._client._friends:
+        if not kwargs.get('flid') and self._client._friends:
             return self._client._friends
-        return super(FriendsProxy, self).get()
+        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)):
+    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
@@ -519,33 +638,55 @@
         except ImportError:
             import StringIO
 
-        try:
-            import Image
-        except ImportError:
-            data = StringIO.StringIO(open(image, 'rb').read())
+        # 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:
-            img = Image.open(image)
-            if size:
-                img.thumbnail(size, Image.ANTIALIAS)
-            data = StringIO.StringIO()
-            img.save(data, img.format)
+            # 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)])
-        h = httplib.HTTP('api.facebook.com')
-        h.putrequest('POST', '/restserver.php')
-        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]))
+        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
 
-        response = h.file.read()
+                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')
 
@@ -609,6 +750,12 @@
         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.
 
@@ -639,7 +786,7 @@
 
     """
 
-    def __init__(self, api_key, secret_key, auth_token=None, app_name=None, callback_path=None, internal=None):
+    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.
 
@@ -670,6 +817,15 @@
         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))
@@ -679,7 +835,9 @@
 
     def _hash_args(self, args, secret=None):
         """Hashes arguments by joining key=value pairs, appending a secret, and then taking the MD5 hex digest."""
-        hasher = md5.new(''.join(['%s=%s' % (x, args[x]) for x in sorted(args.keys())]))
+        # @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:
@@ -742,6 +900,8 @@
                 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
@@ -758,7 +918,9 @@
             args = {}
 
         if not self.session_key:
-            raise RuntimeError('Session key not set. Make sure auth.getSession has been called.')
+            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))
@@ -790,15 +952,52 @@
         return result
 
 
-    def __call__(self, method, args=None, secure=False):
-        """Make a call to Facebook's REST server."""
+    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(),
+        )
 
-        post_data = urllib.urlencode(self._build_post_args(method, args))
 
-        if secure:
-            response = urlread(FACEBOOK_SECURE_URL, post_data)
+    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))
+
+        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:
-            response = urlread(FACEBOOK_URL, post_data)
+            if secure:
+                response = urlread(self.facebook_secure_url, post_data)
+            else:
+                response = urlread(self.facebook_url, post_data)
 
         return self._parse_response(response, method)
 
@@ -836,7 +1035,8 @@
 
     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.
+        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'}
@@ -880,6 +1080,31 @@
         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
@@ -917,6 +1142,15 @@
             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':
@@ -942,6 +1176,12 @@
                 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
 
@@ -972,6 +1212,33 @@
         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



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]