diff --git a/gittip/networks/twitter.py b/gittip/networks/twitter.py new file mode 100644 index 0000000000..ef2c03e128 --- /dev/null +++ b/gittip/networks/twitter.py @@ -0,0 +1,95 @@ +import requests +from aspen import json, log, Response +from aspen.website import Website +from aspen.utils import typecheck +from gittip import db, networks + + +def upsert(user_info): + return networks.upsert( 'twitter' + , user_info['id'] + , user_info['screen_name'] + , user_info + ) + + +def oauth_url(website, action, then=u""): + """Given a website object and a string, return a URL string. + + `action' is one of 'opt-in', 'lock' and 'unlock' + + `then' is either a twitter username or an URL starting with '/'. It's + where we'll send the user after we get the redirect back from + GitHub. + + """ + typecheck(website, Website, action, unicode, then, unicode) + assert action in [u'opt-in', u'lock', u'unlock'] + url = u"https://twitter.com/login/oauth/authorize?consumer_key=%s&redirect_uri=%s" + url %= (website.twitter_consumer_key, website.twitter_callback) + + # Pack action,then into data and base64-encode. Querystring isn't + # available because it's consumed by the initial GitHub request. + + data = u'%s,%s' % (action, then) + data = data.encode('UTF-8').encode('base64').decode('US-ASCII') + url += u'?data=%s' % data + return url + + +def oauth_dance(website, qs): + """Given a querystring, return a dict of user_info. + + The querystring should be the querystring that we get from GitHub when + we send the user to the return value of oauth_url above. + + See also: + + http://developer.twitter.com/v3/oauth/ + + """ + + log("Doing an OAuth dance with Github.") + + if 'error' in qs: + raise Response(500, str(qs['error'])) + + data = { 'code': qs['code'].encode('US-ASCII') + , 'client_id': website.twitter_client_id + , 'client_secret': website.twitter_client_secret + } + r = requests.post("https://twitter.com/login/oauth/access_token", data=data) + assert r.status_code == 200, (r.status_code, r.text) + + back = dict([pair.split('=') for pair in r.text.split('&')]) # XXX + if 'error' in back: + raise Response(400, back['error'].encode('utf-8')) + assert back.get('token_type', '') == 'bearer', back + access_token = back['access_token'] + + r = requests.get( "https://api.twitter.com/user" + , headers={'Authorization': 'token %s' % access_token} + ) + assert r.status_code == 200, (r.status_code, r.text) + user_info = json.loads(r.text) + log("Done with OAuth dance with Github for %s (%s)." + % (user_info['login'], user_info['id'])) + + return user_info + + +def resolve(screen_name): + """Given str, return a participant_id. + """ + FETCH = """\ + + SELECT participant_id + FROM social_network_users + WHERE network='twitter' + AND user_info -> 'screen_namec' = %s + + """ # XXX Uniqueness constraint on screen_name? + rec = db.fetchone(FETCH, (screen_name,)) + if rec is None: + raise Exception("Twitter user %s has no participant." % (screen_name)) + return rec['participant_id'] diff --git a/templates/participant.html b/templates/participant.html index 040bf4f0e5..d9b82bf27e 100644 --- a/templates/participant.html +++ b/templates/participant.html @@ -14,7 +14,7 @@

Your weekly {{ tip_or_pledge }} to {{ username }} is:

{% for amount in AMOUNTS %} - {% end %} {% if my_tip not in AMOUNTS %} diff --git a/www/%participant_id/tip.json b/www/%participant_id/tip.json index 0d0c6857e4..0ff683482d 100644 --- a/www/%participant_id/tip.json +++ b/www/%participant_id/tip.json @@ -4,7 +4,6 @@ import decimal from aspen import Response from gittip import AMOUNTS, db -from gittip.networks import github # ========================================================================== ^L @@ -14,14 +13,11 @@ if not user.ANON: # Get tipper and tippee. # ====================== + # XXX We could/should enforce that tips cannot be pledged at all to locked + # accounts. tipper = user.id - if 'participant_id' in path: # gittip - tippee = path['participant_id'] - elif 'login' in path: # github - tippee = github.resolve(path['login']) - else: - raise Response(400, "can't find tippee") + tippee = path['participant_id'] # Get and maybe set amount. diff --git a/www/on/github/%login/index.html b/www/on/github/%login/index.html index 8273300704..995a12c31f 100644 --- a/www/on/github/%login/index.html +++ b/www/on/github/%login/index.html @@ -51,7 +51,7 @@ can_tip = False tip_or_pledge = "pledge" -nbackers = get_number_of_backers(username) +nbackers = get_number_of_backers(participant_id) # ========================================================================== ^L {% extends templates/participant.html %} diff --git a/www/on/twitter/%screen_name/index.html b/www/on/twitter/%screen_name/index.html new file mode 100644 index 0000000000..e814db4ccc --- /dev/null +++ b/www/on/twitter/%screen_name/index.html @@ -0,0 +1,118 @@ +"""Twitter user page on Gittip. +""" +import decimal + +import requests +from aspen import json, Response +from gittip import AMOUNTS, CARDINALS, db, get_tip, get_number_of_backers +from gittip.networks import twitter + + +# ========================================================================== ^L + +# Try to load from Twitter. +# ========================= + +url = "https://api.twitter.com/1/users/show.json?screen_name=%s" +userinfo = requests.get(url % path['screen_name']) +if userinfo.status_code != 200: + raise Response(404) +userinfo = json.loads(userinfo.text) + + +# Try to load from Gittip. +# ======================== + +participant = False +username = userinfo['screen_name'] +name = userinfo.get('name') +if not name: + name = username +userinfo['html_url'] = "https://twitter.com/%s" % username + +participant_id, is_claimed, is_locked, balance = twitter.upsert(userinfo) +can_tip = not is_locked +lock_action = "unlock" if is_locked else "lock" +if is_claimed: + request.redirect('/%s/' % participant_id) + +if not user.ANON: + my_tip = get_tip(user.id, participant_id) + +tip_or_pledge = "pledge" +nbackers = get_number_of_backers(participant_id) + +# ========================================================================== ^L +{% extends templates/participant.html %} + +{% block their_voice %} + {% if is_locked %} + +

{{ username }} has opted out of Gittip.

+ +

If you are {{ username }} + on Twitter, you can unlock your account to allow people to + pledge tips to you on Gittip. We never collect any money on your behalf + until you explicitly opt in.

+ + + + {% else %} + + +

{{ name }} has not joined Gittip.

+ + {% if user.ANON %} + + {% else %} + + {% end %} + + + {% if nbackers == 0 %} + {% elif nbackers == 1 %} +

There is one person ready to give.

+ {% elif nbackers < 10 %} +

There are {{ CARDINALS[nbackers] }} people ready to give.

+ {% else %} +

There are {{ nbackers }} people ready to give.

+ {% end %} + + + {% if not user.ANON %} + +

{{ 'But we' if nbackers > 0 else 'We' }} will never collect money on + behalf of {{ username }} until they ask us to.

+ + {% else %} + +

{{ 'But we' if nbackers > 0 else 'We' }} will never collect money on + your behalf until you ask us to.

+ +

Don’t like what you see?

+ +

If you are {{ username }} you can explicitly opt out of Gittip by + locking this account. We don’t allow new pledges to locked + accounts.

+ + + + {% end %} + {% end %} +{% end %}