Add additional debugging to try and identify cause of MB#11103
[hermes] / package / src / pythonfacebook.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 simplejson
63 except ImportError:
64     try:
65         import json as 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         print post_data
989
990         if self.proxy:
991             proxy_handler = urllib2.ProxyHandler(self.proxy)
992             opener = urllib2.build_opener(proxy_handler)
993             if secure:
994                 response = opener.open(self.facebook_secure_url, post_data).read() 
995             else:
996                 response = opener.open(self.facebook_url, post_data).read()
997         else:
998             if secure:
999                 response = urlread(self.facebook_secure_url, post_data)
1000             else:
1001                 response = urlread(self.facebook_url, post_data)
1002
1003         print response
1004         return self._parse_response(response, method)
1005
1006     
1007     # URL helpers
1008     def get_url(self, page, **args):
1009         """
1010         Returns one of the Facebook URLs (www.facebook.com/SOMEPAGE.php).
1011         Named arguments are passed as GET query string parameters.
1012
1013         """
1014         print 'page, args:', page, args
1015         return 'http://www.facebook.com/%s.php?%s' % (page, urllib.urlencode(args))
1016
1017
1018     def get_app_url(self, path=''):
1019         """
1020         Returns the URL for this app's canvas page, according to app_name.
1021         
1022         """
1023         return 'http://apps.facebook.com/%s/%s' % (self.app_name, path)
1024
1025
1026     def get_add_url(self, next=None):
1027         """
1028         Returns the URL that the user should be redirected to in order to add the application.
1029
1030         """
1031         args = {'api_key': self.api_key, 'v': '1.0'}
1032
1033         if next is not None:
1034             args['next'] = next
1035
1036         return self.get_url('install', **args)
1037
1038
1039     def get_authorize_url(self, next=None, next_cancel=None):
1040         """
1041         Returns the URL that the user should be redirected to in order to
1042         authorize certain actions for application.
1043
1044         """
1045         args = {'api_key': self.api_key, 'v': '1.0'}
1046
1047         if next is not None:
1048             args['next'] = next
1049
1050         if next_cancel is not None:
1051             args['next_cancel'] = next_cancel
1052
1053         return self.get_url('authorize', **args)
1054
1055
1056     def get_login_url(self, next=None, popup=False, canvas=False):
1057         """
1058         Returns the URL that the user should be redirected to in order to login.
1059
1060         next -- the URL that Facebook should redirect to after login
1061
1062         """
1063         args = {'api_key': self.api_key, 'v': '1.0'}
1064
1065         if next is not None:
1066             args['next'] = next
1067
1068         if canvas is True:
1069             args['canvas'] = 1
1070
1071         if popup is True:
1072             args['popup'] = 1
1073
1074         if self.auth_token is not None:
1075             args['auth_token'] = self.auth_token
1076
1077         return self.get_url('login', **args)
1078
1079
1080     def login(self, popup=False):
1081         """Open a web browser telling the user to login to Facebook."""
1082         import webbrowser
1083         webbrowser.open(self.get_login_url(popup=popup))
1084
1085
1086     def get_ext_perm_url(self, ext_perm, next=None, popup=False):
1087         """
1088         Returns the URL that the user should be redirected to in order to grant an extended permission.
1089
1090         ext_perm -- the name of the extended permission to request
1091         next     -- the URL that Facebook should redirect to after login
1092
1093         """
1094         args = {'ext_perm': ext_perm, 'api_key': self.api_key, 'v': '1.0'}
1095
1096         if next is not None:
1097             args['next'] = next
1098
1099         if popup is True:
1100             args['popup'] = 1
1101
1102         return self.get_url('authorize', **args)
1103
1104
1105     def request_extended_permission(self, ext_perm, popup=False):
1106         """Open a web browser telling the user to grant an extended permission."""
1107         import webbrowser
1108         webbrowser.open(self.get_ext_perm_url(ext_perm, popup=popup))
1109
1110
1111     def check_session(self, request):
1112         """
1113         Checks the given Django HttpRequest for Facebook parameters such as
1114         POST variables or an auth token. If the session is valid, returns True
1115         and this object can now be used to access the Facebook API. Otherwise,
1116         it returns False, and the application should take the appropriate action
1117         (either log the user in or have him add the application).
1118
1119         """
1120         self.in_canvas = (request.POST.get('fb_sig_in_canvas') == '1')
1121
1122         if self.session_key and (self.uid or self.page_id):
1123             return True
1124
1125         if request.method == 'POST':
1126             params = self.validate_signature(request.POST)
1127         else:
1128             if 'installed' in request.GET:
1129                 self.added = True
1130
1131             if 'fb_page_id' in request.GET:
1132                 self.page_id = request.GET['fb_page_id']
1133
1134             if 'auth_token' in request.GET:
1135                 self.auth_token = request.GET['auth_token']
1136
1137                 try:
1138                     self.auth.getSession()
1139                 except FacebookError, e:
1140                     self.auth_token = None
1141                     return False
1142
1143                 return True
1144
1145             params = self.validate_signature(request.GET)
1146
1147         if not params:
1148             # first check if we are in django - to check cookies
1149             if hasattr(request, 'COOKIES'):
1150                 params = self.validate_cookie_signature(request.COOKIES)
1151             else:
1152                 # if not, then we might be on GoogleAppEngine, check their request object cookies
1153                 if hasattr(request,'cookies'):
1154                     params = self.validate_cookie_signature(request.cookies)
1155
1156         if not params:
1157             return False
1158
1159         if params.get('in_canvas') == '1':
1160             self.in_canvas = True
1161
1162         if params.get('added') == '1':
1163             self.added = True
1164
1165         if params.get('expires'):
1166             self.session_key_expires = int(params['expires'])
1167
1168         if 'friends' in params:
1169             if params['friends']:
1170                 self._friends = params['friends'].split(',')
1171             else:
1172                 self._friends = []
1173
1174         if 'session_key' in params:
1175             self.session_key = params['session_key']
1176             if 'user' in params:
1177                 self.uid = params['user']
1178             elif 'page_id' in params:
1179                 self.page_id = params['page_id']
1180             else:
1181                 return False
1182         elif 'profile_session_key' in params:
1183             self.session_key = params['profile_session_key']
1184             if 'profile_user' in params:
1185                 self.uid = params['profile_user']
1186             else:
1187                 return False
1188         else:
1189             return False
1190
1191         return True
1192
1193
1194     def validate_signature(self, post, prefix='fb_sig', timeout=None):
1195         """
1196         Validate parameters passed to an internal Facebook app from Facebook.
1197
1198         """
1199         args = post.copy()
1200
1201         if prefix not in args:
1202             return None
1203
1204         del args[prefix]
1205
1206         if timeout and '%s_time' % prefix in post and time.time() - float(post['%s_time' % prefix]) > timeout:
1207             return None
1208
1209         args = dict([(key[len(prefix + '_'):], value) for key, value in args.items() if key.startswith(prefix)])
1210
1211         hash = self._hash_args(args)
1212
1213         if hash == post[prefix]:
1214             return args
1215         else:
1216             return None
1217
1218     def validate_cookie_signature(self, cookies):
1219         """
1220         Validate parameters passed by cookies, namely facebookconnect or js api.
1221         """
1222         if not self.api_key in cookies.keys():
1223             return None
1224
1225         sigkeys = []
1226         params = dict()
1227         for k in sorted(cookies.keys()):
1228             if k.startswith(self.api_key+"_"):
1229                 sigkeys.append(k)
1230                 params[k.replace(self.api_key+"_","")] = cookies[k]
1231
1232
1233         vals = ''.join(['%s=%s' % (x.replace(self.api_key+"_",""), cookies[x]) for x in sigkeys])
1234         hasher = md5.new(vals)
1235         
1236         hasher.update(self.secret_key)
1237         digest = hasher.hexdigest()
1238         if digest == cookies[self.api_key]:
1239             return params
1240         else:
1241             return False
1242
1243
1244
1245
1246 if __name__ == '__main__':
1247     # sample desktop application
1248
1249     api_key = ''
1250     secret_key = ''
1251
1252     facebook = Facebook(api_key, secret_key)
1253
1254     facebook.auth.createToken()
1255
1256     # Show login window
1257     # Set popup=True if you want login without navigational elements
1258     facebook.login()
1259
1260     # Login to the window, then press enter
1261     print 'After logging in, press enter...'
1262     raw_input()
1263
1264     facebook.auth.getSession()
1265     print 'Session Key:   ', facebook.session_key
1266     print 'Your UID:      ', facebook.uid
1267
1268     info = facebook.users.getInfo([facebook.uid], ['name', 'birthday', 'affiliations', 'sex'])[0]
1269
1270     print 'Your Name:     ', info['name']
1271     print 'Your Birthday: ', info['birthday']
1272     print 'Your Gender:   ', info['sex']
1273
1274     friends = facebook.friends.get()
1275     friends = facebook.users.getInfo(friends[0:5], ['name', 'birthday', 'relationship_status'])
1276
1277     for friend in friends:
1278         print friend['name'], 'has a birthday on', friend['birthday'], 'and is', friend['relationship_status']
1279
1280     arefriends = facebook.friends.areFriends([friends[0]['uid']], [friends[1]['uid']])
1281
1282     photos = facebook.photos.getAlbums(facebook.uid)