diff --git a/TODO b/TODO index e17485c40b..0018f70831 100644 --- a/TODO +++ b/TODO @@ -1,3 +1,6 @@ +what about someone who wants to unregister? +don't show locked accounts on unclaimed page +don't show locked accounts on "who I tip" This is here for historical interest. We're now using Github issues to diff --git a/configure-aspen.py b/configure-aspen.py index af52bf53f8..e27e2faa66 100644 --- a/configure-aspen.py +++ b/configure-aspen.py @@ -9,24 +9,17 @@ gittip.wireup.db() gittip.wireup.billing() -website.github_client_id = os.environ['GITHUB_CLIENT_ID'] -website.github_client_secret = os.environ['GITHUB_CLIENT_SECRET'] -website.github_callback = os.environ['GITHUB_CALLBACK'] +website.github_client_id = os.environ['GITHUB_CLIENT_ID'].decode('ASCII') +website.github_client_secret = os.environ['GITHUB_CLIENT_SECRET'].decode('ASCII') +website.github_callback = os.environ['GITHUB_CALLBACK'].decode('ASCII') website.hooks.inbound_early.register(gittip.canonize) website.hooks.inbound_early.register(gittip.authentication.inbound) website.hooks.outbound_late.register(gittip.authentication.outbound) -def github_oauth_url(then=""): - url = "https://github.com/login/oauth/authorize?client_id=%s" - url %= website.github_client_id - if url: - url += "&redirect_uri=%s?then=%s" % (website.github_callback, then) - return url def add_stuff(request): request.context['__version__'] = gittip.__version__ request.context['username'] = None - request.context['github_oauth_url'] = github_oauth_url website.hooks.inbound_early.register(add_stuff) diff --git a/gittip/networks.py b/gittip/networks.py index 3213942f08..80e26dee47 100644 --- a/gittip/networks.py +++ b/gittip/networks.py @@ -1,7 +1,9 @@ import random -from aspen import log +import requests +from aspen import json, log, Response from aspen.utils import typecheck +from aspen.website import Website from gittip import db from psycopg2 import IntegrityError @@ -45,7 +47,75 @@ def upsert(user_info, claim=False): , user_info , claim=claim ) - + + + @staticmethod + 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 github 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://github.com/login/oauth/authorize?client_id=%s&redirect_uri=%s" + url %= (website.github_client_id, website.github_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 + + + @staticmethod + 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.github.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.github_client_id + , 'client_secret': website.github_client_secret + } + r = requests.post("https://github.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.github.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 + + @staticmethod def resolve(login): """Given two str, return a participant_id. @@ -78,7 +148,8 @@ def upsert(network, user_id, username, user_info, claim=False): primary key in the underlying table in our own db. If claim is True, the return value is the participant_id. Otherwise it is a - tuple: (participant_id [unicode], is_claimed [boolean], balance [Decimal]). + tuple: (participant_id [unicode], is_claimed [boolean], is_locked + [boolean], balance [Decimal]). """ typecheck( network, str @@ -153,7 +224,7 @@ def upsert(network, user_id, username, user_info, claim=False): AND ( (participant_id IS NULL) OR (participant_id=%s) ) - RETURNING participant_id + RETURNING participant_id, is_locked """ @@ -162,9 +233,13 @@ def upsert(network, user_id, username, user_info, claim=False): rows = db.fetchall( ASSOCIATE , (participant_id, network, user_id, participant_id) ) - nrows = len(list(rows)) + rows = list(rows) + nrows = len(rows) assert nrows in (0, 1) - if nrows == 0: + + if nrows == 1: + is_locked = rows[0]['is_locked'] + else: # Against all odds, the account was otherwise associated with another # participant while we weren't looking. Maybe someone paid them money @@ -176,7 +251,8 @@ def upsert(network, user_id, username, user_info, claim=False): , (participant_id,) ) - rec = db.fetchone( "SELECT participant_id FROM social_network_users " + rec = db.fetchone( "SELECT participant_id, is_locked " + "FROM social_network_users " "WHERE network=%s AND user_id=%s" , (network, user_id) ) @@ -185,6 +261,7 @@ def upsert(network, user_id, username, user_info, claim=False): # Use the participant associated with this account. participant_id = rec['participant_id'] + is_locked = rec['is_locked'] assert participant_id is not None else: @@ -217,6 +294,10 @@ def upsert(network, user_id, username, user_info, claim=False): , (participant_id,) ) assert rec is not None - out = (participant_id, rec['claimed_time'] is not None, rec['balance']) + out = ( participant_id + , rec['claimed_time'] is not None + , is_locked + , rec['balance'] + ) return out diff --git a/requirements.txt b/requirements.txt index 02edbba0a7..a935c6386d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -./vendor/aspen-0.18.21.tar.bz2 +./vendor/aspen-0.18.25.tar.bz2 ./vendor/psycopg2-2.4.5.tar.gz ./vendor/simplejson-2.3.2.tar.gz ./vendor/certifi-0.0.8.tar.gz diff --git a/sql/schema.sql b/sql/schema.sql index 99ad95fae4..572f1da5de 100644 --- a/sql/schema.sql +++ b/sql/schema.sql @@ -31,7 +31,7 @@ CREATE TABLE social_network_users , network text NOT NULL , user_id text NOT NULL , user_info hstore -, opted_out boolean DEFAULT FALSE +, is_locked boolean NOT NULL DEFAULT FALSE , participant_id text DEFAULT NULL REFERENCES participants ON DELETE RESTRICT , UNIQUE(network, user_id) ); diff --git a/templates/participant.html b/templates/participant.html index c92761742f..33bc163678 100644 --- a/templates/participant.html +++ b/templates/participant.html @@ -5,8 +5,8 @@ {% if can_tip and user.ANON %} -
The amounts above are what the Gittip community is willing to give as a weekly gift to each person, but only if the person accepts it. Gittip is opt-in. We never collect money on a person’s behalf until that -person opts-in by claiming their account.
+person opts in by claiming their account. {% end %} diff --git a/www/about/stats.html b/www/about/stats.html index 725baf5093..9936a273cb 100644 --- a/www/about/stats.html +++ b/www/about/stats.html @@ -18,8 +18,10 @@ ncc = db.fetchone("SELECT count(*) AS n FROM participants WHERE last_bill_result = ''")['n'] ncc = 0 if ncc is None else ncc pcc = "%5.1f" % ((ncc * 100.0) / npeople) -statements = list(db.fetchall("SELECT id, statement FROM participants WHERE statement != '' ORDER BY id")) +statements = db.fetchall("SELECT id, statement FROM participants WHERE statement != '' ORDER BY id") +statements = [] if statements is None else list(statements) amount = db.fetchone("SELECT transfer_volume AS v FROM paydays ORDER BY ts_end DESC LIMIT 1")['v'] +amount = 0 if amount is None else amount tip_amounts = db.fetchone(""" SELECT avg(amount), sum(amount) FROM ( SELECT DISTINCT ON (tipper, tippee) amount @@ -35,7 +37,7 @@ average_tip = 0 total_backed_tips = 0 else: - average_tip = tip_amounts['avg'] + average_tip = tip_amounts['avg'] if tip_amounts['avg'] is not None else 0 total_backed_tips = tip_amounts['sum'] average_tippees = db.fetchone("""\ diff --git a/www/about/unclaimed.html b/www/about/unclaimed.html index d7a8336e89..186e78e737 100644 --- a/www/about/unclaimed.html +++ b/www/about/unclaimed.html @@ -11,8 +11,10 @@ FROM tips JOIN participants p ON p.id = tipper JOIN participants p2 ON p2.id = tippee + JOIN social_network_users snu ON snu.participant_id = tippee WHERE p.last_bill_result = '' AND p2.claimed_time IS NULL + AND snu.is_locked = false ORDER BY tipper, tippee, mtime DESC ) AS foo GROUP BY tippee diff --git a/www/assets/%version/gittip.js b/www/assets/%version/gittip.js index 9aff062e7a..357b73a424 100644 --- a/www/assets/%version/gittip.js +++ b/www/assets/%version/gittip.js @@ -343,6 +343,7 @@ Gittip.initTipButtons = function() }); }; + Gittip.initJumpToPerson = function() { function jump(e) diff --git a/www/credit-card.html b/www/credit-card.html index 9a4d020fde..98c590df46 100644 --- a/www/credit-card.html +++ b/www/credit-card.html @@ -3,6 +3,7 @@ import stripe from aspen import json, log, Response from gittip import billing +from gittip.networks import github # ========================================================================== ^L @@ -34,7 +35,7 @@Sign in with
+ Sign in with
GitHub, and then you’ll be able to add or change your credit card.
Thanks! :-) If you are {{ username }} on GitHub, 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.{{ username }} has opted out of Gittip.
+
+
We never collect money on a person’s behalf until that person - opts-in by claiming their account, which {{ username }} has not done. + opts in by claiming their account, which {{ username }} has not done. Any amount above is what the Gittip community is willing to give {{ username }} as a weekly gift, but only if {{ username }} accepts it by opting in.
+ {% if user.ANON %} + +If you are {{ username }} you can explicitly opt out of Gittip by + locking this account. We don’t allow new pledges to locked + accounts, and we’ll set aside your username on Gittip to prevent + squatting.
+ + + + {% end %} + {% end %} {% elif usertype == "organization" %}Your attempt to lock or unlock this account failed because you’re +logged into GitHub as someone else. Please log out of GitHub and try again.
+