├── examples ├── pyfacebook_sample │ ├── __init__.py │ ├── models.py │ ├── templates │ │ └── facebook │ │ │ └── canvas.fbml │ ├── urls.py │ ├── README │ └── views.py └── examples.py ├── facebook ├── djangofb │ ├── default_app │ │ ├── __init__.py │ │ ├── urls.py │ │ ├── templates │ │ │ └── canvas.fbml │ │ ├── views.py │ │ └── models.py │ ├── context_processors.py │ ├── models.py │ └── __init__.py ├── wsgi.py └── __init__.py ├── MANIFEST.in ├── setup.py ├── README ├── posediff ├── bin └── djangofb.py └── tests ├── minimock.py └── test.py /examples/pyfacebook_sample/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/pyfacebook_sample/models.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /facebook/djangofb/default_app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include examples * 2 | -------------------------------------------------------------------------------- /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 | 8 | -------------------------------------------------------------------------------- /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} -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | 5 | setup(name='pyfacebook', 6 | version='0.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', 'facebook.djangofb']) 12 | -------------------------------------------------------------------------------- /examples/pyfacebook_sample/templates/facebook/canvas.fbml: -------------------------------------------------------------------------------- 1 | 2 | Welcome, {{ name }}! 3 | 4 | 5 |
6 | This is the sample PyFaceBook application. 7 |

8 | 9 |
10 | Display something on your profile: 11 |

