diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a5ae9a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.DS_STORE +*.pyc +private_key +public_key +.twittercredentials diff --git a/README b/README deleted file mode 100644 index 5dde4fc..0000000 --- a/README +++ /dev/null @@ -1,32 +0,0 @@ -*THIS IS REALLY BUGGY/HACKED TOGETHER* - -Basically you create a new keypair with -./rsachat -g - -enter your twitter login information with -./rsachat -a User Pass - -give your friends the file Private_key (dont ask.. -.-) -and start tweeting with -./rsachat -t "#thiswasatriumph Huge Success" - -and reading tweets like -./rsachat -r thiswasatriumph someUser their_key - -You can also now SEND FILES over twitter! -./rsachat -f "#sometextFile" blah.txt - -ikebook:rsatweets ashleyis$ ./rsachat.py -h -RSA Encrypted tweets by ikex -Usage: rsachat.py [options] - -Options: - --version show program's version number and exit - -h, --help show this help message and exit - -g Generate a private/public key for use - -a login Your twitter login specified as - -t #tag tweet Post a tweet starting with the hashtag eg, - "#RSAToMyFriends Hi guys!" - -f #tag filename.txt Post a file encrypted as tweets eg, "#tag " - -r tag author pubkey Reads a tweet with the specified tag author pubkey -ikebook:rsatweets ashleyis$ diff --git a/README.md b/README.md index 856edb4..5c9e66c 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,21 @@ *THIS IS REALLY BUGGY/HACKED TOGETHER* -![Alt text](http://img696.imageshack.us/img696/7217/screenshot20100314at954.png "In action") +Updated with twitter OAUTH login support(and latest supporting libraries as of 8/1/11), thanks to [@mpesce](http://twitter.com/mpesce) +![Alt text](http://img696.imageshack.us/img696/7217/screenshot20100314at954.png "In action") -Basically you create a new keypair with +Basically you create a new keypair with: ``./rsachat -g`` -enter your twitter login information with -``./rsachat -a User Pass`` +Receive a twitter login token with(and follow the prompts): +``./rsachat -o`` -give your friends the file Private_key (dont ask.. -.-) +Give your friends the file private_key and start tweeting with ``./rsachat -t thiswasatriumph "Huge Success" `` and reading tweets like -``./rsachat -r thiswasatriumph someUser their_key`` +``./rsachat -r thiswasatriumph some_user their_key`` You can also now SEND FILES over twitter! ``./rsachat -f "#sometextFile" blah.txt`` diff --git a/httplib2/__init__.py b/httplib2/__init__.py new file mode 100644 index 0000000..3cebcb3 --- /dev/null +++ b/httplib2/__init__.py @@ -0,0 +1,1202 @@ +from __future__ import generators +""" +httplib2 + +A caching http interface that supports ETags and gzip +to conserve bandwidth. + +Requires Python 2.3 or later + +Changelog: +2007-08-18, Rick: Modified so it's able to use a socks proxy if needed. + +""" + +__author__ = "Joe Gregorio (joe@bitworking.org)" +__copyright__ = "Copyright 2006, Joe Gregorio" +__contributors__ = ["Thomas Broyer (t.broyer@ltgt.net)", + "James Antill", + "Xavier Verges Farrero", + "Jonathan Feinberg", + "Blair Zajac", + "Sam Ruby", + "Louis Nyffenegger"] +__license__ = "MIT" +__version__ = "$Rev$" + +import re +import sys +import email +import email.Utils +import email.Message +import email.FeedParser +import StringIO +import gzip +import zlib +import httplib +import urlparse +import base64 +import os +import copy +import calendar +import time +import random +# remove depracated warning in python2.6 +try: + from hashlib import sha1 as _sha, md5 as _md5 +except ImportError: + import sha + import md5 + _sha = sha.new + _md5 = md5.new +import hmac +from gettext import gettext as _ +import socket + +try: + import socks +except ImportError: + socks = None + +# Build the appropriate socket wrapper for ssl +try: + import ssl # python 2.6 + _ssl_wrap_socket = ssl.wrap_socket +except ImportError: + def _ssl_wrap_socket(sock, key_file, cert_file): + ssl_sock = socket.ssl(sock, key_file, cert_file) + return httplib.FakeSocket(sock, ssl_sock) + + +if sys.version_info >= (2,3): + from iri2uri import iri2uri +else: + def iri2uri(uri): + return uri + +def has_timeout(timeout): # python 2.6 + if hasattr(socket, '_GLOBAL_DEFAULT_TIMEOUT'): + return (timeout is not None and timeout is not socket._GLOBAL_DEFAULT_TIMEOUT) + return (timeout is not None) + +__all__ = ['Http', 'Response', 'ProxyInfo', 'HttpLib2Error', + 'RedirectMissingLocation', 'RedirectLimit', 'FailedToDecompressContent', + 'UnimplementedDigestAuthOptionError', 'UnimplementedHmacDigestAuthOptionError', + 'debuglevel'] + + +# The httplib debug level, set to a non-zero value to get debug output +debuglevel = 0 + + +# Python 2.3 support +if sys.version_info < (2,4): + def sorted(seq): + seq.sort() + return seq + +# Python 2.3 support +def HTTPResponse__getheaders(self): + """Return list of (header, value) tuples.""" + if self.msg is None: + raise httplib.ResponseNotReady() + return self.msg.items() + +if not hasattr(httplib.HTTPResponse, 'getheaders'): + httplib.HTTPResponse.getheaders = HTTPResponse__getheaders + +# All exceptions raised here derive from HttpLib2Error +class HttpLib2Error(Exception): pass + +# Some exceptions can be caught and optionally +# be turned back into responses. +class HttpLib2ErrorWithResponse(HttpLib2Error): + def __init__(self, desc, response, content): + self.response = response + self.content = content + HttpLib2Error.__init__(self, desc) + +class RedirectMissingLocation(HttpLib2ErrorWithResponse): pass +class RedirectLimit(HttpLib2ErrorWithResponse): pass +class FailedToDecompressContent(HttpLib2ErrorWithResponse): pass +class UnimplementedDigestAuthOptionError(HttpLib2ErrorWithResponse): pass +class UnimplementedHmacDigestAuthOptionError(HttpLib2ErrorWithResponse): pass + +class RelativeURIError(HttpLib2Error): pass +class ServerNotFoundError(HttpLib2Error): pass + +# Open Items: +# ----------- +# Proxy support + +# Are we removing the cached content too soon on PUT (only delete on 200 Maybe?) + +# Pluggable cache storage (supports storing the cache in +# flat files by default. We need a plug-in architecture +# that can support Berkeley DB and Squid) + +# == Known Issues == +# Does not handle a resource that uses conneg and Last-Modified but no ETag as a cache validator. +# Does not handle Cache-Control: max-stale +# Does not use Age: headers when calculating cache freshness. + + +# The number of redirections to follow before giving up. +# Note that only GET redirects are automatically followed. +# Will also honor 301 requests by saving that info and never +# requesting that URI again. +DEFAULT_MAX_REDIRECTS = 5 + +# Which headers are hop-by-hop headers by default +HOP_BY_HOP = ['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailers', 'transfer-encoding', 'upgrade'] + +def _get_end2end_headers(response): + hopbyhop = list(HOP_BY_HOP) + hopbyhop.extend([x.strip() for x in response.get('connection', '').split(',')]) + return [header for header in response.keys() if header not in hopbyhop] + +URI = re.compile(r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?") + +def parse_uri(uri): + """Parses a URI using the regex given in Appendix B of RFC 3986. + + (scheme, authority, path, query, fragment) = parse_uri(uri) + """ + groups = URI.match(uri).groups() + return (groups[1], groups[3], groups[4], groups[6], groups[8]) + +def urlnorm(uri): + (scheme, authority, path, query, fragment) = parse_uri(uri) + if not scheme or not authority: + raise RelativeURIError("Only absolute URIs are allowed. uri = %s" % uri) + authority = authority.lower() + scheme = scheme.lower() + if not path: + path = "/" + # Could do syntax based normalization of the URI before + # computing the digest. See Section 6.2.2 of Std 66. + request_uri = query and "?".join([path, query]) or path + scheme = scheme.lower() + defrag_uri = scheme + "://" + authority + request_uri + return scheme, authority, request_uri, defrag_uri + + +# Cache filename construction (original borrowed from Venus http://intertwingly.net/code/venus/) +re_url_scheme = re.compile(r'^\w+://') +re_slash = re.compile(r'[?/:|]+') + +def safename(filename): + """Return a filename suitable for the cache. + + Strips dangerous and common characters to create a filename we + can use to store the cache in. + """ + + try: + if re_url_scheme.match(filename): + if isinstance(filename,str): + filename = filename.decode('utf-8') + filename = filename.encode('idna') + else: + filename = filename.encode('idna') + except UnicodeError: + pass + if isinstance(filename,unicode): + filename=filename.encode('utf-8') + filemd5 = _md5(filename).hexdigest() + filename = re_url_scheme.sub("", filename) + filename = re_slash.sub(",", filename) + + # limit length of filename + if len(filename)>200: + filename=filename[:200] + return ",".join((filename, filemd5)) + +NORMALIZE_SPACE = re.compile(r'(?:\r\n)?[ \t]+') +def _normalize_headers(headers): + return dict([ (key.lower(), NORMALIZE_SPACE.sub(value, ' ').strip()) for (key, value) in headers.iteritems()]) + +def _parse_cache_control(headers): + retval = {} + if headers.has_key('cache-control'): + parts = headers['cache-control'].split(',') + parts_with_args = [tuple([x.strip().lower() for x in part.split("=", 1)]) for part in parts if -1 != part.find("=")] + parts_wo_args = [(name.strip().lower(), 1) for name in parts if -1 == name.find("=")] + retval = dict(parts_with_args + parts_wo_args) + return retval + +# Whether to use a strict mode to parse WWW-Authenticate headers +# Might lead to bad results in case of ill-formed header value, +# so disabled by default, falling back to relaxed parsing. +# Set to true to turn on, usefull for testing servers. +USE_WWW_AUTH_STRICT_PARSING = 0 + +# In regex below: +# [^\0-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+ matches a "token" as defined by HTTP +# "(?:[^\0-\x08\x0A-\x1f\x7f-\xff\\\"]|\\[\0-\x7f])*?" matches a "quoted-string" as defined by HTTP, when LWS have already been replaced by a single space +# Actually, as an auth-param value can be either a token or a quoted-string, they are combined in a single pattern which matches both: +# \"?((?<=\")(?:[^\0-\x1f\x7f-\xff\\\"]|\\[\0-\x7f])*?(?=\")|(?@,;:\\\"/[\]?={} \t]+(?!\"))\"? +WWW_AUTH_STRICT = re.compile(r"^(?:\s*(?:,\s*)?([^\0-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+)\s*=\s*\"?((?<=\")(?:[^\0-\x08\x0A-\x1f\x7f-\xff\\\"]|\\[\0-\x7f])*?(?=\")|(?@,;:\\\"/[\]?={} \t]+(?!\"))\"?)(.*)$") +WWW_AUTH_RELAXED = re.compile(r"^(?:\s*(?:,\s*)?([^ \t\r\n=]+)\s*=\s*\"?((?<=\")(?:[^\\\"]|\\.)*?(?=\")|(? current_age: + retval = "FRESH" + return retval + +def _decompressContent(response, new_content): + content = new_content + try: + encoding = response.get('content-encoding', None) + if encoding in ['gzip', 'deflate']: + if encoding == 'gzip': + content = gzip.GzipFile(fileobj=StringIO.StringIO(new_content)).read() + if encoding == 'deflate': + content = zlib.decompress(content) + response['content-length'] = str(len(content)) + # Record the historical presence of the encoding in a way the won't interfere. + response['-content-encoding'] = response['content-encoding'] + del response['content-encoding'] + except IOError: + content = "" + raise FailedToDecompressContent(_("Content purported to be compressed with %s but failed to decompress.") % response.get('content-encoding'), response, content) + return content + +def _updateCache(request_headers, response_headers, content, cache, cachekey): + if cachekey: + cc = _parse_cache_control(request_headers) + cc_response = _parse_cache_control(response_headers) + if cc.has_key('no-store') or cc_response.has_key('no-store'): + cache.delete(cachekey) + else: + info = email.Message.Message() + for key, value in response_headers.iteritems(): + if key not in ['status','content-encoding','transfer-encoding']: + info[key] = value + + # Add annotations to the cache to indicate what headers + # are variant for this request. + vary = response_headers.get('vary', None) + if vary: + vary_headers = vary.lower().replace(' ', '').split(',') + for header in vary_headers: + key = '-varied-%s' % header + try: + info[key] = request_headers[header] + except KeyError: + pass + + status = response_headers.status + if status == 304: + status = 200 + + status_header = 'status: %d\r\n' % response_headers.status + + header_str = info.as_string() + + header_str = re.sub("\r(?!\n)|(? 0: + service = "cl" + # No point in guessing Base or Spreadsheet + #elif request_uri.find("spreadsheets") > 0: + # service = "wise" + + auth = dict(Email=credentials[0], Passwd=credentials[1], service=service, source=headers['user-agent']) + resp, content = self.http.request("https://www.google.com/accounts/ClientLogin", method="POST", body=urlencode(auth), headers={'Content-Type': 'application/x-www-form-urlencoded'}) + lines = content.split('\n') + d = dict([tuple(line.split("=", 1)) for line in lines if line]) + if resp.status == 403: + self.Auth = "" + else: + self.Auth = d['Auth'] + + def request(self, method, request_uri, headers, content): + """Modify the request headers to add the appropriate + Authorization header.""" + headers['authorization'] = 'GoogleLogin Auth=' + self.Auth + + +AUTH_SCHEME_CLASSES = { + "basic": BasicAuthentication, + "wsse": WsseAuthentication, + "digest": DigestAuthentication, + "hmacdigest": HmacDigestAuthentication, + "googlelogin": GoogleLoginAuthentication +} + +AUTH_SCHEME_ORDER = ["hmacdigest", "googlelogin", "digest", "wsse", "basic"] + +class FileCache(object): + """Uses a local directory as a store for cached files. + Not really safe to use if multiple threads or processes are going to + be running on the same cache. + """ + def __init__(self, cache, safe=safename): # use safe=lambda x: md5.new(x).hexdigest() for the old behavior + self.cache = cache + self.safe = safe + if not os.path.exists(cache): + os.makedirs(self.cache) + + def get(self, key): + retval = None + cacheFullPath = os.path.join(self.cache, self.safe(key)) + try: + f = file(cacheFullPath, "rb") + retval = f.read() + f.close() + except IOError: + pass + return retval + + def set(self, key, value): + cacheFullPath = os.path.join(self.cache, self.safe(key)) + f = file(cacheFullPath, "wb") + f.write(value) + f.close() + + def delete(self, key): + cacheFullPath = os.path.join(self.cache, self.safe(key)) + if os.path.exists(cacheFullPath): + os.remove(cacheFullPath) + +class Credentials(object): + def __init__(self): + self.credentials = [] + + def add(self, name, password, domain=""): + self.credentials.append((domain.lower(), name, password)) + + def clear(self): + self.credentials = [] + + def iter(self, domain): + for (cdomain, name, password) in self.credentials: + if cdomain == "" or domain == cdomain: + yield (name, password) + +class KeyCerts(Credentials): + """Identical to Credentials except that + name/password are mapped to key/cert.""" + pass + + +class ProxyInfo(object): + """Collect information required to use a proxy.""" + def __init__(self, proxy_type, proxy_host, proxy_port, proxy_rdns=None, proxy_user=None, proxy_pass=None): + """The parameter proxy_type must be set to one of socks.PROXY_TYPE_XXX + constants. For example: + +p = ProxyInfo(proxy_type=socks.PROXY_TYPE_HTTP, proxy_host='localhost', proxy_port=8000) + """ + self.proxy_type, self.proxy_host, self.proxy_port, self.proxy_rdns, self.proxy_user, self.proxy_pass = proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass + + def astuple(self): + return (self.proxy_type, self.proxy_host, self.proxy_port, self.proxy_rdns, + self.proxy_user, self.proxy_pass) + + def isgood(self): + return socks and (self.proxy_host != None) and (self.proxy_port != None) + + +class HTTPConnectionWithTimeout(httplib.HTTPConnection): + """HTTPConnection subclass that supports timeouts""" + + def __init__(self, host, port=None, strict=None, timeout=None, proxy_info=None): + httplib.HTTPConnection.__init__(self, host, port, strict) + self.timeout = timeout + self.proxy_info = proxy_info + + def connect(self): + """Connect to the host and port specified in __init__.""" + # Mostly verbatim from httplib.py. + msg = "getaddrinfo returns an empty list" + for res in socket.getaddrinfo(self.host, self.port, 0, + socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + try: + if self.proxy_info and self.proxy_info.isgood(): + self.sock = socks.socksocket(af, socktype, proto) + self.sock.setproxy(*self.proxy_info.astuple()) + else: + self.sock = socket.socket(af, socktype, proto) + # Different from httplib: support timeouts. + if has_timeout(self.timeout): + self.sock.settimeout(self.timeout) + # End of difference from httplib. + if self.debuglevel > 0: + print "connect: (%s, %s)" % (self.host, self.port) + + self.sock.connect(sa) + except socket.error, msg: + if self.debuglevel > 0: + print 'connect fail:', (self.host, self.port) + if self.sock: + self.sock.close() + self.sock = None + continue + break + if not self.sock: + raise socket.error, msg + +class HTTPSConnectionWithTimeout(httplib.HTTPSConnection): + "This class allows communication via SSL." + + def __init__(self, host, port=None, key_file=None, cert_file=None, + strict=None, timeout=None, proxy_info=None): + httplib.HTTPSConnection.__init__(self, host, port=port, key_file=key_file, + cert_file=cert_file, strict=strict) + self.timeout = timeout + self.proxy_info = proxy_info + + def connect(self): + "Connect to a host on a given (SSL) port." + + if self.proxy_info and self.proxy_info.isgood(): + sock = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM) + sock.setproxy(*self.proxy_info.astuple()) + else: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + if has_timeout(self.timeout): + sock.settimeout(self.timeout) + sock.connect((self.host, self.port)) + self.sock =_ssl_wrap_socket(sock, self.key_file, self.cert_file) + + + +class Http(object): + """An HTTP client that handles: +- all methods +- caching +- ETags +- compression, +- HTTPS +- Basic +- Digest +- WSSE + +and more. + """ + def __init__(self, cache=None, timeout=None, proxy_info=None): + """The value of proxy_info is a ProxyInfo instance. + +If 'cache' is a string then it is used as a directory name +for a disk cache. Otherwise it must be an object that supports +the same interface as FileCache.""" + self.proxy_info = proxy_info + # Map domain name to an httplib connection + self.connections = {} + # The location of the cache, for now a directory + # where cached responses are held. + if cache and isinstance(cache, str): + self.cache = FileCache(cache) + else: + self.cache = cache + + # Name/password + self.credentials = Credentials() + + # Key/cert + self.certificates = KeyCerts() + + # authorization objects + self.authorizations = [] + + # If set to False then no redirects are followed, even safe ones. + self.follow_redirects = True + + # Which HTTP methods do we apply optimistic concurrency to, i.e. + # which methods get an "if-match:" etag header added to them. + self.optimistic_concurrency_methods = ["PUT"] + + # If 'follow_redirects' is True, and this is set to True then + # all redirecs are followed, including unsafe ones. + self.follow_all_redirects = False + + self.ignore_etag = False + + self.force_exception_to_status_code = False + + self.timeout = timeout + + def _auth_from_challenge(self, host, request_uri, headers, response, content): + """A generator that creates Authorization objects + that can be applied to requests. + """ + challenges = _parse_www_authenticate(response, 'www-authenticate') + for cred in self.credentials.iter(host): + for scheme in AUTH_SCHEME_ORDER: + if challenges.has_key(scheme): + yield AUTH_SCHEME_CLASSES[scheme](cred, host, request_uri, headers, response, content, self) + + def add_credentials(self, name, password, domain=""): + """Add a name and password that will be used + any time a request requires authentication.""" + self.credentials.add(name, password, domain) + + def add_certificate(self, key, cert, domain): + """Add a key and cert that will be used + any time a request requires authentication.""" + self.certificates.add(key, cert, domain) + + def clear_credentials(self): + """Remove all the names and passwords + that are used for authentication""" + self.credentials.clear() + self.authorizations = [] + + def _conn_request(self, conn, request_uri, method, body, headers): + for i in range(2): + try: + conn.request(method, request_uri, body, headers) + except socket.gaierror: + conn.close() + raise ServerNotFoundError("Unable to find the server at %s" % conn.host) + except (socket.error, httplib.HTTPException): + # Just because the server closed the connection doesn't apparently mean + # that the server didn't send a response. + pass + try: + response = conn.getresponse() + except (socket.error, httplib.HTTPException): + if i == 0: + conn.close() + conn.connect() + continue + else: + raise + else: + content = "" + if method == "HEAD": + response.close() + else: + content = response.read() + response = Response(response) + if method != "HEAD": + content = _decompressContent(response, content) + break + return (response, content) + + + def _request(self, conn, host, absolute_uri, request_uri, method, body, headers, redirections, cachekey): + """Do the actual request using the connection object + and also follow one level of redirects if necessary""" + + auths = [(auth.depth(request_uri), auth) for auth in self.authorizations if auth.inscope(host, request_uri)] + auth = auths and sorted(auths)[0][1] or None + if auth: + auth.request(method, request_uri, headers, body) + + (response, content) = self._conn_request(conn, request_uri, method, body, headers) + + if auth: + if auth.response(response, body): + auth.request(method, request_uri, headers, body) + (response, content) = self._conn_request(conn, request_uri, method, body, headers ) + response._stale_digest = 1 + + if response.status == 401: + for authorization in self._auth_from_challenge(host, request_uri, headers, response, content): + authorization.request(method, request_uri, headers, body) + (response, content) = self._conn_request(conn, request_uri, method, body, headers, ) + if response.status != 401: + self.authorizations.append(authorization) + authorization.response(response, body) + break + + if (self.follow_all_redirects or (method in ["GET", "HEAD"]) or response.status == 303): + if self.follow_redirects and response.status in [300, 301, 302, 303, 307]: + # Pick out the location header and basically start from the beginning + # remembering first to strip the ETag header and decrement our 'depth' + if redirections: + if not response.has_key('location') and response.status != 300: + raise RedirectMissingLocation( _("Redirected but the response is missing a Location: header."), response, content) + # Fix-up relative redirects (which violate an RFC 2616 MUST) + if response.has_key('location'): + location = response['location'] + (scheme, authority, path, query, fragment) = parse_uri(location) + if authority == None: + response['location'] = urlparse.urljoin(absolute_uri, location) + if response.status == 301 and method in ["GET", "HEAD"]: + response['-x-permanent-redirect-url'] = response['location'] + if not response.has_key('content-location'): + response['content-location'] = absolute_uri + _updateCache(headers, response, content, self.cache, cachekey) + if headers.has_key('if-none-match'): + del headers['if-none-match'] + if headers.has_key('if-modified-since'): + del headers['if-modified-since'] + if response.has_key('location'): + location = response['location'] + old_response = copy.deepcopy(response) + if not old_response.has_key('content-location'): + old_response['content-location'] = absolute_uri + redirect_method = ((response.status == 303) and (method not in ["GET", "HEAD"])) and "GET" or method + (response, content) = self.request(location, redirect_method, body=body, headers = headers, redirections = redirections - 1) + response.previous = old_response + else: + raise RedirectLimit( _("Redirected more times than rediection_limit allows."), response, content) + elif response.status in [200, 203] and method == "GET": + # Don't cache 206's since we aren't going to handle byte range requests + if not response.has_key('content-location'): + response['content-location'] = absolute_uri + _updateCache(headers, response, content, self.cache, cachekey) + + return (response, content) + + def _normalize_headers(self, headers): + return _normalize_headers(headers) + +# Need to catch and rebrand some exceptions +# Then need to optionally turn all exceptions into status codes +# including all socket.* and httplib.* exceptions. + + + def request(self, uri, method="GET", body=None, headers=None, redirections=DEFAULT_MAX_REDIRECTS, connection_type=None): + """ Performs a single HTTP request. +The 'uri' is the URI of the HTTP resource and can begin +with either 'http' or 'https'. The value of 'uri' must be an absolute URI. + +The 'method' is the HTTP method to perform, such as GET, POST, DELETE, etc. +There is no restriction on the methods allowed. + +The 'body' is the entity body to be sent with the request. It is a string +object. + +Any extra headers that are to be sent with the request should be provided in the +'headers' dictionary. + +The maximum number of redirect to follow before raising an +exception is 'redirections. The default is 5. + +The return value is a tuple of (response, content), the first +being and instance of the 'Response' class, the second being +a string that contains the response entity body. + """ + try: + if headers is None: + headers = {} + else: + headers = self._normalize_headers(headers) + + if not headers.has_key('user-agent'): + headers['user-agent'] = "Python-httplib2/%s" % __version__ + + uri = iri2uri(uri) + + (scheme, authority, request_uri, defrag_uri) = urlnorm(uri) + domain_port = authority.split(":")[0:2] + if len(domain_port) == 2 and domain_port[1] == '443' and scheme == 'http': + scheme = 'https' + authority = domain_port[0] + + conn_key = scheme+":"+authority + if conn_key in self.connections: + conn = self.connections[conn_key] + else: + if not connection_type: + connection_type = (scheme == 'https') and HTTPSConnectionWithTimeout or HTTPConnectionWithTimeout + certs = list(self.certificates.iter(authority)) + if scheme == 'https' and certs: + conn = self.connections[conn_key] = connection_type(authority, key_file=certs[0][0], + cert_file=certs[0][1], timeout=self.timeout, proxy_info=self.proxy_info) + else: + conn = self.connections[conn_key] = connection_type(authority, timeout=self.timeout, proxy_info=self.proxy_info) + conn.set_debuglevel(debuglevel) + + if method in ["GET", "HEAD"] and 'range' not in headers and 'accept-encoding' not in headers: + headers['accept-encoding'] = 'gzip, deflate' + + info = email.Message.Message() + cached_value = None + if self.cache: + cachekey = defrag_uri + cached_value = self.cache.get(cachekey) + if cached_value: + # info = email.message_from_string(cached_value) + # + # Need to replace the line above with the kludge below + # to fix the non-existent bug not fixed in this + # bug report: http://mail.python.org/pipermail/python-bugs-list/2005-September/030289.html + try: + info, content = cached_value.split('\r\n\r\n', 1) + feedparser = email.FeedParser.FeedParser() + feedparser.feed(info) + info = feedparser.close() + feedparser._parse = None + except IndexError: + self.cache.delete(cachekey) + cachekey = None + cached_value = None + else: + cachekey = None + + if method in self.optimistic_concurrency_methods and self.cache and info.has_key('etag') and not self.ignore_etag and 'if-match' not in headers: + # http://www.w3.org/1999/04/Editing/ + headers['if-match'] = info['etag'] + + if method not in ["GET", "HEAD"] and self.cache and cachekey: + # RFC 2616 Section 13.10 + self.cache.delete(cachekey) + + # Check the vary header in the cache to see if this request + # matches what varies in the cache. + if method in ['GET', 'HEAD'] and 'vary' in info: + vary = info['vary'] + vary_headers = vary.lower().replace(' ', '').split(',') + for header in vary_headers: + key = '-varied-%s' % header + value = info[key] + if headers.get(header, '') != value: + cached_value = None + break + + if cached_value and method in ["GET", "HEAD"] and self.cache and 'range' not in headers: + if info.has_key('-x-permanent-redirect-url'): + # Should cached permanent redirects be counted in our redirection count? For now, yes. + (response, new_content) = self.request(info['-x-permanent-redirect-url'], "GET", headers = headers, redirections = redirections - 1) + response.previous = Response(info) + response.previous.fromcache = True + else: + # Determine our course of action: + # Is the cached entry fresh or stale? + # Has the client requested a non-cached response? + # + # There seems to be three possible answers: + # 1. [FRESH] Return the cache entry w/o doing a GET + # 2. [STALE] Do the GET (but add in cache validators if available) + # 3. [TRANSPARENT] Do a GET w/o any cache validators (Cache-Control: no-cache) on the request + entry_disposition = _entry_disposition(info, headers) + + if entry_disposition == "FRESH": + if not cached_value: + info['status'] = '504' + content = "" + response = Response(info) + if cached_value: + response.fromcache = True + return (response, content) + + if entry_disposition == "STALE": + if info.has_key('etag') and not self.ignore_etag and not 'if-none-match' in headers: + headers['if-none-match'] = info['etag'] + if info.has_key('last-modified') and not 'last-modified' in headers: + headers['if-modified-since'] = info['last-modified'] + elif entry_disposition == "TRANSPARENT": + pass + + (response, new_content) = self._request(conn, authority, uri, request_uri, method, body, headers, redirections, cachekey) + + if response.status == 304 and method == "GET": + # Rewrite the cache entry with the new end-to-end headers + # Take all headers that are in response + # and overwrite their values in info. + # unless they are hop-by-hop, or are listed in the connection header. + + for key in _get_end2end_headers(response): + info[key] = response[key] + merged_response = Response(info) + if hasattr(response, "_stale_digest"): + merged_response._stale_digest = response._stale_digest + _updateCache(headers, merged_response, content, self.cache, cachekey) + response = merged_response + response.status = 200 + response.fromcache = True + + elif response.status == 200: + content = new_content + else: + self.cache.delete(cachekey) + content = new_content + else: + cc = _parse_cache_control(headers) + if cc.has_key('only-if-cached'): + info['status'] = '504' + response = Response(info) + content = "" + else: + (response, content) = self._request(conn, authority, uri, request_uri, method, body, headers, redirections, cachekey) + except Exception, e: + if self.force_exception_to_status_code: + if isinstance(e, HttpLib2ErrorWithResponse): + response = e.response + content = e.content + response.status = 500 + response.reason = str(e) + elif isinstance(e, socket.timeout): + content = "Request Timeout" + response = Response( { + "content-type": "text/plain", + "status": "408", + "content-length": len(content) + }) + response.reason = "Request Timeout" + else: + content = str(e) + response = Response( { + "content-type": "text/plain", + "status": "400", + "content-length": len(content) + }) + response.reason = "Bad Request" + else: + raise + + + return (response, content) + + + +class Response(dict): + """An object more like email.Message than httplib.HTTPResponse.""" + + """Is this response from our local cache""" + fromcache = False + + """HTTP protocol version used by server. 10 for HTTP/1.0, 11 for HTTP/1.1. """ + version = 11 + + "Status code returned by server. " + status = 200 + + """Reason phrase returned by server.""" + reason = "Ok" + + previous = None + + def __init__(self, info): + # info is either an email.Message or + # an httplib.HTTPResponse object. + if isinstance(info, httplib.HTTPResponse): + for key, value in info.getheaders(): + self[key.lower()] = value + self.status = info.status + self['status'] = str(self.status) + self.reason = info.reason + self.version = info.version + elif isinstance(info, email.Message.Message): + for key, value in info.items(): + self[key] = value + self.status = int(self['status']) + else: + for key, value in info.iteritems(): + self[key] = value + self.status = int(self.get('status', self.status)) + + + def __getattr__(self, name): + if name == 'dict': + return self + else: + raise AttributeError, name diff --git a/httplib2/iri2uri.py b/httplib2/iri2uri.py new file mode 100644 index 0000000..70667ed --- /dev/null +++ b/httplib2/iri2uri.py @@ -0,0 +1,110 @@ +""" +iri2uri + +Converts an IRI to a URI. + +""" +__author__ = "Joe Gregorio (joe@bitworking.org)" +__copyright__ = "Copyright 2006, Joe Gregorio" +__contributors__ = [] +__version__ = "1.0.0" +__license__ = "MIT" +__history__ = """ +""" + +import urlparse + + +# Convert an IRI to a URI following the rules in RFC 3987 +# +# The characters we need to enocde and escape are defined in the spec: +# +# iprivate = %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD +# ucschar = %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF +# / %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD +# / %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD +# / %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD +# / %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD +# / %xD0000-DFFFD / %xE1000-EFFFD + +escape_range = [ + (0xA0, 0xD7FF ), + (0xE000, 0xF8FF ), + (0xF900, 0xFDCF ), + (0xFDF0, 0xFFEF), + (0x10000, 0x1FFFD ), + (0x20000, 0x2FFFD ), + (0x30000, 0x3FFFD), + (0x40000, 0x4FFFD ), + (0x50000, 0x5FFFD ), + (0x60000, 0x6FFFD), + (0x70000, 0x7FFFD ), + (0x80000, 0x8FFFD ), + (0x90000, 0x9FFFD), + (0xA0000, 0xAFFFD ), + (0xB0000, 0xBFFFD ), + (0xC0000, 0xCFFFD), + (0xD0000, 0xDFFFD ), + (0xE1000, 0xEFFFD), + (0xF0000, 0xFFFFD ), + (0x100000, 0x10FFFD) +] + +def encode(c): + retval = c + i = ord(c) + for low, high in escape_range: + if i < low: + break + if i >= low and i <= high: + retval = "".join(["%%%2X" % ord(o) for o in c.encode('utf-8')]) + break + return retval + + +def iri2uri(uri): + """Convert an IRI to a URI. Note that IRIs must be + passed in a unicode strings. That is, do not utf-8 encode + the IRI before passing it into the function.""" + if isinstance(uri ,unicode): + (scheme, authority, path, query, fragment) = urlparse.urlsplit(uri) + authority = authority.encode('idna') + # For each character in 'ucschar' or 'iprivate' + # 1. encode as utf-8 + # 2. then %-encode each octet of that utf-8 + uri = urlparse.urlunsplit((scheme, authority, path, query, fragment)) + uri = "".join([encode(c) for c in uri]) + return uri + +if __name__ == "__main__": + import unittest + + class Test(unittest.TestCase): + + def test_uris(self): + """Test that URIs are invariant under the transformation.""" + invariant = [ + u"ftp://ftp.is.co.za/rfc/rfc1808.txt", + u"http://www.ietf.org/rfc/rfc2396.txt", + u"ldap://[2001:db8::7]/c=GB?objectClass?one", + u"mailto:John.Doe@example.com", + u"news:comp.infosystems.www.servers.unix", + u"tel:+1-816-555-1212", + u"telnet://192.0.2.16:80/", + u"urn:oasis:names:specification:docbook:dtd:xml:4.1.2" ] + for uri in invariant: + self.assertEqual(uri, iri2uri(uri)) + + def test_iri(self): + """ Test that the right type of escaping is done for each part of the URI.""" + self.assertEqual("http://xn--o3h.com/%E2%98%84", iri2uri(u"http://\N{COMET}.com/\N{COMET}")) + self.assertEqual("http://bitworking.org/?fred=%E2%98%84", iri2uri(u"http://bitworking.org/?fred=\N{COMET}")) + self.assertEqual("http://bitworking.org/#%E2%98%84", iri2uri(u"http://bitworking.org/#\N{COMET}")) + self.assertEqual("#%E2%98%84", iri2uri(u"#\N{COMET}")) + self.assertEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}")) + self.assertEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}"))) + self.assertNotEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}".encode('utf-8'))) + + unittest.main() + + diff --git a/oauth2/__init__.py b/oauth2/__init__.py new file mode 100644 index 0000000..0baaa13 --- /dev/null +++ b/oauth2/__init__.py @@ -0,0 +1,732 @@ +""" +The MIT License + +Copyright (c) 2007-2010 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +import urllib +import time +import random +import urlparse +import hmac +import binascii +import httplib2 + +try: + from urlparse import parse_qs, parse_qsl +except ImportError: + from cgi import parse_qs, parse_qsl + + +VERSION = '1.0' # Hi Blaine! +HTTP_METHOD = 'GET' +SIGNATURE_METHOD = 'PLAINTEXT' + + +class Error(RuntimeError): + """Generic exception class.""" + + def __init__(self, message='OAuth error occured.'): + self._message = message + + @property + def message(self): + """A hack to get around the deprecation errors in 2.6.""" + return self._message + + def __str__(self): + return self._message + + +class MissingSignature(Error): + pass + + +def build_authenticate_header(realm=''): + """Optional WWW-Authenticate header (401 error)""" + return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} + + +def build_xoauth_string(url, consumer, token=None): + """Build an XOAUTH string for use in SMTP/IMPA authentication.""" + request = Request.from_consumer_and_token(consumer, token, + "GET", url) + + signing_method = SignatureMethod_HMAC_SHA1() + request.sign_request(signing_method, consumer, token) + + params = [] + for k, v in sorted(request.iteritems()): + if v is not None: + params.append('%s="%s"' % (k, escape(v))) + + return "%s %s %s" % ("GET", url, ','.join(params)) + + +def escape(s): + """Escape a URL including any /.""" + return urllib.quote(s, safe='~') + + +def generate_timestamp(): + """Get seconds since epoch (UTC).""" + return int(time.time()) + + +def generate_nonce(length=8): + """Generate pseudorandom number.""" + return ''.join([str(random.randint(0, 9)) for i in range(length)]) + + +def generate_verifier(length=8): + """Generate pseudorandom number.""" + return ''.join([str(random.randint(0, 9)) for i in range(length)]) + + +class Consumer(object): + """A consumer of OAuth-protected services. + + The OAuth consumer is a "third-party" service that wants to access + protected resources from an OAuth service provider on behalf of an end + user. It's kind of the OAuth client. + + Usually a consumer must be registered with the service provider by the + developer of the consumer software. As part of that process, the service + provider gives the consumer a *key* and a *secret* with which the consumer + software can identify itself to the service. The consumer will include its + key in each request to identify itself, but will use its secret only when + signing requests, to prove that the request is from that particular + registered consumer. + + Once registered, the consumer can then use its consumer credentials to ask + the service provider for a request token, kicking off the OAuth + authorization process. + """ + + key = None + secret = None + + def __init__(self, key, secret): + self.key = key + self.secret = secret + + if self.key is None or self.secret is None: + raise ValueError("Key and secret must be set.") + + def __str__(self): + data = {'oauth_consumer_key': self.key, + 'oauth_consumer_secret': self.secret} + + return urllib.urlencode(data) + + +class Token(object): + """An OAuth credential used to request authorization or a protected + resource. + + Tokens in OAuth comprise a *key* and a *secret*. The key is included in + requests to identify the token being used, but the secret is used only in + the signature, to prove that the requester is who the server gave the + token to. + + When first negotiating the authorization, the consumer asks for a *request + token* that the live user authorizes with the service provider. The + consumer then exchanges the request token for an *access token* that can + be used to access protected resources. + """ + + key = None + secret = None + callback = None + callback_confirmed = None + verifier = None + + def __init__(self, key, secret): + self.key = key + self.secret = secret + + if self.key is None or self.secret is None: + raise ValueError("Key and secret must be set.") + + def set_callback(self, callback): + self.callback = callback + self.callback_confirmed = 'true' + + def set_verifier(self, verifier=None): + if verifier is not None: + self.verifier = verifier + else: + self.verifier = generate_verifier() + + def get_callback_url(self): + if self.callback and self.verifier: + # Append the oauth_verifier. + parts = urlparse.urlparse(self.callback) + scheme, netloc, path, params, query, fragment = parts[:6] + if query: + query = '%s&oauth_verifier=%s' % (query, self.verifier) + else: + query = 'oauth_verifier=%s' % self.verifier + return urlparse.urlunparse((scheme, netloc, path, params, + query, fragment)) + return self.callback + + def to_string(self): + """Returns this token as a plain string, suitable for storage. + + The resulting string includes the token's secret, so you should never + send or store this string where a third party can read it. + """ + + data = { + 'oauth_token': self.key, + 'oauth_token_secret': self.secret, + } + + if self.callback_confirmed is not None: + data['oauth_callback_confirmed'] = self.callback_confirmed + return urllib.urlencode(data) + + @staticmethod + def from_string(s): + """Deserializes a token from a string like one returned by + `to_string()`.""" + + if not len(s): + raise ValueError("Invalid parameter string.") + + params = parse_qs(s, keep_blank_values=False) + if not len(params): + raise ValueError("Invalid parameter string.") + + try: + key = params['oauth_token'][0] + except Exception: + raise ValueError("'oauth_token' not found in OAuth request.") + + try: + secret = params['oauth_token_secret'][0] + except Exception: + raise ValueError("'oauth_token_secret' not found in " + "OAuth request.") + + token = Token(key, secret) + try: + token.callback_confirmed = params['oauth_callback_confirmed'][0] + except KeyError: + pass # 1.0, no callback confirmed. + return token + + def __str__(self): + return self.to_string() + + +def setter(attr): + name = attr.__name__ + + def getter(self): + try: + return self.__dict__[name] + except KeyError: + raise AttributeError(name) + + def deleter(self): + del self.__dict__[name] + + return property(getter, attr, deleter) + + +class Request(dict): + + """The parameters and information for an HTTP request, suitable for + authorizing with OAuth credentials. + + When a consumer wants to access a service's protected resources, it does + so using a signed HTTP request identifying itself (the consumer) with its + key, and providing an access token authorized by the end user to access + those resources. + + """ + + version = VERSION + + def __init__(self, method=HTTP_METHOD, url=None, parameters=None): + self.method = method + self.url = url + if parameters is not None: + self.update(parameters) + + @setter + def url(self, value): + self.__dict__['url'] = value + if value is not None: + scheme, netloc, path, params, query, fragment = urlparse.urlparse(value) + + # Exclude default port numbers. + if scheme == 'http' and netloc[-3:] == ':80': + netloc = netloc[:-3] + elif scheme == 'https' and netloc[-4:] == ':443': + netloc = netloc[:-4] + if scheme not in ('http', 'https'): + raise ValueError("Unsupported URL %s (%s)." % (value, scheme)) + + # Normalized URL excludes params, query, and fragment. + self.normalized_url = urlparse.urlunparse((scheme, netloc, path, None, None, None)) + else: + self.normalized_url = None + self.__dict__['url'] = None + + @setter + def method(self, value): + self.__dict__['method'] = value.upper() + + def _get_timestamp_nonce(self): + return self['oauth_timestamp'], self['oauth_nonce'] + + def get_nonoauth_parameters(self): + """Get any non-OAuth parameters.""" + return dict([(k, v) for k, v in self.iteritems() + if not k.startswith('oauth_')]) + + def to_header(self, realm=''): + """Serialize as a header for an HTTPAuth request.""" + oauth_params = ((k, v) for k, v in self.items() + if k.startswith('oauth_')) + stringy_params = ((k, escape(str(v))) for k, v in oauth_params) + header_params = ('%s="%s"' % (k, v) for k, v in stringy_params) + params_header = ', '.join(header_params) + + auth_header = 'OAuth realm="%s"' % realm + if params_header: + auth_header = "%s, %s" % (auth_header, params_header) + + return {'Authorization': auth_header} + + def to_postdata(self): + """Serialize as post data for a POST request.""" + # tell urlencode to deal with sequence values and map them correctly + # to resulting querystring. for example self["k"] = ["v1", "v2"] will + # result in 'k=v1&k=v2' and not k=%5B%27v1%27%2C+%27v2%27%5D + return urllib.urlencode(self, True) + + def to_url(self): + """Serialize as a URL for a GET request.""" + base_url = urlparse.urlparse(self.url) + query = parse_qs(base_url.query) + for k, v in self.items(): + query.setdefault(k, []).append(v) + url = (base_url.scheme, base_url.netloc, base_url.path, base_url.params, + urllib.urlencode(query, True), base_url.fragment) + return urlparse.urlunparse(url) + + def get_parameter(self, parameter): + ret = self.get(parameter) + if ret is None: + raise Error('Parameter not found: %s' % parameter) + + return ret + + def get_normalized_parameters(self): + """Return a string that contains the parameters that must be signed.""" + items = [] + for key, value in self.iteritems(): + if key == 'oauth_signature': + continue + # 1.0a/9.1.1 states that kvp must be sorted by key, then by value, + # so we unpack sequence values into multiple items for sorting. + if hasattr(value, '__iter__'): + items.extend((key, item) for item in value) + else: + items.append((key, value)) + + # Include any query string parameters from the provided URL + query = urlparse.urlparse(self.url)[4] + items.extend(self._split_url_string(query).items()) + + encoded_str = urllib.urlencode(sorted(items)) + # Encode signature parameters per Oauth Core 1.0 protocol + # spec draft 7, section 3.6 + # (http://tools.ietf.org/html/draft-hammer-oauth-07#section-3.6) + # Spaces must be encoded with "%20" instead of "+" + return encoded_str.replace('+', '%20') + + def sign_request(self, signature_method, consumer, token): + """Set the signature parameter to the result of sign.""" + + if 'oauth_consumer_key' not in self: + self['oauth_consumer_key'] = consumer.key + + if token and 'oauth_token' not in self: + self['oauth_token'] = token.key + + self['oauth_signature_method'] = signature_method.name + self['oauth_signature'] = signature_method.sign(self, consumer, token) + + @classmethod + def make_timestamp(cls): + """Get seconds since epoch (UTC).""" + return str(int(time.time())) + + @classmethod + def make_nonce(cls): + """Generate pseudorandom number.""" + return str(random.randint(0, 100000000)) + + @classmethod + def from_request(cls, http_method, http_url, headers=None, parameters=None, + query_string=None): + """Combines multiple parameter sources.""" + if parameters is None: + parameters = {} + + # Headers + if headers and 'Authorization' in headers: + auth_header = headers['Authorization'] + # Check that the authorization header is OAuth. + if auth_header[:6] == 'OAuth ': + auth_header = auth_header[6:] + try: + # Get the parameters from the header. + header_params = cls._split_header(auth_header) + parameters.update(header_params) + except: + raise Error('Unable to parse OAuth parameters from ' + 'Authorization header.') + + # GET or POST query string. + if query_string: + query_params = cls._split_url_string(query_string) + parameters.update(query_params) + + # URL parameters. + param_str = urlparse.urlparse(http_url)[4] # query + url_params = cls._split_url_string(param_str) + parameters.update(url_params) + + if parameters: + return cls(http_method, http_url, parameters) + + return None + + @classmethod + def from_consumer_and_token(cls, consumer, token=None, + http_method=HTTP_METHOD, http_url=None, parameters=None): + if not parameters: + parameters = {} + + defaults = { + 'oauth_consumer_key': consumer.key, + 'oauth_timestamp': cls.make_timestamp(), + 'oauth_nonce': cls.make_nonce(), + 'oauth_version': cls.version, + } + + defaults.update(parameters) + parameters = defaults + + if token: + parameters['oauth_token'] = token.key + if token.verifier: + parameters['oauth_verifier'] = token.verifier + + return Request(http_method, http_url, parameters) + + @classmethod + def from_token_and_callback(cls, token, callback=None, + http_method=HTTP_METHOD, http_url=None, parameters=None): + + if not parameters: + parameters = {} + + parameters['oauth_token'] = token.key + + if callback: + parameters['oauth_callback'] = callback + + return cls(http_method, http_url, parameters) + + @staticmethod + def _split_header(header): + """Turn Authorization: header into parameters.""" + params = {} + parts = header.split(',') + for param in parts: + # Ignore realm parameter. + if param.find('realm') > -1: + continue + # Remove whitespace. + param = param.strip() + # Split key-value. + param_parts = param.split('=', 1) + # Remove quotes and unescape the value. + params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"')) + return params + + @staticmethod + def _split_url_string(param_str): + """Turn URL string into parameters.""" + parameters = parse_qs(param_str, keep_blank_values=False) + for k, v in parameters.iteritems(): + parameters[k] = urllib.unquote(v[0]) + return parameters + + +class Client(httplib2.Http): + """OAuthClient is a worker to attempt to execute a request.""" + + def __init__(self, consumer, token=None, cache=None, timeout=None, + proxy_info=None): + + if consumer is not None and not isinstance(consumer, Consumer): + raise ValueError("Invalid consumer.") + + if token is not None and not isinstance(token, Token): + raise ValueError("Invalid token.") + + self.consumer = consumer + self.token = token + self.method = SignatureMethod_HMAC_SHA1() + + httplib2.Http.__init__(self, cache=cache, timeout=timeout, + proxy_info=proxy_info) + + def set_signature_method(self, method): + if not isinstance(method, SignatureMethod): + raise ValueError("Invalid signature method.") + + self.method = method + + def request(self, uri, method="GET", body=None, headers=None, + redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None): + DEFAULT_CONTENT_TYPE = 'application/x-www-form-urlencoded' + + if not isinstance(headers, dict): + headers = {} + + is_multipart = method == 'POST' and headers.get('Content-Type', + DEFAULT_CONTENT_TYPE) != DEFAULT_CONTENT_TYPE + + if body and method == "POST" and not is_multipart: + parameters = dict(parse_qsl(body)) + else: + parameters = None + + req = Request.from_consumer_and_token(self.consumer, + token=self.token, http_method=method, http_url=uri, + parameters=parameters) + + req.sign_request(self.method, self.consumer, self.token) + + if method == "POST": + headers['Content-Type'] = headers.get('Content-Type', + DEFAULT_CONTENT_TYPE) + if is_multipart: + headers.update(req.to_header()) + else: + body = req.to_postdata() + elif method == "GET": + uri = req.to_url() + else: + headers.update(req.to_header()) + + return httplib2.Http.request(self, uri, method=method, body=body, + headers=headers, redirections=redirections, + connection_type=connection_type) + + +class Server(object): + """A skeletal implementation of a service provider, providing protected + resources to requests from authorized consumers. + + This class implements the logic to check requests for authorization. You + can use it with your web server or web framework to protect certain + resources with OAuth. + """ + + timestamp_threshold = 300 # In seconds, five minutes. + version = VERSION + signature_methods = None + + def __init__(self, signature_methods=None): + self.signature_methods = signature_methods or {} + + def add_signature_method(self, signature_method): + self.signature_methods[signature_method.name] = signature_method + return self.signature_methods + + def verify_request(self, request, consumer, token): + """Verifies an api call and checks all the parameters.""" + + version = self._get_version(request) + self._check_signature(request, consumer, token) + parameters = request.get_nonoauth_parameters() + return parameters + + def build_authenticate_header(self, realm=''): + """Optional support for the authenticate header.""" + return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} + + def _get_version(self, request): + """Verify the correct version request for this server.""" + try: + version = request.get_parameter('oauth_version') + except: + version = VERSION + + if version and version != self.version: + raise Error('OAuth version %s not supported.' % str(version)) + + return version + + def _get_signature_method(self, request): + """Figure out the signature with some defaults.""" + try: + signature_method = request.get_parameter('oauth_signature_method') + except: + signature_method = SIGNATURE_METHOD + + try: + # Get the signature method object. + signature_method = self.signature_methods[signature_method] + except: + signature_method_names = ', '.join(self.signature_methods.keys()) + raise Error('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names)) + + return signature_method + + def _get_verifier(self, request): + return request.get_parameter('oauth_verifier') + + def _check_signature(self, request, consumer, token): + timestamp, nonce = request._get_timestamp_nonce() + self._check_timestamp(timestamp) + signature_method = self._get_signature_method(request) + + try: + signature = request.get_parameter('oauth_signature') + except: + raise MissingSignature('Missing oauth_signature.') + + # Validate the signature. + valid = signature_method.check(request, consumer, token, signature) + + if not valid: + key, base = signature_method.signing_base(request, consumer, token) + + raise Error('Invalid signature. Expected signature base ' + 'string: %s' % base) + + built = signature_method.sign(request, consumer, token) + + def _check_timestamp(self, timestamp): + """Verify that timestamp is recentish.""" + timestamp = int(timestamp) + now = int(time.time()) + lapsed = now - timestamp + if lapsed > self.timestamp_threshold: + raise Error('Expired timestamp: given %d and now %s has a ' + 'greater difference than threshold %d' % (timestamp, now, + self.timestamp_threshold)) + + +class SignatureMethod(object): + """A way of signing requests. + + The OAuth protocol lets consumers and service providers pick a way to sign + requests. This interface shows the methods expected by the other `oauth` + modules for signing requests. Subclass it and implement its methods to + provide a new way to sign requests. + """ + + def signing_base(self, request, consumer, token): + """Calculates the string that needs to be signed. + + This method returns a 2-tuple containing the starting key for the + signing and the message to be signed. The latter may be used in error + messages to help clients debug their software. + + """ + raise NotImplementedError + + def sign(self, request, consumer, token): + """Returns the signature for the given request, based on the consumer + and token also provided. + + You should use your implementation of `signing_base()` to build the + message to sign. Otherwise it may be less useful for debugging. + + """ + raise NotImplementedError + + def check(self, request, consumer, token, signature): + """Returns whether the given signature is the correct signature for + the given consumer and token signing the given request.""" + built = self.sign(request, consumer, token) + return built == signature + + +class SignatureMethod_HMAC_SHA1(SignatureMethod): + name = 'HMAC-SHA1' + + def signing_base(self, request, consumer, token): + sig = ( + escape(request.method), + escape(request.normalized_url), + escape(request.get_normalized_parameters()), + ) + + key = '%s&' % escape(consumer.secret) + if token: + key += escape(token.secret) + raw = '&'.join(sig) + return key, raw + + def sign(self, request, consumer, token): + """Builds the base signature string.""" + key, raw = self.signing_base(request, consumer, token) + + # HMAC object. + try: + from hashlib import sha1 as sha + except ImportError: + import sha # Deprecated + + hashed = hmac.new(key, raw, sha) + + # Calculate the digest base 64. + return binascii.b2a_base64(hashed.digest())[:-1] + + +class SignatureMethod_PLAINTEXT(SignatureMethod): + + name = 'PLAINTEXT' + + def signing_base(self, request, consumer, token): + """Concatenates the consumer key and secret with the token's + secret.""" + sig = '%s&' % escape(consumer.secret) + if token: + sig = sig + escape(token.secret) + return sig, sig + + def sign(self, request, consumer, token): + key, raw = self.signing_base(request, consumer, token) + return raw diff --git a/oauth2/clients/__init__.py b/oauth2/clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/oauth2/clients/imap.py b/oauth2/clients/imap.py new file mode 100644 index 0000000..68b7cd8 --- /dev/null +++ b/oauth2/clients/imap.py @@ -0,0 +1,40 @@ +""" +The MIT License + +Copyright (c) 2007-2010 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +import oauth2 +import imaplib + + +class IMAP4_SSL(imaplib.IMAP4_SSL): + """IMAP wrapper for imaplib.IMAP4_SSL that implements XOAUTH.""" + + def authenticate(self, url, consumer, token): + if consumer is not None and not isinstance(consumer, oauth2.Consumer): + raise ValueError("Invalid consumer.") + + if token is not None and not isinstance(token, oauth2.Token): + raise ValueError("Invalid token.") + + imaplib.IMAP4_SSL.authenticate(self, 'XOAUTH', + lambda x: oauth2.build_xoauth_string(url, consumer, token)) diff --git a/oauth2/clients/smtp.py b/oauth2/clients/smtp.py new file mode 100644 index 0000000..3e7bf0b --- /dev/null +++ b/oauth2/clients/smtp.py @@ -0,0 +1,41 @@ +""" +The MIT License + +Copyright (c) 2007-2010 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +import oauth2 +import smtplib +import base64 + + +class SMTP(smtplib.SMTP): + """SMTP wrapper for smtplib.SMTP that implements XOAUTH.""" + + def authenticate(self, url, consumer, token): + if consumer is not None and not isinstance(consumer, oauth2.Consumer): + raise ValueError("Invalid consumer.") + + if token is not None and not isinstance(token, oauth2.Token): + raise ValueError("Invalid token.") + + self.docmd('AUTH', 'XOAUTH %s' % \ + base64.b64encode(oauth2.build_xoauth_string(url, consumer, token))) diff --git a/rsachat.py b/rsachat.py index f037190..2d452e7 100755 --- a/rsachat.py +++ b/rsachat.py @@ -29,13 +29,12 @@ def genKey(): writeFile(keypair[1], "private_key") print "Done!" -def twitterInfo(userpassarr): - print "Storing your twitter information as .twitterlogin" - print "THIS /IS/ IN CLEAR TEXT!" - fp = open(".twitterlogin", "w") - pickle.dump(userpassarr, fp) - fp.close() - +def authorize(): + result = get_access_token() + fp = open(".twittercredentials", "w") + pickle.dump(result, fp) + fp.close() + def getPubPriv(): if (os.path.isfile("public_key") and os.path.isfile("private_key")): fp = open("public_key", "r") @@ -49,8 +48,8 @@ def getPubPriv(): return -1 def getLogin(): - if (os.path.isfile(".twitterlogin")): - fp = open(".twitterlogin", "r") + if (os.path.isfile(".twittercredentials")): + fp = open(".twittercredentials", "r") a = pickle.load(fp) fp.close() return a @@ -59,16 +58,14 @@ def getLogin(): def tweetThis((htag, etweet)): keys = getPubPriv() - login = getLogin() if keys == -1: print "You have not created the proper keys, try ./%s -h" % (sys.argv[0]) exit(-1) - if login == -1: - print "You have not specified your twitter details, try ./%s -h" % (sys.argv[0]) - exit (-1) + credentials = getLogin() + if credentials == -1: + print "You have not authorized Twitter access from this application, try ./%s -h" % (sys.argv[0]) + exit(-1) # All seems g2g - #etweet = tweet[len(tweet.split(' ')[0]):] - #htag = tweet[:len(tweet.split(' ')[0])] if htag[0:1] != "#": htag = "#%s" % htag print "Tweeting: %s [%s]" % (etweet, htag) enctweet = rsa.encrypt(etweet, keys["pub"]) @@ -78,7 +75,9 @@ def tweetThis((htag, etweet)): tweets[i] = "%s" % (enctweet[(i*120):(i+1)*120]) header = "[%s/%s] %s" % ("%s", len(tweets), htag) #posttweets = {} - api = twitter.Api(username=login[0], password=login[1]) + #api = twitter.Api(username=login[0], password=login[1]) + api = twitter.Api(consumer_key="LPIi3EFUT0LM7plWL2ZO3w", consumer_secret="hv44nSmJSsiNeWkhy9FNcb5qH0vmCSAZv2GyI2JqYdc", + access_token_key=credentials['oauth_token'], access_token_secret=credentials['oauth_token_secret']) try: for i in range(0, len(tweets)): api.PostUpdate("%s %s" % ((header % (i+1)), tweets[i])) @@ -165,20 +164,94 @@ def fileThis((tag, filename)): api.PostUpdate("%s %s" % ((header % (i+1)), tweets[i])) print "Done!" - - +def get_access_token(): + import os + import sys + + # parse_qsl moved to urlparse module in v2.6 + try: + from urlparse import parse_qsl + except: + from cgi import parse_qsl + + import oauth2 as oauth + + REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token' + ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token' + AUTHORIZATION_URL = 'https://api.twitter.com/oauth/authorize' + SIGNIN_URL = 'https://api.twitter.com/oauth/authenticate' + + consumer_key = "LPIi3EFUT0LM7plWL2ZO3w" + consumer_secret = "hv44nSmJSsiNeWkhy9FNcb5qH0vmCSAZv2GyI2JqYdc" + + if consumer_key is None or consumer_secret is None: + print 'You need to edit this script and provide values for the' + print 'consumer_key and also consumer_secret.' + print '' + print 'The values you need come from Twitter - you need to register' + print 'as a developer your "application". This is needed only until' + print 'Twitter finishes the idea they have of a way to allow open-source' + print 'based libraries to have a token that can be used to generate a' + print 'one-time use key that will allow the library to make the request' + print 'on your behalf.' + print '' + sys.exit(1) + + signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1() + oauth_consumer = oauth.Consumer(key=consumer_key, secret=consumer_secret) + oauth_client = oauth.Client(oauth_consumer) + + print 'Requesting temp token from Twitter' + + resp, content = oauth_client.request(REQUEST_TOKEN_URL, 'GET') + + if resp['status'] != '200': + print 'Invalid respond from Twitter requesting temp token: %s' % resp['status'] + else: + request_token = dict(parse_qsl(content)) + + print '' + print 'Please visit this Twitter page and retrieve the pincode to be used' + print 'in the next step to obtaining an Authentication Token:' + print '' + print '%s?oauth_token=%s' % (AUTHORIZATION_URL, request_token['oauth_token']) + print '' + + pincode = raw_input('Pincode? ') + + token = oauth.Token(request_token['oauth_token'], request_token['oauth_token_secret']) + token.set_verifier(pincode) + + print '' + print 'Generating and signing request for an access token' + print '' + + oauth_client = oauth.Client(oauth_consumer, token) + resp, content = oauth_client.request(ACCESS_TOKEN_URL, method='POST', body='oauth_verifier=%s' % pincode) + access_token = dict(parse_qsl(content)) + + if resp['status'] != '200': + print 'The request for a Token did not succeed: %s' % resp['status'] + print access_token + else: + print 'Your Twitter Access Token key: %s' % access_token['oauth_token'] + print ' Access Token secret: %s' % access_token['oauth_token_secret'] + print '' + return access_token + if __name__ == "__main__": - print "RSA Encrypted tweets by ikex " - parser = OptionParser(version="%prog 0.1") - parser.add_option("-g", dest="genkey", help="Generate a private/public key for use", action="store_true") - parser.add_option("-a", dest="twitter", help="Your twitter login specified as ", metavar="login", nargs=2) - parser.add_option("-t", dest="tweet", help="Post a tweet starting with the hashtag eg, \"#RSAToMyFriends Hi guys!\"", metavar="#tag tweet", nargs=2) - parser.add_option("-f", dest="tweetfile", help="Post a file encrypted as tweets eg, \"#tag \"", metavar="#tag filename.txt", nargs=2) - parser.add_option("-r", dest="readtweet", help="Reads a tweet with the specified tag author pubkey", metavar="tag author pubkey", nargs=3) - (options, args) = parser.parse_args() - if options.genkey: genKey() - elif options.twitter: twitterInfo(options.twitter) - elif options.tweet: tweetThis(options.tweet) - elif options.readtweet: readTweet(options.readtweet) - elif options.tweetfile: fileThis(options.tweetfile) - else: parser.print_help() \ No newline at end of file + print "RSA Encrypted tweets by ikex " + parser = OptionParser(version="%prog 0.2") + parser.add_option("-g", dest="genkey", help="Generate a private/public key for use", action="store_true") + parser.add_option("-o", dest="oauth", help="Authorize access to your Twitter account", action="store_true") + parser.add_option("-t", dest="tweet", help="Post a tweet starting with the hashtag eg, \"#RSAToMyFriends Hi guys!\"", metavar="#tag tweet", nargs=2) + parser.add_option("-f", dest="tweetfile", help="Post a file encrypted as tweets eg, \"#tag \"", metavar="#tag filename.txt", nargs=2) + parser.add_option("-r", dest="readtweet", help="Reads a tweet with the specified tag author pubkey", metavar="tag author pubkey", nargs=3) + (options, args) = parser.parse_args() + if options.genkey: genKey() + elif options.oauth: authorize() + elif options.tweet: tweetThis(options.tweet) + elif options.readtweet: readTweet(options.readtweet) + elif options.tweetfile: fileThis(options.tweetfile) + else: parser.print_help() + \ No newline at end of file diff --git a/twitter.py b/twitter.py index 9de4a95..8da5c37 100755 --- a/twitter.py +++ b/twitter.py @@ -1,6 +1,6 @@ #!/usr/bin/python2.4 # -# Copyright 2007 Google Inc. All Rights Reserved. +# Copyright 2007 The Python-Twitter Developers # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,41 +14,71 @@ # See the License for the specific language governing permissions and # limitations under the License. -'''A library that provides a python interface to the Twitter API''' +'''A library that provides a Python interface to the Twitter API''' -__author__ = 'dewitt@google.com' -__version__ = '0.7-devel' +__author__ = 'python-twitter@googlegroups.com' +__version__ = '0.8.1' import base64 import calendar +import datetime import httplib import os import rfc822 -import simplejson import sys import tempfile import textwrap import time +import calendar import urllib import urllib2 import urlparse +import gzip +import StringIO + +try: + # Python >= 2.6 + import json as simplejson +except ImportError: + try: + # Python < 2.6 + import simplejson + except ImportError: + try: + # Google App Engine + from django.utils import simplejson + except ImportError: + raise ImportError, "Unable to load a json library" + +# parse_qsl moved to urlparse module in v2.6 +try: + from urlparse import parse_qsl, parse_qs +except ImportError: + from cgi import parse_qsl, parse_qs try: from hashlib import md5 except ImportError: from md5 import md5 +import oauth2 as oauth + CHARACTER_LIMIT = 140 # A singleton representing a lazily instantiated FileCache. DEFAULT_CACHE = object() +REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token' +ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token' +AUTHORIZATION_URL = 'https://api.twitter.com/oauth/authorize' +SIGNIN_URL = 'https://api.twitter.com/oauth/authenticate' + class TwitterError(Exception): '''Base class for Twitter errors''' - + @property def message(self): '''Returns the first argument used to construct this error.''' @@ -57,9 +87,9 @@ def message(self): class Status(object): '''A class representing the Status structure used by the twitter API. - + The Status structure exposes the following properties: - + status.created_at status.created_at_in_seconds # read only status.favorited @@ -73,6 +103,13 @@ class Status(object): status.location status.relative_created_at # read only status.user + status.urls + status.user_mentions + status.hashtags + status.geo + status.place + status.coordinates + status.contributors ''' def __init__(self, created_at=None, @@ -86,7 +123,14 @@ def __init__(self, in_reply_to_status_id=None, truncated=None, source=None, - now=None): + now=None, + urls=None, + user_mentions=None, + hashtags=None, + geo=None, + place=None, + coordinates=None, + contributors=None): '''An object to hold a Twitter status message. This class is normally instantiated by the twitter.Api class and @@ -95,18 +139,24 @@ def __init__(self, Note: Dates are posted in the form "Sat Jan 27 04:17:38 +0000 2007" Args: - created_at: The time this status message was posted - favorited: Whether this is a favorite of the authenticated user - id: The unique id of this status message - text: The text of this status message - location: the geolocation string associated with this message + created_at: + The time this status message was posted. [Optional] + favorited: + Whether this is a favorite of the authenticated user. [Optional] + id: + The unique id of this status message. [Optional] + text: + The text of this status message. [Optional] + location: + the geolocation string associated with this message. [Optional] relative_created_at: - A human readable string representing the posting time + A human readable string representing the posting time. [Optional] user: - A twitter.User instance representing the person posting the message + A twitter.User instance representing the person posting the + message. [Optional] now: - The current time, if the client choses to set it. Defaults to the - wall clock time. + The current time, if the client choses to set it. + Defaults to the wall clock time. [Optional] ''' self.created_at = created_at self.favorited = favorited @@ -120,6 +170,13 @@ def __init__(self, self.in_reply_to_status_id = in_reply_to_status_id self.truncated = truncated self.source = source + self.urls = urls + self.user_mentions = user_mentions + self.hashtags = hashtags + self.geo = geo + self.place = place + self.coordinates = coordinates + self.contributors = contributors def GetCreatedAt(self): '''Get the time this status message was posted. @@ -133,7 +190,8 @@ def SetCreatedAt(self, created_at): '''Set the time this status message was posted. Args: - created_at: The time this status message was created + created_at: + The time this status message was created ''' self._created_at = created_at @@ -164,7 +222,8 @@ def SetFavorited(self, favorited): '''Set the favorited state of this status message. Args: - favorited: boolean True/False favorited state of this status message + favorited: + boolean True/False favorited state of this status message ''' self._favorited = favorited @@ -183,7 +242,8 @@ def SetId(self, id): '''Set the unique id of this status message. Args: - id: The unique id of this status message + id: + The unique id of this status message ''' self._id = id @@ -197,7 +257,7 @@ def SetInReplyToScreenName(self, in_reply_to_screen_name): self._in_reply_to_screen_name = in_reply_to_screen_name in_reply_to_screen_name = property(GetInReplyToScreenName, SetInReplyToScreenName, - doc='') + doc='') def GetInReplyToUserId(self): return self._in_reply_to_user_id @@ -206,7 +266,7 @@ def SetInReplyToUserId(self, in_reply_to_user_id): self._in_reply_to_user_id = in_reply_to_user_id in_reply_to_user_id = property(GetInReplyToUserId, SetInReplyToUserId, - doc='') + doc='') def GetInReplyToStatusId(self): return self._in_reply_to_status_id @@ -215,7 +275,7 @@ def SetInReplyToStatusId(self, in_reply_to_status_id): self._in_reply_to_status_id = in_reply_to_status_id in_reply_to_status_id = property(GetInReplyToStatusId, SetInReplyToStatusId, - doc='') + doc='') def GetTruncated(self): return self._truncated @@ -224,7 +284,7 @@ def SetTruncated(self, truncated): self._truncated = truncated truncated = property(GetTruncated, SetTruncated, - doc='') + doc='') def GetSource(self): return self._source @@ -233,7 +293,7 @@ def SetSource(self, source): self._source = source source = property(GetSource, SetSource, - doc='') + doc='') def GetText(self): '''Get the text of this status message. @@ -247,7 +307,8 @@ def SetText(self, text): '''Set the text of this status message. Args: - text: The text of this status message + text: + The text of this status message ''' self._text = text @@ -266,7 +327,8 @@ def SetLocation(self, location): '''Set the geolocation associated with this status message Args: - location: The geolocation string of this status message + location: + The geolocation string of this status message ''' self._location = location @@ -290,17 +352,17 @@ def GetRelativeCreatedAt(self): return 'about a minute ago' elif delta < (60 * 60 * (1/fudge)): return 'about %d minutes ago' % (delta / 60) - elif delta < (60 * 60 * fudge): + elif delta < (60 * 60 * fudge) or delta / (60 * 60) == 1: return 'about an hour ago' elif delta < (60 * 60 * 24 * (1/fudge)): return 'about %d hours ago' % (delta / (60 * 60)) - elif delta < (60 * 60 * 24 * fudge): + elif delta < (60 * 60 * 24 * fudge) or delta / (60 * 60 * 24) == 1: return 'about a day ago' else: return 'about %d days ago' % (delta / (60 * 60 * 24)) relative_created_at = property(GetRelativeCreatedAt, - doc='Get a human readable string representing' + doc='Get a human readable string representing ' 'the posting time') def GetUser(self): @@ -315,7 +377,8 @@ def SetUser(self, user): '''Set a twitter.User reprenting the entity posting this status message. Args: - user: A twitter.User reprenting the entity posting this status message + user: + A twitter.User reprenting the entity posting this status message ''' self._user = user @@ -344,13 +407,49 @@ def SetNow(self, now): the object was instantiated. Args: - now: The wallclock time for this instance. + now: + The wallclock time for this instance. ''' self._now = now now = property(GetNow, SetNow, doc='The wallclock time for this status instance.') + def GetGeo(self): + return self._geo + + def SetGeo(self, geo): + self._geo = geo + + geo = property(GetGeo, SetGeo, + doc='') + + def GetPlace(self): + return self._place + + def SetPlace(self, place): + self._place = place + + place = property(GetPlace, SetPlace, + doc='') + + def GetCoordinates(self): + return self._coordinates + + def SetCoordinates(self, coordinates): + self._coordinates = coordinates + + coordinates = property(GetCoordinates, SetCoordinates, + doc='') + + def GetContributors(self): + return self._contributors + + def SetContributors(self, contributors): + self._contributors = contributors + + contributors = property(GetContributors, SetContributors, + doc='') def __ne__(self, other): return not self.__eq__(other) @@ -368,7 +467,11 @@ def __eq__(self, other): self.in_reply_to_status_id == other.in_reply_to_status_id and \ self.truncated == other.truncated and \ self.favorited == other.favorited and \ - self.source == other.source + self.source == other.source and \ + self.geo == other.geo and \ + self.place == other.place and \ + self.coordinates == other.coordinates and \ + self.contributors == other.contributors except AttributeError: return False @@ -408,7 +511,7 @@ def AsDict(self): if self.text: data['text'] = self.text if self.location: - data['location'] = self.location + data['location'] = self.location if self.user: data['user'] = self.user.AsDict() if self.in_reply_to_screen_name: @@ -423,6 +526,14 @@ def AsDict(self): data['favorited'] = self.favorited if self.source: data['source'] = self.source + if self.geo: + data['geo'] = self.geo + if self.place: + data['place'] = self.place + if self.coordinates: + data['coordinates'] = self.coordinates + if self.contributors: + data['contributors'] = self.contributors return data @staticmethod @@ -438,6 +549,16 @@ def NewFromJsonDict(data): user = User.NewFromJsonDict(data['user']) else: user = None + urls = None + user_mentions = None + hashtags = None + if 'entities' in data: + if 'urls' in data['entities']: + urls = [Url.NewFromJsonDict(u) for u in data['entities']['urls']] + if 'user_mentions' in data['entities']: + user_mentions = [User.NewFromJsonDict(u) for u in data['entities']['user_mentions']] + if 'hashtags' in data['entities']: + hashtags = [Hashtag.NewFromJsonDict(h) for h in data['entities']['hashtags']] return Status(created_at=data.get('created_at', None), favorited=data.get('favorited', None), id=data.get('id', None), @@ -448,7 +569,14 @@ def NewFromJsonDict(data): in_reply_to_status_id=data.get('in_reply_to_status_id', None), truncated=data.get('truncated', None), source=data.get('source', None), - user=user) + user=user, + urls=urls, + user_mentions=user_mentions, + hashtags=hashtags, + geo=data.get('geo', None), + place=data.get('place', None), + coordinates=data.get('coordinates', None), + contributors=data.get('contributors', None)) class User(object): @@ -477,6 +605,7 @@ class User(object): user.followers_count user.friends_count user.favourites_count + user.geo_enabled ''' def __init__(self, id=None, @@ -499,7 +628,8 @@ def __init__(self, statuses_count=None, favourites_count=None, url=None, - status=None): + status=None, + geo_enabled=None): self.id = id self.name = name self.screen_name = screen_name @@ -521,7 +651,7 @@ def __init__(self, self.favourites_count = favourites_count self.url = url self.status = status - + self.geo_enabled = geo_enabled def GetId(self): '''Get the unique id of this user. @@ -562,23 +692,23 @@ def SetName(self, name): doc='The real name of this user.') def GetScreenName(self): - '''Get the short username of this user. + '''Get the short twitter name of this user. Returns: - The short username of this user + The short twitter name of this user ''' return self._screen_name def SetScreenName(self, screen_name): - '''Set the short username of this user. + '''Set the short twitter name of this user. Args: - screen_name: the short username of this user + screen_name: the short twitter name of this user ''' self._screen_name = screen_name screen_name = property(GetScreenName, SetScreenName, - doc='The short username of this user.') + doc='The short twitter name of this user.') def GetLocation(self): '''Get the geographic location of this user. @@ -744,7 +874,8 @@ def SetTimeZone(self, time_zone): '''Sets the user's time zone string. Args: - time_zone: The descriptive time zone to assign for the user. + time_zone: + The descriptive time zone to assign for the user. ''' self._time_zone = time_zone @@ -762,16 +893,17 @@ def SetStatus(self, status): '''Set the latest twitter.Status of this user. Args: - status: The latest twitter.Status of this user + status: + The latest twitter.Status of this user ''' self._status = status status = property(GetStatus, SetStatus, - doc='The latest twitter.Status of this user.') + doc='The latest twitter.Status of this user.') def GetFriendsCount(self): '''Get the friend count for this user. - + Returns: The number of users this user has befriended. ''' @@ -781,16 +913,17 @@ def SetFriendsCount(self, count): '''Set the friend count for this user. Args: - count: The number of users this user has befriended. + count: + The number of users this user has befriended. ''' self._friends_count = count friends_count = property(GetFriendsCount, SetFriendsCount, - doc='The number of friends for this user.') + doc='The number of friends for this user.') def GetFollowersCount(self): '''Get the follower count for this user. - + Returns: The number of users following this user. ''' @@ -800,16 +933,17 @@ def SetFollowersCount(self, count): '''Set the follower count for this user. Args: - count: The number of users following this user. + count: + The number of users following this user. ''' self._followers_count = count followers_count = property(GetFollowersCount, SetFollowersCount, - doc='The number of users following this user.') + doc='The number of users following this user.') def GetStatusesCount(self): '''Get the number of status updates for this user. - + Returns: The number of status updates for this user. ''' @@ -819,16 +953,17 @@ def SetStatusesCount(self, count): '''Set the status update count for this user. Args: - count: The number of updates for this user. + count: + The number of updates for this user. ''' self._statuses_count = count statuses_count = property(GetStatusesCount, SetStatusesCount, - doc='The number of updates for this user.') + doc='The number of updates for this user.') def GetFavouritesCount(self): '''Get the number of favourites for this user. - + Returns: The number of favourites for this user. ''' @@ -838,12 +973,33 @@ def SetFavouritesCount(self, count): '''Set the favourite count for this user. Args: - count: The number of favourites for this user. + count: + The number of favourites for this user. ''' self._favourites_count = count favourites_count = property(GetFavouritesCount, SetFavouritesCount, - doc='The number of favourites for this user.') + doc='The number of favourites for this user.') + + def GetGeoEnabled(self): + '''Get the setting of geo_enabled for this user. + + Returns: + True/False if Geo tagging is enabled + ''' + return self._geo_enabled + + def SetGeoEnabled(self, geo_enabled): + '''Set the latest twitter.geo_enabled of this user. + + Args: + geo_enabled: + True/False if Geo tagging is to be enabled + ''' + self._geo_enabled = geo_enabled + + geo_enabled = property(GetGeoEnabled, SetGeoEnabled, + doc='The value of twitter.geo_enabled for this user.') def __ne__(self, other): return not self.__eq__(other) @@ -871,7 +1027,8 @@ def __eq__(self, other): self.followers_count == other.followers_count and \ self.favourites_count == other.favourites_count and \ self.friends_count == other.friends_count and \ - self.status == other.status + self.status == other.status and \ + self.geo_enabled == other.geo_enabled except AttributeError: return False @@ -942,6 +1099,8 @@ def AsDict(self): data['statuses_count'] = self.statuses_count if self.favourites_count: data['favourites_count'] = self.favourites_count + if self.geo_enabled: + data['geo_enabled'] = self.geo_enabled return data @staticmethod @@ -949,7 +1108,9 @@ def NewFromJsonDict(data): '''Create a new instance based on a JSON dict. Args: - data: A JSON dict, as converted from the JSON in the twitter API + data: + A JSON dict, as converted from the JSON in the twitter API + Returns: A twitter.User instance ''' @@ -977,13 +1138,373 @@ def NewFromJsonDict(data): utc_offset = data.get('utc_offset', None), time_zone = data.get('time_zone', None), url=data.get('url', None), - status=status) + status=status, + geo_enabled=data.get('geo_enabled', None)) + +class List(object): + '''A class representing the List structure used by the twitter API. + + The List structure exposes the following properties: + + list.id + list.name + list.slug + list.description + list.full_name + list.mode + list.uri + list.member_count + list.subscriber_count + list.following + ''' + def __init__(self, + id=None, + name=None, + slug=None, + description=None, + full_name=None, + mode=None, + uri=None, + member_count=None, + subscriber_count=None, + following=None, + user=None): + self.id = id + self.name = name + self.slug = slug + self.description = description + self.full_name = full_name + self.mode = mode + self.uri = uri + self.member_count = member_count + self.subscriber_count = subscriber_count + self.following = following + self.user = user + + def GetId(self): + '''Get the unique id of this list. + + Returns: + The unique id of this list + ''' + return self._id + + def SetId(self, id): + '''Set the unique id of this list. + + Args: + id: + The unique id of this list. + ''' + self._id = id + + id = property(GetId, SetId, + doc='The unique id of this list.') + + def GetName(self): + '''Get the real name of this list. + + Returns: + The real name of this list + ''' + return self._name + + def SetName(self, name): + '''Set the real name of this list. + + Args: + name: + The real name of this list + ''' + self._name = name + + name = property(GetName, SetName, + doc='The real name of this list.') + + def GetSlug(self): + '''Get the slug of this list. + + Returns: + The slug of this list + ''' + return self._slug + + def SetSlug(self, slug): + '''Set the slug of this list. + + Args: + slug: + The slug of this list. + ''' + self._slug = slug + + slug = property(GetSlug, SetSlug, + doc='The slug of this list.') + + def GetDescription(self): + '''Get the description of this list. + + Returns: + The description of this list + ''' + return self._description + + def SetDescription(self, description): + '''Set the description of this list. + + Args: + description: + The description of this list. + ''' + self._description = description + + description = property(GetDescription, SetDescription, + doc='The description of this list.') + + def GetFull_name(self): + '''Get the full_name of this list. + + Returns: + The full_name of this list + ''' + return self._full_name + + def SetFull_name(self, full_name): + '''Set the full_name of this list. + + Args: + full_name: + The full_name of this list. + ''' + self._full_name = full_name + + full_name = property(GetFull_name, SetFull_name, + doc='The full_name of this list.') + + def GetMode(self): + '''Get the mode of this list. + + Returns: + The mode of this list + ''' + return self._mode + + def SetMode(self, mode): + '''Set the mode of this list. + + Args: + mode: + The mode of this list. + ''' + self._mode = mode + + mode = property(GetMode, SetMode, + doc='The mode of this list.') + + def GetUri(self): + '''Get the uri of this list. + + Returns: + The uri of this list + ''' + return self._uri + + def SetUri(self, uri): + '''Set the uri of this list. + + Args: + uri: + The uri of this list. + ''' + self._uri = uri + + uri = property(GetUri, SetUri, + doc='The uri of this list.') + + def GetMember_count(self): + '''Get the member_count of this list. + + Returns: + The member_count of this list + ''' + return self._member_count + + def SetMember_count(self, member_count): + '''Set the member_count of this list. + + Args: + member_count: + The member_count of this list. + ''' + self._member_count = member_count + + member_count = property(GetMember_count, SetMember_count, + doc='The member_count of this list.') + + def GetSubscriber_count(self): + '''Get the subscriber_count of this list. + + Returns: + The subscriber_count of this list + ''' + return self._subscriber_count + + def SetSubscriber_count(self, subscriber_count): + '''Set the subscriber_count of this list. + + Args: + subscriber_count: + The subscriber_count of this list. + ''' + self._subscriber_count = subscriber_count + + subscriber_count = property(GetSubscriber_count, SetSubscriber_count, + doc='The subscriber_count of this list.') + + def GetFollowing(self): + '''Get the following status of this list. + + Returns: + The following status of this list + ''' + return self._following + + def SetFollowing(self, following): + '''Set the following status of this list. + + Args: + following: + The following of this list. + ''' + self._following = following + + following = property(GetFollowing, SetFollowing, + doc='The following status of this list.') + + def GetUser(self): + '''Get the user of this list. + + Returns: + The owner of this list + ''' + return self._user + + def SetUser(self, user): + '''Set the user of this list. + + Args: + user: + The owner of this list. + ''' + self._user = user + + user = property(GetUser, SetUser, + doc='The owner of this list.') + + def __ne__(self, other): + return not self.__eq__(other) + + def __eq__(self, other): + try: + return other and \ + self.id == other.id and \ + self.name == other.name and \ + self.slug == other.slug and \ + self.description == other.description and \ + self.full_name == other.full_name and \ + self.mode == other.mode and \ + self.uri == other.uri and \ + self.member_count == other.member_count and \ + self.subscriber_count == other.subscriber_count and \ + self.following == other.following and \ + self.user == other.user + + except AttributeError: + return False + + def __str__(self): + '''A string representation of this twitter.List instance. + + The return value is the same as the JSON string representation. + + Returns: + A string representation of this twitter.List instance. + ''' + return self.AsJsonString() + + def AsJsonString(self): + '''A JSON string representation of this twitter.List instance. + + Returns: + A JSON string representation of this twitter.List instance + ''' + return simplejson.dumps(self.AsDict(), sort_keys=True) + + def AsDict(self): + '''A dict representation of this twitter.List instance. + + The return value uses the same key names as the JSON representation. + + Return: + A dict representing this twitter.List instance + ''' + data = {} + if self.id: + data['id'] = self.id + if self.name: + data['name'] = self.name + if self.slug: + data['slug'] = self.slug + if self.description: + data['description'] = self.description + if self.full_name: + data['full_name'] = self.full_name + if self.mode: + data['mode'] = self.mode + if self.uri: + data['uri'] = self.uri + if self.member_count is not None: + data['member_count'] = self.member_count + if self.subscriber_count is not None: + data['subscriber_count'] = self.subscriber_count + if self.following is not None: + data['following'] = self.following + if self.user is not None: + data['user'] = self.user + return data + + @staticmethod + def NewFromJsonDict(data): + '''Create a new instance based on a JSON dict. + + Args: + data: + A JSON dict, as converted from the JSON in the twitter API + + Returns: + A twitter.List instance + ''' + if 'user' in data: + user = User.NewFromJsonDict(data['user']) + else: + user = None + return List(id=data.get('id', None), + name=data.get('name', None), + slug=data.get('slug', None), + description=data.get('description', None), + full_name=data.get('full_name', None), + mode=data.get('mode', None), + uri=data.get('uri', None), + member_count=data.get('member_count', None), + subscriber_count=data.get('subscriber_count', None), + following=data.get('following', None), + user=user) class DirectMessage(object): '''A class representing the DirectMessage structure used by the twitter API. - + The DirectMessage structure exposes the following properties: - + direct_message.id direct_message.created_at direct_message.created_at_in_seconds # read only @@ -1010,13 +1531,20 @@ def __init__(self, Note: Dates are posted in the form "Sat Jan 27 04:17:38 +0000 2007" Args: - id: The unique id of this direct message - created_at: The time this direct message was posted - sender_id: The id of the twitter user that sent this message - sender_screen_name: The name of the twitter user that sent this message - recipient_id: The id of the twitter that received this message - recipient_screen_name: The name of the twitter that received this message - text: The text of this direct message + id: + The unique id of this direct message. [Optional] + created_at: + The time this direct message was posted. [Optional] + sender_id: + The id of the twitter user that sent this message. [Optional] + sender_screen_name: + The name of the twitter user that sent this message. [Optional] + recipient_id: + The id of the twitter that received this message. [Optional] + recipient_screen_name: + The name of the twitter that received this message. [Optional] + text: + The text of this direct message. [Optional] ''' self.id = id self.created_at = created_at @@ -1038,7 +1566,8 @@ def SetId(self, id): '''Set the unique id of this direct message. Args: - id: The unique id of this direct message + id: + The unique id of this direct message ''' self._id = id @@ -1057,7 +1586,8 @@ def SetCreatedAt(self, created_at): '''Set the time this direct message was posted. Args: - created_at: The time this direct message was created + created_at: + The time this direct message was created ''' self._created_at = created_at @@ -1088,7 +1618,8 @@ def SetSenderId(self, sender_id): '''Set the unique sender id of this direct message. Args: - sender id: The unique sender id of this direct message + sender_id: + The unique sender id of this direct message ''' self._sender_id = sender_id @@ -1107,7 +1638,8 @@ def SetSenderScreenName(self, sender_screen_name): '''Set the unique sender screen name of this direct message. Args: - sender_screen_name: The unique sender screen name of this direct message + sender_screen_name: + The unique sender screen name of this direct message ''' self._sender_screen_name = sender_screen_name @@ -1126,7 +1658,8 @@ def SetRecipientId(self, recipient_id): '''Set the unique recipient id of this direct message. Args: - recipient id: The unique recipient id of this direct message + recipient_id: + The unique recipient id of this direct message ''' self._recipient_id = recipient_id @@ -1145,7 +1678,8 @@ def SetRecipientScreenName(self, recipient_screen_name): '''Set the unique recipient screen name of this direct message. Args: - recipient_screen_name: The unique recipient screen name of this direct message + recipient_screen_name: + The unique recipient screen name of this direct message ''' self._recipient_screen_name = recipient_screen_name @@ -1164,7 +1698,8 @@ def SetText(self, text): '''Set the text of this direct message. Args: - text: The text of this direct message + text: + The text of this direct message ''' self._text = text @@ -1235,7 +1770,9 @@ def NewFromJsonDict(data): '''Create a new instance based on a JSON dict. Args: - data: A JSON dict, as converted from the JSON in the twitter API + data: + A JSON dict, as converted from the JSON in the twitter API + Returns: A twitter.DirectMessage instance ''' @@ -1247,11 +1784,81 @@ def NewFromJsonDict(data): id=data.get('id', None), recipient_screen_name=data.get('recipient_screen_name', None)) -class Api(object): - '''A python interface into the Twitter API - - By default, the Api caches results for 1 minute. - +class Hashtag(object): + ''' A class represeinting a twitter hashtag + ''' + def __init__(self, + text=None): + self.text = text + + @staticmethod + def NewFromJsonDict(data): + '''Create a new instance based on a JSON dict. + + Args: + data: + A JSON dict, as converted from the JSON in the twitter API + + Returns: + A twitter.Hashtag instance + ''' + return Hashtag(text = data.get('text', None)) + +class Trend(object): + ''' A class representing a trending topic + ''' + def __init__(self, name=None, query=None, timestamp=None): + self.name = name + self.query = query + self.timestamp = timestamp + + def __str__(self): + return 'Name: %s\nQuery: %s\nTimestamp: %s\n' % (self.name, self.query, self.timestamp) + + @staticmethod + def NewFromJsonDict(data, timestamp = None): + '''Create a new instance based on a JSON dict + + Args: + data: + A JSON dict + timestamp: + Gets set as the timestamp property of the new object + + Returns: + A twitter.Trend object + ''' + return Trend(name=data.get('name', None), + query=data.get('query', None), + timestamp=timestamp) + +class Url(object): + '''A class representing an URL contained in a tweet''' + def __init__(self, + url=None, + expanded_url=None): + self.url = url + self.expanded_url = expanded_url + + @staticmethod + def NewFromJsonDict(data): + '''Create a new instance based on a JSON dict. + + Args: + data: + A JSON dict, as converted from the JSON in the twitter API + + Returns: + A twitter.Url instance + ''' + return Url(url=data.get('url', None), + expanded_url=data.get('expanded_url', None)) + +class Api(object): + '''A python interface into the Twitter API + + By default, the Api caches results for 1 minute. + Example usage: To create an instance of the twitter.Api class, with no authentication: @@ -1272,9 +1879,12 @@ class Api(object): >>> print [s.text for s in statuses] To use authentication, instantiate the twitter.Api class with a - username and password: + consumer key and secret; and the oAuth key and secret: - >>> api = twitter.Api(username='twitter user', password='twitter pass') + >>> api = twitter.Api(consumer_key='twitter consumer key', + consumer_secret='twitter consumer secret', + access_token_key='the_key_given', + access_token_secret='the_key_secret') To fetch your friends (after being authenticated): @@ -1306,146 +1916,386 @@ class Api(object): >>> api.DestroyFriendship(user) >>> api.CreateFriendship(user) >>> api.GetUserByEmail(email) + >>> api.VerifyCredentials() ''' DEFAULT_CACHE_TIMEOUT = 60 # cache for 1 minute - _API_REALM = 'Twitter API' def __init__(self, - username=None, - password=None, + consumer_key=None, + consumer_secret=None, + access_token_key=None, + access_token_secret=None, input_encoding=None, request_headers=None, - cache=DEFAULT_CACHE): + cache=DEFAULT_CACHE, + shortner=None, + base_url=None, + use_gzip_compression=False, + debugHTTP=False): '''Instantiate a new twitter.Api object. Args: - username: The username of the twitter account. [optional] - password: The password for the twitter account. [optional] - input_encoding: The encoding used to encode input strings. [optional] - request_header: A dictionary of additional HTTP request headers. [optional] - cache: - The cache instance to use. Defaults to DEFAULT_CACHE. Use - None to disable caching. [optional] + consumer_key: + Your Twitter user's consumer_key. + consumer_secret: + Your Twitter user's consumer_secret. + access_token_key: + The oAuth access token key value you retrieved + from running get_access_token.py. + access_token_secret: + The oAuth access token's secret, also retrieved + from the get_access_token.py run. + input_encoding: + The encoding used to encode input strings. [Optional] + request_header: + A dictionary of additional HTTP request headers. [Optional] + cache: + The cache instance to use. Defaults to DEFAULT_CACHE. + Use None to disable caching. [Optional] + shortner: + The shortner instance to use. Defaults to None. + See shorten_url.py for an example shortner. [Optional] + base_url: + The base URL to use to contact the Twitter API. + Defaults to https://twitter.com. [Optional] + use_gzip_compression: + Set to True to tell enable gzip compression for any call + made to Twitter. Defaults to False. [Optional] + debugHTTP: + Set to True to enable debug output from urllib2 when performing + any HTTP requests. Defaults to False. [Optional] ''' self.SetCache(cache) - self._urllib = urllib2 - self._cache_timeout = Api.DEFAULT_CACHE_TIMEOUT + self._urllib = urllib2 + self._cache_timeout = Api.DEFAULT_CACHE_TIMEOUT + self._input_encoding = input_encoding + self._use_gzip = use_gzip_compression + self._debugHTTP = debugHTTP + self._oauth_consumer = None + self._InitializeRequestHeaders(request_headers) self._InitializeUserAgent() self._InitializeDefaultParameters() - self._input_encoding = input_encoding - self.SetCredentials(username, password) - def GetPublicTimeline(self, since_id=None): - '''Fetch the sequnce of public twitter.Status message for all users. + if base_url is None: + self.base_url = 'https://api.twitter.com/1' + else: + self.base_url = base_url + + if consumer_key is not None and (access_token_key is None or + access_token_secret is None): + print >> sys.stderr, 'Twitter now requires an oAuth Access Token for API calls.' + print >> sys.stderr, 'If your using this library from a command line utility, please' + print >> sys.stderr, 'run the the included get_access_token.py tool to generate one.' + + raise TwitterError('Twitter requires oAuth Access Token for all API access') + + self.SetCredentials(consumer_key, consumer_secret, access_token_key, access_token_secret) + + def SetCredentials(self, + consumer_key, + consumer_secret, + access_token_key=None, + access_token_secret=None): + '''Set the consumer_key and consumer_secret for this instance + + Args: + consumer_key: + The consumer_key of the twitter account. + consumer_secret: + The consumer_secret for the twitter account. + access_token_key: + The oAuth access token key value you retrieved + from running get_access_token.py. + access_token_secret: + The oAuth access token's secret, also retrieved + from the get_access_token.py run. + ''' + self._consumer_key = consumer_key + self._consumer_secret = consumer_secret + self._access_token_key = access_token_key + self._access_token_secret = access_token_secret + self._oauth_consumer = None + + if consumer_key is not None and consumer_secret is not None and \ + access_token_key is not None and access_token_secret is not None: + self._signature_method_plaintext = oauth.SignatureMethod_PLAINTEXT() + self._signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1() + + self._oauth_token = oauth.Token(key=access_token_key, secret=access_token_secret) + self._oauth_consumer = oauth.Consumer(key=consumer_key, secret=consumer_secret) + + def ClearCredentials(self): + '''Clear the any credentials for this instance + ''' + self._consumer_key = None + self._consumer_secret = None + self._access_token_key = None + self._access_token_secret = None + self._oauth_consumer = None + + def GetPublicTimeline(self, + since_id=None, + include_rts=None, + include_entities=None): + '''Fetch the sequence of public twitter.Status message for all users. Args: since_id: - Returns only public statuses with an ID greater than (that is, - more recent than) the specified ID. [Optional] + Returns results with an ID greater than (that is, more recent + than) the specified ID. There are limits to the number of + Tweets which can be accessed through the API. If the limit of + Tweets has occured since the since_id, the since_id will be + forced to the oldest ID available. [Optional] + include_rts: + If True, the timeline will contain native retweets (if they + exist) in addition to the standard stream of tweets. [Optional] + include_entities: + If True, each tweet will include a node called "entities,". + This node offers a variety of metadata about the tweet in a + discreet structure, including: user_mentions, urls, and + hashtags. [Optional] Returns: An sequence of twitter.Status instances, one for each message ''' parameters = {} + if since_id: parameters['since_id'] = since_id - url = 'http://twitter.com/statuses/public_timeline.json' + if include_rts: + parameters['include_rts'] = 1 + if include_entities: + parameters['include_entities'] = 1 + + url = '%s/statuses/public_timeline.json' % self.base_url json = self._FetchUrl(url, parameters=parameters) data = simplejson.loads(json) + self._CheckForTwitterError(data) + return [Status.NewFromJsonDict(x) for x in data] - def FilterPublicTimeline(self, term, since_id=None): - ''' Filter the public twitter timeline by a given search term on - the local machine. - Args: - term: - term to search by. - since_id: - Returns only public statuses with an ID greater than (that is, - more recent than) the specified ID. [Optional] - - Returns: - A sequence of twitter.Status instances, one for each message - containing the term - ''' - statuses = self.GetPublicTimeline(since_id) - results = [] - - for s in statuses: - if s.text.lower().find(term.lower()) != -1: - results.append(s) - return results - - def GetSearch(self, term, geocode=None, since_id=None, - per_page=15, page=1, lang="en", show_user="true", query_users=False): - ''' Return twitter search results for a given term. + def FilterPublicTimeline(self, + term, + since_id=None): + '''Filter the public twitter timeline by a given search term on + the local machine. + + Args: + term: + term to search by. + since_id: + Returns results with an ID greater than (that is, more recent + than) the specified ID. There are limits to the number of + Tweets which can be accessed through the API. If the limit of + Tweets has occured since the since_id, the since_id will be + forced to the oldest ID available. [Optional] + + Returns: + A sequence of twitter.Status instances, one for each message + containing the term + ''' + statuses = self.GetPublicTimeline(since_id) + results = [] + + for s in statuses: + if s.text.lower().find(term.lower()) != -1: + results.append(s) + + return results + + def GetSearch(self, + term=None, + geocode=None, + since_id=None, + per_page=15, + page=1, + lang="en", + show_user="true", + query_users=False): + '''Return twitter search results for a given term. Args: term: - term to search by. + term to search by. Optional if you include geocode. since_id: - Returns only public statuses with an ID greater than (that is, - more recent than) the specified ID. [Optional] + Returns results with an ID greater than (that is, more recent + than) the specified ID. There are limits to the number of + Tweets which can be accessed through the API. If the limit of + Tweets has occured since the since_id, the since_id will be + forced to the oldest ID available. [Optional] geocode: - geolocation information in the form (latitude, longitude, radius) [Optional] + geolocation information in the form (latitude, longitude, radius) + [Optional] per_page: - number of results to return [Optional] default=15 + number of results to return. Default is 15 [Optional] page: - which page of search results to return + Specifies the page of results to retrieve. + Note: there are pagination limits. [Optional] lang: - language for results [Optional] default english + language for results. Default is English [Optional] show_user: - prefixes screen name in status + prefixes screen name in status query_users: - If sets to False, then all users only have screen_name and - profile_image_url available. If sets to True, all information of users - are available, but it uses lots of request quota, one per status. + If set to False, then all users only have screen_name and + profile_image_url available. + If set to True, all information of users are available, + but it uses lots of request quota, one per status. + Returns: - A sequence of twitter.Status instances, one for each - message containing the term + A sequence of twitter.Status instances, one for each message containing + the term ''' # Build request parameters parameters = {} + if since_id: parameters['since_id'] = since_id - if not term: + + if term is None and geocode is None: return [] - parameters['q'] = urllib.quote_plus(term) + + if term is not None: + parameters['q'] = term + + if geocode is not None: + parameters['geocode'] = ','.join(map(str, geocode)) + parameters['show_user'] = show_user parameters['lang'] = lang parameters['rpp'] = per_page parameters['page'] = page - if geocode is not None: - parameters['geocode'] = ','.join(map(str, geocode)) # Make and send requests - url = 'http://search.twitter.com/search.json' - json = self._FetchUrl(url, parameters=parameters) + url = 'http://search.twitter.com/search.json' + json = self._FetchUrl(url, parameters=parameters) data = simplejson.loads(json) + self._CheckForTwitterError(data) results = [] + for x in data['results']: temp = Status.NewFromJsonDict(x) + if query_users: # Build user object with new request temp.user = self.GetUser(urllib.quote(x['from_user'])) else: temp.user = User(screen_name=x['from_user'], profile_image_url=x['profile_image_url']) + results.append(temp) # Return built list of statuses return results # [Status.NewFromJsonDict(x) for x in data['results']] + def GetTrendsCurrent(self, exclude=None): + '''Get the current top trending topics + + Args: + exclude: + Appends the exclude parameter as a request parameter. + Currently only exclude=hashtags is supported. [Optional] + + Returns: + A list with 10 entries. Each entry contains the twitter. + ''' + parameters = {} + if exclude: + parameters['exclude'] = exclude + url = '%s/trends/current.json' % self.base_url + json = self._FetchUrl(url, parameters=parameters) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + trends = [] + + for t in data['trends']: + for item in data['trends'][t]: + trends.append(Trend.NewFromJsonDict(item, timestamp = t)) + return trends + + def GetTrendsDaily(self, exclude=None, startdate=None): + '''Get the current top trending topics for each hour in a given day + + Args: + startdate: + The start date for the report. + Should be in the format YYYY-MM-DD. [Optional] + exclude: + Appends the exclude parameter as a request parameter. + Currently only exclude=hashtags is supported. [Optional] + + Returns: + A list with 24 entries. Each entry contains the twitter. + Trend elements that were trending at the corresponding hour of the day. + ''' + parameters = {} + if exclude: + parameters['exclude'] = exclude + if not startdate: + startdate = time.strftime('%Y-%m-%d', time.gmtime()) + parameters['date'] = startdate + url = '%s/trends/daily.json' % self.base_url + json = self._FetchUrl(url, parameters=parameters) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + trends = [] + for i in xrange(24): + trends.append(None) + for t in data['trends']: + idx = int(time.strftime('%H', time.strptime(t, '%Y-%m-%d %H:%M'))) + trends[idx] = [Trend.NewFromJsonDict(x, timestamp = t) + for x in data['trends'][t]] + return trends + + def GetTrendsWeekly(self, exclude=None, startdate=None): + '''Get the top 30 trending topics for each day in a given week. + + Args: + startdate: + The start date for the report. + Should be in the format YYYY-MM-DD. [Optional] + exclude: + Appends the exclude parameter as a request parameter. + Currently only exclude=hashtags is supported. [Optional] + Returns: + A list with each entry contains the twitter. + Trend elements of trending topics for the corrsponding day of the week + ''' + parameters = {} + if exclude: + parameters['exclude'] = exclude + if not startdate: + startdate = time.strftime('%Y-%m-%d', time.gmtime()) + parameters['date'] = startdate + url = '%s/trends/weekly.json' % self.base_url + json = self._FetchUrl(url, parameters=parameters) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + trends = [] + for i in xrange(7): + trends.append(None) + # use the epochs of the dates as keys for a dictionary + times = dict([(calendar.timegm(time.strptime(t, '%Y-%m-%d')),t) + for t in data['trends']]) + cnt = 0 + # create the resulting structure ordered by the epochs of the dates + for e in sorted(times.keys()): + trends[cnt] = [Trend.NewFromJsonDict(x, timestamp = times[e]) + for x in data['trends'][times[e]]] + cnt +=1 + return trends + def GetFriendsTimeline(self, user=None, count=None, - since=None, - since_id=None): + page=None, + since_id=None, + retweets=None, + include_entities=None): '''Fetch the sequence of twitter.Status messages for a user's friends The twitter.Api instance must be authenticated if the user is private. @@ -1453,39 +2303,57 @@ def GetFriendsTimeline(self, Args: user: Specifies the ID or screen name of the user for whom to return - the friends_timeline. If unspecified, the username and password - must be set in the twitter.Api instance. [Optional] - count: + the friends_timeline. If not specified then the authenticated + user set in the twitter.Api instance will be used. [Optional] + count: Specifies the number of statuses to retrieve. May not be - greater than 200. [Optional] - since: - Narrows the returned results to just those statuses created - after the specified HTTP-formatted date. [Optional] + greater than 100. [Optional] + page: + Specifies the page of results to retrieve. + Note: there are pagination limits. [Optional] since_id: - Returns only public statuses with an ID greater than (that is, - more recent than) the specified ID. [Optional] + Returns results with an ID greater than (that is, more recent + than) the specified ID. There are limits to the number of + Tweets which can be accessed through the API. If the limit of + Tweets has occured since the since_id, the since_id will be + forced to the oldest ID available. [Optional] + retweets: + If True, the timeline will contain native retweets. [Optional] + include_entities: + If True, each tweet will include a node called "entities,". + This node offers a variety of metadata about the tweet in a + discreet structure, including: user_mentions, urls, and + hashtags. [Optional] Returns: A sequence of twitter.Status instances, one for each message ''' - if not user and not self._username: + if not user and not self._oauth_consumer: raise TwitterError("User must be specified if API is not authenticated.") + url = '%s/statuses/friends_timeline' % self.base_url if user: - url = 'http://twitter.com/statuses/friends_timeline/%s.json' % user + url = '%s/%s.json' % (url, user) else: - url = 'http://twitter.com/statuses/friends_timeline.json' + url = '%s.json' % url parameters = {} if count is not None: try: - if int(count) > 200: - raise TwitterError("'count' may not be greater than 200") + if int(count) > 100: + raise TwitterError("'count' may not be greater than 100") except ValueError: raise TwitterError("'count' must be an integer") parameters['count'] = count - if since: - parameters['since'] = since + if page is not None: + try: + parameters['page'] = int(page) + except ValueError: + raise TwitterError("'page' must be an integer") if since_id: parameters['since_id'] = since_id + if retweets: + parameters['include_rts'] = True + if include_entities: + parameters['include_entities'] = True json = self._FetchUrl(url, parameters=parameters) data = simplejson.loads(json) self._CheckForTwitterError(data) @@ -1498,7 +2366,9 @@ def GetUserTimeline(self, since_id=None, max_id=None, count=None, - page=None): + page=None, + include_rts=None, + include_entities=None): '''Fetch the sequence of public Status messages for a single user. The twitter.Api instance must be authenticated if the user is private. @@ -1506,27 +2376,38 @@ def GetUserTimeline(self, Args: id: Specifies the ID or screen name of the user for whom to return - the user_timeline. [optional] + the user_timeline. [Optional] user_id: Specfies the ID of the user for whom to return the user_timeline. Helpful for disambiguating when a valid user ID - is also a valid screen name. [optional] + is also a valid screen name. [Optional] screen_name: Specfies the screen name of the user for whom to return the user_timeline. Helpful for disambiguating when a valid screen - name is also a user ID. [optional] + name is also a user ID. [Optional] since_id: - Returns only public statuses with an ID greater than (that is, - more recent than) the specified ID. [optional] + Returns results with an ID greater than (that is, more recent + than) the specified ID. There are limits to the number of + Tweets which can be accessed through the API. If the limit of + Tweets has occured since the since_id, the since_id will be + forced to the oldest ID available. [Optional] max_id: Returns only statuses with an ID less than (that is, older - than) or equal to the specified ID. [optional] + than) or equal to the specified ID. [Optional] count: Specifies the number of statuses to retrieve. May not be - greater than 200. [optional] + greater than 200. [Optional] page: - Specifies the page of results to retrieve. Note: there are - pagination limits. [optional] + Specifies the page of results to retrieve. + Note: there are pagination limits. [Optional] + include_rts: + If True, the timeline will contain native retweets (if they + exist) in addition to the standard stream of tweets. [Optional] + include_entities: + If True, each tweet will include a node called "entities,". + This node offers a variety of metadata about the tweet in a + discreet structure, including: user_mentions, urls, and + hashtags. [Optional] Returns: A sequence of Status instances, one for each message up to count @@ -1534,16 +2415,16 @@ def GetUserTimeline(self, parameters = {} if id: - url = 'http://twitter.com/statuses/user_timeline/%s.json' % id + url = '%s/statuses/user_timeline/%s.json' % (self.base_url, id) elif user_id: - url = 'http://twitter.com/statuses/user_timeline.json?user_id=%d' % user_id + url = '%s/statuses/user_timeline.json?user_id=%d' % (self.base_url, user_id) elif screen_name: - url = ('http://twitter.com/statuses/user_timeline.json?screen_name=%s' % - screen_name) - elif not self._username: + url = ('%s/statuses/user_timeline.json?screen_name=%s' % (self.base_url, + screen_name)) + elif not self._oauth_consumer: raise TwitterError("User must be specified if API is not authenticated.") else: - url = 'http://twitter.com/statuses/user_timeline.json' + url = '%s/statuses/user_timeline.json' % self.base_url if since_id: try: @@ -1569,6 +2450,12 @@ def GetUserTimeline(self, except: raise TwitterError("page must be an integer") + if include_rts: + parameters['include_rts'] = 1 + + if include_entities: + parameters['include_entities'] = 1 + json = self._FetchUrl(url, parameters=parameters) data = simplejson.loads(json) self._CheckForTwitterError(data) @@ -1577,10 +2464,12 @@ def GetUserTimeline(self, def GetStatus(self, id): '''Returns a single status message. - The twitter.Api instance must be authenticated if the status message is private. + The twitter.Api instance must be authenticated if the + status message is private. Args: - id: The numerical ID of the status you're trying to retrieve. + id: + The numeric ID of the status you are trying to retrieve. Returns: A twitter.Status instance representing that status message @@ -1590,7 +2479,7 @@ def GetStatus(self, id): long(id) except: raise TwitterError("id must be an long integer") - url = 'http://twitter.com/statuses/show/%s.json' % id + url = '%s/statuses/show/%s.json' % (self.base_url, id) json = self._FetchUrl(url) data = simplejson.loads(json) self._CheckForTwitterError(data) @@ -1599,11 +2488,12 @@ def GetStatus(self, id): def DestroyStatus(self, id): '''Destroys the status specified by the required ID parameter. - The twitter.Api instance must be authenticated and thee + The twitter.Api instance must be authenticated and the authenticating user must be the author of the specified status. Args: - id: The numerical ID of the status you're trying to destroy. + id: + The numerical ID of the status you're trying to destroy. Returns: A twitter.Status instance representing the destroyed status message @@ -1613,8 +2503,8 @@ def DestroyStatus(self, id): long(id) except: raise TwitterError("id must be an integer") - url = 'http://twitter.com/statuses/destroy/%s.json' % id - json = self._FetchUrl(url, post_data={}) + url = '%s/statuses/destroy/%s.json' % (self.base_url, id) + json = self._FetchUrl(url, post_data={'id': id}) data = simplejson.loads(json) self._CheckForTwitterError(data) return Status.NewFromJsonDict(data) @@ -1626,8 +2516,8 @@ def PostUpdate(self, status, in_reply_to_status_id=None): Args: status: - The message text to be posted. Must be less than or equal to - 140 characters. + The message text to be posted. + Must be less than or equal to 140 characters. in_reply_to_status_id: The ID of an existing status that the status to be posted is in reply to. This implicitly sets the in_reply_to_user_id @@ -1637,12 +2527,17 @@ def PostUpdate(self, status, in_reply_to_status_id=None): Returns: A twitter.Status instance representing the message posted. ''' - if not self._username: + if not self._oauth_consumer: raise TwitterError("The twitter.Api instance must be authenticated.") - url = 'http://twitter.com/statuses/update.json' + url = '%s/statuses/update.json' % self.base_url - if len(status) > CHARACTER_LIMIT: + if isinstance(status, unicode) or self._input_encoding is None: + u_status = status + else: + u_status = unicode(status, self._input_encoding) + + if len(u_status) > CHARACTER_LIMIT: raise TwitterError("Text must be less than or equal to %d characters. " "Consider using PostUpdates." % CHARACTER_LIMIT) @@ -1664,7 +2559,8 @@ def PostUpdates(self, status, continuation=None, **kwargs): Args: status: - The message text to be posted. May be longer than 140 characters. + The message text to be posted. + May be longer than 140 characters. continuation: The character string, if any, to be appended to all but the last message. Note that Twitter strips trailing '...' strings @@ -1672,6 +2568,7 @@ def PostUpdates(self, status, continuation=None, **kwargs): (horizontal ellipsis) instead. [Defaults to None] **kwargs: See api.PostUpdate for a list of accepted parameters. + Returns: A of list twitter.Status instance representing the messages posted. ''' @@ -1685,25 +2582,75 @@ def PostUpdates(self, status, continuation=None, **kwargs): results.append(self.PostUpdate(lines[-1], **kwargs)) return results - def GetReplies(self, since=None, since_id=None, page=None): - '''Get a sequence of status messages representing the 20 most recent - replies (status updates prefixed with @username) to the authenticating - user. + def GetUserRetweets(self, count=None, since_id=None, max_id=None, include_entities=False): + '''Fetch the sequence of retweets made by a single user. + + The twitter.Api instance must be authenticated. + + Args: + count: + The number of status messages to retrieve. [Optional] + since_id: + Returns results with an ID greater than (that is, more recent + than) the specified ID. There are limits to the number of + Tweets which can be accessed through the API. If the limit of + Tweets has occured since the since_id, the since_id will be + forced to the oldest ID available. [Optional] + max_id: + Returns results with an ID less than (that is, older than) or + equal to the specified ID. [Optional] + include_entities: + If True, each tweet will include a node called "entities,". + This node offers a variety of metadata about the tweet in a + discreet structure, including: user_mentions, urls, and + hashtags. [Optional] + + Returns: + A sequence of twitter.Status instances, one for each message up to count + ''' + url = '%s/statuses/retweeted_by_me.json' % self.base_url + if not self._oauth_consumer: + raise TwitterError("The twitter.Api instance must be authenticated.") + parameters = {} + if count is not None: + try: + if int(count) > 100: + raise TwitterError("'count' may not be greater than 100") + except ValueError: + raise TwitterError("'count' must be an integer") + if count: + parameters['count'] = count + if since_id: + parameters['since_id'] = since_id + if include_entities: + parameters['include_entities'] = True + json = self._FetchUrl(url, parameters=parameters) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return [Status.NewFromJsonDict(x) for x in data] + + def GetReplies(self, since=None, since_id=None, page=None): + '''Get a sequence of status messages representing the 20 most + recent replies (status updates prefixed with @twitterID) to the + authenticating user. Args: - page: - since: - Narrows the returned results to just those statuses created - after the specified HTTP-formatted date. [optional] since_id: - Returns only public statuses with an ID greater than (that is, - more recent than) the specified ID. [Optional] + Returns results with an ID greater than (that is, more recent + than) the specified ID. There are limits to the number of + Tweets which can be accessed through the API. If the limit of + Tweets has occured since the since_id, the since_id will be + forced to the oldest ID available. [Optional] + page: + Specifies the page of results to retrieve. + Note: there are pagination limits. [Optional] + since: Returns: A sequence of twitter.Status instances, one for each reply to the user. ''' - url = 'http://twitter.com/statuses/replies.json' - if not self._username: + url = '%s/statuses/replies.json' % self.base_url + if not self._oauth_consumer: raise TwitterError("The twitter.Api instance must be authenticated.") parameters = {} if since: @@ -1717,76 +2664,111 @@ def GetReplies(self, since=None, since_id=None, page=None): self._CheckForTwitterError(data) return [Status.NewFromJsonDict(x) for x in data] - def GetFriends(self, user=None, page=None): - '''Fetch the sequence of twitter.User instances, one for each friend. + def GetRetweets(self, statusid): + '''Returns up to 100 of the first retweets of the tweet identified + by statusid Args: - user: the username or id of the user whose friends you are fetching. If - not specified, defaults to the authenticated user. [optional] + statusid: + The ID of the tweet for which retweets should be searched for + + Returns: + A list of twitter.Status instances, which are retweets of statusid + ''' + if not self._oauth_consumer: + raise TwitterError("The twitter.Api instsance must be authenticated.") + url = '%s/statuses/retweets/%s.json?include_entities=true&include_rts=true' % (self.base_url, statusid) + parameters = {} + json = self._FetchUrl(url, parameters=parameters) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return [Status.NewFromJsonDict(s) for s in data] + + def GetFriends(self, user=None, cursor=-1): + '''Fetch the sequence of twitter.User instances, one for each friend. The twitter.Api instance must be authenticated. + Args: + user: + The twitter name or id of the user whose friends you are fetching. + If not specified, defaults to the authenticated user. [Optional] + Returns: A sequence of twitter.User instances, one for each friend ''' - if not user and not self._username: + if not user and not self._oauth_consumer: raise TwitterError("twitter.Api instance must be authenticated") if user: - url = 'http://twitter.com/statuses/friends/%s.json' % user + url = '%s/statuses/friends/%s.json' % (self.base_url, user) else: - url = 'http://twitter.com/statuses/friends.json' + url = '%s/statuses/friends.json' % self.base_url parameters = {} - if page: - parameters['page'] = page + parameters['cursor'] = cursor json = self._FetchUrl(url, parameters=parameters) data = simplejson.loads(json) self._CheckForTwitterError(data) - return [User.NewFromJsonDict(x) for x in data] + return [User.NewFromJsonDict(x) for x in data['users']] - def GetFriendIDs(self, user=None, page=None): + def GetFriendIDs(self, user=None, cursor=-1): '''Returns a list of twitter user id's for every person the specified user is following. Args: user: The id or screen_name of the user to retrieve the id list for - [optional] - page: - Specifies the page number of the results beginning at 1. - A single page contains 5000 ids. This is recommended for users - with large id lists. If not provided all id's are returned. - (Please note that the result set isn't guaranteed to be 5000 - every time as suspended users will be filtered.) - [optional] + [Optional] Returns: A list of integers, one for each user id. ''' - if not user and not self._username: + if not user and not self._oauth_consumer: raise TwitterError("twitter.Api instance must be authenticated") if user: - url = 'http://twitter.com/friends/ids/%s.json' % user + url = '%s/friends/ids/%s.json' % (self.base_url, user) else: - url = 'http://twitter.com/friends/ids.json' + url = '%s/friends/ids.json' % self.base_url parameters = {} - if page: - parameters['page'] = page + parameters['cursor'] = cursor json = self._FetchUrl(url, parameters=parameters) data = simplejson.loads(json) self._CheckForTwitterError(data) return data + def GetFollowerIDs(self, userid=None, cursor=-1): + '''Fetch the sequence of twitter.User instances, one for each follower + + The twitter.Api instance must be authenticated. + + Returns: + A sequence of twitter.User instances, one for each follower + ''' + url = 'http://twitter.com/followers/ids.json' + parameters = {} + parameters['cursor'] = cursor + if userid: + parameters['user_id'] = userid + json = self._FetchUrl(url, parameters=parameters) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return data + def GetFollowers(self, page=None): '''Fetch the sequence of twitter.User instances, one for each follower The twitter.Api instance must be authenticated. + Args: + page: + Specifies the page of results to retrieve. + Note: there are pagination limits. [Optional] + Returns: A sequence of twitter.User instances, one for each follower ''' - if not self._username: + if not self._oauth_consumer: raise TwitterError("twitter.Api instance must be authenticated") - url = 'http://twitter.com/statuses/followers.json' + url = '%s/statuses/followers.json' % self.base_url parameters = {} if page: parameters['page'] = page @@ -1803,24 +2785,68 @@ def GetFeatured(self): Returns: A sequence of twitter.User instances ''' - url = 'http://twitter.com/statuses/featured.json' + url = '%s/statuses/featured.json' % self.base_url json = self._FetchUrl(url) data = simplejson.loads(json) self._CheckForTwitterError(data) return [User.NewFromJsonDict(x) for x in data] + def UsersLookup(self, user_id=None, screen_name=None, users=None): + '''Fetch extended information for the specified users. + + Users may be specified either as lists of either user_ids, + screen_names, or twitter.User objects. The list of users that + are queried is the union of all specified parameters. + + The twitter.Api instance must be authenticated. + + Args: + user_id: + A list of user_ids to retrieve extended information. + [Optional] + screen_name: + A list of screen_names to retrieve extended information. + [Optional] + users: + A list of twitter.User objects to retrieve extended information. + [Optional] + + Returns: + A list of twitter.User objects for the requested users + ''' + + if not self._oauth_consumer: + raise TwitterError("The twitter.Api instance must be authenticated.") + if not user_id and not screen_name and not users: + raise TwitterError("Specify at least on of user_id, screen_name, or users.") + url = '%s/users/lookup.json' % self.base_url + parameters = {} + uids = list() + if user_id: + uids.extend(user_id) + if users: + uids.extend([u.id for u in users]) + if len(uids): + parameters['user_id'] = ','.join(["%s" % u for u in uids]) + if screen_name: + parameters['screen_name'] = ','.join(screen_name) + json = self._FetchUrl(url, parameters=parameters) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return [User.NewFromJsonDict(u) for u in data] + def GetUser(self, user): '''Returns a single user. The twitter.Api instance must be authenticated. Args: - user: The username or id of the user to retrieve. + user: The twitter name or id of the user to retrieve. Returns: A twitter.User instance representing that user ''' - url = 'http://twitter.com/users/show/%s.json' % user + url = '%s/users/show/%s.json' % (self.base_url, user) json = self._FetchUrl(url) data = simplejson.loads(json) self._CheckForTwitterError(data) @@ -1834,16 +2860,22 @@ def GetDirectMessages(self, since=None, since_id=None, page=None): Args: since: Narrows the returned results to just those statuses created - after the specified HTTP-formatted date. [optional] + after the specified HTTP-formatted date. [Optional] since_id: - Returns only public statuses with an ID greater than (that is, - more recent than) the specified ID. [Optional] + Returns results with an ID greater than (that is, more recent + than) the specified ID. There are limits to the number of + Tweets which can be accessed through the API. If the limit of + Tweets has occured since the since_id, the since_id will be + forced to the oldest ID available. [Optional] + page: + Specifies the page of results to retrieve. + Note: there are pagination limits. [Optional] Returns: A sequence of twitter.DirectMessage instances ''' - url = 'http://twitter.com/direct_messages.json' - if not self._username: + url = '%s/direct_messages.json' % self.base_url + if not self._oauth_consumer: raise TwitterError("The twitter.Api instance must be authenticated.") parameters = {} if since: @@ -1851,7 +2883,7 @@ def GetDirectMessages(self, since=None, since_id=None, page=None): if since_id: parameters['since_id'] = since_id if page: - parameters['page'] = page + parameters['page'] = page json = self._FetchUrl(url, parameters=parameters) data = simplejson.loads(json) self._CheckForTwitterError(data) @@ -1869,9 +2901,9 @@ def PostDirectMessage(self, user, text): Returns: A twitter.DirectMessage instance representing the message posted ''' - if not self._username: + if not self._oauth_consumer: raise TwitterError("The twitter.Api instance must be authenticated.") - url = 'http://twitter.com/direct_messages/new.json' + url = '%s/direct_messages/new.json' % self.base_url data = {'text': text, 'user': user} json = self._FetchUrl(url, post_data=data) data = simplejson.loads(json) @@ -1891,8 +2923,8 @@ def DestroyDirectMessage(self, id): Returns: A twitter.DirectMessage instance representing the message destroyed ''' - url = 'http://twitter.com/direct_messages/destroy/%s.json' % id - json = self._FetchUrl(url, post_data={}) + url = '%s/direct_messages/destroy/%s.json' % (self.base_url, id) + json = self._FetchUrl(url, post_data={'id': id}) data = simplejson.loads(json) self._CheckForTwitterError(data) return DirectMessage.NewFromJsonDict(data) @@ -1907,8 +2939,8 @@ def CreateFriendship(self, user): Returns: A twitter.User instance representing the befriended user. ''' - url = 'http://twitter.com/friendships/create/%s.json' % user - json = self._FetchUrl(url, post_data={}) + url = '%s/friendships/create/%s.json' % (self.base_url, user) + json = self._FetchUrl(url, post_data={'user': user}) data = simplejson.loads(json) self._CheckForTwitterError(data) return User.NewFromJsonDict(data) @@ -1923,8 +2955,8 @@ def DestroyFriendship(self, user): Returns: A twitter.User instance representing the discontinued friend. ''' - url = 'http://twitter.com/friendships/destroy/%s.json' % user - json = self._FetchUrl(url, post_data={}) + url = '%s/friendships/destroy/%s.json' % (self.base_url, user) + json = self._FetchUrl(url, post_data={'user': user}) data = simplejson.loads(json) self._CheckForTwitterError(data) return User.NewFromJsonDict(data) @@ -1940,8 +2972,8 @@ def CreateFavorite(self, status): Returns: A twitter.Status instance representing the newly-marked favorite. ''' - url = 'http://twitter.com/favorites/create/%s.json' % status.id - json = self._FetchUrl(url, post_data={}) + url = '%s/favorites/create/%s.json' % (self.base_url, status.id) + json = self._FetchUrl(url, post_data={'id': status.id}) data = simplejson.loads(json) self._CheckForTwitterError(data) return Status.NewFromJsonDict(data) @@ -1957,21 +2989,254 @@ def DestroyFavorite(self, status): Returns: A twitter.Status instance representing the newly-unmarked favorite. ''' - url = 'http://twitter.com/favorites/destroy/%s.json' % status.id - json = self._FetchUrl(url, post_data={}) + url = '%s/favorites/destroy/%s.json' % (self.base_url, status.id) + json = self._FetchUrl(url, post_data={'id': status.id}) data = simplejson.loads(json) self._CheckForTwitterError(data) return Status.NewFromJsonDict(data) + def GetFavorites(self, + user=None, + page=None): + '''Return a list of Status objects representing favorited tweets. + By default, returns the (up to) 20 most recent tweets for the + authenticated user. + + Args: + user: + The twitter name or id of the user whose favorites you are fetching. + If not specified, defaults to the authenticated user. [Optional] + page: + Specifies the page of results to retrieve. + Note: there are pagination limits. [Optional] + ''' + parameters = {} + + if page: + parameters['page'] = page + + if user: + url = '%s/favorites/%s.json' % (self.base_url, user) + elif not user and not self._oauth_consumer: + raise TwitterError("User must be specified if API is not authenticated.") + else: + url = '%s/favorites.json' % self.base_url + + json = self._FetchUrl(url, parameters=parameters) + data = simplejson.loads(json) + + self._CheckForTwitterError(data) + + return [Status.NewFromJsonDict(x) for x in data] + + def GetMentions(self, + since_id=None, + max_id=None, + page=None): + '''Returns the 20 most recent mentions (status containing @twitterID) + for the authenticating user. + + Args: + since_id: + Returns results with an ID greater than (that is, more recent + than) the specified ID. There are limits to the number of + Tweets which can be accessed through the API. If the limit of + Tweets has occured since the since_id, the since_id will be + forced to the oldest ID available. [Optional] + max_id: + Returns only statuses with an ID less than + (that is, older than) the specified ID. [Optional] + page: + Specifies the page of results to retrieve. + Note: there are pagination limits. [Optional] + + Returns: + A sequence of twitter.Status instances, one for each mention of the user. + ''' + + url = '%s/statuses/mentions.json' % self.base_url + + if not self._oauth_consumer: + raise TwitterError("The twitter.Api instance must be authenticated.") + + parameters = {} + + if since_id: + parameters['since_id'] = since_id + if max_id: + parameters['max_id'] = max_id + if page: + parameters['page'] = page + + json = self._FetchUrl(url, parameters=parameters) + data = simplejson.loads(json) + + self._CheckForTwitterError(data) + + return [Status.NewFromJsonDict(x) for x in data] + + def CreateList(self, user, name, mode=None, description=None): + '''Creates a new list with the give name + + The twitter.Api instance must be authenticated. + + Args: + user: + Twitter name to create the list for + name: + New name for the list + mode: + 'public' or 'private'. + Defaults to 'public'. [Optional] + description: + Description of the list. [Optional] + + Returns: + A twitter.List instance representing the new list + ''' + url = '%s/%s/lists.json' % (self.base_url, user) + parameters = {'name': name} + if mode is not None: + parameters['mode'] = mode + if description is not None: + parameters['description'] = description + json = self._FetchUrl(url, post_data=parameters) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return List.NewFromJsonDict(data) + + def DestroyList(self, user, id): + '''Destroys the list from the given user + + The twitter.Api instance must be authenticated. + + Args: + user: + The user to remove the list from. + id: + The slug or id of the list to remove. + Returns: + A twitter.List instance representing the removed list. + ''' + url = '%s/%s/lists/%s.json' % (self.base_url, user, id) + json = self._FetchUrl(url, post_data={'_method': 'DELETE'}) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return List.NewFromJsonDict(data) + + def CreateSubscription(self, owner, list): + '''Creates a subscription to a list by the authenticated user + + The twitter.Api instance must be authenticated. + + Args: + owner: + User name or id of the owner of the list being subscribed to. + list: + The slug or list id to subscribe the user to + + Returns: + A twitter.List instance representing the list subscribed to + ''' + url = '%s/%s/%s/subscribers.json' % (self.base_url, owner, list) + json = self._FetchUrl(url, post_data={'list_id': list}) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return List.NewFromJsonDict(data) + + def DestroySubscription(self, owner, list): + '''Destroys the subscription to a list for the authenticated user + + The twitter.Api instance must be authenticated. + + Args: + owner: + The user id or screen name of the user that owns the + list that is to be unsubscribed from + list: + The slug or list id of the list to unsubscribe from + + Returns: + A twitter.List instance representing the removed list. + ''' + url = '%s/%s/%s/subscribers.json' % (self.base_url, owner, list) + json = self._FetchUrl(url, post_data={'_method': 'DELETE', 'list_id': list}) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return List.NewFromJsonDict(data) + + def GetSubscriptions(self, user, cursor=-1): + '''Fetch the sequence of Lists that the given user is subscribed to + + The twitter.Api instance must be authenticated. + + Args: + user: + The twitter name or id of the user + cursor: + "page" value that Twitter will use to start building the + list sequence from. -1 to start at the beginning. + Twitter will return in the result the values for next_cursor + and previous_cursor. [Optional] + + Returns: + A sequence of twitter.List instances, one for each list + ''' + if not self._oauth_consumer: + raise TwitterError("twitter.Api instance must be authenticated") + + url = '%s/%s/lists/subscriptions.json' % (self.base_url, user) + parameters = {} + parameters['cursor'] = cursor + + json = self._FetchUrl(url, parameters=parameters) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + print data + return [List.NewFromJsonDict(x) for x in data['lists']] + + def GetLists(self, user, cursor=-1): + '''Fetch the sequence of lists for a user. + + The twitter.Api instance must be authenticated. + + Args: + user: + The twitter name or id of the user whose friends you are fetching. + If the passed in user is the same as the authenticated user + then you will also receive private list data. + cursor: + "page" value that Twitter will use to start building the + list sequence from. -1 to start at the beginning. + Twitter will return in the result the values for next_cursor + and previous_cursor. [Optional] + + Returns: + A sequence of twitter.List instances, one for each list + ''' + if not self._oauth_consumer: + raise TwitterError("twitter.Api instance must be authenticated") + + url = '%s/%s/lists.json' % (self.base_url, user) + parameters = {} + parameters['cursor'] = cursor + + json = self._FetchUrl(url, parameters=parameters) + data = simplejson.loads(json) + self._CheckForTwitterError(data) + return [List.NewFromJsonDict(x) for x in data['lists']] + def GetUserByEmail(self, email): '''Returns a single user by email address. Args: - email: The email of the user to retrieve. + email: + The email of the user to retrieve. + Returns: A twitter.User instance representing that user ''' - url = 'http://twitter.com/users/show.json?email=%s' % email + url = '%s/users/show.json?email=%s' % (self.base_url, email) json = self._FetchUrl(url) data = simplejson.loads(json) self._CheckForTwitterError(data) @@ -1980,13 +3245,13 @@ def GetUserByEmail(self, email): def VerifyCredentials(self): '''Returns a twitter.User instance if the authenticating user is valid. - Returns: + Returns: A twitter.User instance representing that user if the credentials are valid, None otherwise. ''' - if not self._username: + if not self._oauth_consumer: raise TwitterError("Api instance must first be given user credentials.") - url = 'http://twitter.com/account/verify_credentials.json' + url = '%s/account/verify_credentials.json' % self.base_url try: json = self._FetchUrl(url, no_cache=True) except urllib2.HTTPError, http_error: @@ -1998,27 +3263,12 @@ def VerifyCredentials(self): self._CheckForTwitterError(data) return User.NewFromJsonDict(data) - def SetCredentials(self, username, password): - '''Set the username and password for this instance - - Args: - username: The twitter username. - password: The twitter password. - ''' - self._username = username - self._password = password - - def ClearCredentials(self): - '''Clear the username and password for this instance - ''' - self._username = None - self._password = None - def SetCache(self, cache): '''Override the default cache. Set to None to prevent caching. Args: - cache: an instance that supports the same API as the twitter._FileCache + cache: + An instance that supports the same API as the twitter._FileCache ''' if cache == DEFAULT_CACHE: self._cache = _FileCache() @@ -2029,7 +3279,8 @@ def SetUrllib(self, urllib): '''Override the default urllib implementation. Args: - urllib: an instance that supports the same API as the urllib2 module + urllib: + An instance that supports the same API as the urllib2 module ''' self._urllib = urllib @@ -2037,7 +3288,8 @@ def SetCacheTimeout(self, cache_timeout): '''Override the default cache timeout. Args: - cache_timeout: time, in seconds, that responses should be reused. + cache_timeout: + Time, in seconds, that responses should be reused. ''' self._cache_timeout = cache_timeout @@ -2045,7 +3297,8 @@ def SetUserAgent(self, user_agent): '''Override the default user agent Args: - user_agent: a string that should be send to the server as the User-agent + user_agent: + A string that should be send to the server as the User-agent ''' self._request_headers['User-Agent'] = user_agent @@ -2081,6 +3334,51 @@ def SetSource(self, source): ''' self._default_params['source'] = source + def GetRateLimitStatus(self): + '''Fetch the rate limit status for the currently authorized user. + + Returns: + A dictionary containing the time the limit will reset (reset_time), + the number of remaining hits allowed before the reset (remaining_hits), + the number of hits allowed in a 60-minute period (hourly_limit), and + the time of the reset in seconds since The Epoch (reset_time_in_seconds). + ''' + url = '%s/account/rate_limit_status.json' % self.base_url + json = self._FetchUrl(url, no_cache=True) + data = simplejson.loads(json) + + self._CheckForTwitterError(data) + + return data + + def MaximumHitFrequency(self): + '''Determines the minimum number of seconds that a program must wait + before hitting the server again without exceeding the rate_limit + imposed for the currently authenticated user. + + Returns: + The minimum second interval that a program must use so as to not + exceed the rate_limit imposed for the user. + ''' + rate_status = self.GetRateLimitStatus() + reset_time = rate_status.get('reset_time', None) + limit = rate_status.get('remaining_hits', None) + + if reset_time and limit: + # put the reset time into a datetime object + reset = datetime.datetime(*rfc822.parsedate(reset_time)[:7]) + + # find the difference in time between now and the reset time + 1 hour + delta = reset + datetime.timedelta(hours=1) - datetime.datetime.utcnow() + + # determine the minimum number of seconds allowed as a regular interval + max_frequency = int(delta.seconds / limit) + + # return the number of seconds + return max_frequency + + return 0 + def _BuildUrl(self, url, path_elements=None, extra_params=None): # Break url into consituent parts (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(url) @@ -2119,26 +3417,13 @@ def _InitializeUserAgent(self): def _InitializeDefaultParameters(self): self._default_params = {} - def _AddAuthorizationHeader(self, username, password): - if username and password: - basic_auth = base64.encodestring('%s:%s' % (username, password))[:-1] - self._request_headers['Authorization'] = 'Basic %s' % basic_auth - - def _RemoveAuthorizationHeader(self): - if self._request_headers and 'Authorization' in self._request_headers: - del self._request_headers['Authorization'] - - def _GetOpener(self, url, username=None, password=None): - if username and password: - self._AddAuthorizationHeader(username, password) - handler = self._urllib.HTTPBasicAuthHandler() - (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(url) - handler.add_password(Api._API_REALM, netloc, username, password) - opener = self._urllib.build_opener(handler) + def _DecompressGzippedResponse(self, response): + raw_data = response.read() + if response.headers.get('content-encoding', None) == 'gzip': + url_data = gzip.GzipFile(fileobj=StringIO.StringIO(raw_data)).read() else: - opener = self._urllib.build_opener() - opener.addheaders = self._request_headers.items() - return opener + url_data = raw_data + return url_data def _Encode(self, s): if self._input_encoding: @@ -2155,6 +3440,7 @@ def _EncodeParameters(self, parameters): parameters: A dict of (key, value) tuples, where value is encoded as specified by self._encoding + Returns: A URL-encoded string in "key=value&key=value" form ''' @@ -2173,6 +3459,7 @@ def _EncodePostData(self, post_data): post_data: A dict of (key, value) tuples, where value is encoded as specified by self._encoding + Returns: A URL-encoded string in "key=value&key=value" form ''' @@ -2185,7 +3472,9 @@ def _CheckForTwitterError(self, data): """Raises a TwitterError if twitter returns an error message. Args: - data: A python dict created from the Twitter json response + data: + A python dict created from the Twitter json response + Raises: TwitterError wrapping the twitter error message if one exists. """ @@ -2198,17 +3487,26 @@ def _FetchUrl(self, url, post_data=None, parameters=None, - no_cache=None): + no_cache=None, + use_gzip_compression=None): '''Fetch a URL, optionally caching for a specified time. Args: - url: The URL to retrieve - post_data: - A dict of (str, unicode) key/value pairs. If set, POST will be used. + url: + The URL to retrieve + post_data: + A dict of (str, unicode) key/value pairs. + If set, POST will be used. parameters: - A dict whose key/value pairs should encoded and added - to the query string. [OPTIONAL] - no_cache: If true, overrides the cache on the current request + A dict whose key/value pairs should encoded and added + to the query string. [Optional] + no_cache: + If true, overrides the cache on the current request + use_gzip_compression: + If True, tells the server to gzip-compress the response. + It does not apply to POST requests. + Defaults to None, which will get the value to use from + the instance variable self._use_gzip [Optional] Returns: A string containing the body of the response. @@ -2220,22 +3518,63 @@ def _FetchUrl(self, if parameters: extra_params.update(parameters) - # Add key/value parameters to the query string of the url - url = self._BuildUrl(url, extra_params=extra_params) + if post_data: + http_method = "POST" + else: + http_method = "GET" + + if self._debugHTTP: + _debug = 1 + else: + _debug = 0 + + http_handler = self._urllib.HTTPHandler(debuglevel=_debug) + https_handler = self._urllib.HTTPSHandler(debuglevel=_debug) - # Get a url opener that can handle basic auth - opener = self._GetOpener(url, username=self._username, password=self._password) + opener = self._urllib.OpenerDirector() + opener.add_handler(http_handler) + opener.add_handler(https_handler) - encoded_post_data = self._EncodePostData(post_data) + if use_gzip_compression is None: + use_gzip = self._use_gzip + else: + use_gzip = use_gzip_compression + + # Set up compression + if use_gzip and not post_data: + opener.addheaders.append(('Accept-Encoding', 'gzip')) + + if self._oauth_consumer is not None: + if post_data and http_method == "POST": + parameters = post_data.copy() + + req = oauth.Request.from_consumer_and_token(self._oauth_consumer, + token=self._oauth_token, + http_method=http_method, + http_url=url, parameters=parameters) + + req.sign_request(self._signature_method_hmac_sha1, self._oauth_consumer, self._oauth_token) + + headers = req.to_header() + + if http_method == "POST": + encoded_post_data = req.to_postdata() + else: + encoded_post_data = None + url = req.to_url() + else: + url = self._BuildUrl(url, extra_params=extra_params) + encoded_post_data = self._EncodePostData(post_data) # Open and return the URL immediately if we're not going to cache if encoded_post_data or no_cache or not self._cache or not self._cache_timeout: - url_data = opener.open(url, encoded_post_data).read() + response = opener.open(url, encoded_post_data) + url_data = self._DecompressGzippedResponse(response) opener.close() else: - # Unique keys are a combination of the url and the username - if self._username: - key = self._username + ':' + url + # Unique keys are a combination of the url and the oAuth Consumer Key + if self._consumer_key: + key = self._consumer_key + ':' + url else: key = url @@ -2244,16 +3583,19 @@ def _FetchUrl(self, # If the cached version is outdated then fetch another and store it if not last_cached or time.time() >= last_cached + self._cache_timeout: - url_data = opener.open(url, encoded_post_data).read() + try: + response = opener.open(url, encoded_post_data) + url_data = self._DecompressGzippedResponse(response) + self._cache.Set(key, url_data) + except urllib2.HTTPError, e: + print e opener.close() - self._cache.Set(key, url_data) else: url_data = self._cache.Get(key) # Always return the latest version return url_data - class _FileCacheError(Exception): '''Base exception class for FileCache related errors''' @@ -2336,7 +3678,7 @@ def _GetPath(self,key): hashed_key = md5(key).hexdigest() except TypeError: hashed_key = md5.new(key).hexdigest() - + return os.path.join(self._root_directory, self._GetPrefix(hashed_key), hashed_key)