diff --git a/setup.py b/setup.py index d27204f..d7ccba5 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,7 @@ install_requires = [ 'six', 'anyjson', + 'requests' ] extra = {} if PY3: diff --git a/src/fbconsole.py b/src/fbconsole.py index 99e935c..7c19501 100644 --- a/src/fbconsole.py +++ b/src/fbconsole.py @@ -15,34 +15,18 @@ # under the License. import BaseHTTPServer -import cookielib -import httplib import anyjson as json -import random -import mimetypes import os import os.path -import stat +import requests 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 @@ -56,30 +40,10 @@ 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 @@ -97,7 +61,6 @@ __all__ = [ 'help', 'authenticate', - 'automatically_authenticate', 'logout', 'graph_url', 'oauth_url', @@ -116,78 +79,6 @@ '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): @@ -208,7 +99,7 @@ def do_GET(self): data['expires_at'] = 'never' else: data['expires_at'] = int(time.time()+int(expiration)) - open(ACCESS_TOKEN_FILE,'w').write(json.dumps(data)) + open(ACCESS_TOKEN_FILE, 'w').write(json.dumps(data)) self.wfile.write(b(AUTH_SUCCESS_HTML)) else: self.wfile.write(b('' @@ -242,18 +133,6 @@ class UnknownApiException(ApiException): 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: @@ -268,58 +147,6 @@ def _handle_http_error(e): 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 ''' @@ -365,79 +192,6 @@ def authenticate(): 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): @@ -480,7 +234,7 @@ class Batch: 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 + facebook. You canFile file0 has not been attached still use the results in other requests using the specialized syntax, but facebook won't send the results back. >>> image = open("icon.gif", "rb") @@ -597,7 +351,7 @@ def __build_params(self): payload['attached_files'] = ','.join(files) batch.append(payload) - params = {'batch':json.dumps(batch)} + params = {'batch': json.dumps(batch)} for i, f in enumerate(all_files): params['file%s' % i] = f @@ -657,33 +411,32 @@ def __get_url(self, path, args=None): return endpoint+str(path)+'?'+urlencode(args) def get(self, path, params=None): - return _safe_json_load(self.__get_url(path, args=params)) + response = requests.get(self.__get_url(path, args=params)) + return response.json() 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 + + files = {} + non_files = {} + + for key in params: + value = params[key] + if isinstance(value, FileType): + files[key] = value + else: + non_files[key] = value + + response = requests.post(self.__get_url(path), data=non_files, files=files) + return response.json() def delete(self, path, params=None): - if not params: - params = {} - params['method'] = 'delete' - return post(path, params) + params = params or {} + return requests.delete(self.__get_url(path), params=params).json() def fql(self, query): url = self.__get_url('/fql', args={'q': query}) - return _safe_json_load(url)['data'] + return requests.get(url).json()['data'] def graph_url(self, path, params=None): return self.__get_url(path, args=params) @@ -731,14 +484,16 @@ def iter_pages(json_response): There are at least 5 feed stories """ - while len(json_response.get('data','')): + + while len(json_response.get('data', '')): for item in json_response['data']: yield item try: next_url = json_response['paging']['next'] + json_response = requests.get(next_url).json() except KeyError: break - json_response = _safe_json_load(next_url) + def post(path, params=None): """Send a POST request to the graph api.