12 | 13 |
14 | 15 | 16 |
17 |
18 |
19 | -------------------------------------------------------------------------------- /examples/pyfacebook_sample/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | # Hack to get the project name 4 | project = __name__.split('.')[0] 5 | 6 | # You'd want to change this to wherever your app lives 7 | urlpatterns = patterns(project + '.pyfacebook_sample.views', 8 | # Some functionality - users can post text to their homepage 9 | (r'^canvas/post/', 'post'), 10 | 11 | # For the mock AJAX functionality 12 | (r'^canvas/ajax/', 'ajax'), 13 | 14 | # This is the canvas callback, i.e. what will be seen 15 | # when you visit http://apps.facebook.com/. 16 | (r'^canvas/', 'canvas'), 17 | 18 | # Extra callbacks can be set in the Facebook app settings 19 | # page. For example, post_add will be called when a user 20 | # has added the application. 21 | (r'^post_add/', 'post_add'), 22 | 23 | ) 24 | -------------------------------------------------------------------------------- /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/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 | class MessageManager(models.Manager): 12 | def get_and_delete_all(self, uid): 13 | messages = [] 14 | for m in self.filter(uid=uid): 15 | messages.append(m) 16 | m.delete() 17 | return messages 18 | 19 | class Message(models.Model): 20 | """Represents a message for a Facebook user.""" 21 | uid = models.CharField(max_length=25) 22 | status = models.IntegerField(choices=FB_MESSAGE_STATUS) 23 | message = models.CharField(max_length=300) 24 | objects = MessageManager() 25 | 26 | def __unicode__(self): 27 | return self.message 28 | 29 | def _fb_tag(self): 30 | return self.get_status_display().lower() 31 | 32 | def as_fbml(self): 33 | return mark_safe(u'' % ( 34 | self._fb_tag(), 35 | escape(self.message), 36 | )) 37 | -------------------------------------------------------------------------------- /facebook/djangofb/default_app/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.views.generic.simple import direct_to_template 3 | 4 | # Import the Django helpers 5 | import facebook.djangofb as facebook 6 | 7 | # The User model defined in models.py 8 | from models import User 9 | 10 | # We'll require login for our canvas page. This 11 | # isn't necessarily a good idea, as we might want 12 | # to let users see the page without granting our app 13 | # access to their info. See the wiki for details on how 14 | # to do this. 15 | @facebook.require_login() 16 | def canvas(request): 17 | # Get the User object for the currently logged in user 18 | user = User.objects.get_current() 19 | 20 | # Check if we were POSTed the user's new language of choice 21 | if 'language' in request.POST: 22 | user.language = request.POST['language'][:64] 23 | user.save() 24 | 25 | # User is guaranteed to be logged in, so pass canvas.fbml 26 | # an extra 'fbuser' parameter that is the User object for 27 | # the currently logged in user. 28 | return direct_to_template(request, 'canvas.fbml', extra_context={'fbuser': user}) 29 | 30 | @facebook.require_login() 31 | def ajax(request): 32 | return HttpResponse('hello world') 33 | -------------------------------------------------------------------------------- /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 | class UserManager(models.Manager): 8 | """Custom manager for a Facebook User.""" 9 | 10 | def get_current(self): 11 | """Gets a User object for the logged-in Facebook user.""" 12 | facebook = get_facebook_client() 13 | user, created = self.get_or_create(id=int(facebook.uid)) 14 | if created: 15 | # we could do some custom actions for new users here... 16 | pass 17 | return user 18 | 19 | class User(models.Model): 20 | """A simple User model for Facebook users.""" 21 | 22 | # We use the user's UID as the primary key in our database. 23 | id = models.IntegerField(primary_key=True) 24 | 25 | # TODO: The data that you want to store for each user would go here. 26 | # For this sample, we let users let people know their favorite progamming 27 | # language, in the spirit of Extended Info. 28 | language = models.CharField(maxlength=64, default='Python') 29 | 30 | # Add the custom manager 31 | objects = UserManager() 32 | -------------------------------------------------------------------------------- /examples/pyfacebook_sample/README: -------------------------------------------------------------------------------- 1 | This is a sample Django application for PyFacebook. 2 | 3 | To use this application, copy the entire folder to an existing 4 | Django application. Then you need to edit the following settings: 5 | 6 | * In settings.py, make sure that TEMPLATE_LOADERS contains 7 | 'django.template.loaders.app_directories.load_template_source'. This 8 | is so that templates can be loaded from this template directory. 9 | 10 | * In settings.py, define FACEBOOK_API_KEY and FACEBOOK_SECRET_KEY to your 11 | own values. 12 | 13 | * In settings.py, add the following line to the variable MIDDLEWARE_CLASSES: 14 | 'facebook.djangofb.FacebookMiddleware'. This will attach a facebook object 15 | to every incoming request. 16 | 17 | * In urls.py, have something in your urlpatterns like: 18 | (r'^facebook/', include('YOUR_PROJECT_NAME.pyfacebook_sample.urls')), 19 | This will tell the sample application to live under /facebook/. 20 | 21 | * On the Facebook applications page, make sure that you set your callback 22 | to the appropriate URL. In this example, it would be 23 | 'http://YOUR_IP/facebook/canvas/', and DON'T FORGET THE TRAILING SLASH :-) 24 | 25 | * Change any occurrences of pyfacebook to your own application name. 26 | 27 | That should be about it... 28 | -------------------------------------------------------------------------------- /examples/pyfacebook_sample/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse, HttpResponseRedirect 2 | from django.shortcuts import render_to_response 3 | 4 | import facebook.djangofb as facebook 5 | 6 | @facebook.require_login() 7 | def canvas(request): 8 | # If you're using FBML, it'd be better to use than to do this - this is just as an example 9 | values = request.facebook.users.getInfo([request.facebook.uid], ['first_name', 'is_app_user', 'has_added_app'])[0] 10 | 11 | name, is_app_user, has_added_app = values['first_name'], values['is_app_user'], values['has_added_app'] 12 | 13 | if has_added_app == '0': 14 | return request.facebook.redirect(request.facebook.get_add_url()) 15 | 16 | return render_to_response('facebook/canvas.fbml', {'name': name}) 17 | 18 | @facebook.require_login() 19 | def post(request): 20 | request.facebook.profile.setFBML(request.POST['profile_text'], request.facebook.uid) 21 | 22 | return request.facebook.redirect(request.facebook.get_url('profile', id=request.facebook.uid)) 23 | 24 | @facebook.require_login() 25 | def post_add(request): 26 | request.facebook.profile.setFBML(uid=request.facebook.uid, profile='Congratulations on adding PyFaceBook. Please click on the PyFaceBook link on the left side to change this text.') 27 | 28 | return request.facebook.redirect('http://apps.facebook.com/pyfacebook/') 29 | 30 | @facebook.require_login() 31 | def ajax(request): 32 | return HttpResponse('hello world') 33 | -------------------------------------------------------------------------------- /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 | 18 | http://pyfacebook.googlecode.com/ 19 | 20 | 21 | == Usage == 22 | 23 | To use this in your own projects, do the standard: 24 | 25 | python setup.py install 26 | 27 | 28 | == Documentation == 29 | 30 | Have a look at the examples/ directory. Most of the stuff should be 31 | self-explanatory. There is also an example Django app in 32 | examples/facebook, which should have enough to get you started. 33 | 34 | See the developer wiki for more information. 35 | 36 | 37 | == License == 38 | 39 | Copyright (c) 2008, Samuel Cormier-Iijima 40 | All rights reserved. 41 | 42 | Redistribution and use in source and binary forms, with or without 43 | modification, are permitted provided that the following conditions are met: 44 | * Redistributions of source code must retain the above copyright 45 | notice, this list of conditions and the following disclaimer. 46 | * Redistributions in binary form must reproduce the above copyright 47 | notice, this list of conditions and the following disclaimer in the 48 | documentation and/or other materials provided with the distribution. 49 | * Neither the name of the author nor the names of its contributors may 50 | be used to endorse or promote products derived from this software 51 | without specific prior written permission. 52 | 53 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS``AS IS'' AND ANY 54 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 55 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 56 | DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY 57 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 58 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 59 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 60 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 61 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 62 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 63 | 64 | -------------------------------------------------------------------------------- /examples/examples.py: -------------------------------------------------------------------------------- 1 | # ----------------------- 2 | # Web application example 3 | # ----------------------- 4 | 5 | def simple_web_app(request, api_key, secret_key): 6 | fb = Facebook(api_key, secret_key, request.GET['auth_token']) 7 | fb.auth.getSession() 8 | 9 | friend_ids = fb.friends.get() 10 | info = fb.users.getInfo(friend_ids, ['name', 'pic']) 11 | 12 | print '' 13 | for friend in info: 14 | print '%(name)s' % friend 15 | print '' 16 | 17 | def web_app(request): 18 | """Get the user's friends and their pictures. This example uses 19 | the Django web framework, but should be adaptable to others.""" 20 | 21 | # Get api_key and secret_key from a file 22 | fb_file = open('facebook_keys.txt').readlines() 23 | api_key = fb_file[0].strip() 24 | secret_key = fb_file[1].strip() 25 | fb = Facebook(api_key, secret_key) 26 | 27 | # Use the data from the cookie if present 28 | if 'session_key' in request.session and 'uid' in request.session: 29 | fb.session_key = request.session['session_key'] 30 | fb.uid = request.session['uid'] 31 | else: 32 | 33 | try: 34 | fb.auth_token = request.GET['auth_token'] 35 | except KeyError: 36 | # Send user to the Facebook to login 37 | return HttpResponseRedirect(fb.get_login_url()) 38 | 39 | # getSession sets the session_key and uid 40 | # Store these in the cookie so we don't have to get them again 41 | fb.auth.getSession() 42 | request.session['session_key'] = fb.session_key 43 | request.session['uid'] = fb.uid 44 | 45 | try: 46 | friend_ids = fb.friends.get() 47 | except FacebookError, e: 48 | # Error 102 means the session has expired. 49 | # Delete the cookie and send the user to Facebook to login 50 | if e.info['code'] == u'102': 51 | del request.session['session_key'] 52 | del request.session['uid'] 53 | return HttpResponseRedirect(fb.get_login_url()) 54 | else: 55 | # Other Facebook errors are possible too. Don't ignore them. 56 | raise 57 | 58 | info = fb.users.getInfo(friend_ids, ['name', 'pic']) 59 | # info is a list of dictionaries 60 | 61 | # you would never do this in an actual Django application, 62 | # it's just an example of accessing the results. 63 | links = [] 64 | for friend in info: 65 | html = '%(name)s' % friend 66 | links.append(html) 67 | 68 | return render_to_response('template.html', {'links': links}) 69 | 70 | # --------------------------- 71 | # Desktop application example 72 | # --------------------------- 73 | 74 | def desktop_app(): 75 | from facebook import Facebook 76 | 77 | # Get api_key and secret_key from a file 78 | fbs = open(FB_SETTINGS).readlines() 79 | facebook = Facebook(fbs[0].strip(), fbs[1].strip()) 80 | 81 | facebook.auth.createToken() 82 | # Show login window 83 | facebook.login() 84 | 85 | # Login to the window, then press enter 86 | print 'After logging in, press enter...' 87 | raw_input() 88 | 89 | facebook.auth.getSession() 90 | info = facebook.users.getInfo([facebook.uid], ['name', 'birthday', 'affiliations', 'sex'])[0] 91 | 92 | for attr in info: 93 | print '%s: %s' % (attr, info[attr]) 94 | 95 | friends = facebook.friends.get() 96 | friends = facebook.users.getInfo(friends[0:5], ['name', 'birthday', 'relationship_status']) 97 | 98 | for friend in friends: 99 | if 'birthday' in friend: 100 | print friend['name'], 'has a birthday on', friend['birthday'], 'and is', friend['relationship_status'] 101 | else: 102 | print friend['name'], 'has no birthday and is', friend['relationship_status'] 103 | 104 | arefriends = facebook.friends.areFriends([friends[0]['uid']], [friends[1]['uid']]) 105 | 106 | photos = facebook.photos.getAlbums(friends[1]['uid']) 107 | print photos 108 | -------------------------------------------------------------------------------- /posediff: -------------------------------------------------------------------------------- 1 | diff --git a/facebook/__init__.py b/facebook/__init__.py 2 | index 3813142..e20fb6e 100644 3 | --- a/facebook/__init__.py 4 | +++ b/facebook/__init__.py 5 | @@ -136,6 +136,12 @@ METHODS = { 6 | ], 7 | }, 8 | 9 | + # batch methods 10 | + 'batch': { 11 | + 'begin': [], 12 | + 'end': [] 13 | + }, 14 | + 15 | # feed methods 16 | 'feed': { 17 | 'publishStoryToUser': [ 18 | @@ -812,6 +818,49 @@ class PhotosProxy(PhotosProxy): 19 | """Returns a guess at the MIME type of the file from the filename.""" 20 | return str(mimetypes.guess_type(filename)[0]) or 'application/octet-stream' 21 | 22 | +class BatchProxy(object): 23 | + """ 24 | + Provides Batch API calls: 25 | + 26 | + facebook.batch.begin 27 | + facebook.batch.end 28 | + 29 | + Example: 30 | + 31 | + >> b = self.fb.batch.begin() 32 | + >> b.users.getInfo([self.fb.uid], ['name', 'birthday', 'affiliations', 'sex']) 33 | + >> b.friends.get() 34 | + >> b.photos.getAlbums(self.fb.uid) 35 | + >> result = self.fb.batch.end() 36 | + result[0]()[0] #getInfo result 37 | + 38 | + result[1]() #friends.get result 39 | + 40 | + result[2]() #photos result 41 | + 42 | + """ 43 | + def __init__(self, client, name): 44 | + self._client = client 45 | + self._name = name 46 | + self.myfb = None 47 | + 48 | + def begin(self): 49 | + #TODO: Ver si no es muy caro copiar todo el objeto, lo idea seria un objecto Session 50 | + import copy 51 | + 52 | + self.myfb = copy.deepcopy(self._client) 53 | + self.myfb.autocommit = False 54 | + return self.myfb 55 | + 56 | + def end(self): 57 | + """ Returns a list with the results as callables. """ 58 | + if self.myfb is None: 59 | + raise RuntimeError('Called batch.end before batch.begin') 60 | + result = self.myfb.flush() 61 | + del self.myfb 62 | + return result 63 | + 64 | + 65 | 66 | class Facebook(object): 67 | """ 68 | @@ -928,6 +977,11 @@ class Facebook(object): 69 | self.profile_update_time = None 70 | self.ext_perms = None 71 | self.proxy = proxy 72 | + 73 | + self.autocommit = True 74 | + self.active_transaction = [] 75 | + self.secure_transaction = False 76 | + 77 | if facebook_url is None: 78 | self.facebook_url = FACEBOOK_URL 79 | else: 80 | @@ -1094,6 +1148,41 @@ class Facebook(object): 81 | # fix for bug of UnicodeEncodeError 82 | post_data = self.unicode_urlencode(self._build_post_args(method, args)) 83 | 84 | + if not self.autocommit: 85 | + #TODO: Poner limite de 15 llamadas a la API 86 | + #TODO: Probar casos de escritura, no solo lectura 87 | + if not hasattr(self, 'active_transaction'): 88 | + self.active_transaction = [] 89 | + 90 | + if secure: 91 | + self.secure_transaction = True 92 | + 93 | + self.active_transaction.append({'method': method, 'post_data': post_data}) 94 | + return 95 | + 96 | + return self.send(method, post_data, secure) 97 | + 98 | + def flush(self): 99 | + post_data = self.unicode_urlencode( self._build_post_args('facebook.batch.run', 100 | + self._add_session_args({ 'method_feed':simplejson.dumps( 101 | + [query['post_data'] for query in self.active_transaction])}))) 102 | + 103 | + print post_data 104 | + 105 | + 106 | + results = self.send('facebook.batch.run', post_data) 107 | + print results 108 | + 109 | + 110 | + from itertools import count, izip 111 | + from functools import partial 112 | + 113 | + results = [partial(self._parse_response,result, self.active_transaction[i]['method']) for result,i in izip(results, count())] 114 | + 115 | + self.active_transaction = [] 116 | + return results 117 | + 118 | + def send(self,method=None, post_data=None, secure=False): 119 | if self.proxy: 120 | proxy_handler = urllib2.ProxyHandler(self.proxy) 121 | opener = urllib2.build_opener(proxy_handler) 122 | -------------------------------------------------------------------------------- /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 | from paste.util.quoting import strip_html, html_quote, no_quote 27 | except ImportError: 28 | pass 29 | else: 30 | facebook = StackedObjectProxy(name="PyFacebook Facebook Connection") 31 | 32 | 33 | class CanvasRedirect(_HTTPMove): 34 | 35 | """This is for canvas redirects.""" 36 | 37 | title = "See Other" 38 | code = 200 39 | template = '' 40 | 41 | def html(self, environ): 42 | """ text/html representation of the exception """ 43 | body = self.make_body(environ, self.template, html_quote, no_quote) 44 | return body 45 | 46 | class FacebookWSGIMiddleware(object): 47 | 48 | """This is WSGI middleware for Facebook.""" 49 | 50 | def __init__(self, app, config, facebook_class=Facebook): 51 | """Initialize the Facebook middleware. 52 | 53 | ``app`` 54 | This is the WSGI application being wrapped. 55 | 56 | ``config`` 57 | This is a dict containing the keys "pyfacebook.apikey" and 58 | "pyfacebook.secret". 59 | 60 | ``facebook_class`` 61 | If you want to subclass the Facebook class, you can pass in 62 | your replacement here. Pylons users will want to use 63 | PylonsFacebook. 64 | 65 | """ 66 | self.app = app 67 | self.config = config 68 | self.facebook_class = facebook_class 69 | 70 | def __call__(self, environ, start_response): 71 | config = self.config 72 | real_facebook = self.facebook_class(config["pyfacebook.apikey"], 73 | config["pyfacebook.secret"]) 74 | registry = environ.get('paste.registry') 75 | if registry: 76 | registry.register(facebook, real_facebook) 77 | environ['pyfacebook.facebook'] = real_facebook 78 | return self.app(environ, start_response) 79 | 80 | 81 | # The remainder is Pylons specific. 82 | 83 | try: 84 | import pylons 85 | from pylons.controllers.util import redirect_to as pylons_redirect_to 86 | from routes import url_to 87 | except ImportError: 88 | pass 89 | else: 90 | 91 | 92 | class PylonsFacebook(Facebook): 93 | 94 | """Subclass Facebook to add Pylons goodies.""" 95 | 96 | def check_session(self, request=None): 97 | """The request parameter is now optional.""" 98 | if request is None: 99 | request = pylons.request 100 | return Facebook.check_session(self, request) 101 | 102 | # The Django request object is similar enough to the Paste 103 | # request object that check_session and validate_signature 104 | # should *just work*. 105 | 106 | def redirect_to(self, url): 107 | """Wrap Pylons' redirect_to function so that it works in_canvas. 108 | 109 | By the way, this won't work until after you call 110 | check_session(). 111 | 112 | """ 113 | if self.in_canvas: 114 | raise CanvasRedirect(url) 115 | pylons_redirect_to(url) 116 | 117 | def apps_url_for(self, *args, **kargs): 118 | """Like url_for, but starts with "http://apps.facebook.com".""" 119 | return "http://apps.facebook.com" + url_to(*args, **kargs) 120 | 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 | -------------------------------------------------------------------------------- /bin/djangofb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | if __name__ == '__main__': 4 | import sys, os, re 5 | 6 | def usage(): 7 | sys.stderr.write('Usage: djangofb.py startapp \n') 8 | sys.exit(1) 9 | 10 | if len(sys.argv) not in (2, 3): 11 | usage() 12 | 13 | if sys.argv[1] != 'startapp': 14 | usage() 15 | 16 | app_name = len(sys.argv) == 3 and sys.argv[2] or 'fbapp' 17 | 18 | try: 19 | sys.path.insert(0, os.getcwd()) 20 | import settings # Assumed to be in the same directory or current directory. 21 | except ImportError: 22 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r or in the current directory. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 23 | sys.exit(1) 24 | 25 | from django.core import management 26 | 27 | directory = management.setup_environ(settings) 28 | 29 | if hasattr(management, 'color'): 30 | # Current svn version of django 31 | from django.core.management.color import color_style 32 | style = color_style() 33 | else: 34 | # Compatibility with 0.96 35 | from django.core.management import style 36 | 37 | project_dir = os.path.normpath(os.path.join(directory, '..')) 38 | parent_dir = os.path.basename(project_dir) 39 | project_name = os.path.basename(directory) 40 | if app_name == project_name: 41 | sys.stderr.write(style.ERROR('Error: You cannot create an app with the same name (%r) as your project.\n' % app_name)) 42 | sys.exit(1) 43 | if app_name == 'facebook': 44 | sys.stderr.write(style.ERROR('Error: You cannot name your app "facebook", since this can cause conflicts with imports in Python < 2.5.\n')) 45 | sys.exit(1) 46 | if not re.search(r'^\w+$', app_name): 47 | sys.stderr.write(style.ERROR('Error: %r is not a valid app name. Please use only numbers, letters and underscores.\n' % (app_name))) 48 | sys.exit(1) 49 | 50 | top_dir = os.path.join(directory, app_name) 51 | try: 52 | os.mkdir(top_dir) 53 | except OSError, e: 54 | sys.stderr.write(style.ERROR("Error: %s\n" % e)) 55 | sys.exit(1) 56 | 57 | import facebook 58 | 59 | template_dir = os.path.join(facebook.__path__[0], 'djangofb', 'default_app') 60 | 61 | sys.stderr.write('Creating Facebook application %r...\n' % app_name) 62 | 63 | for d, subdirs, files in os.walk(template_dir): 64 | relative_dir = d[len(template_dir) + 1:] 65 | if relative_dir: 66 | os.mkdir(os.path.join(top_dir, relative_dir)) 67 | subdirs[:] = [s for s in subdirs if not s.startswith('.')] 68 | for f in files: 69 | if f.endswith('.pyc'): 70 | continue 71 | path_old = os.path.join(d, f) 72 | path_new = os.path.join(top_dir, relative_dir, f) 73 | f_old = open(path_old, 'r') 74 | f_new = open(path_new, 'w') 75 | sys.stderr.write('Writing %s...\n' % path_new) 76 | f_new.write(f_old.read().replace('{{ project }}', project_name).replace('{{ app }}', app_name)) 77 | f_new.close() 78 | f_old.close() 79 | 80 | sys.stderr.write('Done!\n\n') 81 | 82 | from django.conf import settings 83 | 84 | need_api_key = not hasattr(settings, 'FACEBOOK_API_KEY') 85 | need_middleware = not 'facebook.djangofb.FacebookMiddleware' in settings.MIDDLEWARE_CLASSES 86 | need_loader = not 'django.template.loaders.app_directories.load_template_source' in settings.TEMPLATE_LOADERS 87 | need_install_app = not '%s.%s' % (project_name, app_name) in settings.INSTALLED_APPS 88 | 89 | if need_api_key or need_middleware or need_loader or need_install_app: 90 | sys.stderr.write("""There are a couple of things you NEED to do before you can use this app:\n\n""") 91 | if need_api_key: 92 | sys.stderr.write(""" * Set FACEBOOK_API_KEY and FACEBOOK_SECRET_KEY to the appropriate values in settings.py\n\n""") 93 | if need_middleware: 94 | sys.stderr.write(""" * Add 'facebook.djangofb.FacebookMiddleware' to your MIDDLEWARE_CLASSES in settings.py\n\n""") 95 | if need_loader: 96 | sys.stderr.write(""" * Add 'django.template.loaders.app_directories.load_template_source' to your TEMPLATE_LOADERS in settings.py\n\n""") 97 | if need_install_app: 98 | sys.stderr.write(""" * Add '%s.%s' to your INSTALLED_APPS in settings.py\n\n""" % (project_name, app_name)) 99 | 100 | sys.stderr.write("""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/'. 101 | 102 | Good luck! 103 | 104 | """ % (project_name, project_name, app_name, project_name)) 105 | -------------------------------------------------------------------------------- /facebook/djangofb/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import facebook 4 | 5 | from django.http import HttpResponse, HttpResponseRedirect 6 | from django.core.exceptions import ImproperlyConfigured 7 | from django.conf import settings 8 | 9 | try: 10 | from threading import local 11 | except ImportError: 12 | from django.utils._threading_local import local 13 | 14 | __all__ = ['Facebook', 'FacebookMiddleware', 'get_facebook_client', 'require_login', 'require_add'] 15 | 16 | _thread_locals = local() 17 | 18 | class Facebook(facebook.Facebook): 19 | def redirect(self, url): 20 | """ 21 | Helper for Django which redirects to another page. If inside a 22 | canvas page, writes a instead to achieve the same effect. 23 | 24 | """ 25 | if self.in_canvas: 26 | return HttpResponse('' % (url, )) 27 | elif re.search("^https?:\/\/([^\/]*\.)?facebook\.com(:\d+)?", url.lower()): 28 | return HttpResponse('' % url) 29 | else: 30 | return HttpResponseRedirect(url) 31 | 32 | 33 | def get_facebook_client(): 34 | """ 35 | Get the current Facebook object for the calling thread. 36 | 37 | """ 38 | try: 39 | return _thread_locals.facebook 40 | except AttributeError: 41 | raise ImproperlyConfigured('Make sure you have the Facebook middleware installed.') 42 | 43 | 44 | def require_login(next=None, internal=None): 45 | """ 46 | Decorator for Django views that requires the user to be logged in. 47 | The FacebookMiddleware must be installed. 48 | 49 | Standard usage: 50 | @require_login() 51 | def some_view(request): 52 | ... 53 | 54 | Redirecting after login: 55 | To use the 'next' parameter to redirect to a specific page after login, a callable should 56 | return a path relative to the Post-add URL. 'next' can also be an integer specifying how many 57 | parts of request.path to strip to find the relative URL of the canvas page. If 'next' is None, 58 | settings.callback_path and settings.app_name are checked to redirect to the same page after logging 59 | in. (This is the default behavior.) 60 | @require_login(next=some_callable) 61 | def some_view(request): 62 | ... 63 | """ 64 | def decorator(view): 65 | def newview(request, *args, **kwargs): 66 | next = newview.next 67 | internal = newview.internal 68 | 69 | try: 70 | fb = request.facebook 71 | except: 72 | raise ImproperlyConfigured('Make sure you have the Facebook middleware installed.') 73 | 74 | if internal is None: 75 | internal = request.facebook.internal 76 | 77 | if callable(next): 78 | next = next(request.path) 79 | elif isinstance(next, int): 80 | next = '/'.join(request.path.split('/')[next + 1:]) 81 | elif next is None and fb.callback_path and request.path.startswith(fb.callback_path): 82 | next = request.path[len(fb.callback_path):] 83 | elif not isinstance(next, str): 84 | next = '' 85 | 86 | if not fb.check_session(request): 87 | #If user has never logged in before, the get_login_url will redirect to the TOS page 88 | return fb.redirect(fb.get_login_url(next=next)) 89 | 90 | if internal and request.method == 'GET' and fb.app_name: 91 | return fb.redirect('%s%s' % (fb.get_app_url(), next)) 92 | 93 | return view(request, *args, **kwargs) 94 | newview.next = next 95 | newview.internal = internal 96 | return newview 97 | return decorator 98 | 99 | 100 | def require_add(next=None, internal=None, on_install=None): 101 | """ 102 | Decorator for Django views that requires application installation. 103 | The FacebookMiddleware must be installed. 104 | 105 | Standard usage: 106 | @require_add() 107 | def some_view(request): 108 | ... 109 | 110 | Redirecting after installation: 111 | To use the 'next' parameter to redirect to a specific page after login, a callable should 112 | return a path relative to the Post-add URL. 'next' can also be an integer specifying how many 113 | parts of request.path to strip to find the relative URL of the canvas page. If 'next' is None, 114 | settings.callback_path and settings.app_name are checked to redirect to the same page after logging 115 | in. (This is the default behavior.) 116 | @require_add(next=some_callable) 117 | def some_view(request): 118 | ... 119 | 120 | Post-install processing: 121 | Set the on_install parameter to a callable in order to handle special post-install processing. 122 | The callable should take a request object as the parameter. 123 | @require_add(on_install=some_callable) 124 | def some_view(request): 125 | ... 126 | """ 127 | def decorator(view): 128 | def newview(request, *args, **kwargs): 129 | next = newview.next 130 | internal = newview.internal 131 | 132 | try: 133 | fb = request.facebook 134 | except: 135 | raise ImproperlyConfigured('Make sure you have the Facebook middleware installed.') 136 | 137 | if internal is None: 138 | internal = request.facebook.internal 139 | 140 | if callable(next): 141 | next = next(request.path) 142 | elif isinstance(next, int): 143 | next = '/'.join(request.path.split('/')[next + 1:]) 144 | elif next is None and fb.callback_path and request.path.startswith(fb.callback_path): 145 | next = request.path[len(fb.callback_path):] 146 | else: 147 | next = '' 148 | 149 | if not fb.check_session(request): 150 | if fb.added: 151 | if request.method == 'GET' and fb.app_name: 152 | return fb.redirect('%s%s' % (fb.get_app_url(), next)) 153 | return fb.redirect(fb.get_login_url(next=next)) 154 | else: 155 | return fb.redirect(fb.get_add_url(next=next)) 156 | 157 | if not fb.added: 158 | return fb.redirect(fb.get_add_url(next=next)) 159 | 160 | if 'installed' in request.GET and callable(on_install): 161 | on_install(request) 162 | 163 | if internal and request.method == 'GET' and fb.app_name: 164 | return fb.redirect('%s%s' % (fb.get_app_url(), next)) 165 | 166 | return view(request, *args, **kwargs) 167 | newview.next = next 168 | newview.internal = internal 169 | return newview 170 | return decorator 171 | 172 | # try to preserve the argspecs 173 | try: 174 | import decorator 175 | except ImportError: 176 | pass 177 | else: 178 | def updater(f): 179 | def updated(*args, **kwargs): 180 | original = f(*args, **kwargs) 181 | def newdecorator(view): 182 | return decorator.new_wrapper(original(view), view) 183 | return decorator.new_wrapper(newdecorator, original) 184 | return decorator.new_wrapper(updated, f) 185 | require_login = updater(require_login) 186 | require_add = updater(require_add) 187 | 188 | class FacebookMiddleware(object): 189 | """ 190 | Middleware that attaches a Facebook object to every incoming request. 191 | The Facebook object created can also be accessed from models for the 192 | current thread by using get_facebook_client(). 193 | 194 | """ 195 | 196 | def __init__(self, api_key=None, secret_key=None, app_name=None, callback_path=None, internal=None): 197 | self.api_key = api_key or settings.FACEBOOK_API_KEY 198 | self.secret_key = secret_key or settings.FACEBOOK_SECRET_KEY 199 | self.app_name = app_name or getattr(settings, 'FACEBOOK_APP_NAME', None) 200 | self.callback_path = callback_path or getattr(settings, 'FACEBOOK_CALLBACK_PATH', None) 201 | self.internal = internal or getattr(settings, 'FACEBOOK_INTERNAL', True) 202 | self.proxy = None 203 | if getattr(settings, 'USE_HTTP_PROXY', False): 204 | self.proxy = settings.HTTP_PROXY 205 | 206 | def process_request(self, request): 207 | _thread_locals.facebook = request.facebook = Facebook(self.api_key, self.secret_key, app_name=self.app_name, callback_path=self.callback_path, internal=self.internal, proxy=self.proxy) 208 | if not self.internal and 'facebook_session_key' in request.session and 'facebook_user_id' in request.session: 209 | request.facebook.session_key = request.session['facebook_session_key'] 210 | request.facebook.uid = request.session['facebook_user_id'] 211 | 212 | def process_response(self, request, response): 213 | if not self.internal and request.facebook.session_key and request.facebook.uid: 214 | request.session['facebook_session_key'] = request.facebook.session_key 215 | request.session['facebook_user_id'] = request.facebook.uid 216 | return response 217 | -------------------------------------------------------------------------------- /tests/minimock.py: -------------------------------------------------------------------------------- 1 | # (c) 2006 Ian Bicking, Mike Beachy, and contributors 2 | # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php 3 | r""" 4 | minimock is a simple library for doing Mock objects with doctest. 5 | When using doctest, mock objects can be very simple. 6 | 7 | Here's an example of something we might test, a simple email sender:: 8 | 9 | >>> import smtplib 10 | >>> def send_email(from_addr, to_addr, subject, body): 11 | ... conn = smtplib.SMTP('localhost') 12 | ... msg = 'To: %s\nFrom: %s\nSubject: %s\n\n%s' % ( 13 | ... to_addr, from_addr, subject, body) 14 | ... conn.sendmail(from_addr, [to_addr], msg) 15 | ... conn.quit() 16 | 17 | Now we want to make a mock ``smtplib.SMTP`` object. We'll have to 18 | inject our mock into the ``smtplib`` module:: 19 | 20 | >>> smtplib.SMTP = Mock('smtplib.SMTP') 21 | >>> smtplib.SMTP.mock_returns = Mock('smtp_connection') 22 | 23 | Now we do the test:: 24 | 25 | >>> send_email('ianb@colorstudy.com', 'joe@example.com', 26 | ... 'Hi there!', 'How is it going?') 27 | Called smtplib.SMTP('localhost') 28 | Called smtp_connection.sendmail( 29 | 'ianb@colorstudy.com', 30 | ['joe@example.com'], 31 | 'To: joe@example.com\nFrom: ianb@colorstudy.com\nSubject: Hi there!\n\nHow is it going?') 32 | Called smtp_connection.quit() 33 | 34 | Voila! We've tested implicitly that no unexpected methods were called 35 | on the object. We've also tested the arguments that the mock object 36 | got. We've provided fake return calls (for the ``smtplib.SMTP()`` 37 | constructor). These are all the core parts of a mock library. The 38 | implementation is simple because most of the work is done by doctest. 39 | """ 40 | 41 | import warnings 42 | warnings.warn( 43 | "The module from http://svn.colorstudy.com/home/ianb/recipes/minimock.py is deprecated; " 44 | "please install the MiniMock package", 45 | DeprecationWarning, stacklevel=2) 46 | 47 | __all__ = ["mock", "restore", "Mock"] 48 | 49 | import inspect 50 | 51 | # A list of mocked objects. Each item is a tuple of (original object, 52 | # namespace dict, object name, and a list of object attributes). 53 | # 54 | mocked = [] 55 | 56 | def lookup_by_name(name, nsdicts): 57 | """ 58 | Look up an object by name from a sequence of namespace dictionaries. 59 | Returns a tuple of (nsdict, object, attributes); nsdict is the 60 | dictionary the name was found in, object is the base object the name is 61 | bound to, and the attributes list is the chain of attributes of the 62 | object that complete the name. 63 | 64 | >>> import os 65 | >>> nsdict, name, attributes = lookup_by_name("os.path.isdir", 66 | ... (locals(),)) 67 | >>> name, attributes 68 | ('os', ['path', 'isdir']) 69 | >>> nsdict, name, attributes = lookup_by_name("os.monkey", (locals(),)) 70 | Traceback (most recent call last): 71 | ... 72 | NameError: name 'os.monkey' is not defined 73 | 74 | """ 75 | for nsdict in nsdicts: 76 | attrs = name.split(".") 77 | names = [] 78 | 79 | while attrs: 80 | names.append(attrs.pop(0)) 81 | obj_name = ".".join(names) 82 | 83 | if obj_name in nsdict: 84 | attr_copy = attrs[:] 85 | tmp = nsdict[obj_name] 86 | try: 87 | while attr_copy: 88 | tmp = getattr(tmp, attr_copy.pop(0)) 89 | except AttributeError: 90 | pass 91 | else: 92 | return nsdict, obj_name, attrs 93 | 94 | raise NameError("name '%s' is not defined" % name) 95 | 96 | def mock(name, nsdicts=None, mock_obj=None, **kw): 97 | """ 98 | Mock the named object, placing a Mock instance in the correct namespace 99 | dictionary. If no iterable of namespace dicts is provided, use 100 | introspection to get the locals and globals of the caller of this 101 | function. 102 | 103 | All additional keyword args are passed on to the Mock object 104 | initializer. 105 | 106 | An example of how os.path.isfile is replaced: 107 | 108 | >>> import os 109 | >>> os.path.isfile 110 | 111 | >>> isfile_id = id(os.path.isfile) 112 | >>> mock("os.path.isfile", returns=True) 113 | >>> os.path.isfile 114 | 115 | >>> os.path.isfile("/foo/bar/baz") 116 | Called os.path.isfile('/foo/bar/baz') 117 | True 118 | >>> mock_id = id(os.path.isfile) 119 | >>> mock_id != isfile_id 120 | True 121 | 122 | A second mock object will replace the first, but the original object 123 | will be the one replaced with the replace() function. 124 | 125 | >>> mock("os.path.isfile", returns=False) 126 | >>> mock_id != id(os.path.isfile) 127 | True 128 | >>> restore() 129 | >>> os.path.isfile 130 | 131 | >>> isfile_id == id(os.path.isfile) 132 | True 133 | 134 | """ 135 | if nsdicts is None: 136 | stack = inspect.stack() 137 | try: 138 | # stack[1][0] is the frame object of the caller to this function 139 | globals_ = stack[1][0].f_globals 140 | locals_ = stack[1][0].f_locals 141 | nsdicts = (locals_, globals_) 142 | finally: 143 | del(stack) 144 | 145 | if mock_obj is None: 146 | mock_obj = Mock(name, **kw) 147 | 148 | nsdict, obj_name, attrs = lookup_by_name(name, nsdicts) 149 | 150 | # Get the original object and replace it with the mock object. 151 | tmp = nsdict[obj_name] 152 | if not attrs: 153 | original = tmp 154 | nsdict[obj_name] = mock_obj 155 | else: 156 | for attr in attrs[:-1]: 157 | tmp = getattr(tmp, attr) 158 | original = getattr(tmp, attrs[-1]) 159 | setattr(tmp, attrs[-1], mock_obj) 160 | 161 | mocked.append((original, nsdict, obj_name, attrs)) 162 | 163 | def restore(): 164 | """ 165 | Restore all mocked objects. 166 | 167 | """ 168 | global mocked 169 | 170 | # Restore the objects in the reverse order of their mocking to assure 171 | # the original state is retrieved. 172 | while mocked: 173 | original, nsdict, name, attrs = mocked.pop() 174 | if not attrs: 175 | nsdict[name] = original 176 | else: 177 | tmp = nsdict[name] 178 | for attr in attrs[:-1]: 179 | tmp = getattr(tmp, attr) 180 | setattr(tmp, attrs[-1], original) 181 | return 182 | 183 | class Mock(object): 184 | 185 | def __init__(self, name, returns=None, returns_iter=None, 186 | returns_func=None, raises=None): 187 | self.mock_name = name 188 | self.mock_returns = returns 189 | if returns_iter is not None: 190 | returns_iter = iter(returns_iter) 191 | self.mock_returns_iter = returns_iter 192 | self.mock_returns_func = returns_func 193 | self.mock_raises = raises 194 | self.mock_attrs = {} 195 | 196 | def __repr__(self): 197 | return '' % (hex(id(self)), self.mock_name) 198 | 199 | def __call__(self, *args, **kw): 200 | parts = [repr(a) for a in args] 201 | parts.extend( 202 | '%s=%r' % (items) for items in sorted(kw.items())) 203 | msg = 'Called %s(%s)' % (self.mock_name, ', '.join(parts)) 204 | if len(msg) > 80: 205 | msg = 'Called %s(\n %s)' % ( 206 | self.mock_name, ',\n '.join(parts)) 207 | print msg 208 | return self._mock_return(*args, **kw) 209 | 210 | def _mock_return(self, *args, **kw): 211 | if self.mock_raises is not None: 212 | raise self.mock_raises 213 | elif self.mock_returns is not None: 214 | return self.mock_returns 215 | elif self.mock_returns_iter is not None: 216 | try: 217 | return self.mock_returns_iter.next() 218 | except StopIteration: 219 | raise Exception("No more mock return values are present.") 220 | elif self.mock_returns_func is not None: 221 | return self.mock_returns_func(*args, **kw) 222 | else: 223 | return None 224 | 225 | def __getattr__(self, attr): 226 | if attr not in self.mock_attrs: 227 | if self.mock_name: 228 | new_name = self.mock_name + '.' + attr 229 | else: 230 | new_name = attr 231 | self.mock_attrs[attr] = Mock(new_name) 232 | return self.mock_attrs[attr] 233 | 234 | __test__ = { 235 | "mock" : 236 | r""" 237 | An additional test for mocking a function accessed directly (i.e. 238 | not via object attributes). 239 | 240 | >>> import os 241 | >>> rename = os.rename 242 | >>> orig_id = id(rename) 243 | >>> mock("rename") 244 | >>> mock_id = id(rename) 245 | >>> mock("rename") 246 | >>> mock_id != id(rename) 247 | True 248 | >>> restore() 249 | >>> orig_id == id(rename) == id(os.rename) 250 | True 251 | 252 | The example from the module docstring, done with the mock/restore 253 | functions. 254 | 255 | >>> import smtplib 256 | >>> def send_email(from_addr, to_addr, subject, body): 257 | ... conn = smtplib.SMTP('localhost') 258 | ... msg = 'To: %s\nFrom: %s\nSubject: %s\n\n%s' % ( 259 | ... to_addr, from_addr, subject, body) 260 | ... conn.sendmail(from_addr, [to_addr], msg) 261 | ... conn.quit() 262 | 263 | >>> mock("smtplib.SMTP", returns=Mock('smtp_connection')) 264 | >>> send_email('ianb@colorstudy.com', 'joe@example.com', 265 | ... 'Hi there!', 'How is it going?') 266 | Called smtplib.SMTP('localhost') 267 | Called smtp_connection.sendmail( 268 | 'ianb@colorstudy.com', 269 | ['joe@example.com'], 270 | 'To: joe@example.com\nFrom: ianb@colorstudy.com\nSubject: Hi there!\n\nHow is it going?') 271 | Called smtp_connection.quit() 272 | >>> restore() 273 | 274 | """, 275 | } 276 | 277 | if __name__ == '__main__': 278 | import doctest 279 | doctest.testmod(optionflags=doctest.ELLIPSIS) 280 | -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | import os 4 | import facebook 5 | import urllib2 6 | import md5 7 | try: 8 | import simplejson 9 | except ImportError: 10 | from django.utils import simplejson 11 | import httplib 12 | from minimock import Mock 13 | 14 | my_api_key = "e1e9cfeb5e0d7a52e4fbd5d09e1b873e" 15 | my_secret_key = "1bebae7283f5b79aaf9b851addd55b90" 16 | #'{"error_code":100,\ 17 | #"error_msg":"Invalid parameter",\ 18 | #"request_args":[{"key":"format","value":"JSON"},\ 19 | #{"key":"auth_token","value":"24626e24bb12919f2f142145070542e8"},\ 20 | #{"key":"sig","value":"36af2af3b93da784149301e77cb1621a"},\ 21 | #{"key":"v","value":"1.0"},\ 22 | #{"key":"api_key","value":"e1e9cfeb5e0d7a52e4fbd5d09e1b873e"},\ 23 | #{"key":"method","value":"facebook.auth.getSession"}]}' 24 | response_str = '{"stuff":"abcd"}' 25 | class MyUrlOpen: 26 | def __init__(self,*args,**kwargs): 27 | pass 28 | 29 | def read(self): 30 | global response_str 31 | return response_str 32 | 33 | class pyfacebook_UnitTests(unittest.TestCase): 34 | def setUp(self): 35 | facebook.urllib2.urlopen = Mock('urllib2.urlopen') 36 | facebook.urllib2.urlopen.mock_returns_func = MyUrlOpen 37 | pass 38 | 39 | def tearDown(self): 40 | pass 41 | 42 | def login(self): 43 | pass 44 | 45 | def test1(self): 46 | f = facebook.Facebook(api_key=my_api_key, secret_key=my_secret_key) 47 | f.login = self.login 48 | self.assertEquals(f.api_key,my_api_key) 49 | self.assertEquals(f.secret_key,my_secret_key) 50 | self.assertEquals(f.auth_token,None) 51 | self.assertEquals(f.app_name,None) 52 | self.assertEquals(f.callback_path,None) 53 | self.assertEquals(f.internal,None) 54 | 55 | def test2(self): 56 | args = {"arg1":"a","arg2":"b","arg3":"c"} 57 | hasher = md5.new(''.join(['%s=%s' % (x, args[x]) for x in sorted(args.keys())])) 58 | hasher.update("acdnj") 59 | f = facebook.Facebook(api_key="abcdf", secret_key="acdnj") 60 | f.login = self.login 61 | digest = f._hash_args(args) 62 | self.assertEquals(hasher.hexdigest(),digest) 63 | hasher = md5.new(''.join(['%s=%s' % (x, args[x]) for x in sorted(args.keys())])) 64 | hasher.update("klmn") 65 | # trunk code has error hash.updated instead of hash.update 66 | digest = f._hash_args(args,secret="klmn") 67 | self.assertEquals(hasher.hexdigest(),digest) 68 | 69 | hasher = md5.new(''.join(['%s=%s' % (x, args[x]) for x in sorted(args.keys())])) 70 | f.secret = "klmn" 71 | hasher.update(f.secret) 72 | # trunk code has error hash.updated instead of hash.update 73 | digest = f._hash_args(args) 74 | self.assertEquals(hasher.hexdigest(),digest) 75 | 76 | def test3(self): 77 | global response_str 78 | response = {'stuff':'abcd'} 79 | response_str = simplejson.dumps(response) 80 | fb = facebook.Facebook(my_api_key, my_secret_key) 81 | fb.login = self.login 82 | fb.auth.createToken() 83 | self.assertEquals(str(fb.auth_token['stuff']),"abcd") 84 | fb.login() 85 | response = {"session_key":"key","uid":"my_uid","secret":"my_secret","expires":"my_expires"} 86 | response_str = simplejson.dumps(response) 87 | res = fb.auth.getSession() 88 | self.assertEquals(str(res["expires"]),response["expires"]) 89 | self.assertEquals(str(res["secret"]),response["secret"]) 90 | self.assertEquals(str(res["session_key"]),response["session_key"]) 91 | self.assertEquals(str(res["uid"]),response["uid"]) 92 | 93 | def test4(self): 94 | global response_str 95 | response = 'abcdef' 96 | response_str = simplejson.dumps(response) 97 | fb = facebook.Facebook(my_api_key, my_secret_key) 98 | fb.login = self.login 99 | fb.auth.createToken() 100 | self.assertEquals(str(fb.auth_token),"abcdef") 101 | url = fb.get_login_url(next="nowhere", popup=True, canvas=True) 102 | self.assertEquals(url, 103 | 'http://www.facebook.com/login.php?canvas=1&popup=1&auth_token=abcdef&next=nowhere&v=1.0&api_key=%s'%(my_api_key,)) 104 | 105 | def test5(self): 106 | class Request: 107 | def __init__(self,post,get,method): 108 | self.POST = post 109 | self.GET = get 110 | self.method = method 111 | 112 | req = Request({'fb_sig_in_canvas':1},{},'POST') 113 | fb = facebook.Facebook(my_api_key, my_secret_key) 114 | fb.login = self.login 115 | res = fb.check_session(req) 116 | self.assertFalse(res) 117 | req = Request({'fb_sig':1},{},'POST') 118 | res = fb.check_session(req) 119 | self.assertFalse(res) 120 | req = Request({'fb_sig':fb._hash_args({'in_canvas':'1', 121 | 'added':'1', 122 | 'expires':'1', 123 | 'friends':'joe,mary', 124 | 'session_key':'abc', 125 | 'user':'bob'}), 126 | 'fb_sig_in_canvas':'1', 127 | 'fb_sig_added':'1', 128 | 'fb_sig_expires':'1', 129 | 'fb_sig_friends':'joe,mary', 130 | 'fb_sig_session_key':'abc', 131 | 'fb_sig_user':'bob'}, 132 | {},'POST') 133 | res = fb.check_session(req) 134 | self.assertTrue(res) 135 | fb = facebook.Facebook(my_api_key, my_secret_key) 136 | fb.login = self.login 137 | req = Request({'fb_sig':fb._hash_args({'in_canvas':'1', 138 | 'added':'1', 139 | 'expires':'1', 140 | 'friends':'', 141 | 'session_key':'abc', 142 | 'user':'bob'}), 143 | 'fb_sig_in_canvas':'1', 144 | 'fb_sig_added':'1', 145 | 'fb_sig_expires':'1', 146 | 'fb_sig_friends':'', 147 | 'fb_sig_session_key':'abc', 148 | 'fb_sig_user':'bob'}, 149 | {},'POST') 150 | res = fb.check_session(req) 151 | self.assertTrue(res) 152 | fb = facebook.Facebook(my_api_key, my_secret_key) 153 | fb.login = self.login 154 | req = Request({'fb_sig':fb._hash_args({'in_canvas':'1', 155 | 'added':'1', 156 | 'expires':'1', 157 | 'friends':'', 158 | 'session_key':'abc', 159 | 'page_id':'id'}), 160 | 'fb_sig_in_canvas':'1', 161 | 'fb_sig_added':'1', 162 | 'fb_sig_expires':'1', 163 | 'fb_sig_friends':'', 164 | 'fb_sig_session_key':'abc', 165 | 'fb_sig_page_id':'id'}, 166 | {},'POST') 167 | res = fb.check_session(req) 168 | self.assertTrue(res) 169 | 170 | def test6(self): 171 | global response_str 172 | response = 'abcdef' 173 | response_str = simplejson.dumps(response) 174 | fb = facebook.Facebook(my_api_key, my_secret_key) 175 | fb.login = self.login 176 | fb.auth.createToken() 177 | # self.failUnlessRaises(RuntimeError,fb._add_session_args) 178 | response = {"session_key":"key","uid":"my_uid","secret":"my_secret","expires":"my_expires"} 179 | response_str = simplejson.dumps(response) 180 | fb.auth.getSession() 181 | args = fb._add_session_args() 182 | 183 | def test7(self): 184 | global response_str 185 | response = 'abcdef' 186 | response_str = simplejson.dumps(response) 187 | fb = facebook.Facebook(my_api_key, my_secret_key) 188 | fb.login = self.login 189 | fb.auth.createToken() 190 | self.assertEquals(str(fb.auth_token),"abcdef") 191 | url = fb.get_authorize_url(next="next",next_cancel="next_cancel") 192 | self.assertEquals(url, 193 | 'http://www.facebook.com/authorize.php?api_key=%s&next_cancel=next_cancel&v=1.0&next=next' % (my_api_key,)) 194 | 195 | def test8(self): 196 | class Request: 197 | def __init__(self,post,get,method): 198 | self.POST = post 199 | self.GET = get 200 | self.method = method 201 | 202 | global response_str 203 | response = {"session_key":"abcdef","uid":"my_uid","secret":"my_secret","expires":"my_expires"} 204 | response_str = simplejson.dumps(response) 205 | req = Request({},{'installed':1,'fb_page_id':'id','auth_token':'abcdef'},'GET') 206 | fb = facebook.Facebook(my_api_key, my_secret_key) 207 | fb.login = self.login 208 | res = fb.check_session(req) 209 | self.assertTrue(res) 210 | 211 | def test9(self): 212 | global response_str 213 | response = 'abcdef' 214 | response_str = simplejson.dumps(response) 215 | fb = facebook.Facebook(my_api_key, my_secret_key) 216 | fb.login = self.login 217 | fb.auth.createToken() 218 | self.assertEquals(str(fb.auth_token),"abcdef") 219 | url = fb.get_add_url(next="next") 220 | self.assertEquals(url, 221 | 'http://www.facebook.com/install.php?api_key=%s&v=1.0&next=next' % (my_api_key,)) 222 | 223 | def send(self,xml): 224 | self.xml = xml 225 | 226 | def test10(self): 227 | import Image 228 | image1 = Image.new("RGB", (400, 300), (255, 255, 255)) 229 | filename = "image_file.jpg" 230 | image1.save(filename) 231 | global response_str 232 | fb = facebook.Facebook(my_api_key, my_secret_key) 233 | fb.login = self.login 234 | 235 | facebook.httplib.HTTP = Mock('httplib.HTTP') 236 | http_connection = Mock('http_connection') 237 | facebook.httplib.HTTP.mock_returns = http_connection 238 | http_connection.send.mock_returns_func = self.send 239 | def _http_passes(): 240 | return [200,] 241 | http_connection.getreply.mock_returns_func = _http_passes 242 | 243 | def read(): 244 | response = {"stuff":"stuff"} 245 | response_str = simplejson.dumps(response) 246 | return response_str 247 | http_connection.file.read.mock_returns_func = read 248 | 249 | response = {"session_key":"key","uid":"my_uid","secret":"my_secret","expires":"my_expires"} 250 | response_str = simplejson.dumps(response) 251 | res = fb.auth.getSession() 252 | result = fb.photos.upload(image=filename,aid="aid",caption="a caption") 253 | self.assertEquals(str(result["stuff"]),"stuff") 254 | os.remove(filename) 255 | 256 | if __name__ == "__main__": 257 | 258 | # Build the test suite 259 | suite = unittest.TestSuite() 260 | suite.addTest(unittest.makeSuite(pyfacebook_UnitTests)) 261 | 262 | # Execute the test suite 263 | print("Testing Proxy class\n") 264 | result = unittest.TextTestRunner(verbosity=2).run(suite) 265 | sys.exit(len(result.errors) + len(result.failures)) 266 | 267 | -------------------------------------------------------------------------------- /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 md5 48 | import sys 49 | import time 50 | import struct 51 | import urllib 52 | import urllib2 53 | import httplib 54 | import hashlib 55 | import binascii 56 | import urlparse 57 | import mimetypes 58 | 59 | # try to use simplejson first, otherwise fallback to XML 60 | RESPONSE_FORMAT = 'JSON' 61 | try: 62 | import json as simplejson 63 | except ImportError: 64 | try: 65 | import simplejson 66 | except ImportError: 67 | try: 68 | from django.utils import simplejson 69 | except ImportError: 70 | try: 71 | import jsonlib as simplejson 72 | simplejson.loads 73 | except (ImportError, AttributeError): 74 | from xml.dom import minidom 75 | RESPONSE_FORMAT = 'XML' 76 | 77 | # support Google App Engine. GAE does not have a working urllib.urlopen. 78 | try: 79 | from google.appengine.api import urlfetch 80 | 81 | def urlread(url, data=None, headers=None): 82 | if data is not None: 83 | if headers is None: 84 | headers = {"Content-type": "application/x-www-form-urlencoded"} 85 | method = urlfetch.POST 86 | else: 87 | if headers is None: 88 | headers = {} 89 | method = urlfetch.GET 90 | 91 | result = urlfetch.fetch(url, method=method, 92 | payload=data, headers=headers) 93 | 94 | if result.status_code == 200: 95 | return result.content 96 | else: 97 | raise urllib2.URLError("fetch error url=%s, code=%d" % (url, result.status_code)) 98 | 99 | except ImportError: 100 | def urlread(url, data=None): 101 | res = urllib2.urlopen(url, data=data) 102 | return res.read() 103 | 104 | __all__ = ['Facebook'] 105 | 106 | VERSION = '0.1' 107 | 108 | FACEBOOK_URL = 'http://api.facebook.com/restserver.php' 109 | FACEBOOK_SECURE_URL = 'https://api.facebook.com/restserver.php' 110 | 111 | class json(object): pass 112 | 113 | # simple IDL for the Facebook API 114 | METHODS = { 115 | 'application': { 116 | 'getPublicInfo': [ 117 | ('application_id', int, ['optional']), 118 | ('application_api_key', str, ['optional']), 119 | ('application_canvas_name ', str,['optional']), 120 | ], 121 | }, 122 | 123 | # admin methods 124 | 'admin': { 125 | 'getAllocation': [ 126 | ('integration_point_name', str, []), 127 | ], 128 | }, 129 | 130 | # feed methods 131 | 'feed': { 132 | 'publishStoryToUser': [ 133 | ('title', str, []), 134 | ('body', str, ['optional']), 135 | ('image_1', str, ['optional']), 136 | ('image_1_link', str, ['optional']), 137 | ('image_2', str, ['optional']), 138 | ('image_2_link', str, ['optional']), 139 | ('image_3', str, ['optional']), 140 | ('image_3_link', str, ['optional']), 141 | ('image_4', str, ['optional']), 142 | ('image_4_link', str, ['optional']), 143 | ('priority', int, ['optional']), 144 | ], 145 | 146 | 'publishActionOfUser': [ 147 | ('title', str, []), 148 | ('body', str, ['optional']), 149 | ('image_1', str, ['optional']), 150 | ('image_1_link', str, ['optional']), 151 | ('image_2', str, ['optional']), 152 | ('image_2_link', str, ['optional']), 153 | ('image_3', str, ['optional']), 154 | ('image_3_link', str, ['optional']), 155 | ('image_4', str, ['optional']), 156 | ('image_4_link', str, ['optional']), 157 | ('priority', int, ['optional']), 158 | ], 159 | 160 | 'publishTemplatizedAction': [ 161 | ('title_template', str, []), 162 | ('page_actor_id', int, ['optional']), 163 | ('title_data', json, ['optional']), 164 | ('body_template', str, ['optional']), 165 | ('body_data', json, ['optional']), 166 | ('body_general', str, ['optional']), 167 | ('image_1', str, ['optional']), 168 | ('image_1_link', str, ['optional']), 169 | ('image_2', str, ['optional']), 170 | ('image_2_link', str, ['optional']), 171 | ('image_3', str, ['optional']), 172 | ('image_3_link', str, ['optional']), 173 | ('image_4', str, ['optional']), 174 | ('image_4_link', str, ['optional']), 175 | ('target_ids', list, ['optional']), 176 | ], 177 | 178 | 'registerTemplateBundle': [ 179 | ('one_line_story_templates', json, []), 180 | ('short_story_templates', json, ['optional']), 181 | ('full_story_template', json, ['optional']), 182 | ('action_links', json, ['optional']), 183 | ], 184 | 185 | 'deactivateTemplateBundleByID': [ 186 | ('template_bundle_id', int, []), 187 | ], 188 | 189 | 'getRegisteredTemplateBundles': [], 190 | 191 | 'getRegisteredTemplateBundleByID': [ 192 | ('template_bundle_id', str, []), 193 | ], 194 | 195 | 'publishUserAction': [ 196 | ('template_bundle_id', int, []), 197 | ('template_data', json, ['optional']), 198 | ('target_ids', list, ['optional']), 199 | ('body_general', str, ['optional']), 200 | ], 201 | }, 202 | 203 | # fql methods 204 | 'fql': { 205 | 'query': [ 206 | ('query', str, []), 207 | ], 208 | }, 209 | 210 | # friends methods 211 | 'friends': { 212 | 'areFriends': [ 213 | ('uids1', list, []), 214 | ('uids2', list, []), 215 | ], 216 | 217 | 'get': [ 218 | ('flid', int, ['optional']), 219 | ], 220 | 221 | 'getLists': [], 222 | 223 | 'getAppUsers': [], 224 | }, 225 | 226 | # notifications methods 227 | 'notifications': { 228 | 'get': [], 229 | 230 | 'send': [ 231 | ('to_ids', list, []), 232 | ('notification', str, []), 233 | ('email', str, ['optional']), 234 | ('type', str, ['optional']), 235 | ], 236 | 237 | 'sendRequest': [ 238 | ('to_ids', list, []), 239 | ('type', str, []), 240 | ('content', str, []), 241 | ('image', str, []), 242 | ('invite', bool, []), 243 | ], 244 | 245 | 'sendEmail': [ 246 | ('recipients', list, []), 247 | ('subject', str, []), 248 | ('text', str, ['optional']), 249 | ('fbml', str, ['optional']), 250 | ] 251 | }, 252 | 253 | # profile methods 254 | 'profile': { 255 | 'setFBML': [ 256 | ('markup', str, ['optional']), 257 | ('uid', int, ['optional']), 258 | ('profile', str, ['optional']), 259 | ('profile_action', str, ['optional']), 260 | ('mobile_fbml', str, ['optional']), 261 | ('profile_main', str, ['optional']), 262 | ], 263 | 264 | 'getFBML': [ 265 | ('uid', int, ['optional']), 266 | ('type', int, ['optional']), 267 | ], 268 | 269 | 'setInfo': [ 270 | ('title', str, []), 271 | ('type', int, []), 272 | ('info_fields', json, []), 273 | ('uid', int, []), 274 | ], 275 | 276 | 'getInfo': [ 277 | ('uid', int, []), 278 | ], 279 | 280 | 'setInfoOptions': [ 281 | ('field', str, []), 282 | ('options', json, []), 283 | ], 284 | 285 | 'getInfoOptions': [ 286 | ('field', str, []), 287 | ], 288 | }, 289 | 290 | # users methods 291 | 'users': { 292 | 'getInfo': [ 293 | ('uids', list, []), 294 | ('fields', list, [('default', ['name'])]), 295 | ], 296 | 297 | 'getStandardInfo': [ 298 | ('uids', list, []), 299 | ('fields', list, [('default', ['uid'])]), 300 | ], 301 | 302 | 'getLoggedInUser': [], 303 | 304 | 'isAppAdded': [], 305 | 306 | 'hasAppPermission': [ 307 | ('ext_perm', str, []), 308 | ('uid', int, ['optional']), 309 | ], 310 | 311 | 'setStatus': [ 312 | ('status', str, []), 313 | ('clear', bool, []), 314 | ('status_includes_verb', bool, ['optional']), 315 | ('uid', int, ['optional']), 316 | ], 317 | }, 318 | 319 | # events methods 320 | 'events': { 321 | 'get': [ 322 | ('uid', int, ['optional']), 323 | ('eids', list, ['optional']), 324 | ('start_time', int, ['optional']), 325 | ('end_time', int, ['optional']), 326 | ('rsvp_status', str, ['optional']), 327 | ], 328 | 329 | 'getMembers': [ 330 | ('eid', int, []), 331 | ], 332 | 333 | 'create': [ 334 | ('event_info', json, []), 335 | ], 336 | }, 337 | 338 | # update methods 339 | 'update': { 340 | 'decodeIDs': [ 341 | ('ids', list, []), 342 | ], 343 | }, 344 | 345 | # groups methods 346 | 'groups': { 347 | 'get': [ 348 | ('uid', int, ['optional']), 349 | ('gids', list, ['optional']), 350 | ], 351 | 352 | 'getMembers': [ 353 | ('gid', int, []), 354 | ], 355 | }, 356 | 357 | # marketplace methods 358 | 'marketplace': { 359 | 'createListing': [ 360 | ('listing_id', int, []), 361 | ('show_on_profile', bool, []), 362 | ('listing_attrs', str, []), 363 | ], 364 | 365 | 'getCategories': [], 366 | 367 | 'getListings': [ 368 | ('listing_ids', list, []), 369 | ('uids', list, []), 370 | ], 371 | 372 | 'getSubCategories': [ 373 | ('category', str, []), 374 | ], 375 | 376 | 'removeListing': [ 377 | ('listing_id', int, []), 378 | ('status', str, []), 379 | ], 380 | 381 | 'search': [ 382 | ('category', str, ['optional']), 383 | ('subcategory', str, ['optional']), 384 | ('query', str, ['optional']), 385 | ], 386 | }, 387 | 388 | # pages methods 389 | 'pages': { 390 | 'getInfo': [ 391 | ('page_ids', list, ['optional']), 392 | ('uid', int, ['optional']), 393 | ], 394 | 395 | 'isAdmin': [ 396 | ('page_id', int, []), 397 | ], 398 | 399 | 'isAppAdded': [ 400 | ('page_id', int, []), 401 | ], 402 | 403 | 'isFan': [ 404 | ('page_id', int, []), 405 | ('uid', int, []), 406 | ], 407 | }, 408 | 409 | # photos methods 410 | 'photos': { 411 | 'addTag': [ 412 | ('pid', int, []), 413 | ('tag_uid', int, [('default', 0)]), 414 | ('tag_text', str, [('default', '')]), 415 | ('x', float, [('default', 50)]), 416 | ('y', float, [('default', 50)]), 417 | ('tags', str, ['optional']), 418 | ], 419 | 420 | 'createAlbum': [ 421 | ('name', str, []), 422 | ('location', str, ['optional']), 423 | ('description', str, ['optional']), 424 | ], 425 | 426 | 'get': [ 427 | ('subj_id', int, ['optional']), 428 | ('aid', int, ['optional']), 429 | ('pids', list, ['optional']), 430 | ], 431 | 432 | 'getAlbums': [ 433 | ('uid', int, ['optional']), 434 | ('aids', list, ['optional']), 435 | ], 436 | 437 | 'getTags': [ 438 | ('pids', list, []), 439 | ], 440 | }, 441 | 442 | # fbml methods 443 | 'fbml': { 444 | 'refreshImgSrc': [ 445 | ('url', str, []), 446 | ], 447 | 448 | 'refreshRefUrl': [ 449 | ('url', str, []), 450 | ], 451 | 452 | 'setRefHandle': [ 453 | ('handle', str, []), 454 | ('fbml', str, []), 455 | ], 456 | }, 457 | 458 | # SMS Methods 459 | 'sms' : { 460 | 'canSend' : [ 461 | ('uid', int, []), 462 | ], 463 | 464 | 'send' : [ 465 | ('uid', int, []), 466 | ('message', str, []), 467 | ('session_id', int, []), 468 | ('req_session', bool, []), 469 | ], 470 | }, 471 | 472 | 'data': { 473 | 'getCookies': [ 474 | ('uid', int, []), 475 | ('string', str, []), 476 | ], 477 | 478 | 'setCookie': [ 479 | ('uid', int, []), 480 | ('name', str, []), 481 | ('value', str, []), 482 | ('expires', int, ['optional']), 483 | ('path', str, ['optional']), 484 | ], 485 | }, 486 | 487 | # connect methods 488 | 'connect': { 489 | 'registerUsers': [ 490 | ('accounts', json, []), 491 | ], 492 | 493 | 'unregisterUsers': [ 494 | ('email_hashes', json, []), 495 | ], 496 | 497 | 'getUnconnectedFriendsCount': [ 498 | ], 499 | }, 500 | } 501 | 502 | class Proxy(object): 503 | """Represents a "namespace" of Facebook API calls.""" 504 | 505 | def __init__(self, client, name): 506 | self._client = client 507 | self._name = name 508 | 509 | def __call__(self, method=None, args=None, add_session_args=True): 510 | # for Django templates 511 | if method is None: 512 | return self 513 | 514 | if add_session_args: 515 | self._client._add_session_args(args) 516 | 517 | return self._client('%s.%s' % (self._name, method), args) 518 | 519 | 520 | # generate the Facebook proxies 521 | def __generate_proxies(): 522 | for namespace in METHODS: 523 | methods = {} 524 | 525 | for method in METHODS[namespace]: 526 | params = ['self'] 527 | body = ['args = {}'] 528 | 529 | for param_name, param_type, param_options in METHODS[namespace][method]: 530 | param = param_name 531 | 532 | for option in param_options: 533 | if isinstance(option, tuple) and option[0] == 'default': 534 | if param_type == list: 535 | param = '%s=None' % param_name 536 | body.append('if %s is None: %s = %s' % (param_name, param_name, repr(option[1]))) 537 | else: 538 | param = '%s=%s' % (param_name, repr(option[1])) 539 | 540 | if param_type == json: 541 | # we only jsonify the argument if it's a list or a dict, for compatibility 542 | body.append('if isinstance(%s, list) or isinstance(%s, dict): %s = simplejson.dumps(%s)' % ((param_name,) * 4)) 543 | 544 | if 'optional' in param_options: 545 | param = '%s=None' % param_name 546 | body.append('if %s is not None: args[\'%s\'] = %s' % (param_name, param_name, param_name)) 547 | else: 548 | body.append('args[\'%s\'] = %s' % (param_name, param_name)) 549 | 550 | params.append(param) 551 | 552 | # simple docstring to refer them to Facebook API docs 553 | body.insert(0, '"""Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=%s.%s"""' % (namespace, method)) 554 | 555 | body.insert(0, 'def %s(%s):' % (method, ', '.join(params))) 556 | 557 | body.append('return self(\'%s\', args)' % method) 558 | 559 | exec('\n '.join(body)) 560 | 561 | methods[method] = eval(method) 562 | 563 | proxy = type('%sProxy' % namespace.title(), (Proxy, ), methods) 564 | 565 | globals()[proxy.__name__] = proxy 566 | 567 | 568 | __generate_proxies() 569 | 570 | 571 | class FacebookError(Exception): 572 | """Exception class for errors received from Facebook.""" 573 | 574 | def __init__(self, code, msg, args=None): 575 | self.code = code 576 | self.msg = msg 577 | self.args = args 578 | 579 | def __str__(self): 580 | return 'Error %s: %s' % (self.code, self.msg) 581 | 582 | 583 | class AuthProxy(Proxy): 584 | """Special proxy for facebook.auth.""" 585 | 586 | def getSession(self): 587 | """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=auth.getSession""" 588 | args = {} 589 | try: 590 | args['auth_token'] = self._client.auth_token 591 | except AttributeError: 592 | raise RuntimeError('Client does not have auth_token set.') 593 | result = self._client('%s.getSession' % self._name, args) 594 | self._client.session_key = result['session_key'] 595 | self._client.uid = result['uid'] 596 | self._client.secret = result.get('secret') 597 | self._client.session_key_expires = result['expires'] 598 | return result 599 | 600 | def createToken(self): 601 | """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=auth.createToken""" 602 | token = self._client('%s.createToken' % self._name) 603 | self._client.auth_token = token 604 | return token 605 | 606 | 607 | class FriendsProxy(FriendsProxy): 608 | """Special proxy for facebook.friends.""" 609 | 610 | def get(self, **kwargs): 611 | """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=friends.get""" 612 | if not kwargs.get('flid') and self._client._friends: 613 | return self._client._friends 614 | return super(FriendsProxy, self).get(**kwargs) 615 | 616 | 617 | class PhotosProxy(PhotosProxy): 618 | """Special proxy for facebook.photos.""" 619 | 620 | def upload(self, image, aid=None, caption=None, size=(604, 1024), filename=None): 621 | """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=photos.upload 622 | 623 | size -- an optional size (width, height) to resize the image to before uploading. Resizes by default 624 | to Facebook's maximum display width of 604. 625 | """ 626 | args = {} 627 | 628 | if aid is not None: 629 | args['aid'] = aid 630 | 631 | if caption is not None: 632 | args['caption'] = caption 633 | 634 | args = self._client._build_post_args('facebook.photos.upload', self._client._add_session_args(args)) 635 | 636 | try: 637 | import cStringIO as StringIO 638 | except ImportError: 639 | import StringIO 640 | 641 | # check for a filename specified...if the user is passing binary data in 642 | # image then a filename will be specified 643 | if filename is None: 644 | try: 645 | import Image 646 | except ImportError: 647 | data = StringIO.StringIO(open(image, 'rb').read()) 648 | else: 649 | img = Image.open(image) 650 | if size: 651 | img.thumbnail(size, Image.ANTIALIAS) 652 | data = StringIO.StringIO() 653 | img.save(data, img.format) 654 | else: 655 | # there was a filename specified, which indicates that image was not 656 | # the path to an image file but rather the binary data of a file 657 | data = StringIO.StringIO(image) 658 | image = filename 659 | 660 | content_type, body = self.__encode_multipart_formdata(list(args.iteritems()), [(image, data)]) 661 | urlinfo = urlparse.urlsplit(self._client.facebook_url) 662 | try: 663 | h = httplib.HTTP(urlinfo[1]) 664 | h.putrequest('POST', urlinfo[2]) 665 | h.putheader('Content-Type', content_type) 666 | h.putheader('Content-Length', str(len(body))) 667 | h.putheader('MIME-Version', '1.0') 668 | h.putheader('User-Agent', 'PyFacebook Client Library') 669 | h.endheaders() 670 | h.send(body) 671 | 672 | reply = h.getreply() 673 | 674 | if reply[0] != 200: 675 | raise Exception('Error uploading photo: Facebook returned HTTP %s (%s)' % (reply[0], reply[1])) 676 | 677 | response = h.file.read() 678 | except: 679 | # sending the photo failed, perhaps we are using GAE 680 | try: 681 | from google.appengine.api import urlfetch 682 | 683 | try: 684 | response = urlread(url=self._client.facebook_url,data=body,headers={'POST':urlinfo[2],'Content-Type':content_type,'MIME-Version':'1.0'}) 685 | except urllib2.URLError: 686 | raise Exception('Error uploading photo: Facebook returned %s' % (response)) 687 | except ImportError: 688 | # could not import from google.appengine.api, so we are not running in GAE 689 | raise Exception('Error uploading photo.') 690 | 691 | return self._client._parse_response(response, 'facebook.photos.upload') 692 | 693 | 694 | def __encode_multipart_formdata(self, fields, files): 695 | """Encodes a multipart/form-data message to upload an image.""" 696 | boundary = '-------tHISiStheMulTIFoRMbOUNDaRY' 697 | crlf = '\r\n' 698 | l = [] 699 | 700 | for (key, value) in fields: 701 | l.append('--' + boundary) 702 | l.append('Content-Disposition: form-data; name="%s"' % str(key)) 703 | l.append('') 704 | l.append(str(value)) 705 | for (filename, value) in files: 706 | l.append('--' + boundary) 707 | l.append('Content-Disposition: form-data; filename="%s"' % (str(filename), )) 708 | l.append('Content-Type: %s' % self.__get_content_type(filename)) 709 | l.append('') 710 | l.append(value.getvalue()) 711 | l.append('--' + boundary + '--') 712 | l.append('') 713 | body = crlf.join(l) 714 | content_type = 'multipart/form-data; boundary=%s' % boundary 715 | return content_type, body 716 | 717 | 718 | def __get_content_type(self, filename): 719 | """Returns a guess at the MIME type of the file from the filename.""" 720 | return str(mimetypes.guess_type(filename)[0]) or 'application/octet-stream' 721 | 722 | 723 | class Facebook(object): 724 | """ 725 | Provides access to the Facebook API. 726 | 727 | Instance Variables: 728 | 729 | added 730 | True if the user has added this application. 731 | 732 | api_key 733 | Your API key, as set in the constructor. 734 | 735 | app_name 736 | Your application's name, i.e. the APP_NAME in http://apps.facebook.com/APP_NAME/ if 737 | this is for an internal web application. Optional, but useful for automatic redirects 738 | to canvas pages. 739 | 740 | auth_token 741 | The auth token that Facebook gives you, either with facebook.auth.createToken, 742 | or through a GET parameter. 743 | 744 | callback_path 745 | The path of the callback set in the Facebook app settings. If your callback is set 746 | to http://www.example.com/facebook/callback/, this should be '/facebook/callback/'. 747 | Optional, but useful for automatic redirects back to the same page after login. 748 | 749 | desktop 750 | True if this is a desktop app, False otherwise. Used for determining how to 751 | authenticate. 752 | 753 | facebook_url 754 | The url to use for Facebook requests. 755 | 756 | facebook_secure_url 757 | The url to use for secure Facebook requests. 758 | 759 | in_canvas 760 | True if the current request is for a canvas page. 761 | 762 | internal 763 | True if this Facebook object is for an internal application (one that can be added on Facebook) 764 | 765 | page_id 766 | Set to the page_id of the current page (if any) 767 | 768 | secret 769 | Secret that is used after getSession for desktop apps. 770 | 771 | secret_key 772 | Your application's secret key, as set in the constructor. 773 | 774 | session_key 775 | The current session key. Set automatically by auth.getSession, but can be set 776 | manually for doing infinite sessions. 777 | 778 | session_key_expires 779 | The UNIX time of when this session key expires, or 0 if it never expires. 780 | 781 | uid 782 | After a session is created, you can get the user's UID with this variable. Set 783 | automatically by auth.getSession. 784 | 785 | ---------------------------------------------------------------------- 786 | 787 | """ 788 | 789 | def __init__(self, api_key, secret_key, auth_token=None, app_name=None, callback_path=None, internal=None, proxy=None, facebook_url=None, facebook_secure_url=None): 790 | """ 791 | Initializes a new Facebook object which provides wrappers for the Facebook API. 792 | 793 | If this is a desktop application, the next couple of steps you might want to take are: 794 | 795 | facebook.auth.createToken() # create an auth token 796 | facebook.login() # show a browser window 797 | wait_login() # somehow wait for the user to log in 798 | facebook.auth.getSession() # get a session key 799 | 800 | For web apps, if you are passed an auth_token from Facebook, pass that in as a named parameter. 801 | Then call: 802 | 803 | facebook.auth.getSession() 804 | 805 | """ 806 | self.api_key = api_key 807 | self.secret_key = secret_key 808 | self.session_key = None 809 | self.session_key_expires = None 810 | self.auth_token = auth_token 811 | self.secret = None 812 | self.uid = None 813 | self.page_id = None 814 | self.in_canvas = False 815 | self.added = False 816 | self.app_name = app_name 817 | self.callback_path = callback_path 818 | self.internal = internal 819 | self._friends = None 820 | self.proxy = proxy 821 | if facebook_url is None: 822 | self.facebook_url = FACEBOOK_URL 823 | else: 824 | self.facebook_url = facebook_url 825 | if facebook_secure_url is None: 826 | self.facebook_secure_url = FACEBOOK_SECURE_URL 827 | else: 828 | self.facebook_secure_url = facebook_secure_url 829 | 830 | for namespace in METHODS: 831 | self.__dict__[namespace] = eval('%sProxy(self, \'%s\')' % (namespace.title(), 'facebook.%s' % namespace)) 832 | 833 | self.auth = AuthProxy(self, 'facebook.auth') 834 | 835 | 836 | def _hash_args(self, args, secret=None): 837 | """Hashes arguments by joining key=value pairs, appending a secret, and then taking the MD5 hex digest.""" 838 | # @author: houyr 839 | # fix for UnicodeEncodeError 840 | hasher = md5.new(''.join(['%s=%s' % (isinstance(x, unicode) and x.encode("utf-8") or x, isinstance(args[x], unicode) and args[x].encode("utf-8") or args[x]) for x in sorted(args.keys())])) 841 | if secret: 842 | hasher.update(secret) 843 | elif self.secret: 844 | hasher.update(self.secret) 845 | else: 846 | hasher.update(self.secret_key) 847 | return hasher.hexdigest() 848 | 849 | 850 | def _parse_response_item(self, node): 851 | """Parses an XML response node from Facebook.""" 852 | if node.nodeType == node.DOCUMENT_NODE and \ 853 | node.childNodes[0].hasAttributes() and \ 854 | node.childNodes[0].hasAttribute('list') and \ 855 | node.childNodes[0].getAttribute('list') == "true": 856 | return {node.childNodes[0].nodeName: self._parse_response_list(node.childNodes[0])} 857 | elif node.nodeType == node.ELEMENT_NODE and \ 858 | node.hasAttributes() and \ 859 | node.hasAttribute('list') and \ 860 | node.getAttribute('list')=="true": 861 | return self._parse_response_list(node) 862 | elif len(filter(lambda x: x.nodeType == x.ELEMENT_NODE, node.childNodes)) > 0: 863 | return self._parse_response_dict(node) 864 | else: 865 | return ''.join(node.data for node in node.childNodes if node.nodeType == node.TEXT_NODE) 866 | 867 | 868 | def _parse_response_dict(self, node): 869 | """Parses an XML dictionary response node from Facebook.""" 870 | result = {} 871 | for item in filter(lambda x: x.nodeType == x.ELEMENT_NODE, node.childNodes): 872 | result[item.nodeName] = self._parse_response_item(item) 873 | if node.nodeType == node.ELEMENT_NODE and node.hasAttributes(): 874 | if node.hasAttribute('id'): 875 | result['id'] = node.getAttribute('id') 876 | return result 877 | 878 | 879 | def _parse_response_list(self, node): 880 | """Parses an XML list response node from Facebook.""" 881 | result = [] 882 | for item in filter(lambda x: x.nodeType == x.ELEMENT_NODE, node.childNodes): 883 | result.append(self._parse_response_item(item)) 884 | return result 885 | 886 | 887 | def _check_error(self, response): 888 | """Checks if the given Facebook response is an error, and then raises the appropriate exception.""" 889 | if type(response) is dict and response.has_key('error_code'): 890 | raise FacebookError(response['error_code'], response['error_msg'], response['request_args']) 891 | 892 | 893 | def _build_post_args(self, method, args=None): 894 | """Adds to args parameters that are necessary for every call to the API.""" 895 | if args is None: 896 | args = {} 897 | 898 | for arg in args.items(): 899 | if type(arg[1]) == list: 900 | args[arg[0]] = ','.join(str(a) for a in arg[1]) 901 | elif type(arg[1]) == unicode: 902 | args[arg[0]] = arg[1].encode("UTF-8") 903 | elif type(arg[1]) == bool: 904 | args[arg[0]] = str(arg[1]).lower() 905 | 906 | args['method'] = method 907 | args['api_key'] = self.api_key 908 | args['v'] = '1.0' 909 | args['format'] = RESPONSE_FORMAT 910 | args['sig'] = self._hash_args(args) 911 | 912 | return args 913 | 914 | 915 | def _add_session_args(self, args=None): 916 | """Adds 'session_key' and 'call_id' to args, which are used for API calls that need sessions.""" 917 | if args is None: 918 | args = {} 919 | 920 | if not self.session_key: 921 | return args 922 | #some calls don't need a session anymore. this might be better done in the markup 923 | #raise RuntimeError('Session key not set. Make sure auth.getSession has been called.') 924 | 925 | args['session_key'] = self.session_key 926 | args['call_id'] = str(int(time.time() * 1000)) 927 | 928 | return args 929 | 930 | 931 | def _parse_response(self, response, method, format=None): 932 | """Parses the response according to the given (optional) format, which should be either 'JSON' or 'XML'.""" 933 | if not format: 934 | format = RESPONSE_FORMAT 935 | 936 | if format == 'JSON': 937 | result = simplejson.loads(response) 938 | 939 | self._check_error(result) 940 | elif format == 'XML': 941 | dom = minidom.parseString(response) 942 | result = self._parse_response_item(dom) 943 | dom.unlink() 944 | 945 | if 'error_response' in result: 946 | self._check_error(result['error_response']) 947 | 948 | result = result[method[9:].replace('.', '_') + '_response'] 949 | else: 950 | raise RuntimeError('Invalid format specified.') 951 | 952 | return result 953 | 954 | 955 | def hash_email(self, email): 956 | """ 957 | Hash an email address in a format suitable for Facebook Connect. 958 | 959 | """ 960 | email = email.lower().strip() 961 | return "%s_%s" % ( 962 | struct.unpack("I", struct.pack("i", binascii.crc32(email)))[0], 963 | hashlib.md5(email).hexdigest(), 964 | ) 965 | 966 | 967 | def unicode_urlencode(self, params): 968 | """ 969 | @author: houyr 970 | A unicode aware version of urllib.urlencode. 971 | """ 972 | if isinstance(params, dict): 973 | params = params.items() 974 | return urllib.urlencode([(k, isinstance(v, unicode) and v.encode('utf-8') or v) 975 | for k, v in params]) 976 | 977 | 978 | def __call__(self, method=None, args=None, secure=False): 979 | """Make a call to Facebook's REST server.""" 980 | # for Django templates, if this object is called without any arguments 981 | # return the object itself 982 | if method is None: 983 | return self 984 | 985 | # @author: houyr 986 | # fix for bug of UnicodeEncodeError 987 | post_data = self.unicode_urlencode(self._build_post_args(method, args)) 988 | 989 | if self.proxy: 990 | proxy_handler = urllib2.ProxyHandler(self.proxy) 991 | opener = urllib2.build_opener(proxy_handler) 992 | if secure: 993 | response = opener.open(self.facebook_secure_url, post_data).read() 994 | else: 995 | response = opener.open(self.facebook_url, post_data).read() 996 | else: 997 | if secure: 998 | response = urlread(self.facebook_secure_url, post_data) 999 | else: 1000 | response = urlread(self.facebook_url, post_data) 1001 | 1002 | return self._parse_response(response, method) 1003 | 1004 | 1005 | # URL helpers 1006 | def get_url(self, page, **args): 1007 | """ 1008 | Returns one of the Facebook URLs (www.facebook.com/SOMEPAGE.php). 1009 | Named arguments are passed as GET query string parameters. 1010 | 1011 | """ 1012 | return 'http://www.facebook.com/%s.php?%s' % (page, urllib.urlencode(args)) 1013 | 1014 | 1015 | def get_app_url(self, path=''): 1016 | """ 1017 | Returns the URL for this app's canvas page, according to app_name. 1018 | 1019 | """ 1020 | return 'http://apps.facebook.com/%s/%s' % (self.app_name, path) 1021 | 1022 | 1023 | def get_add_url(self, next=None): 1024 | """ 1025 | Returns the URL that the user should be redirected to in order to add the application. 1026 | 1027 | """ 1028 | args = {'api_key': self.api_key, 'v': '1.0'} 1029 | 1030 | if next is not None: 1031 | args['next'] = next 1032 | 1033 | return self.get_url('install', **args) 1034 | 1035 | 1036 | def get_authorize_url(self, next=None, next_cancel=None): 1037 | """ 1038 | Returns the URL that the user should be redirected to in order to 1039 | authorize certain actions for application. 1040 | 1041 | """ 1042 | args = {'api_key': self.api_key, 'v': '1.0'} 1043 | 1044 | if next is not None: 1045 | args['next'] = next 1046 | 1047 | if next_cancel is not None: 1048 | args['next_cancel'] = next_cancel 1049 | 1050 | return self.get_url('authorize', **args) 1051 | 1052 | 1053 | def get_login_url(self, next=None, popup=False, canvas=True): 1054 | """ 1055 | Returns the URL that the user should be redirected to in order to login. 1056 | 1057 | next -- the URL that Facebook should redirect to after login 1058 | 1059 | """ 1060 | args = {'api_key': self.api_key, 'v': '1.0'} 1061 | 1062 | if next is not None: 1063 | args['next'] = next 1064 | 1065 | if canvas is True: 1066 | args['canvas'] = 1 1067 | 1068 | if popup is True: 1069 | args['popup'] = 1 1070 | 1071 | if self.auth_token is not None: 1072 | args['auth_token'] = self.auth_token 1073 | 1074 | return self.get_url('login', **args) 1075 | 1076 | 1077 | def login(self, popup=False): 1078 | """Open a web browser telling the user to login to Facebook.""" 1079 | import webbrowser 1080 | webbrowser.open(self.get_login_url(popup=popup)) 1081 | 1082 | 1083 | def get_ext_perm_url(self, ext_perm, next=None, popup=False): 1084 | """ 1085 | Returns the URL that the user should be redirected to in order to grant an extended permission. 1086 | 1087 | ext_perm -- the name of the extended permission to request 1088 | next -- the URL that Facebook should redirect to after login 1089 | 1090 | """ 1091 | args = {'ext_perm': ext_perm, 'api_key': self.api_key, 'v': '1.0'} 1092 | 1093 | if next is not None: 1094 | args['next'] = next 1095 | 1096 | if popup is True: 1097 | args['popup'] = 1 1098 | 1099 | return self.get_url('authorize', **args) 1100 | 1101 | 1102 | def request_extended_permission(self, ext_perm, popup=False): 1103 | """Open a web browser telling the user to grant an extended permission.""" 1104 | import webbrowser 1105 | webbrowser.open(self.get_ext_perm_url(ext_perm, popup=popup)) 1106 | 1107 | 1108 | def check_session(self, request): 1109 | """ 1110 | Checks the given Django HttpRequest for Facebook parameters such as 1111 | POST variables or an auth token. If the session is valid, returns True 1112 | and this object can now be used to access the Facebook API. Otherwise, 1113 | it returns False, and the application should take the appropriate action 1114 | (either log the user in or have him add the application). 1115 | 1116 | """ 1117 | self.in_canvas = (request.POST.get('fb_sig_in_canvas') == '1') 1118 | 1119 | if self.session_key and (self.uid or self.page_id): 1120 | return True 1121 | 1122 | if request.method == 'POST': 1123 | params = self.validate_signature(request.POST) 1124 | else: 1125 | if 'installed' in request.GET: 1126 | self.added = True 1127 | 1128 | if 'fb_page_id' in request.GET: 1129 | self.page_id = request.GET['fb_page_id'] 1130 | 1131 | if 'auth_token' in request.GET: 1132 | self.auth_token = request.GET['auth_token'] 1133 | 1134 | try: 1135 | self.auth.getSession() 1136 | except FacebookError, e: 1137 | self.auth_token = None 1138 | return False 1139 | 1140 | return True 1141 | 1142 | params = self.validate_signature(request.GET) 1143 | 1144 | if not params: 1145 | # first check if we are in django - to check cookies 1146 | if hasattr(request, 'COOKIES'): 1147 | params = self.validate_cookie_signature(request.COOKIES) 1148 | else: 1149 | # if not, then we might be on GoogleAppEngine, check their request object cookies 1150 | if hasattr(request,'cookies'): 1151 | params = self.validate_cookie_signature(request.cookies) 1152 | 1153 | if not params: 1154 | return False 1155 | 1156 | if params.get('in_canvas') == '1': 1157 | self.in_canvas = True 1158 | 1159 | if params.get('added') == '1': 1160 | self.added = True 1161 | 1162 | if params.get('expires'): 1163 | self.session_key_expires = int(params['expires']) 1164 | 1165 | if 'friends' in params: 1166 | if params['friends']: 1167 | self._friends = params['friends'].split(',') 1168 | else: 1169 | self._friends = [] 1170 | 1171 | if 'session_key' in params: 1172 | self.session_key = params['session_key'] 1173 | if 'user' in params: 1174 | self.uid = params['user'] 1175 | elif 'page_id' in params: 1176 | self.page_id = params['page_id'] 1177 | else: 1178 | return False 1179 | elif 'profile_session_key' in params: 1180 | self.session_key = params['profile_session_key'] 1181 | if 'profile_user' in params: 1182 | self.uid = params['profile_user'] 1183 | else: 1184 | return False 1185 | else: 1186 | return False 1187 | 1188 | return True 1189 | 1190 | 1191 | def validate_signature(self, post, prefix='fb_sig', timeout=None): 1192 | """ 1193 | Validate parameters passed to an internal Facebook app from Facebook. 1194 | 1195 | """ 1196 | args = post.copy() 1197 | 1198 | if prefix not in args: 1199 | return None 1200 | 1201 | del args[prefix] 1202 | 1203 | if timeout and '%s_time' % prefix in post and time.time() - float(post['%s_time' % prefix]) > timeout: 1204 | return None 1205 | 1206 | args = dict([(key[len(prefix + '_'):], value) for key, value in args.items() if key.startswith(prefix)]) 1207 | 1208 | hash = self._hash_args(args) 1209 | 1210 | if hash == post[prefix]: 1211 | return args 1212 | else: 1213 | return None 1214 | 1215 | def validate_cookie_signature(self, cookies): 1216 | """ 1217 | Validate parameters passed by cookies, namely facebookconnect or js api. 1218 | """ 1219 | if not self.api_key in cookies.keys(): 1220 | return None 1221 | 1222 | sigkeys = [] 1223 | params = dict() 1224 | for k in sorted(cookies.keys()): 1225 | if k.startswith(self.api_key+"_"): 1226 | sigkeys.append(k) 1227 | params[k.replace(self.api_key+"_","")] = cookies[k] 1228 | 1229 | 1230 | vals = ''.join(['%s=%s' % (x.replace(self.api_key+"_",""), cookies[x]) for x in sigkeys]) 1231 | hasher = md5.new(vals) 1232 | 1233 | hasher.update(self.secret_key) 1234 | digest = hasher.hexdigest() 1235 | if digest == cookies[self.api_key]: 1236 | return params 1237 | else: 1238 | return False 1239 | 1240 | 1241 | 1242 | 1243 | if __name__ == '__main__': 1244 | # sample desktop application 1245 | 1246 | api_key = '' 1247 | secret_key = '' 1248 | 1249 | facebook = Facebook(api_key, secret_key) 1250 | 1251 | facebook.auth.createToken() 1252 | 1253 | # Show login window 1254 | # Set popup=True if you want login without navigational elements 1255 | facebook.login() 1256 | 1257 | # Login to the window, then press enter 1258 | print 'After logging in, press enter...' 1259 | raw_input() 1260 | 1261 | facebook.auth.getSession() 1262 | print 'Session Key: ', facebook.session_key 1263 | print 'Your UID: ', facebook.uid 1264 | 1265 | info = facebook.users.getInfo([facebook.uid], ['name', 'birthday', 'affiliations', 'sex'])[0] 1266 | 1267 | print 'Your Name: ', info['name'] 1268 | print 'Your Birthday: ', info['birthday'] 1269 | print 'Your Gender: ', info['sex'] 1270 | 1271 | friends = facebook.friends.get() 1272 | friends = facebook.users.getInfo(friends[0:5], ['name', 'birthday', 'relationship_status']) 1273 | 1274 | for friend in friends: 1275 | print friend['name'], 'has a birthday on', friend['birthday'], 'and is', friend['relationship_status'] 1276 | 1277 | arefriends = facebook.friends.areFriends([friends[0]['uid']], [friends[1]['uid']]) 1278 | 1279 | photos = facebook.photos.getAlbums(facebook.uid) 1280 | 1281 | --------------------------------------------------------------------------------