├── .gitignore ├── .project ├── .pydevproject ├── MANIFEST.in ├── README ├── bin └── djangofb.py ├── examples └── fbsample │ ├── .project │ ├── .pydevproject │ ├── __init__.py │ ├── db.sqlite3 │ ├── fbapp │ ├── __init__.py │ ├── models.py │ ├── templates │ │ └── canvas.fbml │ ├── urls.py │ └── views.py │ ├── manage.py │ ├── run.bat │ ├── settings.py │ └── urls.py ├── facebook ├── __init__.py ├── djangofb │ ├── __init__.py │ ├── context_processors.py │ ├── default_app │ │ ├── __init__.py │ │ ├── models.py │ │ ├── templates │ │ │ └── canvas.fbml │ │ ├── urls.py │ │ └── views.py │ └── models.py ├── webappfb.py └── wsgi.py ├── setup.py └── tests ├── __init__.py └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py? 2 | *.egg-info 3 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | pyfacebook 4 | 5 | 6 | 7 | 8 | 9 | org.python.pydev.PyDevBuilder 10 | 11 | 12 | 13 | 14 | 15 | org.python.pydev.pythonNature 16 | 17 | 18 | -------------------------------------------------------------------------------- /.pydevproject: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | python 2.6 6 | Default 7 | 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include examples * 2 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | == PyFacebook == 2 | 3 | PyFacebook is a Python client library for the Facebook API. 4 | 5 | Samuel Cormier-Iijima (sciyoshi@gmail.com) 6 | Niran Babalola (iamniran@gmail.com) 7 | Shannon -jj Behrens (jjinux@gmail.com) 8 | David B. Edelstein (David.B.Edelstein@gmail.com) 9 | Max Battcher (max.battcher@gmail.com) 10 | Rohan Deshpande (rohan.deshpande@gmail.com) 11 | Matthew Stevens (matthew.stevens@gmail.com) 12 | Sandro Turriate (sandro.turriate@gmail.com) 13 | Benjamin Zimmerman (benjamin.zimmerman@gmail.com) 14 | Gisle Aas (gisle.aas@gmail.com) 15 | Rand Bradley (rand.bradley@gmail.com) 16 | Luke Worth (luke.worth@gmail.com) 17 | Andreas Cederström (andreas@klydd.se) 18 | Samuel Hoffstaetter (samuel@hoffstaetter.com) 19 | Andreas Ehn (ehn@a8n.se) 20 | Lee Li (shuge.lee@gmail.com) 21 | 22 | http://github.com/sciyoshi/pyfacebook/ 23 | 24 | == Usage == 25 | 26 | To use this in your own projects, do the standard: 27 | 28 | python setup.py install 29 | 30 | 31 | == Documentation == 32 | 33 | Have a look at the examples/ directory. Most of the stuff should be 34 | self-explanatory. There is also an example Django app in 35 | examples/facebook, which should have enough to get you started. 36 | 37 | See the developer wiki for more information. 38 | 39 | 40 | == License == 41 | 42 | Copyright (c) 2008, Samuel Cormier-Iijima 43 | All rights reserved. 44 | 45 | Redistribution and use in source and binary forms, with or without 46 | modification, are permitted provided that the following conditions are met: 47 | * Redistributions of source code must retain the above copyright 48 | notice, this list of conditions and the following disclaimer. 49 | * Redistributions in binary form must reproduce the above copyright 50 | notice, this list of conditions and the following disclaimer in the 51 | documentation and/or other materials provided with the distribution. 52 | * Neither the name of the author nor the names of its contributors may 53 | be used to endorse or promote products derived from this software 54 | without specific prior written permission. 55 | 56 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS``AS IS'' AND ANY 57 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 58 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 59 | DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY 60 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 61 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 62 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 63 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 64 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 65 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 66 | 67 | -------------------------------------------------------------------------------- /bin/djangofb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | if __name__ == '__main__': 4 | import sys 5 | import os 6 | import re 7 | 8 | def usage(): 9 | sys.stderr.write('Usage: djangofb.py startapp \n') 10 | sys.exit(1) 11 | 12 | if len(sys.argv) not in (2, 3): 13 | usage() 14 | 15 | if sys.argv[1] != 'startapp': 16 | usage() 17 | 18 | app_name = len(sys.argv) == 3 and sys.argv[2] or 'fbapp' 19 | 20 | try: 21 | sys.path.insert(0, os.getcwd()) 22 | # Assumed to be in the same directory or current directory. 23 | import settings 24 | except ImportError: 25 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r " 26 | "or in the current directory. It appears you've customized things.\n" 27 | "You'll have to run django-admin.py, passing it your settings module.\n" 28 | "(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" 29 | % __file__) 30 | sys.exit(1) 31 | 32 | from django.core import management 33 | 34 | directory = management.setup_environ(settings) 35 | 36 | if hasattr(management, 'color'): 37 | # Current svn version of django 38 | from django.core.management.color import color_style 39 | style = color_style() 40 | else: 41 | # Compatibility with 0.96 42 | from django.core.management import style 43 | 44 | project_dir = os.path.normpath(os.path.join(directory, '..')) 45 | parent_dir = os.path.basename(project_dir) 46 | project_name = os.path.basename(directory) 47 | if app_name == project_name: 48 | sys.stderr.write( 49 | style.ERROR( 50 | 'Error: You cannot create an app with the same name (%r) as your project.\n' % 51 | app_name)) 52 | sys.exit(1) 53 | if app_name == 'facebook': 54 | sys.stderr.write(style.ERROR( 55 | 'Error: You cannot name your app "facebook", ' 56 | 'since this can cause conflicts with imports in Python < 2.5.\n')) 57 | sys.exit(1) 58 | if not re.search(r'^\w+$', app_name): 59 | sys.stderr.write( 60 | style.ERROR( 61 | 'Error: %r is not a valid app name. Please use only numbers, letters and underscores.\n' % 62 | app_name)) 63 | sys.exit(1) 64 | 65 | top_dir = os.path.join(directory, app_name) 66 | try: 67 | os.mkdir(top_dir) 68 | except OSError as e: 69 | sys.stderr.write(style.ERROR("Error: %s\n" % e)) 70 | sys.exit(1) 71 | 72 | import facebook 73 | 74 | template_dir = os.path.join( 75 | facebook.__path__[0], 76 | 'djangofb', 77 | 'default_app') 78 | 79 | sys.stderr.write('Creating Facebook application %r...\n' % app_name) 80 | 81 | for d, subdirs, files in os.walk(template_dir): 82 | relative_dir = d[len(template_dir) + 1:] 83 | if relative_dir: 84 | os.mkdir(os.path.join(top_dir, relative_dir)) 85 | subdirs[:] = [s for s in subdirs if not s.startswith('.')] 86 | for f in files: 87 | if f.endswith('.pyc'): 88 | continue 89 | path_old = os.path.join(d, f) 90 | path_new = os.path.join(top_dir, relative_dir, f) 91 | f_old = open(path_old, 'r') 92 | f_new = open(path_new, 'w') 93 | sys.stderr.write('Writing %s...\n' % path_new) 94 | f_new.write( 95 | f_old.read().replace( 96 | '{{ project }}', 97 | project_name).replace( 98 | '{{ app }}', 99 | app_name)) 100 | f_new.close() 101 | f_old.close() 102 | 103 | sys.stderr.write('Done!\n\n') 104 | 105 | from django.conf import settings 106 | 107 | need_api_key = not hasattr(settings, 'FACEBOOK_API_KEY') 108 | need_middleware = 'facebook.djangofb.FacebookMiddleware' not in settings.MIDDLEWARE_CLASSES 109 | need_loader = 'django.template.loaders.app_directories.load_template_source' not in settings.TEMPLATE_LOADERS 110 | need_install_app = not '%s.%s' % ( 111 | project_name, app_name) in settings.INSTALLED_APPS 112 | 113 | if need_api_key or need_middleware or need_loader or need_install_app: 114 | sys.stderr.write( 115 | """There are a couple of things you NEED to do before you can use this app:\n\n""") 116 | if need_api_key: 117 | sys.stderr.write( 118 | """ * Set FACEBOOK_API_KEY and FACEBOOK_SECRET_KEY to the appropriate values in settings.py\n\n""") 119 | if need_middleware: 120 | sys.stderr.write( 121 | """ * Add 'facebook.djangofb.FacebookMiddleware' to your MIDDLEWARE_CLASSES in settings.py\n\n""") 122 | if need_loader: 123 | sys.stderr.write( 124 | """ * Add 'django.template.loaders.app_directories.load_template_source' 125 | to your TEMPLATE_LOADERS in settings.py\n\n""") 126 | if need_install_app: 127 | sys.stderr.write( 128 | """ * Add '%s.%s' to your INSTALLED_APPS in settings.py\n\n""" % 129 | (project_name, app_name)) 130 | 131 | sys.stderr.write( 132 | """The final step is to add (r'^%s/', include('%s.%s.urls')) to your urls.py, and then set your callback page in the application settings on Facebook to 'http://your.domain.com/%s/'. 133 | 134 | Good luck! 135 | 136 | """ % 137 | (project_name, project_name, app_name, project_name)) 138 | -------------------------------------------------------------------------------- /examples/fbsample/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | fbsample 4 | 5 | 6 | 7 | 8 | 9 | org.python.pydev.PyDevBuilder 10 | 11 | 12 | 13 | 14 | 15 | org.python.pydev.pythonNature 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/fbsample/.pydevproject: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | python 2.6 6 | Default 7 | 8 | -------------------------------------------------------------------------------- /examples/fbsample/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sciyoshi/pyfacebook/ec2bbfb0c687f2775ae89e1c5201688ed6e69b8e/examples/fbsample/__init__.py -------------------------------------------------------------------------------- /examples/fbsample/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sciyoshi/pyfacebook/ec2bbfb0c687f2775ae89e1c5201688ed6e69b8e/examples/fbsample/db.sqlite3 -------------------------------------------------------------------------------- /examples/fbsample/fbapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sciyoshi/pyfacebook/ec2bbfb0c687f2775ae89e1c5201688ed6e69b8e/examples/fbsample/fbapp/__init__.py -------------------------------------------------------------------------------- /examples/fbsample/fbapp/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # get_facebook_client lets us get the current Facebook object 4 | # from outside of a view, which lets us have cleaner code 5 | from facebook.djangofb import get_facebook_client 6 | 7 | 8 | def _2int(d, k): 9 | try: 10 | d = d.__dict__ 11 | except: 12 | pass 13 | 14 | t = d.get(k, '') 15 | if t == 'None': 16 | t = 0 17 | else: 18 | t = int(t) 19 | return t 20 | 21 | 22 | class UserManager(models.Manager): 23 | """Custom manager for a Facebook User.""" 24 | 25 | def get_current(self): 26 | """Gets a User object for the logged-in Facebook user.""" 27 | facebook = get_facebook_client() 28 | user, created = self.get_or_create(id=_2int(facebook, 'uid')) 29 | if created: 30 | # we could do some custom actions for new users here... 31 | pass 32 | return user 33 | 34 | 35 | class User(models.Model): 36 | """A simple User model for Facebook users.""" 37 | 38 | # We use the user's UID as the primary key in our database. 39 | id = models.IntegerField(primary_key=True) 40 | 41 | # TODO: The data that you want to store for each user would go here. 42 | # For this sample, we let users let people know their favorite progamming 43 | # language, in the spirit of Extended Info. 44 | language = models.CharField(max_length=64, default='Python') 45 | 46 | # Add the custom manager 47 | objects = UserManager() 48 | -------------------------------------------------------------------------------- /examples/fbsample/fbapp/templates/canvas.fbml: -------------------------------------------------------------------------------- 1 | 2 | {% comment %} 3 | We can use {{ fbuser }} to get at the current user. 4 | {{ fbuser.id }} will be the user's UID, and {{ fbuser.language }} 5 | is his/her favorite language (Python :-). 6 | {% endcomment %} 7 | Welcome, ! 8 | 9 | 10 |
11 | Your favorite language is {{ fbuser.language|escape }}. 12 |

13 | 14 |
15 |

