#!/usr/bin/env python # # Copyright 2014-present, Facebook, Inc. # All rights reserved. # # This source code is licensed under the license found in the # LICENSE file in the root directory of this source tree. import BaseHTTPServer import cookielib import httplib import anyjson as json import random import mimetypes import os import os.path import stat import time import types import urllib import webbrowser import StringIO import six from six import b poster_is_available = False try: # try to use poster if it is available import poster.streaminghttp import poster.encode poster.streaminghttp.register_openers() poster_is_available = True except ImportError: pass # we can live without this. from urlparse import urlparse from pprint import pprint try: from urlparse import parse_qs except ImportError: from cgi import parse_qs if six.PY3: import io FileType = io.IOBase else: FileType = types.FileType if six.PY3: from urllib.request import build_opener from urllib.request import HTTPCookieProcessor from urllib.request import BaseHandler from urllib.request import HTTPHandler from urllib.request import urlopen from urllib.request import Request from urllib.parse import urlencode from urllib.error import HTTPError else: from urllib2 import build_opener from urllib2 import HTTPCookieProcessor from urllib2 import BaseHandler from urllib2 import HTTPHandler from urllib2 import urlopen from urllib2 import HTTPError from urllib2 import Request from urllib import urlencode # import mechanize in python 2.x if six.PY3: mechanize = None else: import mechanize APP_ID = '179745182062082' SERVER_PORT = 8080 ACCESS_TOKEN = None CLIENT = None ACCESS_TOKEN_FILE = '.fb_access_token' AUTH_SCOPE = [] BATCH_REQUEST_LIMIT = 50 SANDBOX_DOMAIN = None AUTH_SUCCESS_HTML = """ You have successfully logged in to facebook with fbconsole. You can close this window now. """ __all__ = [ 'help', 'authenticate', 'automatically_authenticate', 'logout', 'graph_url', 'oauth_url', 'Batch', 'get', 'post', 'delete', 'shell', 'fql', 'iter_pages', 'Client', 'APP_ID', 'SERVER_PORT', 'ACCESS_TOKEN', 'AUTH_SCOPE', 'ACCESS_TOKEN_FILE', 'SANDBOX_DOMAIN'] class _MultipartPostHandler(BaseHandler): handler_order = HTTPHandler.handler_order - 10 # needs to run first def http_request(self, request): data = request.get_data() if data is not None and not isinstance(data, types.StringTypes): files = [] params = [] try: for key, value in data.items(): if isinstance(value, FileType): files.append((key, value)) else: params.append((key, value)) except TypeError: raise TypeError("not a valid non-string sequence or mapping object") if len(files) == 0: data = urlencode(params) if six.PY3: data = data.encode('utf-8') else: boundary, data = self.multipart_encode(params, files) contenttype = 'multipart/form-data; boundary=%s' % boundary request.add_unredirected_header('Content-Type', contenttype) request.add_data(data) return request https_request = http_request def multipart_encode(self, params, files, boundary=None, buffer=None): if six.PY3: boundary = boundary or b('--------------------%s---' % random.random()) buffer = buffer or b('') for key, value in params: buffer += b('--%s\r\n' % boundary) buffer += b('Content-Disposition: form-data; name="%s"' % key) buffer += b('\r\n\r\n' + value + '\r\n') for key, fd in files: file_size = os.fstat(fd.fileno())[stat.ST_SIZE] filename = fd.name.split('/')[-1] contenttype = mimetypes.guess_type(filename)[0] or 'application/octet-stream' buffer += b('--%s\r\n' % boundary) buffer += b('Content-Disposition: form-data; ') buffer += b('name="%s"; filename="%s"\r\n' % (key, filename)) buffer += b('Content-Type: %s\r\n' % contenttype) fd.seek(0) buffer += b('\r\n') + fd.read() + b('\r\n') buffer += b('--%s--\r\n\r\n' % boundary) else: boundary = boundary or '--------------------%s---' % random.random() buffer = buffer or '' for key, value in params: buffer += '--%s\r\n' % boundary buffer += 'Content-Disposition: form-data; name="%s"' % key buffer += '\r\n\r\n' + value + '\r\n' for key, fd in files: file_size = os.fstat(fd.fileno())[stat.ST_SIZE] filename = fd.name.split('/')[-1] contenttype = mimetypes.guess_type(filename)[0] or 'application/octet-stream' buffer += '--%s\r\n' % boundary buffer += 'Content-Disposition: form-data; ' buffer += 'name="%s"; filename="%s"\r\n' % (key, filename) buffer += 'Content-Type: %s\r\n' % contenttype fd.seek(0) buffer += '\r\n' + fd.read() + '\r\n' buffer += '--%s--\r\n\r\n' % boundary return boundary, buffer class _RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): def do_GET(self): global ACCESS_TOKEN self.send_response(200) self.send_header("Content-type", "text/html") self.end_headers() params = parse_qs(urlparse(self.path).query) ACCESS_TOKEN = params.get('access_token', [None])[0] if ACCESS_TOKEN: data = {'scope': AUTH_SCOPE, 'access_token': ACCESS_TOKEN} expiration = params.get('expires_in', [None])[0] if expiration: if expiration == '0': # this is what's returned when offline_access is requested data['expires_at'] = 'never' else: data['expires_at'] = int(time.time()+int(expiration)) open(ACCESS_TOKEN_FILE,'w').write(json.dumps(data)) self.wfile.write(b(AUTH_SUCCESS_HTML)) else: self.wfile.write(b('' '' '')) class ApiException(Exception): def __init__(self, message, error_type, code): super(ApiException, self).__init__(message) self.error_type = error_type self.code = code @staticmethod def from_json(data): error_type = data.get('type') for subclass in ApiException.__subclasses__(): if subclass.__name__ == error_type: return subclass(data.get('message'), data.get('error_type'), data.get('code')) return UnknownApiException(data.get('message'), data.get('error_type'), data.get('code')) class UnknownApiException(ApiException): """Some unknown error.""" class OAuthException(ApiException): """Just an oath exception.""" class AutomaticLoginError(Exception): """ An error has occurred during login. This can occur for a number of reasons. Make sure you have correctly specified the username, password, client_secret, redirect_uri, and APP_ID for your facebook app: https://developers.facebook.com/apps """ def __str__(self): return self.__class__.__doc__ def _handle_http_error(e): body = e.read() if six.PY3: body = body.decode('utf-8') try: body = json.loads(body) except ValueError: pass else: error = body.get('error') if error: return ApiException.from_json(error) return e def _safe_url_load(*args, **kwargs): """Wrapper around urlopen that translates http errors into nicer exceptions.""" try: return urlopen(*args, **kwargs) except HTTPError, e: error = _handle_http_error(e) raise error def _safe_json_load(*args, **kwargs): f = _safe_url_load(*args, **kwargs) if six.PY3: return json.loads(f.read().decode('utf-8')) else: return json.loads(f.read()) def _instantiate_browser(debug=False): """Setup the mechanize browser to handle redirects, robots, etc to programmatically complete oauth. Helpful resources: * http://stockrt.github.com/p/emulating-a-browser-in-python-with-mechanize/ """ # instantiate the mechanize browser browser = mechanize.Browser() # set different settings browser.set_handle_equiv(True) # browser.set_handle_gzip(True) # this is experimental browser.set_handle_redirect(True) browser.set_handle_referer(True) browser.set_handle_robots(False) browser.set_handle_refresh( mechanize._http.HTTPRefreshProcessor(), max_time=1, ) # add debug logging if debug: browser.set_debug_http(True) browser.set_debug_redirects(True) browser.set_debug_responses(True) # add the cookie jar cj = cookielib.LWPCookieJar() browser.set_cookiejar(cj) # setup the headers browser.addheaders = [('User-agent', 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.1) Gecko/2008071615 Fedora/3.0.1-1.fc9 Firefox/3.0.1')] return browser def help(): """Print out some helpful information""" print ''' The following commands are available: help() - display this help message authenticate() - authenticate with facebook. logout() - Remove the cached access token, forcing authenticate() to get a new access token graph_url(path, params) - get the full url to a graph api path get(path, params) - call the graph api with the given path and query parameters post(path, data) - post data to the graph api with the given path delete(path, params) - send a delete request fql(query) - make an fql request ''' def authenticate(): """Authenticate with facebook so you can make api calls that require auth. Alternatively you can just set the ACCESS_TOKEN global variable in this module to set an access token you get from facebook. If you want to request certain permissions, set the AUTH_SCOPE global variable to the list of permissions you want. """ global ACCESS_TOKEN needs_auth = True if os.path.exists(ACCESS_TOKEN_FILE): data = json.loads(open(ACCESS_TOKEN_FILE).read()) expires_at = data.get('expires_at') still_valid = expires_at and (expires_at == 'never' or expires_at > time.time()) if still_valid and set(data['scope']).issuperset(AUTH_SCOPE): ACCESS_TOKEN = data['access_token'] needs_auth = False if needs_auth: webbrowser.open(oauth_url( APP_ID, 'http://local.fbconsole.com:%s/' % SERVER_PORT, AUTH_SCOPE )) httpd = BaseHTTPServer.HTTPServer(('127.0.0.1', SERVER_PORT), _RequestHandler) while ACCESS_TOKEN is None: httpd.handle_request() def automatically_authenticate(username, password, app_secret, redirect_uri, debug=False): """Authenticate with facebook automatically so that server-side facebook apps can make api calls that require authorization. A username, password, and app_secret must be specified (http://developers.facebook.com/docs/authentication/server-side/) This method automatically sets the ACCESS_TOKEN so that all subsequent calls to facebook are authenticated. If you want to request certain permissions, set the AUTH_SCOPE global variable to the list of permissions you want. """ # use the global APP_ID and AUTH_SCOPE for authentication. this # method sets the ACCESS_TOKEN at the end global APP_ID global AUTH_SCOPE global ACCESS_TOKEN # throw an import error as mechanize does not work with python # 3. upset? contribute! https://github.com/jjlee/mechanize if mechanize is None: msg = "automatically_authenticate method is not compatible with " msg += "python 3.x due to mechanize incapatability." raise ImportError(msg) # instantiate the browser browser = _instantiate_browser(debug=debug) # the state is a random string that is used in subsequent requests chars = "abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ" state = ''.join((random.choice(chars) for i in range(30))) # 1. redirect the "user" (a server-side user, in this case) to the # OAuth dialog url = "https://www.facebook.com/dialog/oauth?" + urllib.urlencode({ "client_id": APP_ID, "redirect_uri": redirect_uri, "scope": ','.join(AUTH_SCOPE), "state": state, }) browser.open(url) # 2. "user" is prompted to authorize your application browser.select_form(nr=0) browser.form["email"] = username browser.form["pass"] = password response = browser.submit() # 3. Once the user is redirected back to our app, parse out the # code generated by facebook auth_url = urlparse(response.geturl()) oauth = parse_qs(auth_url.query) if "state" not in oauth: raise AutomaticLoginError assert oauth["state"][0] == state, "Inconsistent state: %s != %s" % ( oauth["state"][0], state, ) code = oauth["code"][0] # 4. Exchange the code for a user access token for this user's data url="https://graph.facebook.com/oauth/access_token?"+urllib.urlencode({ "client_id": APP_ID, "redirect_uri": redirect_uri, "client_secret": app_secret, "code": code, }) browser.open(url) response = browser.response() oauth = parse_qs(response.read()) ACCESS_TOKEN = oauth["access_token"][0] def logout(): """Logout of facebook. This just removes the cached access token.""" if os.path.exists(ACCESS_TOKEN_FILE): os.remove(ACCESS_TOKEN_FILE) class _GraphRequest: def __init__(self, method, path, params, name, ignore_result): self.method = method self.path = path self.params = params or {} self.name = name self.ignore_result = ignore_result self.result = None self.error = None def get_result(self): if self.error: raise self.error return self.result class Batch: """A class that lets you batch multiple graph api calls into a single request. First we create a new batch instance. >>> batch = Batch() Then we can start fetching a bunch of stuff by calling get/post/delete/etc. on the batch object. When calling these methods, a request object will be returned which can be used to fetch the result of that request after the batch has been sent. By passing in a name, you can refer to the results of a previous request in a subsequent request using the special syntax defined documented here: https://developers.facebook.com/docs/reference/api/batch/ >>> me = batch.get('/me', name='me') >>> coke = batch.get('/cocacola', name='coke') If you pass in ignore_result=True when making the request, then no request object will be returned and the results will not be passed down from facebook. You can still use the results in other requests using the specialized syntax, but facebook won't send the results back. >>> image = open("icon.gif", "rb") >>> batch.post('/me/photos', ... {'name': '{result=me:$.name} likes {result=coke:$.name}', ... 'source': image}, ... name='photo', ... ignore_result=True) >>> photo = batch.get('/{result=photo:$.id}') Now we can send the request: >>> batch.send() >>> image.close() And look at the results: >>> print me.get_result()['name'] David Amcafiaddddh Yangstein >>> print coke.get_result()['name'] Coca-Cola >>> print photo.get_result()['name'] David Amcafiaddddh Yangstein likes Coca-Cola If you try to send a batch request twice, it will fail. You must reconstruct a new batch. >>> batch.send() Traceback (most recent call last): ... RuntimeError: This batch request has already been sent There is also a limit to the number of requests that can be sent in a single batch. Going over this limit will cause an exception to be thrown. >>> batch = Batch() >>> for i in xrange(BATCH_REQUEST_LIMIT+1): ... batch.get('/me') Traceback (most recent call last): ... RuntimeError: You can't send more than 50 requests in a single batch """ def __init__(self, client=None): self.client = client self.__api_calls = [] self.__batch_request_sent = False def __add_request(self, request): if len(self.__api_calls) >= BATCH_REQUEST_LIMIT: raise RuntimeError( "You can't send more than %s requests in a single batch" % BATCH_REQUEST_LIMIT) self.__api_calls.append(request) if request.ignore_result: return None return request def get(self, path, params=None, name=None, ignore_result=False): return self.__add_request(_GraphRequest('GET', path[1:], params, name, ignore_result)) def post(self, path, params=None, name=None, ignore_result=False): return self.__add_request(_GraphRequest('POST', path[1:], params, name, ignore_result)) def delete(self, path, params=None, name=None, ignore_result=False): return self.__add_request(_GraphRequest('DELETE', path[1:], params, name, ignore_result)) def fql(self, query, name=None, ignore_result=False): return self.__add_request(_GraphRequest('GET', 'fql', {'q': query}, name, ignore_result)) def __build_params(self): # See https://developers.facebook.com/docs/reference/api/batch/ # for documentation on how the batch api is supposed to work. batch = [] all_files = [] for request in self.__api_calls: payload = {'method': request.method} if not request.ignore_result: payload['omit_response_on_success'] = False if request.name: payload['name'] = request.name if request.method in ['GET', 'DELETE']: payload['relative_url'] = request.path+'?'+urlencode(request.params) elif request.method == 'POST': payload['relative_url'] = request.path files = [] params = {} for key, value in request.params.iteritems(): if isinstance(value, FileType): all_files.append(value) files.append('file%s' % (len(all_files) - 1)) else: params[key] = value payload['body'] = urlencode(params) payload['attached_files'] = ','.join(files) batch.append(payload) params = {'batch':json.dumps(batch)} for i, f in enumerate(all_files): params['file%s' % i] = f return params def send(self): if self.__batch_request_sent: raise RuntimeError("This batch request has already been sent") self.__batch_request_sent = True client = self.client or _get_client() responses = client.post('', self.__build_params()) # process the response for request, response in zip(self.__api_calls, responses): if response is None: # this happens when you use the result in a following request continue if response['code'] == 200: request.result = json.loads(response['body']) else: request.error = ApiException.from_json(json.loads(response['body'])['error']) class Client: """A class that encapsulates a client for a single access token. Using a Client object, you can make requests using different access tokens within the same application. >>> user1 = Client('AAACjeiZB6FgIBAJhJMaspnA8V06q859FvUJsJtVbEsXpEKOv5H6RIvU7hWU5EgINj5fBPoGlVt0JIkWVYHVemOmehqMyiQFSWDbDq0AZDZD') >>> user2 = Client('AAACjeiZB6FgIBAB8eZABg7So8ALDisFLugfIJSZCg3FEDRy82yEmdXYYfNvdv2kWVMWxaJgWqqVMPtG5v5n4lMG5VXmZBZBykQkeluhpFPQZDZD') >>> print user1.get('/me')['name'] David Amcfbdajbbhi Alisonsen >>> print user2.get('/me')['name'] David Amcafiaddddh Yangstein """ def __init__(self, access_token=None): self.access_token = access_token def __get_url(self, path, args=None): args = args or {} if self.access_token: args['access_token'] = self.access_token subdomain = 'graph' if '/videos' in path: subdomain = 'graph-video' if SANDBOX_DOMAIN: subdomain = '.'.join([subdomain, SANDBOX_DOMAIN]) protocol = 'http://' if 'access_token' in args or 'client_secret' in args: protocol = 'https://' endpoint = "%s%s.facebook.com" % (protocol, subdomain) return endpoint+str(path)+'?'+urlencode(args) def get(self, path, params=None): return _safe_json_load(self.__get_url(path, args=params)) def post(self, path, params=None): params = params or {} if poster_is_available: data, headers = poster.encode.multipart_encode(params) request = Request(self.__get_url(path), data, headers) return _safe_json_load(request) else: opener = build_opener( HTTPCookieProcessor(cookielib.CookieJar()), _MultipartPostHandler) try: return json.loads(opener.open(self.__get_url(path), params).read().decode('utf-8')) except HTTPError, e: error = _handle_http_error(e) raise error def delete(self, path, params=None): if not params: params = {} params['method'] = 'delete' return post(path, params) def fql(self, query): url = self.__get_url('/fql', args={'q': query}) return _safe_json_load(url)['data'] def graph_url(self, path, params=None): return self.__get_url(path, args=params) def _get_client(): global CLIENT if not CLIENT or CLIENT.access_token != ACCESS_TOKEN: CLIENT = Client(ACCESS_TOKEN) return CLIENT def get(path, params=None): """Send a GET request to the graph api. For example: >>> user = get('/me') >>> print user['first_name'] David >>> short_user = get('/me', {'fields':'id,first_name'}) >>> print short_user['id'], short_user['first_name'] 100003169144448 David """ return _get_client().get(path, params=params) def iter_pages(json_response): """Iterate over multiple pages of data. The graph api can return a lot of data, but will only return a limited amount of data in a single request. To get more data, you must query for it explicitly using paging. This function will do automatic paging in the form of an iterator. For example to print the id of every photo tagged with the logged in user: >>> total = 0 >>> for photo in iter_pages(get('/19292868552/feed', {'limit':2})): ... total += 1 ... print "There are at least", total, "feed stories" ... if total > 4: break There are at least 1 feed stories There are at least 2 feed stories There are at least 3 feed stories There are at least 4 feed stories There are at least 5 feed stories """ while len(json_response.get('data','')): for item in json_response['data']: yield item try: next_url = json_response['paging']['next'] except KeyError: break json_response = _safe_json_load(next_url) def post(path, params=None): """Send a POST request to the graph api. You can also upload files using this function. For example: >>> image = open("icon.gif", "rb") >>> photo_id = post('/me/photos', ... {'name': 'My Photo', ... 'source': image})['id'] >>> image.close() >>> print get('/'+photo_id)['name'] My Photo Or like an object: >>> success = post('/'+photo_id+'/likes') >>> print get('/'+photo_id+'/likes')['data'][0]['name'] David Amcafiaddddh Yangstein """ return _get_client().post(path, params=params) def delete(path, params=None): """Send a DELETE request to the graph api. For example: >>> msg_id = post('/me/feed', {'message':'hello world'})['id'] >>> delete('/'+msg_id) True """ return _get_client().delete(path, params=params) def fql(query): """Make an fql request. For example: >>> data = fql('SELECT name FROM user WHERE uid = me()') >>> print data[0]['name'] David Amcafiaddddh Yangstein """ return _get_client().fql(query) def graph_url(path, params=None): """Get the full url to the graph api for the given path and query args. This is useful if you want to use your own method of making http requests or are not interested in the json parsing that occurs by default. For example, download a large profile picture of Mark Zuckerberg: >>> url = graph_url('/zuck/picture', {"type":"large"}) >>> filename, response = urllib.urlretrieve(url, 'mark.jpg') """ return _get_client().graph_url(path, params=params) def oauth_url(app_id, redirect_uri, auth_scope): """Generates a url to an oath authentication dialog. >>> from urlparse import urlparse, parse_qsl >>> url = urlparse(oauth_url(APP_ID, 'http://127.0.0.1:8080/', ['publish_stream'])) >>> url.scheme 'https' >>> url.path '/dialog/oauth' >>> url.hostname 'www.facebook.com' >>> sorted(parse_qsl(url.query)) [('client_id', '179745182062082'), ('redirect_uri', 'http://127.0.0.1:8080/'), ('response_type', 'token'), ('scope', 'publish_stream')] """ return 'https://www.facebook.com/dialog/oauth?' + \ urlencode({'client_id':app_id, 'redirect_uri':redirect_uri, 'response_type':'token', 'scope':','.join(auth_scope)}) INTRO_MESSAGE = '''\ __ _ _ / _| | | | | |_| |__ ___ ___ _ __ ___ ___ | | ___ | _| '_ \ / __|/ _ \| '_ \/ __|/ _ \| |/ _ \\ | | | |_) | (__| (_) | | | \__ \ (_) | | __/ |_| |_.__/ \___|\___/|_| |_|___/\___/|_|\___| Type help() for a list of commands. quick start: >>> authenticate() >>> print "Hello", get('/me')['name'] ''' def shell(): try: from IPython.Shell import IPShellEmbed IPShellEmbed()(INTRO_MESSAGE) except ImportError: import code code.InteractiveConsole(globals()).interact(INTRO_MESSAGE) def test_suite(): import doctest global ACCESS_TOKEN ACCESS_TOKEN = 'AAACjeiZB6FgIBAB8eZABg7So8ALDisFLugfIJSZCg3FEDRy82yEmdXYYfNvdv2kWVMWxaJgWqqVMPtG5v5n4lMG5VXmZBZBykQkeluhpFPQZDZD' return doctest.DocTestSuite() if __name__ == '__main__': shell()