16 | 17 |
18 | 19 | 20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /examples/fbsample/fbapp/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | urlpatterns = patterns('fbsample.fbapp.views', 4 | (r'^$', 'canvas'), 5 | # Define other pages you want to create here 6 | ) 7 | -------------------------------------------------------------------------------- /examples/fbsample/fbapp/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.views.generic.simple import direct_to_template 3 | # uncomment the following two lines and the one below 4 | # if you don't want to use a decorator instead of the middleware 5 | # from django.utils.decorators import decorator_from_middleware 6 | # from facebook.djangofb import FacebookMiddleware 7 | 8 | # Import the Django helpers 9 | import facebook.djangofb as facebook 10 | 11 | # The User model defined in models.py 12 | from models import User 13 | 14 | # We'll require login for our canvas page. This 15 | # isn't necessarily a good idea, as we might want 16 | # to let users see the page without granting our app 17 | # access to their info. See the wiki for details on how 18 | # to do this. 19 | # @decorator_from_middleware(FacebookMiddleware) 20 | 21 | 22 | @facebook.require_login() 23 | def canvas(request): 24 | # Get the User object for the currently logged in user 25 | user = User.objects.get_current() 26 | 27 | # Check if we were POSTed the user's new language of choice 28 | if 'language' in request.POST: 29 | user.language = request.POST['language'][:64] 30 | user.save() 31 | 32 | # User is guaranteed to be logged in, so pass canvas.fbml 33 | # an extra 'fbuser' parameter that is the User object for 34 | # the currently logged in user. 35 | return direct_to_template( 36 | request, 37 | 'canvas.fbml', 38 | extra_context={ 39 | 'fbuser': user}) 40 | 41 | 42 | @facebook.require_login() 43 | def ajax(request): 44 | return HttpResponse('hello world') 45 | -------------------------------------------------------------------------------- /examples/fbsample/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | try: 4 | import settings # Assumed to be in the same directory. 5 | except ImportError: 6 | import sys 7 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. " 8 | "It appears you've customized things.\nYou'll have to run django-admin.py, " 9 | "passing it your settings module.\n(If the file settings.py does indeed exist, " 10 | "it's causing an ImportError somehow.)\n" % __file__) 11 | sys.exit(1) 12 | 13 | if __name__ == "__main__": 14 | execute_manager(settings) 15 | -------------------------------------------------------------------------------- /examples/fbsample/run.bat: -------------------------------------------------------------------------------- 1 | python manage.py runserver 0.0.0.0:80 -------------------------------------------------------------------------------- /examples/fbsample/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for fbsample project. 2 | 3 | DEBUG = True 4 | TEMPLATE_DEBUG = DEBUG 5 | 6 | ADMINS = ( 7 | # ('Your Name', 'your_email@domain.com'), 8 | ) 9 | 10 | MANAGERS = ADMINS 11 | 12 | # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 13 | DATABASE_ENGINE = 'sqlite3' 14 | # Or path to database file if using sqlite3. 15 | DATABASE_NAME = 'db.sqlite3' 16 | DATABASE_USER = '' # Not used with sqlite3. 17 | DATABASE_PASSWORD = '' # Not used with sqlite3. 18 | # Set to empty string for localhost. Not used with sqlite3. 19 | DATABASE_HOST = '' 20 | # Set to empty string for default. Not used with sqlite3. 21 | DATABASE_PORT = '' 22 | 23 | # Local time zone for this installation. Choices can be found here: 24 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 25 | # although not all choices may be available on all operating systems. 26 | # If running in a Windows environment this must be set to the same as your 27 | # system time zone. 28 | TIME_ZONE = 'America/Chicago' 29 | 30 | # Language code for this installation. All choices can be found here: 31 | # http://www.i18nguy.com/unicode/language-identifiers.html 32 | LANGUAGE_CODE = 'en-us' 33 | 34 | SITE_ID = 1 35 | 36 | # If you set this to False, Django will make some optimizations so as not 37 | # to load the internationalization machinery. 38 | USE_I18N = True 39 | 40 | # Absolute path to the directory that holds media. 41 | # Example: "/home/media/media.lawrence.com/" 42 | MEDIA_ROOT = '' 43 | 44 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 45 | # trailing slash if there is a path component (optional in other cases). 46 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 47 | MEDIA_URL = '' 48 | 49 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 50 | # trailing slash. 51 | # Examples: "http://foo.com/media/", "/media/". 52 | ADMIN_MEDIA_PREFIX = '/media/' 53 | 54 | # Make this unique, and don't share it with anybody. 55 | SECRET_KEY = 'yg6zh@+u^w3agtjwy^da)#277d3j#a%3m@)pev8_j0ozztwe4+' 56 | 57 | # List of callables that know how to import templates from various sources. 58 | TEMPLATE_LOADERS = ( 59 | 'django.template.loaders.filesystem.load_template_source', 60 | 'django.template.loaders.app_directories.load_template_source', 61 | # 'django.template.loaders.eggs.load_template_source', 62 | ) 63 | 64 | MIDDLEWARE_CLASSES = ( 65 | 'django.middleware.common.CommonMiddleware', 66 | 'django.contrib.sessions.middleware.SessionMiddleware', 67 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 68 | 69 | 'facebook.djangofb.FacebookMiddleware', 70 | ) 71 | 72 | ROOT_URLCONF = 'fbsample.urls' 73 | 74 | TEMPLATE_DIRS = ( 75 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 76 | # Always use forward slashes, even on Windows. 77 | # Don't forget to use absolute paths, not relative paths. 78 | ) 79 | 80 | INSTALLED_APPS = ( 81 | 'django.contrib.auth', 82 | 'django.contrib.contenttypes', 83 | 'django.contrib.sessions', 84 | 'django.contrib.sites', 85 | 86 | 'fbsample.fbapp', 87 | ) 88 | 89 | # get it from here 90 | # http://www.facebook.com/editapps.php?ref=mb 91 | FACEBOOK_API_KEY = 'x' 92 | FACEBOOK_SECRET_KEY = 'xx' 93 | -------------------------------------------------------------------------------- /examples/fbsample/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | # Uncomment the next two lines to enable the admin: 4 | # from django.contrib import admin 5 | # admin.autodiscover() 6 | 7 | urlpatterns = patterns('', 8 | # Example: 9 | # (r'^fbsample/', include('fbsample.foo.urls')), 10 | 11 | # Uncomment the admin/doc line below and add 'django.contrib.admindocs' 12 | # to INSTALLED_APPS to enable admin documentation: 13 | # (r'^admin/doc/', include('django.contrib.admindocs.urls')), 14 | 15 | # Uncomment the next line to enable the admin: 16 | # (r'^admin/', include(admin.site.urls)), 17 | 18 | (r'^fbsample/', include('fbsample.fbapp.urls')), 19 | ) 20 | -------------------------------------------------------------------------------- /facebook/__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # 3 | # pyfacebook - Python bindings for the Facebook API 4 | # 5 | # Copyright (c) 2008, Samuel Cormier-Iijima 6 | # All rights reserved. 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions are met: 10 | # * Redistributions of source code must retain the above copyright 11 | # notice, this list of conditions and the following disclaimer. 12 | # * Redistributions in binary form must reproduce the above copyright 13 | # notice, this list of conditions and the following disclaimer in the 14 | # documentation and/or other materials provided with the distribution. 15 | # * Neither the name of the author nor the names of its contributors may 16 | # be used to endorse or promote products derived from this software 17 | # without specific prior written permission. 18 | # 19 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS``AS IS'' AND ANY 20 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | # DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY 23 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 26 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | """ 31 | Python bindings for the Facebook API (pyfacebook - http://code.google.com/p/pyfacebook) 32 | 33 | PyFacebook is a client library that wraps the Facebook API. 34 | 35 | For more information, see 36 | 37 | Home Page: http://code.google.com/p/pyfacebook 38 | Developer Wiki: http://wiki.developers.facebook.com/index.php/Python 39 | Facebook IRC Channel: #facebook on irc.freenode.net 40 | 41 | PyFacebook can use simplejson if it is installed, which 42 | is much faster than XML and also uses less bandwith. Go to 43 | http://undefined.org/python/#simplejson to download it, or do 44 | apt-get install python-simplejson on a Debian-like system. 45 | """ 46 | 47 | import sys 48 | import time 49 | import struct 50 | import urllib 51 | import urllib2 52 | import httplib 53 | import hmac 54 | try: 55 | import hashlib 56 | except ImportError: 57 | import md5 as hashlib 58 | import binascii 59 | import urlparse 60 | import mimetypes 61 | 62 | # try to use simplejson first, otherwise fallback to XML 63 | RESPONSE_FORMAT = 'JSON' 64 | try: 65 | import json as simplejson 66 | simplejson.loads 67 | except (ImportError, AttributeError): 68 | try: 69 | import simplejson 70 | simplejson.loads 71 | except (ImportError, AttributeError): 72 | try: 73 | from django.utils import simplejson 74 | simplejson.loads 75 | except (ImportError, AttributeError): 76 | try: 77 | import jsonlib as simplejson 78 | simplejson.loads 79 | except (ImportError, AttributeError): 80 | from xml.dom import minidom 81 | RESPONSE_FORMAT = 'XML' 82 | 83 | # support Google App Engine. GAE does not have a working urllib.urlopen. 84 | try: 85 | from google.appengine.api import urlfetch 86 | 87 | def urlread(url, data=None, headers=None): 88 | if data is not None: 89 | if headers is None: 90 | headers = {"Content-type": "application/x-www-form-urlencoded"} 91 | method = urlfetch.POST 92 | else: 93 | if headers is None: 94 | headers = {} 95 | method = urlfetch.GET 96 | 97 | result = urlfetch.fetch(url, method=method, 98 | payload=data, headers=headers) 99 | 100 | if result.status_code == 200: 101 | return result.content 102 | else: 103 | raise urllib2.URLError( 104 | "fetch error url=%s, code=%d" % 105 | (url, result.status_code)) 106 | 107 | except ImportError: 108 | def urlread(url, data=None): 109 | res = urllib2.urlopen(url, data=data) 110 | return res.read() 111 | 112 | __all__ = ['Facebook', 'create_hmac'] 113 | 114 | VERSION = '1.0a2' 115 | 116 | FACEBOOK_URL = 'http://api.facebook.com/restserver.php' 117 | FACEBOOK_VIDEO_URL = 'http://api-video.facebook.com/restserver.php' 118 | FACEBOOK_SECURE_URL = 'https://api.facebook.com/restserver.php' 119 | 120 | 121 | def create_hmac(key, tbhashed): 122 | return hmac.new(key, tbhashed, hashlib.sha1).hexdigest() 123 | 124 | 125 | class json(object): 126 | pass 127 | 128 | # simple IDL for the Facebook API 129 | METHODS = { 130 | # dashboard methods 131 | 'dashboard': { 132 | 'incrementCount': [ 133 | ('uid', int, ['optional']) 134 | ], 135 | 'decrementCount': [ 136 | ('uid', int, ['optional']) 137 | ], 138 | 'getCount': [ 139 | ('uid', int, ['optional']) 140 | ], 141 | 'setCount': [ 142 | ('count', int, []), 143 | ('uid', int, ['optional']) 144 | ], 145 | 'getActivity': [ 146 | ('activity_ids', list, ['optional']), 147 | ('uid', int, ['optional']) 148 | ], 149 | 'publishActivity': [ 150 | ('activity', json, []), 151 | 152 | ] 153 | 154 | }, 155 | 'application': { 156 | 'getPublicInfo': [ 157 | ('application_id', int, ['optional']), 158 | ('application_api_key', str, ['optional']), 159 | ('application_canvas_name', str, ['optional']), 160 | ], 161 | }, 162 | 163 | # admin methods 164 | 'admin': { 165 | 'getAllocation': [ 166 | ('integration_point_name', str, []), 167 | ], 168 | 'getRestrictionInfo': [], 169 | 'setRestrictionInfo': [ 170 | ('format', str, ['optional']), 171 | ('callback', str, ['optional']), 172 | ('restriction_str', json, ['optional']), 173 | ], 174 | # Some methods don't work with access_tokens, the signed option forces 175 | # use of the secret_key signature (avoids error 15 and, sometimes, 8) 176 | 'getAppProperties': [ 177 | ('properties', list, []), 178 | 'signed' 179 | ], 180 | 'setAppProperties': [ 181 | ('properties', json, []), 182 | 'signed' 183 | ], 184 | }, 185 | 186 | # auth methods 187 | 'auth': { 188 | 'revokeAuthorization': [ 189 | ('uid', int, ['optional']), 190 | ], 191 | 'revokeExtendedPermission': [ 192 | ('perm', str, []), 193 | ('uid', int, ['optional']), 194 | ], 195 | }, 196 | 197 | # feed methods 198 | 'feed': { 199 | 'publishStoryToUser': [ 200 | ('title', str, []), 201 | ('body', str, ['optional']), 202 | ('image_1', str, ['optional']), 203 | ('image_1_link', str, ['optional']), 204 | ('image_2', str, ['optional']), 205 | ('image_2_link', str, ['optional']), 206 | ('image_3', str, ['optional']), 207 | ('image_3_link', str, ['optional']), 208 | ('image_4', str, ['optional']), 209 | ('image_4_link', str, ['optional']), 210 | ('priority', int, ['optional']), 211 | ], 212 | 213 | 'publishActionOfUser': [ 214 | ('title', str, []), 215 | ('body', str, ['optional']), 216 | ('image_1', str, ['optional']), 217 | ('image_1_link', str, ['optional']), 218 | ('image_2', str, ['optional']), 219 | ('image_2_link', str, ['optional']), 220 | ('image_3', str, ['optional']), 221 | ('image_3_link', str, ['optional']), 222 | ('image_4', str, ['optional']), 223 | ('image_4_link', str, ['optional']), 224 | ('priority', int, ['optional']), 225 | ], 226 | 227 | 'publishTemplatizedAction': [ 228 | ('title_template', str, []), 229 | ('page_actor_id', int, ['optional']), 230 | ('title_data', json, ['optional']), 231 | ('body_template', str, ['optional']), 232 | ('body_data', json, ['optional']), 233 | ('body_general', str, ['optional']), 234 | ('image_1', str, ['optional']), 235 | ('image_1_link', str, ['optional']), 236 | ('image_2', str, ['optional']), 237 | ('image_2_link', str, ['optional']), 238 | ('image_3', str, ['optional']), 239 | ('image_3_link', str, ['optional']), 240 | ('image_4', str, ['optional']), 241 | ('image_4_link', str, ['optional']), 242 | ('target_ids', list, ['optional']), 243 | ], 244 | 245 | 'registerTemplateBundle': [ 246 | ('one_line_story_templates', json, []), 247 | ('short_story_templates', json, ['optional']), 248 | ('full_story_template', json, ['optional']), 249 | ('action_links', json, ['optional']), 250 | ], 251 | 252 | 'deactivateTemplateBundleByID': [ 253 | ('template_bundle_id', int, []), 254 | ], 255 | 256 | 'getRegisteredTemplateBundles': [], 257 | 258 | 'getRegisteredTemplateBundleByID': [ 259 | ('template_bundle_id', str, []), 260 | ], 261 | 262 | 'publishUserAction': [ 263 | ('template_bundle_id', int, []), 264 | ('template_data', json, ['optional']), 265 | ('target_ids', list, ['optional']), 266 | ('body_general', str, ['optional']), 267 | ('story_size', int, ['optional']), 268 | ], 269 | }, 270 | 271 | # fql methods 272 | 'fql': { 273 | 'query': [ 274 | ('query', str, []), 275 | ], 276 | 'multiquery': [ 277 | ('queries', json, []), 278 | ], 279 | }, 280 | 281 | # friends methods 282 | 'friends': { 283 | 'areFriends': [ 284 | ('uids1', list, []), 285 | ('uids2', list, []), 286 | ], 287 | 288 | 'get': [ 289 | ('flid', int, ['optional']), 290 | ], 291 | 292 | 'getLists': [], 293 | 294 | 'getAppUsers': [], 295 | 296 | 'getMutualFriends': [ 297 | ('target_uid', int, []), 298 | ('source_uid', int, ['optional']), 299 | ], 300 | }, 301 | 302 | # notifications methods 303 | 'notifications': { 304 | 'get': [], 305 | 306 | 'send': [ 307 | ('to_ids', list, []), 308 | ('notification', str, []), 309 | ('email', str, ['optional']), 310 | ('type', str, ['optional']), 311 | ], 312 | 313 | 'sendRequest': [ 314 | ('to_ids', list, []), 315 | ('type', str, []), 316 | ('content', str, []), 317 | ('image', str, []), 318 | ('invite', bool, []), 319 | ], 320 | 321 | 'sendEmail': [ 322 | ('recipients', list, []), 323 | ('subject', str, []), 324 | ('text', str, ['optional']), 325 | ('fbml', str, ['optional']), 326 | ] 327 | }, 328 | 329 | # profile methods 330 | 'profile': { 331 | 'setFBML': [ 332 | ('markup', str, ['optional']), 333 | ('uid', int, ['optional']), 334 | ('profile', str, ['optional']), 335 | ('profile_action', str, ['optional']), 336 | ('mobile_fbml', str, ['optional']), 337 | ('profile_main', str, ['optional']), 338 | ], 339 | 340 | 'getFBML': [ 341 | ('uid', int, ['optional']), 342 | ('type', int, ['optional']), 343 | ], 344 | 345 | 'setInfo': [ 346 | ('title', str, []), 347 | ('type', int, []), 348 | ('info_fields', json, []), 349 | ('uid', int, []), 350 | ], 351 | 352 | 'getInfo': [ 353 | ('uid', int, []), 354 | ], 355 | 356 | 'setInfoOptions': [ 357 | ('field', str, []), 358 | ('options', json, []), 359 | ], 360 | 361 | 'getInfoOptions': [ 362 | ('field', str, []), 363 | ], 364 | }, 365 | 366 | # users methods 367 | 'users': { 368 | 'getInfo': [ 369 | ('uids', list, []), 370 | ('fields', list, [('default', ['name'])]), 371 | ], 372 | 373 | 'getStandardInfo': [ 374 | ('uids', list, []), 375 | ('fields', list, [('default', ['uid'])]), 376 | ], 377 | 378 | 'getLoggedInUser': [], 379 | 380 | 'isAppAdded': [], 381 | 382 | 'isAppUser': [ 383 | ('uid', int, []), 384 | ], 385 | 386 | 'hasAppPermission': [ 387 | ('ext_perm', str, []), 388 | ('uid', int, ['optional']), 389 | ], 390 | 391 | 'setStatus': [ 392 | ('status', str, []), 393 | ('clear', bool, []), 394 | ('status_includes_verb', bool, ['optional']), 395 | ('uid', int, ['optional']), 396 | ], 397 | }, 398 | 399 | # events methods 400 | 'events': { 401 | 'cancel': [ 402 | ('eid', int, []), 403 | ('cancel_message', str, ['optional']), 404 | ], 405 | 406 | 'create': [ 407 | ('event_info', json, []), 408 | ], 409 | 410 | 'edit': [ 411 | ('eid', int, []), 412 | ('event_info', json, []), 413 | ], 414 | 415 | 'get': [ 416 | ('uid', int, ['optional']), 417 | ('eids', list, ['optional']), 418 | ('start_time', int, ['optional']), 419 | ('end_time', int, ['optional']), 420 | ('rsvp_status', str, ['optional']), 421 | ], 422 | 423 | 'getMembers': [ 424 | ('eid', int, []), 425 | ], 426 | 427 | 'invite': [ 428 | ('eid', int, []), 429 | ('uids', list, []), 430 | ('personal_message', str, ['optional']), 431 | ], 432 | 433 | 'rsvp': [ 434 | ('eid', int, []), 435 | ('rsvp_status', str, []), 436 | ], 437 | 438 | 'edit': [ 439 | ('eid', int, []), 440 | ('event_info', json, []), 441 | ], 442 | 443 | 'invite': [ 444 | ('eid', int, []), 445 | ('uids', list, []), 446 | ('personal_message', str, ['optional']), 447 | ], 448 | }, 449 | 450 | # update methods 451 | 'update': { 452 | 'decodeIDs': [ 453 | ('ids', list, []), 454 | ], 455 | }, 456 | 457 | # groups methods 458 | 'groups': { 459 | 'get': [ 460 | ('uid', int, ['optional']), 461 | ('gids', list, ['optional']), 462 | ], 463 | 464 | 'getMembers': [ 465 | ('gid', int, []), 466 | ], 467 | }, 468 | 469 | # marketplace methods 470 | 'marketplace': { 471 | 'createListing': [ 472 | ('listing_id', int, []), 473 | ('show_on_profile', bool, []), 474 | ('listing_attrs', str, []), 475 | ], 476 | 477 | 'getCategories': [], 478 | 479 | 'getListings': [ 480 | ('listing_ids', list, []), 481 | ('uids', list, []), 482 | ], 483 | 484 | 'getSubCategories': [ 485 | ('category', str, []), 486 | ], 487 | 488 | 'removeListing': [ 489 | ('listing_id', int, []), 490 | ('status', str, []), 491 | ], 492 | 493 | 'search': [ 494 | ('category', str, ['optional']), 495 | ('subcategory', str, ['optional']), 496 | ('query', str, ['optional']), 497 | ], 498 | }, 499 | 500 | # pages methods 501 | 'pages': { 502 | 'getInfo': [ 503 | ('fields', list, [('default', ['page_id', 'name'])]), 504 | ('page_ids', list, ['optional']), 505 | ('uid', int, ['optional']), 506 | ], 507 | 508 | 'isAdmin': [ 509 | ('page_id', int, []), 510 | ], 511 | 512 | 'isAppAdded': [ 513 | ('page_id', int, []), 514 | ], 515 | 516 | 'isFan': [ 517 | ('page_id', int, []), 518 | ('uid', int, []), 519 | ], 520 | }, 521 | 522 | # photos methods 523 | 'photos': { 524 | 'addTag': [ 525 | ('pid', int, []), 526 | ('tag_uid', int, [('default', 0)]), 527 | ('tag_text', str, [('default', '')]), 528 | ('x', float, [('default', 50)]), 529 | ('y', float, [('default', 50)]), 530 | ('tags', json, ['optional']), 531 | ], 532 | 533 | 'createAlbum': [ 534 | ('name', str, []), 535 | ('location', str, ['optional']), 536 | ('description', str, ['optional']), 537 | ], 538 | 539 | 'get': [ 540 | ('subj_id', int, ['optional']), 541 | ('aid', int, ['optional']), 542 | ('pids', list, ['optional']), 543 | ], 544 | 545 | 'getAlbums': [ 546 | ('uid', int, ['optional']), 547 | ('aids', list, ['optional']), 548 | ], 549 | 550 | 'getTags': [ 551 | ('pids', list, []), 552 | ], 553 | }, 554 | 555 | # videos methods 556 | 'video': { 557 | 'getUploadLimits': [ 558 | ], 559 | }, 560 | 561 | # status methods 562 | 'status': { 563 | 'get': [ 564 | ('uid', int, ['optional']), 565 | ('limit', int, ['optional']), 566 | ], 567 | 'set': [ 568 | ('status', str, ['optional']), 569 | ('uid', int, ['optional']), 570 | ], 571 | }, 572 | 573 | # fbml methods 574 | 'fbml': { 575 | 'refreshImgSrc': [ 576 | ('url', str, []), 577 | ], 578 | 579 | 'refreshRefUrl': [ 580 | ('url', str, []), 581 | ], 582 | 583 | 'setRefHandle': [ 584 | ('handle', str, []), 585 | ('fbml', str, []), 586 | ], 587 | }, 588 | 589 | # SMS Methods 590 | 'sms': { 591 | 'canSend': [ 592 | ('uid', int, []), 593 | ], 594 | 595 | 'send': [ 596 | ('uid', int, []), 597 | ('message', str, []), 598 | ('session_id', int, []), 599 | ('req_session', bool, []), 600 | ], 601 | }, 602 | 603 | 'data': { 604 | 'getCookies': [ 605 | ('uid', int, []), 606 | ('string', str, ['optional']), 607 | ], 608 | 609 | 'setCookie': [ 610 | ('uid', int, []), 611 | ('name', str, []), 612 | ('value', str, []), 613 | ('expires', int, ['optional']), 614 | ('path', str, ['optional']), 615 | ], 616 | }, 617 | 618 | # connect methods 619 | 'connect': { 620 | 'registerUsers': [ 621 | ('accounts', json, []), 622 | ], 623 | 624 | 'unregisterUsers': [ 625 | ('email_hashes', json, []), 626 | ], 627 | 628 | 'getUnconnectedFriendsCount': [ 629 | ], 630 | }, 631 | 632 | 'links': { 633 | 'post': [ 634 | ('url', str, []), 635 | ('comment', str, []), 636 | ('uid', int, []), 637 | ('image', str, ['optional']), 638 | ('callback', str, ['optional']), 639 | ], 640 | 'preview': [ 641 | ('url', str, []), 642 | ('callback', str, ['optional']), 643 | ], 644 | }, 645 | 646 | # stream methods (beta) 647 | 'stream': { 648 | 'addComment': [ 649 | ('post_id', int, []), 650 | ('comment', str, []), 651 | ('uid', int, ['optional']), 652 | ], 653 | 654 | 'addLike': [ 655 | ('uid', int, ['optional']), 656 | ('post_id', int, ['optional']), 657 | ], 658 | 659 | 'get': [ 660 | ('viewer_id', int, ['optional']), 661 | ('source_ids', list, ['optional']), 662 | ('start_time', int, ['optional']), 663 | ('end_time', int, ['optional']), 664 | ('limit', int, ['optional']), 665 | ('filter_key', str, ['optional']), 666 | ], 667 | 668 | 'getComments': [ 669 | ('post_id', int, []), 670 | ], 671 | 672 | 'getFilters': [ 673 | ('uid', int, ['optional']), 674 | ], 675 | 676 | 'publish': [ 677 | ('message', str, ['optional']), 678 | ('attachment', json, ['optional']), 679 | ('action_links', json, ['optional']), 680 | ('target_id', str, ['optional']), 681 | ('uid', str, ['optional']), 682 | ], 683 | 684 | 'remove': [ 685 | ('post_id', int, []), 686 | ('uid', int, ['optional']), 687 | ], 688 | 689 | 'removeComment': [ 690 | ('comment_id', int, []), 691 | ('uid', int, ['optional']), 692 | ], 693 | 694 | 'removeLike': [ 695 | ('uid', int, ['optional']), 696 | ('post_id', int, ['optional']), 697 | ], 698 | }, 699 | 700 | # livemessage methods (beta) 701 | 'livemessage': { 702 | 'send': [ 703 | ('recipient', int, []), 704 | ('event_name', str, []), 705 | ('message', str, []), 706 | ], 707 | }, 708 | 709 | # dashboard methods (beta) 710 | 'dashboard': { 711 | # setting counters for a single user 712 | 'decrementCount': [ 713 | ('uid', int, []), 714 | ], 715 | 716 | 'incrementCount': [ 717 | ('uid', int, []), 718 | ], 719 | 720 | 'getCount': [ 721 | ('uid', int, []), 722 | ], 723 | 724 | 'setCount': [ 725 | ('uid', int, []), 726 | ('count', int, []), 727 | ], 728 | 729 | # setting counters for multiple users 730 | 'multiDecrementCount': [ 731 | ('uids', list, []), 732 | ], 733 | 734 | 'multiIncrementCount': [ 735 | ('uids', list, []), 736 | ], 737 | 738 | 'multiGetCount': [ 739 | ('uids', list, []), 740 | ], 741 | 742 | 'multiSetCount': [ 743 | ('uids', list, []), 744 | ], 745 | 746 | # setting news for a single user 747 | 'addNews': [ 748 | ('news', json, []), 749 | ('image', str, ['optional']), 750 | ('uid', int, ['optional']), 751 | ], 752 | 753 | 'getNews': [ 754 | ('uid', int, []), 755 | ('news_ids', list, ['optional']), 756 | ], 757 | 758 | 'clearNews': [ 759 | ('uid', int, []), 760 | ('news_ids', list, ['optional']), 761 | ], 762 | 763 | # setting news for multiple users 764 | 'multiAddNews': [ 765 | ('uids', list, []), 766 | ('news', json, []), 767 | ('image', str, ['optional']), 768 | ], 769 | 770 | 'multiGetNews': [ 771 | ('uids', json, []), 772 | ], 773 | 774 | 'multiClearNews': [ 775 | ('uids', json, []), 776 | ], 777 | 778 | # setting application news for all users 779 | 'addGlobalNews': [ 780 | ('news', json, []), 781 | ('image', str, ['optional']), 782 | ], 783 | 784 | 'getGlobalNews': [ 785 | ('news_ids', list, ['optional']), 786 | ], 787 | 788 | 'clearGlobalNews': [ 789 | ('news_ids', list, ['optional']), 790 | ], 791 | 792 | # user activity 793 | 'getActivity': [ 794 | ('activity_ids', list, ['optional']), 795 | ], 796 | 797 | 'publishActivity': [ 798 | ('activity', json, []), 799 | ], 800 | 801 | 'removeActivity': [ 802 | ('activity_ids', list, []), 803 | ], 804 | }, 805 | 806 | # comments methods (beta) 807 | 'comments': { 808 | 'add': [ 809 | # text should be after xid/object_id, but is required 810 | ('text', str, []), 811 | # One of xid and object_is is required. Can this be expressed? 812 | ('xid', str, ['optional']), 813 | ('object_id', str, ['optional']), 814 | ('uid', int, ['optional']), 815 | ('title', str, ['optional']), 816 | ('url', str, ['optional']), 817 | ('publish_to_stream', bool, [('default', False)]), 818 | ], 819 | 820 | 'remove': [ 821 | # One of xid and object_is is required. Can this be expressed? 822 | ('xid', str, ['optional']), 823 | ('object_id', str, ['optional']), 824 | # comment_id should be required 825 | ('comment_id', str, ['optional']), 826 | ], 827 | 828 | 'get': [ 829 | # One of xid and object_is is required. Can this be expressed? 830 | ('xid', str, ['optional']), 831 | ('object_id', str, ['optional']), 832 | ], 833 | } 834 | } 835 | 836 | 837 | def __fixup_param(name, klass, options, param): 838 | optional = 'optional' in options 839 | default = [x[1] for x in options if isinstance(x, tuple) and x[ 840 | 0] == 'default'] 841 | if default: 842 | default = default[0] 843 | else: 844 | default = None 845 | if param is None: 846 | if klass is list and default: 847 | param = default[:] 848 | else: 849 | param = default 850 | if klass is json and isinstance(param, (list, dict)): 851 | param = simplejson.dumps(param) 852 | return param 853 | 854 | 855 | def __generate_facebook_method(namespace, method_name, param_data): 856 | # This method-level option forces the method to be signed rather than using 857 | # the access_token 858 | signed = False 859 | if 'signed' in param_data: 860 | signed = True 861 | param_data.remove('signed') 862 | 863 | # a required parameter doesn't have 'optional' in the options, 864 | # and has no tuple option that starts with 'default' 865 | required = [x for x in param_data if 'optional' not in x[2] and not [ 866 | y for y in x[2] if isinstance(y, tuple) and y and y[0] == 'default']] 867 | 868 | def facebook_method(self, *args, **kwargs): 869 | params = {} 870 | for i, arg in enumerate(args): 871 | params[param_data[i][0]] = arg 872 | params.update(kwargs) 873 | 874 | for param in required: 875 | if param[0] not in params: 876 | raise TypeError("missing parameter %s" % param[0]) 877 | 878 | for name, klass, options in param_data: 879 | if name in params: 880 | params[name] = __fixup_param( 881 | name, klass, options, params[name]) 882 | 883 | return self(method_name, params, signed=signed) 884 | 885 | facebook_method.__name__ = method_name 886 | facebook_method.__doc__ = "Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=%s.%s" % ( 887 | namespace, method_name) 888 | 889 | return facebook_method 890 | 891 | 892 | class Proxy(object): 893 | """Represents a "namespace" of Facebook API calls.""" 894 | 895 | def __init__(self, client, name): 896 | self._client = client 897 | self._name = name 898 | 899 | def __call__( 900 | self, 901 | method=None, 902 | args=None, 903 | add_session_args=True, 904 | signed=False): 905 | # for Django templates 906 | if method is None: 907 | return self 908 | 909 | if add_session_args: 910 | self._client._add_session_args(args) 911 | 912 | return self._client( 913 | '%s.%s' % 914 | (self._name, method), args, signed=signed) 915 | 916 | 917 | # generate the Facebook proxies 918 | def __generate_proxies(): 919 | for namespace in METHODS: 920 | methods = {} 921 | 922 | for method, param_data in METHODS[namespace].iteritems(): 923 | methods[method] = __generate_facebook_method( 924 | namespace, method, param_data) 925 | 926 | proxy = type('%sProxy' % namespace.title(), (Proxy,), methods) 927 | 928 | globals()[proxy.__name__] = proxy 929 | 930 | __generate_proxies() 931 | 932 | 933 | class FacebookError(Exception): 934 | """Exception class for errors received from Facebook.""" 935 | 936 | def __init__(self, code, msg, args=None): 937 | self.code = code 938 | self.msg = msg 939 | self.extra_args = args 940 | Exception.__init__(self, code, msg, args) 941 | 942 | def __str__(self): 943 | return 'Error %s: %s' % (self.code, self.msg) 944 | 945 | 946 | class AuthProxy(AuthProxy): 947 | """Special proxy for facebook.auth.""" 948 | 949 | def getSession(self): 950 | """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=auth.getSession""" 951 | args = {} 952 | try: 953 | args['auth_token'] = self._client.auth_token 954 | except AttributeError: 955 | raise RuntimeError('Client does not have auth_token set.') 956 | try: 957 | args['generate_session_secret'] = self._client.generate_session_secret 958 | except AttributeError: 959 | pass 960 | result = self._client('%s.getSession' % self._name, args) 961 | self._client.session_key = result['session_key'] 962 | self._client.uid = result['uid'] 963 | self._client.secret = result.get('secret') 964 | self._client.session_key_expires = result['expires'] 965 | return result 966 | 967 | def createToken(self): 968 | """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=auth.createToken""" 969 | token = self._client('%s.createToken' % self._name) 970 | self._client.auth_token = token 971 | return token 972 | 973 | 974 | class FriendsProxy(FriendsProxy): 975 | """Special proxy for facebook.friends.""" 976 | 977 | def get(self, **kwargs): 978 | """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=friends.get""" 979 | if not kwargs.get('flid') and self._client._friends: 980 | return self._client._friends 981 | return super(FriendsProxy, self).get(**kwargs) 982 | 983 | 984 | class PhotosProxy(PhotosProxy): 985 | """Special proxy for facebook.photos.""" 986 | 987 | def upload( 988 | self, 989 | image, 990 | aid=None, 991 | uid=None, 992 | caption=None, 993 | size=( 994 | 720, 995 | 720), 996 | filename=None, 997 | callback=None): 998 | """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=photos.upload 999 | 1000 | size -- an optional size (width, height) to resize the image to before uploading. Resizes by default 1001 | to Facebook's maximum display dimension of 720. 1002 | """ 1003 | args = {} 1004 | 1005 | if uid is not None: 1006 | args['uid'] = uid 1007 | 1008 | if aid is not None: 1009 | args['aid'] = aid 1010 | 1011 | if caption is not None: 1012 | args['caption'] = caption 1013 | 1014 | if self._client.oauth2: 1015 | url = self._client.facebook_secure_url 1016 | else: 1017 | url = self._client.facebook_url 1018 | 1019 | args = self._client._build_post_args( 1020 | 'facebook.photos.upload', 1021 | self._client._add_session_args(args)) 1022 | 1023 | try: 1024 | import cStringIO as StringIO 1025 | except ImportError: 1026 | import StringIO 1027 | 1028 | # check for a filename specified...if the user is passing binary data in 1029 | # image then a filename will be specified 1030 | if filename is None: 1031 | try: 1032 | import Image 1033 | except ImportError: 1034 | data = StringIO.StringIO(open(image, 'rb').read()) 1035 | else: 1036 | img = Image.open(image) 1037 | if size: 1038 | img.thumbnail(size, Image.ANTIALIAS) 1039 | data = StringIO.StringIO() 1040 | img.save(data, img.format) 1041 | else: 1042 | # there was a filename specified, which indicates that image was not 1043 | # the path to an image file but rather the binary data of a file 1044 | data = StringIO.StringIO(image) 1045 | image = filename 1046 | 1047 | content_type, body = self.__encode_multipart_formdata( 1048 | list(args.iteritems()), [(image, data)]) 1049 | urlinfo = urlparse.urlsplit(url) 1050 | try: 1051 | content_length = len(body) 1052 | chunk_size = 4096 1053 | 1054 | if self._client.oauth2: 1055 | h = httplib.HTTPSConnection(urlinfo[1]) 1056 | else: 1057 | h = httplib.HTTPConnection(urlinfo[1]) 1058 | h.putrequest('POST', urlinfo[2]) 1059 | h.putheader('Content-Type', content_type) 1060 | h.putheader('Content-Length', str(content_length)) 1061 | h.putheader('MIME-Version', '1.0') 1062 | h.putheader('User-Agent', 'PyFacebook Client Library') 1063 | h.endheaders() 1064 | 1065 | if callback: 1066 | count = 0 1067 | while len(body) > 0: 1068 | if len(body) < chunk_size: 1069 | data = body 1070 | body = '' 1071 | else: 1072 | data = body[0:chunk_size] 1073 | body = body[chunk_size:] 1074 | 1075 | h.send(data) 1076 | count += 1 1077 | callback(count, chunk_size, content_length) 1078 | else: 1079 | h.send(body) 1080 | 1081 | response = h.getresponse() 1082 | 1083 | if response.status != 200: 1084 | raise Exception( 1085 | 'Error uploading photo: Facebook returned HTTP %s (%s)' % 1086 | (response.status, response.reason)) 1087 | response = response.read() 1088 | except: 1089 | # sending the photo failed, perhaps we are using GAE 1090 | try: 1091 | from google.appengine.api import urlfetch 1092 | 1093 | try: 1094 | response = urlread( 1095 | url=url, 1096 | data=body, 1097 | headers={ 1098 | 'POST': urlinfo[2], 1099 | 'Content-Type': content_type, 1100 | 'MIME-Version': '1.0'}) 1101 | except urllib2.URLError: 1102 | raise Exception( 1103 | 'Error uploading photo: Facebook returned %s' % 1104 | (response)) 1105 | except ImportError: 1106 | # could not import from google.appengine.api, so we are not 1107 | # running in GAE 1108 | raise Exception('Error uploading photo.') 1109 | 1110 | return self._client._parse_response(response, 'facebook.photos.upload') 1111 | 1112 | def __encode_multipart_formdata(self, fields, files): 1113 | """Encodes a multipart/form-data message to upload an image.""" 1114 | boundary = '-------tHISiStheMulTIFoRMbOUNDaRY' 1115 | crlf = '\r\n' 1116 | l = [] 1117 | 1118 | for (key, value) in fields: 1119 | l.append('--' + boundary) 1120 | l.append('Content-Disposition: form-data; name="%s"' % str(key)) 1121 | l.append('') 1122 | l.append(str(value)) 1123 | for (filename, value) in files: 1124 | l.append('--' + boundary) 1125 | l.append( 1126 | 'Content-Disposition: form-data; filename="%s"' % 1127 | (str(filename), )) 1128 | l.append('Content-Type: %s' % self.__get_content_type(filename)) 1129 | l.append('') 1130 | l.append(value.getvalue()) 1131 | l.append('--' + boundary + '--') 1132 | l.append('') 1133 | body = crlf.join(l) 1134 | content_type = 'multipart/form-data; boundary=%s' % boundary 1135 | return content_type, body 1136 | 1137 | def __get_content_type(self, filename): 1138 | """Returns a guess at the MIME type of the file from the filename.""" 1139 | return str(mimetypes.guess_type(filename)[ 1140 | 0]) or 'application/octet-stream' 1141 | 1142 | 1143 | class VideoProxy(VideoProxy): 1144 | """Special proxy for facebook.video.""" 1145 | 1146 | def upload( 1147 | self, 1148 | video, 1149 | aid=None, 1150 | title=None, 1151 | description=None, 1152 | filename=None, 1153 | uid=None, 1154 | privacy=None, 1155 | callback=None): 1156 | """Facebook API call. See http://wiki.developers.facebook.com/index.php/Video.upload""" 1157 | args = {} 1158 | 1159 | if aid is not None: 1160 | args['aid'] = aid 1161 | 1162 | if title is not None: 1163 | args['title'] = title 1164 | 1165 | if description is not None: 1166 | args['description'] = description 1167 | 1168 | if uid is not None: 1169 | args['uid'] = uid 1170 | 1171 | if privacy is not None: 1172 | args['privacy'] = privacy 1173 | 1174 | args = self._client._build_post_args( 1175 | 'facebook.video.upload', 1176 | self._client._add_session_args(args)) 1177 | 1178 | try: 1179 | import cStringIO as StringIO 1180 | except ImportError: 1181 | import StringIO 1182 | 1183 | # check for a filename specified...if the user is passing binary data in 1184 | # video then a filename will be specified 1185 | if filename is None: 1186 | data = StringIO.StringIO(open(video, 'rb').read()) 1187 | else: 1188 | # there was a filename specified, which indicates that video was not 1189 | # the path to an video file but rather the binary data of a file 1190 | data = StringIO.StringIO(video) 1191 | video = filename 1192 | 1193 | content_type, body = self.__encode_multipart_formdata( 1194 | list(args.iteritems()), [(video, data)]) 1195 | 1196 | # Set the correct End Point Url, note this is different to all other FB 1197 | # EndPoints 1198 | urlinfo = urlparse.urlsplit(FACEBOOK_VIDEO_URL) 1199 | try: 1200 | content_length = len(body) 1201 | chunk_size = 4096 1202 | 1203 | h = httplib.HTTPConnection(urlinfo[1]) 1204 | h.putrequest('POST', urlinfo[2]) 1205 | h.putheader('Content-Type', content_type) 1206 | h.putheader('Content-Length', str(content_length)) 1207 | h.putheader('MIME-Version', '1.0') 1208 | h.putheader('User-Agent', 'PyFacebook Client Library') 1209 | h.endheaders() 1210 | 1211 | if callback: 1212 | count = 0 1213 | while len(body) > 0: 1214 | if len(body) < chunk_size: 1215 | data = body 1216 | body = '' 1217 | else: 1218 | data = body[0:chunk_size] 1219 | body = body[chunk_size:] 1220 | 1221 | h.send(data) 1222 | count += 1 1223 | callback(count, chunk_size, content_length) 1224 | else: 1225 | h.send(body) 1226 | 1227 | response = h.getresponse() 1228 | 1229 | if response.status != 200: 1230 | raise Exception( 1231 | 'Error uploading video: Facebook returned HTTP %s (%s)' % 1232 | (response.status, response.reason)) 1233 | response = response.read() 1234 | except: 1235 | # sending the photo failed, perhaps we are using GAE 1236 | try: 1237 | from google.appengine.api import urlfetch 1238 | 1239 | try: 1240 | response = urlread( 1241 | url=FACEBOOK_VIDEO_URL, 1242 | data=body, 1243 | headers={ 1244 | 'POST': urlinfo[2], 1245 | 'Content-Type': content_type, 1246 | 'MIME-Version': '1.0'}) 1247 | except urllib2.URLError: 1248 | raise Exception( 1249 | 'Error uploading video: Facebook returned %s' % 1250 | (response)) 1251 | except ImportError: 1252 | # could not import from google.appengine.api, so we are not 1253 | # running in GAE 1254 | raise Exception('Error uploading video.') 1255 | 1256 | return self._client._parse_response(response, 'facebook.video.upload') 1257 | 1258 | def __encode_multipart_formdata(self, fields, files): 1259 | """Encodes a multipart/form-data message to upload an image.""" 1260 | boundary = '-------tHISiStheMulTIFoRMbOUNDaRY' 1261 | crlf = '\r\n' 1262 | l = [] 1263 | 1264 | for (key, value) in fields: 1265 | l.append('--' + boundary) 1266 | l.append('Content-Disposition: form-data; name="%s"' % str(key)) 1267 | l.append('') 1268 | l.append(str(value)) 1269 | for (filename, value) in files: 1270 | l.append('--' + boundary) 1271 | l.append( 1272 | 'Content-Disposition: form-data; filename="%s"' % 1273 | (str(filename), )) 1274 | l.append('Content-Type: %s' % self.__get_content_type(filename)) 1275 | l.append('') 1276 | l.append(value.getvalue()) 1277 | l.append('--' + boundary + '--') 1278 | l.append('') 1279 | body = crlf.join(l) 1280 | content_type = 'multipart/form-data; boundary=%s' % boundary 1281 | return content_type, body 1282 | 1283 | def __get_content_type(self, filename): 1284 | """Returns a guess at the MIME type of the file from the filename.""" 1285 | return str(mimetypes.guess_type(filename)[ 1286 | 0]) or 'application/octet-stream' 1287 | 1288 | 1289 | class Facebook(object): 1290 | """ 1291 | Provides access to the Facebook API. 1292 | 1293 | Instance Variables: 1294 | 1295 | added 1296 | True if the user has added this application. 1297 | 1298 | api_key 1299 | Your API key, as set in the constructor. 1300 | 1301 | app_id 1302 | Your application id, as set in the constructor or fetched from 1303 | fb_sig_app_id request parameter. 1304 | 1305 | app_name 1306 | Your application's name, i.e. the APP_NAME in http://apps.facebook.com/APP_NAME/ if 1307 | this is for an internal web application. Optional, but useful for automatic redirects 1308 | to canvas pages. 1309 | 1310 | auth_token 1311 | The auth token that Facebook gives you, either with facebook.auth.createToken, 1312 | or through a GET parameter. 1313 | 1314 | callback_path 1315 | The path of the callback set in the Facebook app settings. If your callback is set 1316 | to http://www.example.com/facebook/callback/, this should be '/facebook/callback/'. 1317 | Optional, but useful for automatic redirects back to the same page after login. 1318 | 1319 | desktop 1320 | True if this is a desktop app, False otherwise. Used for determining how to 1321 | authenticate. 1322 | 1323 | ext_perms 1324 | Any extended permissions that the user has granted to your application. 1325 | This parameter is set only if the user has granted any. 1326 | 1327 | facebook_url 1328 | The url to use for Facebook requests. 1329 | 1330 | facebook_secure_url 1331 | The url to use for secure Facebook requests. 1332 | 1333 | in_canvas 1334 | True if the current request is for a canvas page. 1335 | 1336 | in_iframe 1337 | True if the current request is for an HTML page to embed in Facebook inside an iframe. 1338 | 1339 | is_session_from_cookie 1340 | True if the current request session comes from a session cookie. 1341 | 1342 | in_profile_tab 1343 | True if the current request is for a user's tab for your application. 1344 | 1345 | internal 1346 | True if this Facebook object is for an internal application (one that can be added on Facebook) 1347 | 1348 | locale 1349 | The user's locale. Default: 'en_US' 1350 | 1351 | oauth2: 1352 | Whether to use the new OAuth 2.0 authentication mechanism. Default: False 1353 | 1354 | oauth2_token: 1355 | The current OAuth 2.0 token. 1356 | 1357 | oauth2_token_expires: 1358 | The UNIX time when the OAuth 2.0 token expires (seconds). 1359 | 1360 | page_id 1361 | Set to the page_id of the current page (if any) 1362 | 1363 | profile_update_time 1364 | The time when this user's profile was last updated. This is a UNIX timestamp. Default: None if unknown. 1365 | 1366 | secret 1367 | Secret that is used after getSession for desktop apps. 1368 | 1369 | secret_key 1370 | Your application's secret key, as set in the constructor. 1371 | 1372 | session_key 1373 | The current session key. Set automatically by auth.getSession, but can be set 1374 | manually for doing infinite sessions. 1375 | 1376 | session_key_expires 1377 | The UNIX time of when this session key expires, or 0 if it never expires. 1378 | 1379 | uid 1380 | After a session is created, you can get the user's UID with this variable. Set 1381 | automatically by auth.getSession. 1382 | 1383 | ---------------------------------------------------------------------- 1384 | 1385 | """ 1386 | 1387 | def __init__(self, api_key, secret_key, auth_token=None, app_name=None, 1388 | callback_path=None, internal=None, proxy=None, 1389 | facebook_url=None, facebook_secure_url=None, 1390 | generate_session_secret=0, app_id=None, oauth2=False, 1391 | iframe_validation_timeout=10): 1392 | """ 1393 | Initializes a new Facebook object which provides wrappers for the Facebook API. 1394 | 1395 | If this is a desktop application, the next couple of steps you might want to take are: 1396 | 1397 | facebook.auth.createToken() # create an auth token 1398 | facebook.login() # show a browser window 1399 | wait_login() # somehow wait for the user to log in 1400 | facebook.auth.getSession() # get a session key 1401 | 1402 | For web apps, if you are passed an auth_token from Facebook, pass that in as a named parameter. 1403 | Then call: 1404 | 1405 | facebook.auth.getSession() 1406 | 1407 | """ 1408 | self.api_key = api_key 1409 | self.secret_key = secret_key 1410 | self.app_id = app_id 1411 | self.oauth2 = oauth2 1412 | self.oauth2_token = None 1413 | self.oauth2_token_expires = None 1414 | self.session_key = None 1415 | self.session_key_expires = None 1416 | self.auth_token = auth_token 1417 | self.secret = None 1418 | self.generate_session_secret = generate_session_secret 1419 | self.uid = None 1420 | self.page_id = None 1421 | self.in_canvas = False 1422 | self.in_iframe = False 1423 | self.is_session_from_cookie = False 1424 | self.in_profile_tab = False 1425 | self.added = False 1426 | self.app_name = app_name 1427 | self.callback_path = callback_path 1428 | self.internal = internal 1429 | self._friends = None 1430 | self.locale = 'en_US' 1431 | self.profile_update_time = None 1432 | self.ext_perms = [] 1433 | self.proxy = proxy 1434 | self.iframe_validation_timeout = iframe_validation_timeout 1435 | if facebook_url is None: 1436 | self.facebook_url = FACEBOOK_URL 1437 | else: 1438 | self.facebook_url = facebook_url 1439 | if facebook_secure_url is None: 1440 | self.facebook_secure_url = FACEBOOK_SECURE_URL 1441 | else: 1442 | self.facebook_secure_url = facebook_secure_url 1443 | 1444 | for namespace in METHODS: 1445 | self.__dict__[namespace] = eval( 1446 | '%sProxy(self, \'%s\')' % 1447 | (namespace.title(), 1448 | 'facebook.%s' % 1449 | namespace)) 1450 | 1451 | def _hash_args(self, args, secret=None): 1452 | """Hashes arguments by joining key=value pairs, appending a secret, and then taking the MD5 hex digest.""" 1453 | # @author: houyr 1454 | # fix for UnicodeEncodeError 1455 | hasher = hashlib.md5( 1456 | ''.join( 1457 | [ 1458 | '%s=%s' % 1459 | (isinstance( 1460 | x, 1461 | unicode) and x.encode("utf-8") or x, 1462 | isinstance( 1463 | args[x], 1464 | unicode) and args[x].encode("utf-8") or args[x]) for x in sorted( 1465 | args.keys())])) 1466 | if secret: 1467 | hasher.update(secret) 1468 | elif self.secret: 1469 | hasher.update(self.secret) 1470 | else: 1471 | hasher.update(self.secret_key) 1472 | return hasher.hexdigest() 1473 | 1474 | def _parse_response_item(self, node): 1475 | """Parses an XML response node from Facebook.""" 1476 | if node.nodeType == node.DOCUMENT_NODE and \ 1477 | node.childNodes[0].hasAttributes() and \ 1478 | node.childNodes[0].hasAttribute('list') and \ 1479 | node.childNodes[0].getAttribute('list') == "true": 1480 | return { 1481 | node.childNodes[0].nodeName: self._parse_response_list( 1482 | node.childNodes[0])} 1483 | elif node.nodeType == node.ELEMENT_NODE and \ 1484 | node.hasAttributes() and \ 1485 | node.hasAttribute('list') and \ 1486 | node.getAttribute('list') == "true": 1487 | return self._parse_response_list(node) 1488 | elif len(filter(lambda x: x.nodeType == x.ELEMENT_NODE, node.childNodes)) > 0: 1489 | return self._parse_response_dict(node) 1490 | else: 1491 | return ''.join( 1492 | node.data for node in node.childNodes if node.nodeType == node.TEXT_NODE) 1493 | 1494 | def _parse_response_dict(self, node): 1495 | """Parses an XML dictionary response node from Facebook.""" 1496 | result = {} 1497 | for item in filter( 1498 | lambda x: x.nodeType == x.ELEMENT_NODE, 1499 | node.childNodes): 1500 | result[item.nodeName] = self._parse_response_item(item) 1501 | if node.nodeType == node.ELEMENT_NODE and node.hasAttributes(): 1502 | if node.hasAttribute('id'): 1503 | result['id'] = node.getAttribute('id') 1504 | return result 1505 | 1506 | def _parse_response_list(self, node): 1507 | """Parses an XML list response node from Facebook.""" 1508 | result = [] 1509 | for item in filter( 1510 | lambda x: x.nodeType == x.ELEMENT_NODE, 1511 | node.childNodes): 1512 | result.append(self._parse_response_item(item)) 1513 | return result 1514 | 1515 | def _check_error(self, response): 1516 | """Checks if the given Facebook response is an error, and then raises the appropriate exception.""" 1517 | if isinstance(response, dict) and 'error_code' in response: 1518 | raise FacebookError( 1519 | response['error_code'], 1520 | response['error_msg'], 1521 | response['request_args']) 1522 | 1523 | def _build_post_args(self, method, args=None, signed=False): 1524 | """Adds to args parameters that are necessary for every call to the API.""" 1525 | if args is None: 1526 | args = {} 1527 | 1528 | for arg in args.items(): 1529 | if isinstance(arg[1], list): 1530 | args[arg[0]] = ','.join(str(a) for a in arg[1]) 1531 | elif isinstance(arg[1], unicode): 1532 | args[arg[0]] = arg[1].encode("UTF-8") 1533 | elif isinstance(arg[1], bool): 1534 | args[arg[0]] = str(arg[1]).lower() 1535 | 1536 | args['method'] = method 1537 | args['format'] = RESPONSE_FORMAT 1538 | if not signed and self.oauth2 and self.oauth2_token: 1539 | args['access_token'] = self.oauth2_token 1540 | else: 1541 | args['api_key'] = self.api_key 1542 | args['v'] = '1.0' 1543 | args['sig'] = self._hash_args(args) 1544 | 1545 | return args 1546 | 1547 | def _add_session_args(self, args=None): 1548 | """Adds 'session_key' and 'call_id' to args, which are used for API calls that need sessions.""" 1549 | if args is None: 1550 | args = {} 1551 | 1552 | if not self.session_key: 1553 | return args 1554 | # some calls don't need a session anymore. this might be better done in the markup 1555 | # raise RuntimeError('Session key not set. Make sure auth.getSession has been called.') 1556 | 1557 | if not self.oauth2 or not self.oauth2_token: 1558 | args['session_key'] = self.session_key 1559 | args['call_id'] = str(int(time.time() * 1000)) 1560 | 1561 | return args 1562 | 1563 | def _parse_response(self, response, method, format=None): 1564 | """Parses the response according to the given (optional) format, which should be either 'JSON' or 'XML'.""" 1565 | if not format: 1566 | format = RESPONSE_FORMAT 1567 | 1568 | if format == 'JSON': 1569 | result = simplejson.loads(response) 1570 | 1571 | self._check_error(result) 1572 | elif format == 'XML': 1573 | dom = minidom.parseString(response) 1574 | result = self._parse_response_item(dom) 1575 | dom.unlink() 1576 | 1577 | if 'error_response' in result: 1578 | self._check_error(result['error_response']) 1579 | 1580 | result = result[method[9:].replace('.', '_') + '_response'] 1581 | else: 1582 | raise RuntimeError('Invalid format specified.') 1583 | 1584 | return result 1585 | 1586 | def hash_email(self, email): 1587 | """ 1588 | Hash an email address in a format suitable for Facebook Connect. 1589 | 1590 | """ 1591 | email = email.lower().strip() 1592 | return "%s_%s" % ( 1593 | struct.unpack("I", struct.pack("i", binascii.crc32(email)))[0], 1594 | hashlib.md5(email).hexdigest(), 1595 | ) 1596 | 1597 | def unicode_urlencode(self, params): 1598 | """ 1599 | @author: houyr 1600 | A unicode aware version of urllib.urlencode. 1601 | """ 1602 | if isinstance(params, dict): 1603 | params = params.items() 1604 | return urllib.urlencode([(k, isinstance(v, unicode) and v.encode('utf-8') or v) 1605 | for k, v in params]) 1606 | 1607 | def __call__(self, method=None, args=None, secure=False, signed=False): 1608 | """Make a call to Facebook's REST server.""" 1609 | # for Django templates, if this object is called without any arguments 1610 | # return the object itself 1611 | if method is None: 1612 | return self 1613 | 1614 | # __init__ hard-codes into en_US 1615 | if args is not None and 'locale' not in args: 1616 | args['locale'] = self.locale 1617 | 1618 | # @author: houyr 1619 | # fix for bug of UnicodeEncodeError 1620 | post_data = self.unicode_urlencode( 1621 | self._build_post_args(method, args, signed)) 1622 | 1623 | if self.proxy: 1624 | proxy_handler = urllib2.ProxyHandler(self.proxy) 1625 | opener = urllib2.build_opener(proxy_handler) 1626 | if self.oauth2 or secure: 1627 | response = opener.open( 1628 | self.facebook_secure_url, post_data).read() 1629 | else: 1630 | response = opener.open(self.facebook_url, post_data).read() 1631 | else: 1632 | if self.oauth2 or secure: 1633 | response = urlread(self.facebook_secure_url, post_data) 1634 | else: 1635 | response = urlread(self.facebook_url, post_data) 1636 | 1637 | return self._parse_response(response, method) 1638 | 1639 | def oauth2_access_token(self, code, next, required_permissions=None): 1640 | """ 1641 | We've called authorize, and received a code, now we need to convert 1642 | this to an access_token 1643 | 1644 | """ 1645 | args = { 1646 | 'client_id': self.app_id, 1647 | 'client_secret': self.secret_key, 1648 | 'redirect_uri': next, 1649 | 'code': code 1650 | } 1651 | 1652 | if required_permissions: 1653 | args['scope'] = ",".join(required_permissions) 1654 | 1655 | # TODO see if immediate param works as per OAuth 2.0 spec? 1656 | url = self.get_graph_url('oauth/access_token', **args) 1657 | 1658 | if self.proxy: 1659 | proxy_handler = urllib2.ProxyHandler(self.proxy) 1660 | opener = urllib2.build_opener(proxy_handler) 1661 | response = opener.open(url).read() 1662 | else: 1663 | response = urlread(url) 1664 | 1665 | result = urlparse.parse_qs(response) 1666 | self.oauth2_token = result['access_token'][0] 1667 | self.oauth2_token_expires = time.time() + int(result['expires'][0]) 1668 | 1669 | # URL helpers 1670 | def get_url(self, page, **args): 1671 | """ 1672 | Returns one of the Facebook URLs (www.facebook.com/SOMEPAGE.php). 1673 | Named arguments are passed as GET query string parameters. 1674 | 1675 | """ 1676 | return 'http://www.facebook.com/%s.php?%s' % ( 1677 | page, urllib.urlencode(args)) 1678 | 1679 | def get_app_url(self, path=''): 1680 | """ 1681 | Returns the URL for this app's canvas page, according to app_name. 1682 | 1683 | """ 1684 | return 'http://apps.facebook.com/%s/%s' % (self.app_name, path) 1685 | 1686 | def get_graph_url(self, path='', **args): 1687 | """ 1688 | Returns the URL for the graph API with the supplied path and parameters 1689 | 1690 | """ 1691 | return 'https://graph.facebook.com/%s?%s' % ( 1692 | path, urllib.urlencode(args)) 1693 | 1694 | def get_add_url(self, next=None): 1695 | """ 1696 | Returns the URL that the user should be redirected to in order to add the application. 1697 | 1698 | """ 1699 | args = {'api_key': self.api_key, 'v': '1.0'} 1700 | 1701 | if next is not None: 1702 | args['next'] = next 1703 | 1704 | return self.get_url('install', **args) 1705 | 1706 | def get_authorize_url(self, next=None, next_cancel=None): 1707 | """ 1708 | Returns the URL that the user should be redirected to in order to 1709 | authorize certain actions for application. 1710 | 1711 | """ 1712 | args = {'api_key': self.api_key, 'v': '1.0'} 1713 | 1714 | if next is not None: 1715 | args['next'] = next 1716 | 1717 | if next_cancel is not None: 1718 | args['next_cancel'] = next_cancel 1719 | 1720 | return self.get_url('authorize', **args) 1721 | 1722 | def get_login_url(self, next=None, popup=False, canvas=True, 1723 | required_permissions=None): 1724 | """ 1725 | Returns the URL that the user should be redirected to in order to login. 1726 | 1727 | next -- the URL that Facebook should redirect to after login 1728 | required_permissions -- permission required by the application 1729 | 1730 | """ 1731 | if self.oauth2: 1732 | args = { 1733 | 'client_id': self.app_id, 1734 | 'redirect_uri': next 1735 | } 1736 | 1737 | if required_permissions: 1738 | args['scope'] = required_permissions 1739 | 1740 | if popup: 1741 | args['display'] = 'popup' 1742 | 1743 | return self.get_graph_url('oauth/authorize', **args) 1744 | else: 1745 | args = {'api_key': self.api_key, 'v': '1.0'} 1746 | 1747 | if next is not None: 1748 | args['next'] = next 1749 | 1750 | if canvas is True: 1751 | args['canvas'] = 1 1752 | 1753 | if popup is True: 1754 | args['popup'] = 1 1755 | 1756 | if required_permissions: 1757 | args['req_perms'] = ",".join(required_permissions) 1758 | 1759 | if self.auth_token is not None: 1760 | args['auth_token'] = self.auth_token 1761 | 1762 | return self.get_url('login', **args) 1763 | 1764 | def login(self, popup=False): 1765 | """Open a web browser telling the user to login to Facebook.""" 1766 | import webbrowser 1767 | webbrowser.open(self.get_login_url(popup=popup)) 1768 | 1769 | def get_ext_perm_url(self, ext_perm, next=None, popup=False): 1770 | """ 1771 | Returns the URL that the user should be redirected to in order to grant an extended permission. 1772 | 1773 | ext_perm -- the name of the extended permission to request 1774 | next -- the URL that Facebook should redirect to after login 1775 | 1776 | """ 1777 | args = {'ext_perm': ext_perm, 'api_key': self.api_key, 'v': '1.0'} 1778 | 1779 | if next is not None: 1780 | args['next'] = next 1781 | 1782 | if popup is True: 1783 | args['popup'] = 1 1784 | 1785 | return self.get_url('authorize', **args) 1786 | 1787 | def request_extended_permission(self, ext_perm, popup=False): 1788 | """Open a web browser telling the user to grant an extended permission.""" 1789 | import webbrowser 1790 | webbrowser.open(self.get_ext_perm_url(ext_perm, popup=popup)) 1791 | 1792 | def check_session(self, request, params=None): 1793 | """ 1794 | Checks the given Django HttpRequest for Facebook parameters such as 1795 | POST variables or an auth token. If the session is valid, returns True 1796 | and this object can now be used to access the Facebook API. Otherwise, 1797 | it returns False, and the application should take the appropriate action 1798 | (either log the user in or have him add the application). 1799 | 1800 | """ 1801 | self.in_canvas = (request.POST.get('fb_sig_in_canvas') == '1') 1802 | 1803 | if 'auth_token' not in request.GET and self.session_key and ( 1804 | self.uid or self.page_id): 1805 | return True 1806 | 1807 | if not params: 1808 | if request.method == 'POST': 1809 | params = self.validate_signature(request.POST) 1810 | else: 1811 | if 'installed' in request.GET: 1812 | self.added = True 1813 | 1814 | if 'fb_page_id' in request.GET: 1815 | self.page_id = request.GET['fb_page_id'] 1816 | 1817 | if 'auth_token' in request.GET: 1818 | self.added = True # added by Marinho 1819 | self.auth_token = request.GET['auth_token'] 1820 | 1821 | try: 1822 | self.auth.getSession() 1823 | except FacebookError as e: 1824 | self.auth_token = None 1825 | return False 1826 | 1827 | return True 1828 | 1829 | params = self.validate_signature(request.GET) 1830 | 1831 | if not params: 1832 | cookies = None 1833 | 1834 | # first check if we are in django - to check cookies 1835 | if hasattr(request, 'COOKIES'): 1836 | cookies = request.COOKIES 1837 | else: 1838 | if hasattr(request, 'cookies'): 1839 | cookies = request.cookies 1840 | 1841 | if cookies: 1842 | 1843 | params = self.validate_oauth_cookie_signature(cookies) 1844 | 1845 | if params: 1846 | self.is_oauth = True 1847 | self.oauth_token = params['access_token'] 1848 | else: 1849 | params = self.validate_cookie_signature(cookies) 1850 | self.is_session_from_cookie = True 1851 | 1852 | if not params: 1853 | if self.validate_iframe(request): 1854 | return True 1855 | else: 1856 | params = {} 1857 | 1858 | if not params: 1859 | return False 1860 | 1861 | if 'in_canvas' in params and params.get('in_canvas') == '1': 1862 | self.in_canvas = True 1863 | 1864 | if 'in_iframe' in params and params.get('in_iframe') == '1': 1865 | self.in_iframe = True 1866 | 1867 | if 'in_profile_tab' in params and params.get('in_profile_tab') == '1': 1868 | self.in_profile_tab = True 1869 | 1870 | if 'added' in params and params.get('added') == '1': 1871 | self.added = True 1872 | 1873 | if params.get('expires'): 1874 | # Marinho Brandao - fixing problem with no session 1875 | self.session_key_expires = params.get( 1876 | 'expires', '').isdigit() and int( 1877 | params['expires']) or 0 1878 | 1879 | if 'locale' in params: 1880 | self.locale = params['locale'] 1881 | 1882 | if 'profile_update_time' in params: 1883 | try: 1884 | self.profile_update_time = int(params['profile_update_time']) 1885 | except ValueError: 1886 | pass 1887 | 1888 | if 'ext_perms' in params: 1889 | if params['ext_perms']: 1890 | self.ext_perms = params['ext_perms'].split(',') 1891 | else: 1892 | self.ext_perms = [] 1893 | 1894 | if 'friends' in params: 1895 | if params['friends']: 1896 | self._friends = params['friends'].split(',') 1897 | else: 1898 | self._friends = [] 1899 | 1900 | # If app_id is not set explicitly, pick it up from the param 1901 | if not self.app_id and 'app_id' in params: 1902 | self.app_id = params['app_id'] 1903 | 1904 | if 'session_key' in params: 1905 | self.session_key = params['session_key'] 1906 | if 'user' in params: 1907 | self.uid = params['user'] 1908 | elif 'page_id' in params: 1909 | self.page_id = params['page_id'] 1910 | elif 'uid' in params: 1911 | self.uid = params['uid'] 1912 | else: 1913 | return False 1914 | elif 'profile_session_key' in params: 1915 | self.session_key = params['profile_session_key'] 1916 | if 'profile_user' in params: 1917 | self.uid = params['profile_user'] 1918 | else: 1919 | return False 1920 | elif 'canvas_user' in params: 1921 | self.uid = params['canvas_user'] 1922 | elif 'uninstall' in params: 1923 | self.uid = params['user'] 1924 | else: 1925 | return False 1926 | 1927 | return True 1928 | 1929 | def validate_oauth_session(self, session_json): 1930 | session = simplejson.loads(session_json) 1931 | sig = session.pop('sig') 1932 | hash = self._hash_args(session) 1933 | if hash == sig: 1934 | return session 1935 | return None 1936 | 1937 | def validate_signature(self, post, prefix='fb_sig', timeout=None): 1938 | """ 1939 | Validate parameters passed to an internal Facebook app from Facebook. 1940 | 1941 | """ 1942 | args = post.copy() 1943 | 1944 | if prefix not in args: 1945 | return None 1946 | 1947 | del args[prefix] 1948 | 1949 | if timeout and '%s_time' % prefix in post and time.time( 1950 | ) - float(post['%s_time' % prefix]) > timeout: 1951 | return None 1952 | 1953 | args = dict([(key[len(prefix + '_'):], value) 1954 | for key, value in args.items() if key.startswith(prefix)]) 1955 | 1956 | hash = self._hash_args(args) 1957 | 1958 | if hash == post[prefix]: 1959 | return args 1960 | else: 1961 | return None 1962 | 1963 | def validate_iframe(self, request): 1964 | request_dict = request.POST if request.method == 'POST' else request.GET 1965 | if any( 1966 | key not in request_dict for key in [ 1967 | 'userid', 1968 | 'reqtime', 1969 | 'appsig']): 1970 | return False 1971 | request_time = request_dict['reqtime'] 1972 | time_now = int(time.time()) 1973 | if time_now - int(request_time) > self.iframe_validation_timeout: 1974 | return False 1975 | userid = int(request_dict['userid']) 1976 | self.uid = userid 1977 | app_sig = request_dict['appsig'] 1978 | digest = create_hmac( 1979 | self.secret_key, "%s%s" % 1980 | (str(userid), str(request_time))) 1981 | return digest == app_sig 1982 | 1983 | def validate_oauth_cookie_signature(self, cookies): 1984 | cookie_name = 'fbs_%s' % self.app_id 1985 | if cookie_name in cookies: 1986 | # value like 1987 | # "access_token=104302089510310%7C2.HYYLow1Vlib0s_sJSAESjw__.3600.1275037200-100000214342553%7CtC1aolM22Lauj_dZhYnv_tF2CK4.&base_domain=yaycy.com&expires=1275037200&secret=FvIHkbAFwEy_0sueRk2ZYQ__&session_key=2.HYYoow1Vlib0s_sJSAESjw__.3600.1275037200-100000214342553&sig=7bb035a0411be7aa801964ae34416f28&uid=100000214342553" 1988 | params = dict([part.split('=') 1989 | for part in cookies[cookie_name].split('&')]) 1990 | sig = params.pop('sig') 1991 | hash = self._hash_args(params) 1992 | if hash == sig: 1993 | return params 1994 | return None 1995 | 1996 | def validate_cookie_signature(self, cookies): 1997 | """ 1998 | Validate parameters passed by cookies, namely facebookconnect or js api. 1999 | """ 2000 | 2001 | api_key = self.api_key 2002 | if api_key not in cookies: 2003 | return None 2004 | 2005 | prefix = api_key + "_" 2006 | 2007 | params = {} 2008 | vals = '' 2009 | for k in sorted(cookies): 2010 | if k.startswith(prefix): 2011 | key = k.replace(prefix, "") 2012 | value = cookies[k] 2013 | if value != 'None': 2014 | params[key] = value 2015 | vals += '%s=%s' % (key, value) 2016 | 2017 | hasher = hashlib.md5(vals) 2018 | 2019 | hasher.update(self.secret_key) 2020 | digest = hasher.hexdigest() 2021 | if digest == cookies[api_key]: 2022 | params['is_session_from_cookie'] = True 2023 | return params 2024 | else: 2025 | return False 2026 | 2027 | 2028 | if __name__ == '__main__': 2029 | # sample desktop application 2030 | 2031 | api_key = '' 2032 | secret_key = '' 2033 | 2034 | facebook = Facebook(api_key, secret_key) 2035 | 2036 | facebook.auth.createToken() 2037 | 2038 | # Show login window 2039 | # Set popup=True if you want login without navigational elements 2040 | facebook.login() 2041 | 2042 | # Login to the window, then press enter 2043 | print 'After logging in, press enter...' 2044 | raw_input() 2045 | 2046 | facebook.auth.getSession() 2047 | print 'Session Key: ', facebook.session_key 2048 | print 'Your UID: ', facebook.uid 2049 | 2050 | info = facebook.users.getInfo( 2051 | [facebook.uid], ['name', 'birthday', 'affiliations', 'sex'])[0] 2052 | 2053 | print 'Your Name: ', info['name'] 2054 | print 'Your Birthday: ', info['birthday'] 2055 | print 'Your Gender: ', info['sex'] 2056 | 2057 | friends = facebook.friends.get() 2058 | friends = facebook.users.getInfo( 2059 | friends[ 2060 | 0:5], [ 2061 | 'name', 'birthday', 'relationship_status']) 2062 | 2063 | for friend in friends: 2064 | print friend['name'], 'has a birthday on', friend['birthday'], 'and is', friend['relationship_status'] 2065 | 2066 | arefriends = facebook.friends.areFriends( 2067 | [friends[0]['uid']], [friends[1]['uid']]) 2068 | 2069 | photos = facebook.photos.getAlbums(facebook.uid) 2070 | -------------------------------------------------------------------------------- /facebook/djangofb/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | import time 3 | import datetime 4 | import facebook 5 | 6 | from django.http import HttpResponse, HttpResponseRedirect 7 | from django.utils.http import urlquote 8 | from django.core.exceptions import ImproperlyConfigured 9 | from django.conf import settings 10 | 11 | try: 12 | from threading import local 13 | except ImportError: 14 | from django.utils._threading_local import local 15 | 16 | __all__ = [ 17 | 'Facebook', 18 | 'FacebookMiddleware', 19 | 'get_facebook_client', 20 | 'require_login', 21 | 'require_add'] 22 | 23 | _thread_locals = local() 24 | 25 | 26 | class Facebook(facebook.Facebook): 27 | 28 | def redirect(self, url): 29 | """ 30 | Helper for Django which redirects to another page. If inside a 31 | canvas page, writes a instead to achieve the same effect. 32 | 33 | """ 34 | if self.in_canvas: 35 | return HttpResponse('' % (url,)) 36 | elif re.search("^https?:\/\/([^\/]*\.)?facebook\.com(:\d+)?", url.lower()): 37 | return HttpResponse( 38 | '' % 39 | url) 40 | else: 41 | return HttpResponseRedirect(url) 42 | 43 | def url_for(self, path): 44 | """ 45 | Expand the path into a full URL, depending on whether we're in a canvas 46 | page or not. 47 | 48 | """ 49 | if self.in_canvas: 50 | return self.get_app_url(path[1:]) 51 | else: 52 | return '%s%s' % (settings.SITE_URL, path) 53 | 54 | def _oauth2_process_params(self, request): 55 | """ 56 | Check a few key parameters for oauth methods 57 | 58 | """ 59 | self.in_canvas = (request.REQUEST.get('fb_sig_in_canvas') == '1') 60 | self.added = (request.REQUEST.get('fb_sig_added') == '1') 61 | # If app_id is not set explicitly, pick it up from the params 62 | if not self.app_id: 63 | self.app_id = request.REQUEST.get('fb_sig_app_id') 64 | if not self.uid: 65 | self.uid = request.REQUEST.get('fb_sig_user') 66 | 67 | def oauth2_check_session(self, request): 68 | """ 69 | Check to see if we have an access_token in our session 70 | 71 | """ 72 | valid_token = False 73 | 74 | # See if they're in the request 75 | if 'session' in request.POST: 76 | print 'session from POST' 77 | values = self.validate_oauth_session(request.POST['session']) 78 | 79 | # Might be in the query string (e.g. from iframe) 80 | elif 'session' in request.GET: 81 | print 'session from GET' 82 | values = self.validate_oauth_session(request.GET['session']) 83 | 84 | # Look out for an access_token in our cookies from the JS SDK FB.init 85 | elif request.COOKIES: 86 | values = self.validate_oauth_cookie_signature(request.COOKIES) 87 | print 'session from COOKIE %s' % values 88 | 89 | if values and 'access_token' in values: 90 | request.session['oauth2_token'] = values['access_token'] 91 | request.session['oauth2_token_expires'] = values['expires'] 92 | self.session_key = values['session_key'] 93 | self.uid = values['uid'] 94 | self.added = True 95 | 96 | # If we've been accepted by the user 97 | if self.added: 98 | 99 | # See if we've got this user's access_token in our session 100 | if 'oauth2_token' in request.session: 101 | self.oauth2_token = request.session['oauth2_token'] 102 | self.oauth2_token_expires = request.session[ 103 | 'oauth2_token_expires'] 104 | 105 | if self.oauth2_token_expires: 106 | if self.oauth2_token_expires > time.time(): 107 | # Got a token, and it's valid 108 | valid_token = True 109 | else: 110 | del request.session['oauth2_token'] 111 | del request.session['oauth2_token_expires'] 112 | 113 | return valid_token 114 | 115 | def oauth2_check_permissions(self, request, required_permissions, 116 | additional_permissions=None, 117 | fql_check=True, force_check=True): 118 | """ 119 | Check for specific extended_permissions. 120 | 121 | If fql_check is True (default), oauth2_check_session() should be called 122 | first to ensure the access_token is in place and valid to make query. 123 | 124 | """ 125 | has_permissions = False 126 | 127 | req_perms = set(required_permissions.split(',')) 128 | 129 | if 'oauth2_extended_permissions' in request.session: 130 | cached_perms = request.session['oauth2_extended_permissions'] 131 | 132 | # so now, fb_sig_ext_perms seems to contain the right perms (!) 133 | 134 | if not force_check and cached_perms and req_perms.issubset( 135 | cached_perms): 136 | # Note that this has the potential to be out of date! 137 | has_permissions = True 138 | elif fql_check: 139 | # TODO allow option to use preload FQL for this? 140 | perms_query = required_permissions 141 | 142 | # Note that we can query additional permissions that we 143 | # don't require. This can be useful for optional 144 | # functionality (or simply for better caching) 145 | if additional_permissions: 146 | perms_query += ',' + additional_permissions 147 | 148 | perms_results = self.fql.query( 149 | 'select %s from permissions where uid=%s' % 150 | (perms_query, self.uid))[0] 151 | actual_perms = set() 152 | for permission, allowed in perms_results.items(): 153 | if allowed == 1: 154 | actual_perms.add(permission) 155 | request.session['oauth2_extended_permissions'] = actual_perms 156 | has_permissions = req_perms.issubset(actual_perms) 157 | 158 | return has_permissions 159 | 160 | def oauth2_process_code(self, request, redirect_uri): 161 | """ 162 | Convert the code into an access_token. 163 | 164 | """ 165 | if 'code' in request.GET: 166 | # We've got a code from an authorisation, so convert it to a 167 | # access_token 168 | 169 | self.oauth2_access_token(request.GET['code'], next=redirect_uri) 170 | 171 | request.session['oauth2_token'] = self.oauth2_token 172 | request.session['oauth2_token_expires'] = self.oauth2_token_expires 173 | 174 | return True 175 | # else: 'error_reason' in request.GET 176 | 177 | return False 178 | 179 | 180 | def get_facebook_client(): 181 | """ 182 | Get the current Facebook object for the calling thread. 183 | 184 | """ 185 | try: 186 | return _thread_locals.facebook 187 | except AttributeError: 188 | raise ImproperlyConfigured( 189 | 'Make sure you have the Facebook middleware installed.') 190 | 191 | 192 | def _check_middleware(request): 193 | try: 194 | fb = request.facebook 195 | except: 196 | raise ImproperlyConfigured( 197 | 'Make sure you have the Facebook middleware installed.') 198 | 199 | if not fb.oauth2: 200 | raise ImproperlyConfigured( 201 | 'Please ensure that oauth2 is enabled (e.g. via settings.FACEBOOK_OAUTH2).') 202 | 203 | return fb 204 | 205 | 206 | def require_oauth( 207 | redirect_path=None, 208 | keep_state=True, 209 | in_canvas=True, 210 | required_permissions=None, 211 | check_permissions=None, 212 | force_check=True): 213 | """ 214 | Decorator for Django views that requires the user to be OAuth 2.0'd. 215 | The FacebookMiddleware must be installed. 216 | Note that OAuth 2.0 does away with the app added/logged in distinction - 217 | it is now the case that users have now either authorised facebook users or 218 | not, and if they are, they may have granted the app a number of 219 | extended permissions - there is no lightweight/automatic login any more. 220 | 221 | Standard usage: 222 | @require_oauth() 223 | def some_view(request): 224 | ... 225 | """ 226 | def decorator(view): 227 | def newview(request, *args, **kwargs): 228 | # permissions=newview.permissions 229 | 230 | try: 231 | fb = _check_middleware(request) 232 | 233 | valid_token = fb.oauth2_check_session(request) 234 | 235 | if required_permissions: 236 | has_permissions = fb.oauth2_check_permissions( 237 | request, required_permissions, check_permissions, 238 | valid_token, force_check) 239 | else: 240 | has_permissions = True 241 | 242 | if not valid_token or not has_permissions: 243 | if in_canvas: 244 | fb.in_canvas = in_canvas 245 | 246 | return _redirect_login(request, fb, redirect_path, 247 | keep_state, required_permissions) 248 | 249 | return view(request, *args, **kwargs) 250 | except facebook.FacebookError as e: 251 | # Invalid token (I think this can happen if the user logs out) 252 | # Unfortunately we don't find this out until we use the api 253 | if e.code == 190: 254 | del request.session['oauth2_token'] 255 | del request.session['oauth2_token_expires'] 256 | return _redirect_login(request, fb, redirect_path, 257 | keep_state, required_permissions) 258 | # newview.permissions = permissions 259 | return newview 260 | return decorator 261 | 262 | 263 | def _redirect_path(redirect_path, fb, path): 264 | """ 265 | Resolve the path to use for the redirect_uri for authorization 266 | 267 | """ 268 | if not redirect_path and fb.oauth2_redirect: 269 | redirect_path = fb.oauth2_redirect 270 | if redirect_path: 271 | if callable(redirect_path): 272 | redirect_path = redirect_path(path) 273 | else: 274 | redirect_path = path 275 | return redirect_path 276 | 277 | 278 | def _redirect_login( 279 | request, 280 | fb, 281 | redirect_path, 282 | keep_state, 283 | required_permissions): 284 | """ 285 | Fully resolve the redirect path for an oauth login and add in any state 286 | info required to bring us back to the correct place afterwards 287 | """ 288 | redirect_uri = fb.url_for(_redirect_path(redirect_path, fb, request.path)) 289 | 290 | if keep_state: 291 | if callable(keep_state): 292 | state = keep_state(request) 293 | else: 294 | state = request.get_full_path() 295 | # passing state directly to facebook oauth endpoint doesn't work 296 | redirect_uri += '?state=%s' % urlquote(state) 297 | 298 | url = fb.get_login_url(next=redirect_uri, 299 | required_permissions=required_permissions) 300 | 301 | return fb.redirect(url) 302 | 303 | 304 | def process_oauth(restore_state=True, in_canvas=True): 305 | """ 306 | Decorator for Django views that processes the user's code and converts it 307 | into an access_token. 308 | The FacebookMiddleware must be installed. 309 | 310 | Standard usage: 311 | @process_oauth() 312 | def some_view(request): 313 | ... 314 | """ 315 | def decorator(view): 316 | def newview(request, *args, **kwargs): 317 | # permissions=newview.permissions 318 | 319 | fb = _check_middleware(request) 320 | 321 | if in_canvas: 322 | fb.in_canvas = in_canvas 323 | 324 | # Work out what the original redirect_uri value was 325 | redirect_uri = fb.url_for(_strip_code(request.get_full_path())) 326 | 327 | if fb.oauth2_process_code(request, redirect_uri): 328 | if restore_state: 329 | state = request.GET['state'] 330 | if callable(restore_state): 331 | state = restore_state(state) 332 | else: 333 | state = fb.url_for(state) 334 | return fb.redirect(state) 335 | 336 | return view(request, *args, **kwargs) 337 | # newview.permissions = permissions 338 | return newview 339 | return decorator 340 | 341 | 342 | def _strip_code(path): 343 | """ 344 | Restore the path to the original redirect_uri without the code parameter. 345 | 346 | """ 347 | try: 348 | begin = path.find('&code') 349 | if begin == -1: 350 | begin = path.index('?code') 351 | end = path.find('&', begin + 1) 352 | if end == -1: 353 | end = len(path) 354 | return path[:begin] + path[end:] 355 | except ValueError: 356 | # no code, probably failed to authenticate 357 | # TODO strip error_reason instead here? 358 | return path 359 | 360 | 361 | def require_login(next=None, internal=None, required_permissions=None): 362 | """ 363 | Decorator for Django views that requires the user to be logged in. 364 | The FacebookMiddleware must be installed. 365 | 366 | Standard usage: 367 | @require_login() 368 | def some_view(request): 369 | ... 370 | 371 | Redirecting after login: 372 | To use the 'next' parameter to redirect to a specific page after login, a callable should 373 | return a path relative to the Post-add URL. 'next' can also be an integer specifying how many 374 | parts of request.path to strip to find the relative URL of the canvas page. If 'next' is None, 375 | settings.callback_path and settings.app_name are checked to redirect to the same page after logging 376 | in. (This is the default behavior.) 377 | @require_login(next=some_callable) 378 | def some_view(request): 379 | ... 380 | """ 381 | def decorator(view): 382 | def newview(request, *args, **kwargs): 383 | next = newview.next 384 | internal = newview.internal 385 | 386 | try: 387 | fb = request.facebook 388 | except: 389 | raise ImproperlyConfigured( 390 | 'Make sure you have the Facebook middleware installed.') 391 | 392 | if internal is None: 393 | internal = request.facebook.internal 394 | 395 | if callable(next): 396 | next = next(request.path) 397 | elif isinstance(next, int): 398 | next = '/'.join(request.path.split('/')[next + 1:]) 399 | elif next is None and fb.callback_path and request.path.startswith(fb.callback_path): 400 | next = request.path[len(fb.callback_path):] 401 | elif not isinstance(next, str): 402 | next = '' 403 | 404 | if internal and request.method == 'GET' and fb.app_name: 405 | next = "%s%s" % (fb.get_app_url(), next) 406 | 407 | try: 408 | session_check = fb.check_session(request) 409 | except ValueError: 410 | session_check = False 411 | 412 | if session_check and required_permissions: 413 | req_perms = set(required_permissions) 414 | perms = set(fb.ext_perms) 415 | has_permissions = req_perms.issubset(perms) 416 | else: 417 | has_permissions = True 418 | 419 | if not (session_check and has_permissions): 420 | # If user has never logged in before, the get_login_url will 421 | # redirect to the TOS page 422 | return fb.redirect( 423 | fb.get_login_url( 424 | next=next, 425 | required_permissions=required_permissions)) 426 | 427 | return view(request, *args, **kwargs) 428 | newview.next = next 429 | newview.internal = internal 430 | return newview 431 | return decorator 432 | 433 | 434 | def require_add(next=None, internal=None, on_install=None): 435 | """ 436 | Decorator for Django views that requires application installation. 437 | The FacebookMiddleware must be installed. 438 | 439 | Standard usage: 440 | @require_add() 441 | def some_view(request): 442 | ... 443 | 444 | Redirecting after installation: 445 | To use the 'next' parameter to redirect to a specific page after login, a callable should 446 | return a path relative to the Post-add URL. 'next' can also be an integer specifying how many 447 | parts of request.path to strip to find the relative URL of the canvas page. If 'next' is None, 448 | settings.callback_path and settings.app_name are checked to redirect to the same page after logging 449 | in. (This is the default behavior.) 450 | @require_add(next=some_callable) 451 | def some_view(request): 452 | ... 453 | 454 | Post-install processing: 455 | Set the on_install parameter to a callable in order to handle special post-install processing. 456 | The callable should take a request object as the parameter. 457 | @require_add(on_install=some_callable) 458 | def some_view(request): 459 | ... 460 | """ 461 | def decorator(view): 462 | def newview(request, *args, **kwargs): 463 | next = newview.next 464 | internal = newview.internal 465 | 466 | try: 467 | fb = request.facebook 468 | except: 469 | raise ImproperlyConfigured( 470 | 'Make sure you have the Facebook middleware installed.') 471 | 472 | if internal is None: 473 | internal = request.facebook.internal 474 | 475 | if callable(next): 476 | next = next(request.path) 477 | elif isinstance(next, int): 478 | next = '/'.join(request.path.split('/')[next + 1:]) 479 | elif next is None and fb.callback_path and request.path.startswith(fb.callback_path): 480 | next = request.path[len(fb.callback_path):] 481 | else: 482 | next = '' 483 | 484 | if not fb.check_session(request): 485 | if fb.added: 486 | if request.method == 'GET' and fb.app_name: 487 | return fb.redirect('%s%s' % (fb.get_app_url(), next)) 488 | return fb.redirect(fb.get_login_url(next=next)) 489 | else: 490 | return fb.redirect(fb.get_add_url(next=next)) 491 | 492 | if not fb.added: 493 | return fb.redirect(fb.get_add_url(next=next)) 494 | 495 | if 'installed' in request.GET and callable(on_install): 496 | on_install(request) 497 | 498 | if internal and request.method == 'GET' and fb.app_name: 499 | return fb.redirect('%s%s' % (fb.get_app_url(), next)) 500 | 501 | return view(request, *args, **kwargs) 502 | newview.next = next 503 | newview.internal = internal 504 | return newview 505 | return decorator 506 | 507 | # try to preserve the argspecs 508 | try: 509 | import decorator 510 | except ImportError: 511 | pass 512 | else: 513 | # Can this be done with functools.wraps, but maintaining kwargs? 514 | def updater(f): 515 | def updated(*args, **kwargs): 516 | original = f(*args, **kwargs) 517 | 518 | def newdecorator(view): 519 | return decorator.new_wrapper(original(view), view) 520 | return decorator.new_wrapper(newdecorator, original) 521 | return decorator.new_wrapper(updated, f) 522 | require_oauth = updater(require_oauth) 523 | process_oauth = updater(process_oauth) 524 | require_login = updater(require_login) 525 | require_add = updater(require_add) 526 | 527 | 528 | class FacebookMiddleware(object): 529 | """ 530 | Middleware that attaches a Facebook object to every incoming request. 531 | The Facebook object created can also be accessed from models for the 532 | current thread by using get_facebook_client(). 533 | 534 | callback_path can be a string or a callable. Using a callable lets us 535 | pass in something like lambda reverse('our_canvas_view') so we can follow 536 | the DRY principle. 537 | 538 | """ 539 | 540 | def __init__(self, api_key=None, secret_key=None, app_name=None, 541 | callback_path=None, internal=None, app_id=None, 542 | oauth2=None, oauth2_redirect=None): 543 | self.api_key = api_key or settings.FACEBOOK_API_KEY 544 | self.secret_key = secret_key or settings.FACEBOOK_SECRET_KEY 545 | self.app_name = app_name or getattr( 546 | settings, 'FACEBOOK_APP_NAME', None) 547 | self.callback_path = callback_path or getattr( 548 | settings, 'FACEBOOK_CALLBACK_PATH', None) 549 | self.internal = internal or getattr( 550 | settings, 'FACEBOOK_INTERNAL', True) 551 | self.app_id = app_id or getattr(settings, 'FACEBOOK_APP_ID', None) 552 | self.oauth2 = oauth2 or getattr(settings, 'FACEBOOK_OAUTH2', False) 553 | self.oauth2_redirect = oauth2_redirect or getattr( 554 | settings, 'FACEBOOK_OAUTH2_REDIRECT', None) 555 | self.proxy = None 556 | if getattr(settings, 'USE_HTTP_PROXY', False): 557 | self.proxy = settings.HTTP_PROXY 558 | 559 | def process_request(self, request): 560 | callback_path = self.callback_path 561 | if callable(callback_path): 562 | callback_path = callback_path() 563 | _thread_locals.facebook = request.facebook = Facebook( 564 | self.api_key, 565 | self.secret_key, 566 | app_name=self.app_name, 567 | callback_path=callback_path, 568 | internal=self.internal, 569 | proxy=self.proxy, 570 | app_id=self.app_id, 571 | oauth2=self.oauth2) 572 | if self.oauth2: 573 | if self.oauth2_redirect: 574 | request.facebook.oauth2_redirect = self.oauth2_redirect 575 | request.facebook._oauth2_process_params(request) 576 | if not self.internal: 577 | if 'fb_sig_session_key' in request.GET and ( 578 | 'fb_sig_user' in request.GET or 'fb_sig_canvas_user' in request.GET): 579 | request.facebook.session_key = request.session[ 580 | 'facebook_session_key'] = request.GET['fb_sig_session_key'] 581 | request.facebook.uid = request.session['facebook_user_id'] = request.GET[ 582 | 'fb_sig_user'] or request.GET['fb_sig_canvas_user'] 583 | elif int(request.GET.get('fb_sig_added', '1')) and request.session.get('facebook_session_key', None) \ 584 | and request.session.get('facebook_user_id', None): 585 | request.facebook.session_key = request.session[ 586 | 'facebook_session_key'] 587 | request.facebook.uid = request.session['facebook_user_id'] 588 | 589 | def process_response(self, request, response): 590 | 591 | # Don't assume that request.facebook exists 592 | # - it's not necessarily true that all process_requests will have been called 593 | try: 594 | fb = request.facebook 595 | except AttributeError: 596 | return response 597 | 598 | if not self.internal and fb.session_key and fb.uid: 599 | request.session['facebook_session_key'] = fb.session_key 600 | request.session['facebook_user_id'] = fb.uid 601 | 602 | if fb.session_key_expires: 603 | expiry = datetime.datetime.utcfromtimestamp( 604 | fb.session_key_expires) 605 | request.session.set_expiry(expiry) 606 | 607 | if not fb.is_session_from_cookie: 608 | # Make sure the browser accepts our session cookies inside an 609 | # Iframe 610 | response['P3P'] = 'CP="NOI DSP COR NID ADMa OPTa OUR NOR"' 611 | fb_cookies = { 612 | 'expires': fb.session_key_expires, 613 | 'session_key': fb.session_key, 614 | 'user': fb.uid, 615 | } 616 | fb_cookies = dict((k, v) for k, v in fb_cookies.items() 617 | if v is not None) 618 | 619 | expire_time = None 620 | if fb.session_key_expires: 621 | expire_time = datetime.datetime.utcfromtimestamp( 622 | fb.session_key_expires) 623 | 624 | for k in fb_cookies: 625 | response.set_cookie( 626 | self.api_key + '_' + k, 627 | fb_cookies[k], 628 | expires=expire_time) 629 | if fb_cookies: 630 | response.set_cookie( 631 | self.api_key, 632 | fb._hash_args(fb_cookies), 633 | expires=expire_time) 634 | 635 | return response 636 | -------------------------------------------------------------------------------- /facebook/djangofb/context_processors.py: -------------------------------------------------------------------------------- 1 | def messages(request): 2 | """Returns messages similar to ``django.core.context_processors.auth``.""" 3 | if hasattr(request, 'facebook') and request.facebook.uid is not None: 4 | from models import Message 5 | messages = Message.objects.get_and_delete_all(uid=request.facebook.uid) 6 | return {'messages': messages} 7 | -------------------------------------------------------------------------------- /facebook/djangofb/default_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sciyoshi/pyfacebook/ec2bbfb0c687f2775ae89e1c5201688ed6e69b8e/facebook/djangofb/default_app/__init__.py -------------------------------------------------------------------------------- /facebook/djangofb/default_app/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # get_facebook_client lets us get the current Facebook object 4 | # from outside of a view, which lets us have cleaner code 5 | from facebook.djangofb import get_facebook_client 6 | 7 | 8 | def _2int(d, k): 9 | try: 10 | d = d.__dict__ 11 | except: 12 | pass 13 | 14 | t = d.get(k, '') 15 | if t == 'None': 16 | t = 0 17 | else: 18 | t = int(t) 19 | return t 20 | 21 | 22 | class UserManager(models.Manager): 23 | """Custom manager for a Facebook User.""" 24 | 25 | def get_current(self): 26 | """Gets a User object for the logged-in Facebook user.""" 27 | facebook = get_facebook_client() 28 | user, created = self.get_or_create(id=_2int(facebook, 'uid')) 29 | if created: 30 | # we could do some custom actions for new users here... 31 | pass 32 | return user 33 | 34 | 35 | class User(models.Model): 36 | """A simple User model for Facebook users.""" 37 | 38 | # We use the user's UID as the primary key in our database. 39 | id = models.IntegerField(primary_key=True) 40 | 41 | # TODO: The data that you want to store for each user would go here. 42 | # For this sample, we let users let people know their favorite progamming 43 | # language, in the spirit of Extended Info. 44 | language = models.CharField(max_length=64, default='Python') 45 | 46 | # Add the custom manager 47 | objects = UserManager() 48 | -------------------------------------------------------------------------------- /facebook/djangofb/default_app/templates/canvas.fbml: -------------------------------------------------------------------------------- 1 | 2 | {% comment %} 3 | We can use {{ fbuser }} to get at the current user. 4 | {{ fbuser.id }} will be the user's UID, and {{ fbuser.language }} 5 | is his/her favorite language (Python :-). 6 | {% endcomment %} 7 | Welcome, ! 8 | 9 | 10 |
11 | Your favorite language is {{ fbuser.language|escape }}. 12 |

13 | 14 |
15 |

16 | 17 |
18 | 19 | 20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /facebook/djangofb/default_app/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | urlpatterns = patterns('{{ project }}.{{ app }}.views', 4 | (r'^$', 'canvas'), 5 | # Define other pages you want to create here 6 | ) 7 | -------------------------------------------------------------------------------- /facebook/djangofb/default_app/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.views.generic.simple import direct_to_template 3 | # uncomment the following two lines and the one below 4 | # if you dont want to use a decorator instead of the middleware 5 | # from django.utils.decorators import decorator_from_middleware 6 | # from facebook.djangofb import FacebookMiddleware 7 | 8 | # Import the Django helpers 9 | import facebook.djangofb as facebook 10 | 11 | # The User model defined in models.py 12 | from models import User 13 | 14 | # We'll require login for our canvas page. This 15 | # isn't necessarily a good idea, as we might want 16 | # to let users see the page without granting our app 17 | # access to their info. See the wiki for details on how 18 | # to do this. 19 | # @decorator_from_middleware(FacebookMiddleware) 20 | 21 | 22 | @facebook.require_login() 23 | def canvas(request): 24 | # Get the User object for the currently logged in user 25 | user = User.objects.get_current() 26 | 27 | # Check if we were POSTed the user's new language of choice 28 | if 'language' in request.POST: 29 | user.language = request.POST['language'][:64] 30 | user.save() 31 | 32 | # User is guaranteed to be logged in, so pass canvas.fbml 33 | # an extra 'fbuser' parameter that is the User object for 34 | # the currently logged in user. 35 | return direct_to_template( 36 | request, 37 | 'canvas.fbml', 38 | extra_context={ 39 | 'fbuser': user}) 40 | 41 | 42 | @facebook.require_login() 43 | def ajax(request): 44 | return HttpResponse('hello world') 45 | -------------------------------------------------------------------------------- /facebook/djangofb/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.html import escape 3 | from django.utils.safestring import mark_safe 4 | 5 | FB_MESSAGE_STATUS = ( 6 | (0, 'Explanation'), 7 | (1, 'Error'), 8 | (2, 'Success'), 9 | ) 10 | 11 | 12 | class MessageManager(models.Manager): 13 | 14 | def get_and_delete_all(self, uid): 15 | messages = [] 16 | for m in self.filter(uid=uid): 17 | messages.append(m) 18 | m.delete() 19 | return messages 20 | 21 | 22 | class Message(models.Model): 23 | """Represents a message for a Facebook user.""" 24 | uid = models.CharField(max_length=25) 25 | status = models.IntegerField(choices=FB_MESSAGE_STATUS) 26 | message = models.CharField(max_length=300) 27 | objects = MessageManager() 28 | 29 | def __unicode__(self): 30 | return self.message 31 | 32 | def _fb_tag(self): 33 | return self.get_status_display().lower() 34 | 35 | def as_fbml(self): 36 | return mark_safe(u'' % ( 37 | self._fb_tag(), 38 | escape(self.message), 39 | )) 40 | -------------------------------------------------------------------------------- /facebook/webappfb.py: -------------------------------------------------------------------------------- 1 | # 2 | # webappfb - Facebook tools for Google's AppEngine "webapp" Framework 3 | # 4 | # Copyright (c) 2009, Max Battcher 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # * Redistributions of source code must retain the above copyright 10 | # notice, this list of conditions and the following disclaimer. 11 | # * Redistributions in binary form must reproduce the above copyright 12 | # notice, this list of conditions and the following disclaimer in the 13 | # documentation and/or other materials provided with the distribution. 14 | # * Neither the name of the author nor the names of its contributors may 15 | # be used to endorse or promote products derived from this software 16 | # without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS``AS IS'' AND ANY 19 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | # DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY 22 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | from google.appengine.api import memcache 29 | from google.appengine.ext.webapp import RequestHandler 30 | from facebook import Facebook 31 | import yaml 32 | 33 | """ 34 | Facebook tools for Google AppEngine's object-oriented "webapp" framework. 35 | """ 36 | 37 | # This global configuration dictionary is for configuration variables 38 | # for Facebook requests such as the application's API key and secret 39 | # key. Defaults to loading a 'facebook.yaml' YAML file. This should be 40 | # useful and familiar for most AppEngine development. 41 | FACEBOOK_CONFIG = yaml.load(file('facebook.yaml', 'r')) 42 | 43 | 44 | class FacebookRequestHandler(RequestHandler): 45 | """ 46 | Base class for request handlers for Facebook apps, providing useful 47 | Facebook-related tools: a local 48 | """ 49 | 50 | def _fbconfig_value(self, name, default=None): 51 | """ 52 | Checks the global config dictionary and then for a class/instance 53 | variable, using a provided default if no value is found. 54 | """ 55 | if name in FACEBOOK_CONFIG: 56 | default = FACEBOOK_CONFIG[name] 57 | 58 | return getattr(self, name, default) 59 | 60 | def initialize(self, request, response): 61 | """ 62 | Initialize's this request's Facebook client. 63 | """ 64 | super(FacebookRequestHandler, self).initialize(request, response) 65 | 66 | app_name = self._fbconfig_value('app_name', '') 67 | api_key = self._fbconfig_value('api_key', None) 68 | secret_key = self._fbconfig_value('secret_key', None) 69 | 70 | self.facebook = Facebook(api_key, secret_key, 71 | app_name=app_name) 72 | 73 | require_app = self._fbconfig_value('require_app', False) 74 | require_login = self._fbconfig_value('require_login', False) 75 | need_session = self._fbconfig_value('need_session', False) 76 | check_session = self._fbconfig_value('check_session', True) 77 | 78 | self._messages = None 79 | self.redirecting = False 80 | 81 | if require_app or require_login: 82 | if not self.facebook.check_session(request): 83 | self.redirect(self.facebook.get_login_url(next=request.url)) 84 | self.redirecting = True 85 | return 86 | elif check_session: 87 | self.facebook.check_session(request) # ignore response 88 | 89 | # NOTE: require_app is deprecated according to modern Facebook login 90 | # policies. Included for completeness, but unnecessary. 91 | if require_app and not self.facebook.added: 92 | self.redirect(self.facebook.get_add_url(next=request.url)) 93 | self.redirecting = True 94 | return 95 | 96 | if not (require_app or require_login) and need_session: 97 | self.facebook.auth.getSession() 98 | 99 | def redirect(self, url, **kwargs): 100 | """ 101 | For Facebook canvas pages we should use instead of 102 | a normal redirect. 103 | """ 104 | if self.facebook.in_canvas: 105 | self.response.clear() 106 | self.response.out.write('' % (url, )) 107 | else: 108 | super(FacebookRequestHandler, self).redirect(url, **kwargs) 109 | 110 | def add_user_message(self, kind, msg, detail='', time=15 * 60): 111 | """ 112 | Add a message to the current user to memcache. 113 | """ 114 | if self.facebook.uid: 115 | key = 'messages:%s' % self.facebook.uid 116 | self._messages = memcache.get(key) 117 | message = { 118 | 'kind': kind, 119 | 'message': msg, 120 | 'detail': detail, 121 | } 122 | if self._messages is not None: 123 | self._messages.append(message) 124 | else: 125 | self._messages = [message] 126 | memcache.set(key, self._messages, time=time) 127 | 128 | def get_and_delete_user_messages(self): 129 | """ 130 | Get all of the messages for the current user; removing them. 131 | """ 132 | if self.facebook.uid: 133 | key = 'messages:%s' % self.facebook.uid 134 | if not hasattr(self, '_messages') or self._messages is None: 135 | self._messages = memcache.get(key) 136 | memcache.delete(key) 137 | return self._messages 138 | return None 139 | 140 | 141 | class FacebookCanvasHandler(FacebookRequestHandler): 142 | """ 143 | Request handler for Facebook canvas (FBML application) requests. 144 | """ 145 | 146 | def canvas(self, *args, **kwargs): 147 | """ 148 | This will be your handler to deal with Canvas requests. 149 | """ 150 | raise NotImplementedError() 151 | 152 | def get(self, *args): 153 | """ 154 | All valid canvas views are POSTS. 155 | """ 156 | # TODO: Attempt to auto-redirect to Facebook canvas? 157 | self.error(404) 158 | 159 | def post(self, *args, **kwargs): 160 | """ 161 | Check a couple of simple safety checks and then call the canvas 162 | handler. 163 | """ 164 | if self.redirecting: 165 | return 166 | 167 | if not self.facebook.in_canvas: 168 | self.error(404) 169 | return 170 | 171 | self.canvas(*args, **kwargs) 172 | 173 | # vim: ai et ts=4 sts=4 sw=4 174 | -------------------------------------------------------------------------------- /facebook/wsgi.py: -------------------------------------------------------------------------------- 1 | """This is some simple helper code to bridge the Pylons / PyFacebook gap. 2 | 3 | There's some generic WSGI middleware, some Paste stuff, and some Pylons 4 | stuff. Once you put FacebookWSGIMiddleware into your middleware stack, 5 | you'll have access to ``environ["pyfacebook.facebook"]``, which is a 6 | ``facebook.Facebook`` object. If you're using Paste (which includes 7 | Pylons users), you can also access this directly using the facebook 8 | global in this module. 9 | 10 | """ 11 | 12 | # Be careful what you import. Don't expect everyone to have Pylons, 13 | # Paste, etc. installed. Degrade gracefully. 14 | 15 | from facebook import Facebook 16 | 17 | __docformat__ = "restructuredtext" 18 | 19 | 20 | # Setup Paste, if available. This needs to stay in the same module as 21 | # FacebookWSGIMiddleware below. 22 | 23 | try: 24 | from paste.registry import StackedObjectProxy 25 | from webob.exc import _HTTPMove 26 | try: 27 | from string import Template 28 | except ImportError: 29 | from webob.util.stringtemplate import Template 30 | from webob import html_escape 31 | 32 | except ImportError: 33 | pass 34 | else: 35 | facebook = StackedObjectProxy(name="PyFacebook Facebook Connection") 36 | 37 | class CanvasRedirect(_HTTPMove): 38 | """This is for canvas redirects.""" 39 | 40 | title = "See Other" 41 | code = 200 42 | html_template_obj = Template('') 43 | 44 | def html_body(self, environ): 45 | return self.html_template_obj.substitute(location=self.detail) 46 | 47 | 48 | class FacebookWSGIMiddleware(object): 49 | 50 | """This is WSGI middleware for Facebook.""" 51 | 52 | def __init__(self, app, config, facebook_class=Facebook): 53 | """Initialize the Facebook middleware. 54 | 55 | ``app`` 56 | This is the WSGI application being wrapped. 57 | 58 | ``config`` 59 | This is a dict containing the keys "pyfacebook.apikey" and 60 | "pyfacebook.secret". 61 | 62 | ``facebook_class`` 63 | If you want to subclass the Facebook class, you can pass in 64 | your replacement here. Pylons users will want to use 65 | PylonsFacebook. 66 | 67 | """ 68 | self.app = app 69 | self.config = config 70 | self.facebook_class = facebook_class 71 | 72 | def __call__(self, environ, start_response): 73 | config = self.config 74 | real_facebook = self.facebook_class(config["pyfacebook.apikey"], 75 | config["pyfacebook.secret"]) 76 | registry = environ.get('paste.registry') 77 | if registry: 78 | registry.register(facebook, real_facebook) 79 | environ['pyfacebook.facebook'] = real_facebook 80 | return self.app(environ, start_response) 81 | 82 | 83 | # The remainder is Pylons specific. 84 | 85 | try: 86 | import pylons 87 | from pylons.controllers.util import redirect_to as pylons_redirect_to 88 | from routes import url_for 89 | except ImportError: 90 | pass 91 | else: 92 | 93 | class PylonsFacebook(Facebook): 94 | 95 | """Subclass Facebook to add Pylons goodies.""" 96 | 97 | def check_session(self, request=None): 98 | """The request parameter is now optional.""" 99 | if request is None: 100 | request = pylons.request 101 | return Facebook.check_session(self, request) 102 | 103 | # The Django request object is similar enough to the Paste 104 | # request object that check_session and validate_signature 105 | # should *just work*. 106 | 107 | def redirect_to(self, url): 108 | """Wrap Pylons' redirect_to function so that it works in_canvas. 109 | 110 | By the way, this won't work until after you call 111 | check_session(). 112 | 113 | """ 114 | if self.in_canvas: 115 | raise CanvasRedirect(url) 116 | pylons_redirect_to(url) 117 | 118 | def apps_url_for(self, *args, **kargs): 119 | """Like url_for, but starts with "http://apps.facebook.com".""" 120 | return "http://apps.facebook.com" + url_for(*args, **kargs) 121 | 122 | def create_pylons_facebook_middleware(app, config): 123 | """This is a simple wrapper for FacebookWSGIMiddleware. 124 | 125 | It passes the correct facebook_class. 126 | 127 | """ 128 | return FacebookWSGIMiddleware(app, config, 129 | facebook_class=PylonsFacebook) 130 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | 5 | setup(name='pyfacebook', 6 | version='1.0a2.1', 7 | description='Python Client Library for the Facebook API', 8 | author='Samuel Cormier-Iijima', 9 | author_email='sciyoshi@gmail.com', 10 | url='http://code.google.com/p/pyfacebook', 11 | packages=['facebook', 12 | 'facebook.djangofb', 13 | 'facebook.djangofb.default_app'], 14 | test_suite='tests', 15 | tests_require=['MiniMock']) 16 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sciyoshi/pyfacebook/ec2bbfb0c687f2775ae89e1c5201688ed6e69b8e/tests/__init__.py -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | import os 4 | import facebook 5 | import urllib2 6 | try: 7 | from hashlib import md5 8 | md5_constructor = md5 9 | except ImportError: 10 | import md5 11 | md5_constructor = md5.new 12 | try: 13 | import simplejson 14 | except ImportError: 15 | from django.utils import simplejson 16 | import httplib 17 | from minimock import Mock 18 | 19 | my_api_key = "e1e9cfeb5e0d7a52e4fbd5d09e1b873e" 20 | my_secret_key = "1bebae7283f5b79aaf9b851addd55b90" 21 | # '{"error_code":100,\ 22 | # "error_msg":"Invalid parameter",\ 23 | # "request_args":[{"key":"format","value":"JSON"},\ 24 | # {"key":"auth_token","value":"24626e24bb12919f2f142145070542e8"},\ 25 | # {"key":"sig","value":"36af2af3b93da784149301e77cb1621a"},\ 26 | # {"key":"v","value":"1.0"},\ 27 | # {"key":"api_key","value":"e1e9cfeb5e0d7a52e4fbd5d09e1b873e"},\ 28 | # {"key":"method","value":"facebook.auth.getSession"}]}' 29 | response_str = '{"stuff":"abcd"}' 30 | 31 | 32 | class MyUrlOpen: 33 | 34 | def __init__(self, *args, **kwargs): 35 | pass 36 | 37 | def read(self): 38 | global response_str 39 | return response_str 40 | 41 | 42 | class pyfacebook_UnitTests(unittest.TestCase): 43 | 44 | def setUp(self): 45 | facebook.urllib2.urlopen = Mock('urllib2.urlopen') 46 | facebook.urllib2.urlopen.mock_returns_func = MyUrlOpen 47 | pass 48 | 49 | def tearDown(self): 50 | pass 51 | 52 | def login(self): 53 | pass 54 | 55 | def test1(self): 56 | f = facebook.Facebook(api_key=my_api_key, secret_key=my_secret_key) 57 | f.login = self.login 58 | self.assertEquals(f.api_key, my_api_key) 59 | self.assertEquals(f.secret_key, my_secret_key) 60 | self.assertEquals(f.auth_token, None) 61 | self.assertEquals(f.app_name, None) 62 | self.assertEquals(f.callback_path, None) 63 | self.assertEquals(f.internal, None) 64 | 65 | def test2(self): 66 | args = {"arg1": "a", "arg2": "b", "arg3": "c"} 67 | hasher = md5_constructor( 68 | ''.join(['%s=%s' % (x, args[x]) for x in sorted(args.keys())])) 69 | hasher.update("acdnj") 70 | f = facebook.Facebook(api_key="abcdf", secret_key="acdnj") 71 | f.login = self.login 72 | digest = f._hash_args(args) 73 | self.assertEquals(hasher.hexdigest(), digest) 74 | hasher = md5_constructor( 75 | ''.join(['%s=%s' % (x, args[x]) for x in sorted(args.keys())])) 76 | hasher.update("klmn") 77 | # trunk code has error hash.updated instead of hash.update 78 | digest = f._hash_args(args, secret="klmn") 79 | self.assertEquals(hasher.hexdigest(), digest) 80 | 81 | hasher = md5_constructor( 82 | ''.join(['%s=%s' % (x, args[x]) for x in sorted(args.keys())])) 83 | f.secret = "klmn" 84 | hasher.update(f.secret) 85 | # trunk code has error hash.updated instead of hash.update 86 | digest = f._hash_args(args) 87 | self.assertEquals(hasher.hexdigest(), digest) 88 | 89 | def test3(self): 90 | global response_str 91 | response = {'stuff': 'abcd'} 92 | response_str = simplejson.dumps(response) 93 | fb = facebook.Facebook(my_api_key, my_secret_key) 94 | fb.login = self.login 95 | fb.auth.createToken() 96 | self.assertEquals(str(fb.auth_token['stuff']), "abcd") 97 | fb.login() 98 | response = { 99 | "session_key": "key", 100 | "uid": "my_uid", 101 | "secret": "my_secret", 102 | "expires": "my_expires"} 103 | response_str = simplejson.dumps(response) 104 | res = fb.auth.getSession() 105 | self.assertEquals(str(res["expires"]), response["expires"]) 106 | self.assertEquals(str(res["secret"]), response["secret"]) 107 | self.assertEquals(str(res["session_key"]), response["session_key"]) 108 | self.assertEquals(str(res["uid"]), response["uid"]) 109 | 110 | def test4(self): 111 | global response_str 112 | response = 'abcdef' 113 | response_str = simplejson.dumps(response) 114 | fb = facebook.Facebook(my_api_key, my_secret_key) 115 | fb.login = self.login 116 | fb.auth.createToken() 117 | self.assertEquals(str(fb.auth_token), "abcdef") 118 | url = fb.get_login_url(next="nowhere", popup=True, canvas=True) 119 | self.assertEquals( 120 | url, 121 | 'http://www.facebook.com/login.php?canvas=1&popup=1&auth_token=abcdef&next=nowhere&v=1.0&api_key=%s' % 122 | (my_api_key, 123 | )) 124 | 125 | def test5(self): 126 | class Request: 127 | 128 | def __init__(self, post, get, method): 129 | self.POST = post 130 | self.GET = get 131 | self.method = method 132 | 133 | req = Request({'fb_sig_in_canvas': 1}, {}, 'POST') 134 | fb = facebook.Facebook(my_api_key, my_secret_key) 135 | fb.login = self.login 136 | res = fb.check_session(req) 137 | self.assertFalse(res) 138 | req = Request({'fb_sig': 1}, {}, 'POST') 139 | res = fb.check_session(req) 140 | self.assertFalse(res) 141 | req = Request({'fb_sig': fb._hash_args({'in_canvas': '1', 142 | 'added': '1', 143 | 'expires': '1', 144 | 'friends': 'joe,mary', 145 | 'session_key': 'abc', 146 | 'user': 'bob'}), 147 | 'fb_sig_in_canvas': '1', 148 | 'fb_sig_added': '1', 149 | 'fb_sig_expires': '1', 150 | 'fb_sig_friends': 'joe,mary', 151 | 'fb_sig_session_key': 'abc', 152 | 'fb_sig_user': 'bob'}, 153 | {}, 'POST') 154 | res = fb.check_session(req) 155 | self.assertTrue(res) 156 | fb = facebook.Facebook(my_api_key, my_secret_key) 157 | fb.login = self.login 158 | req = Request({'fb_sig': fb._hash_args({'in_canvas': '1', 159 | 'added': '1', 160 | 'expires': '1', 161 | 'friends': '', 162 | 'session_key': 'abc', 163 | 'user': 'bob'}), 164 | 'fb_sig_in_canvas': '1', 165 | 'fb_sig_added': '1', 166 | 'fb_sig_expires': '1', 167 | 'fb_sig_friends': '', 168 | 'fb_sig_session_key': 'abc', 169 | 'fb_sig_user': 'bob'}, 170 | {}, 'POST') 171 | res = fb.check_session(req) 172 | self.assertTrue(res) 173 | fb = facebook.Facebook(my_api_key, my_secret_key) 174 | fb.login = self.login 175 | req = Request({'fb_sig': fb._hash_args({'in_canvas': '1', 176 | 'added': '1', 177 | 'expires': '1', 178 | 'friends': '', 179 | 'session_key': 'abc', 180 | 'page_id': 'id'}), 181 | 'fb_sig_in_canvas': '1', 182 | 'fb_sig_added': '1', 183 | 'fb_sig_expires': '1', 184 | 'fb_sig_friends': '', 185 | 'fb_sig_session_key': 'abc', 186 | 'fb_sig_page_id': 'id'}, 187 | {}, 'POST') 188 | res = fb.check_session(req) 189 | self.assertTrue(res) 190 | 191 | def test6(self): 192 | global response_str 193 | response = 'abcdef' 194 | response_str = simplejson.dumps(response) 195 | fb = facebook.Facebook(my_api_key, my_secret_key) 196 | fb.login = self.login 197 | fb.auth.createToken() 198 | # self.failUnlessRaises(RuntimeError,fb._add_session_args) 199 | response = { 200 | "session_key": "key", 201 | "uid": "my_uid", 202 | "secret": "my_secret", 203 | "expires": "my_expires"} 204 | response_str = simplejson.dumps(response) 205 | fb.auth.getSession() 206 | args = fb._add_session_args() 207 | 208 | def test7(self): 209 | global response_str 210 | response = 'abcdef' 211 | response_str = simplejson.dumps(response) 212 | fb = facebook.Facebook(my_api_key, my_secret_key) 213 | fb.login = self.login 214 | fb.auth.createToken() 215 | self.assertEquals(str(fb.auth_token), "abcdef") 216 | url = fb.get_authorize_url(next="next", next_cancel="next_cancel") 217 | self.assertEquals( 218 | url, 219 | 'http://www.facebook.com/authorize.php?api_key=%s&next_cancel=next_cancel&v=1.0&next=next' % 220 | (my_api_key, 221 | )) 222 | 223 | def test8(self): 224 | class Request: 225 | 226 | def __init__(self, post, get, method): 227 | self.POST = post 228 | self.GET = get 229 | self.method = method 230 | 231 | global response_str 232 | response = { 233 | "session_key": "abcdef", 234 | "uid": "my_uid", 235 | "secret": "my_secret", 236 | "expires": "my_expires"} 237 | response_str = simplejson.dumps(response) 238 | req = Request({}, {'installed': 1, 'fb_page_id': 'id', 239 | 'auth_token': 'abcdef'}, 'GET') 240 | fb = facebook.Facebook(my_api_key, my_secret_key) 241 | fb.login = self.login 242 | res = fb.check_session(req) 243 | self.assertTrue(res) 244 | 245 | def test9(self): 246 | global response_str 247 | response = 'abcdef' 248 | response_str = simplejson.dumps(response) 249 | fb = facebook.Facebook(my_api_key, my_secret_key) 250 | fb.login = self.login 251 | fb.auth.createToken() 252 | self.assertEquals(str(fb.auth_token), "abcdef") 253 | url = fb.get_add_url(next="next") 254 | self.assertEquals( 255 | url, 'http://www.facebook.com/install.php?api_key=%s&v=1.0&next=next' % 256 | (my_api_key,)) 257 | 258 | def send(self, xml): 259 | self.xml = xml 260 | 261 | def test10(self): 262 | import Image 263 | image1 = Image.new("RGB", (400, 300), (255, 255, 255)) 264 | filename = "image_file.jpg" 265 | image1.save(filename) 266 | global response_str 267 | fb = facebook.Facebook(my_api_key, my_secret_key) 268 | fb.login = self.login 269 | 270 | facebook.httplib.HTTP = Mock('httplib.HTTP') 271 | http_connection = Mock('http_connection') 272 | facebook.httplib.HTTP.mock_returns = http_connection 273 | http_connection.send.mock_returns_func = self.send 274 | 275 | def _http_passes(): 276 | return [200, ] 277 | http_connection.getreply.mock_returns_func = _http_passes 278 | 279 | def read(): 280 | response = {"stuff": "stuff"} 281 | response_str = simplejson.dumps(response) 282 | return response_str 283 | http_connection.file.read.mock_returns_func = read 284 | 285 | response = { 286 | "session_key": "key", 287 | "uid": "my_uid", 288 | "secret": "my_secret", 289 | "expires": "my_expires"} 290 | response_str = simplejson.dumps(response) 291 | res = fb.auth.getSession() 292 | result = fb.photos.upload( 293 | image=filename, 294 | aid="aid", 295 | caption="a caption") 296 | self.assertEquals(str(result["stuff"]), "stuff") 297 | os.remove(filename) 298 | 299 | if __name__ == "__main__": 300 | 301 | # Build the test suite 302 | suite = unittest.TestSuite() 303 | suite.addTest(unittest.makeSuite(pyfacebook_UnitTests)) 304 | 305 | # Execute the test suite 306 | print("Testing Proxy class\n") 307 | result = unittest.TextTestRunner(verbosity=2).run(suite) 308 | sys.exit(len(result.errors) + len(result.failures)) 309 | --------------------------------------------------------------------------------