Skip to content
This repository has been archived by the owner on Feb 8, 2018. It is now read-only.

Commit

Permalink
Implement opt-out (#61)
Browse files Browse the repository at this point in the history
Here is a first draft of opt-out functionality. There is now a button on
GitHub proxy pages to "Lock" one's GitHub account on Gittip, so that
Gittip users are prevented from pledging tips to you. There is a
corresponding "Unlock" button on pages of locked GitHub accounts. Care
is taken to ensure that only the owner of the GitHub account is able to
lock and unlock the account on Gittip.
  • Loading branch information
chadwhitacre committed Jun 28, 2012
1 parent 274d23b commit 38a616d
Show file tree
Hide file tree
Showing 18 changed files with 227 additions and 86 deletions.
3 changes: 3 additions & 0 deletions TODO
Original file line number Diff line number Diff line change
@@ -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
Expand Down
13 changes: 3 additions & 10 deletions configure-aspen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
97 changes: 89 additions & 8 deletions gittip/networks.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
"""

Expand All @@ -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
Expand All @@ -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)
)
Expand All @@ -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:
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion sql/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
Expand Down
4 changes: 2 additions & 2 deletions templates/participant.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
</div>

{% if can_tip and user.ANON %}
<h2>Sign in
<a href="{{ github_oauth_url(username) }}">with
<h2>Sign in using
<a href="{{ github.oauth_url(website, u'opt-in', username) }}">
GitHub</a> to tip {{ username }}.</h2>
{% elif can_tip %}
{% if user.id != username %}
Expand Down
Binary file removed vendor/aspen-0.18.21.tar.bz2
Binary file not shown.
Binary file added vendor/aspen-0.18.25.tar.bz2
Binary file not shown.
9 changes: 5 additions & 4 deletions www/%participant_id/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from aspen import json, Response
from gittip import AMOUNTS, db, get_tip, get_tips_and_total, get_tipjar
from gittip.utils import wrap
from gittip.networks import github


tip_suggestions = ('jeresig', 'antirez', 'wycats', 'fabpot', 'mitsuhiko',
Expand Down Expand Up @@ -56,7 +57,7 @@
my_tip = get_tip(user.id, participant['id'])

name = participant['user_info'].get('name', username)
github = participant['user_info']
github_user_info = participant['user_info']
can_tip = True
tipjar = get_tipjar(user.id, pronoun="your", claimed=True)
tip_or_pledge = "tip"
Expand Down Expand Up @@ -217,10 +218,10 @@ <h2 class="first"><b>{{ participant['id'] }}</b> {{ get_tipjar(participant['id']
<h3>Linked Accounts</h3>
<ul id="accounts">
<li>
<img src="{{ github.get('avatar_url', '/assets/%s/no-avatar.png' % __version__) }}" />
<img src="{{ github_user_info.get('avatar_url', '/assets/%s/no-avatar.png' % __version__) }}" />
{{ username }} {% if name %}({{ name }})<br />{% end %}
<a href="{{ github.get('html_url', '') }}">
{{ github.get('html_url', '') }}
<a href="{{ github_user_info.get('html_url', '') }}">
{{ github_user_info.get('html_url', '') }}
</a>
</li>
</ul>
Expand Down
4 changes: 3 additions & 1 deletion www/about/leaderboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
, tippee
FROM tips
JOIN participants p ON p.id = tipper
JOIN social_network_users snu ON snu.participant_id = tippee
WHERE last_bill_result = ''
AND snu.is_locked = false
ORDER BY tipper, tippee, mtime DESC
) AS foo
JOIN participants p ON p.id = tippee
Expand Down Expand Up @@ -67,6 +69,6 @@ <h2>Gittip happens every Friday.</h2>
<p>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
<b>opt-in</b>. We never collect money on a person&rsquo;s behalf until that
person opts-in by claiming their account.</p>
person opts in by claiming their account.</p>

{% end %}
6 changes: 4 additions & 2 deletions www/about/stats.html
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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("""\
Expand Down
2 changes: 2 additions & 0 deletions www/about/unclaimed.html
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions www/assets/%version/gittip.js
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ Gittip.initTipButtons = function()
});
};


Gittip.initJumpToPerson = function()
{
function jump(e)
Expand Down
3 changes: 2 additions & 1 deletion www/credit-card.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import stripe
from aspen import json, log, Response
from gittip import billing
from gittip.networks import github

# ========================================================================== ^L

Expand Down Expand Up @@ -34,7 +35,7 @@

<h2 class="first">Credit Card</h2>

<p>Sign in <a href="{{ github_oauth_url('/credit-card.html') }}">with
<p>Sign in <a href="{{ github.oauth_url(website, u'opt-in', u'/credit-card.html') }}">with
GitHub</a>, and then you&rsquo;ll be able to add or change your credit card.
Thanks! :-)</p>

Expand Down
36 changes: 31 additions & 5 deletions www/github/%login/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"""
import decimal


import requests
from aspen import json, Response
from gittip import AMOUNTS, db, get_tip, get_tipjar
Expand Down Expand Up @@ -33,8 +32,9 @@

usertype = userinfo.get("type", "unknown type of account").lower()
if usertype == "user":
can_tip = True
participant_id, is_claimed, balance = github.upsert(userinfo)
participant_id, is_claimed, is_locked, balance = github.upsert(userinfo)
can_tip = not is_locked
lock_action = "unlock" if is_locked else "lock"
if is_claimed:
request.redirect('/%s/' % participant_id)

Expand All @@ -57,6 +57,18 @@

{% block their_voice %}
{% if usertype == "user" %}
{% if is_locked %}

<h2 class="first"><b>{{ username }}</b> has opted out of Gittip.</h2>

<p>If you are <a href="">{{ username }}</a> 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.</p>

<a href="{{ github.oauth_url(website, u'unlock', username) }}"
><button>Unlock</button></a>

{% else %}
<script>
$(document).ready(Gittip.initTipButtons);
</script>
Expand All @@ -68,7 +80,7 @@ <h2 class="first"><b>{{ name }}</b> has not joined Gittip.</h2>
<li>
<img src="{{ userinfo.get('avatar_url', '/assets/%s/no-avatar.png' % __version__) }}" />
Are you <a href="{{ userinfo['html_url'] }}">{{ userinfo['login'] }}</a> from GitHub?<br />
<a href="{{ github_oauth_url(username) }}">Click here</a> to opt in to Gittip.
<a href="{{ github.oauth_url(website, u'opt-in', username) }}">Click here</a> to opt in to Gittip.
</li>
</ul>
{% else %}
Expand All @@ -85,11 +97,25 @@ <h2 class="first"><b>{{ name }}</b> has not joined Gittip.</h2>
<h3>This person {{ get_tipjar(username, claimed=False) }}</h3>

<p>We never collect money on a person&rsquo;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.</p>

{% if user.ANON %}

<h3>Don&rsquo;t like what you see?</h3>

<p>If you are {{ username }} you can explicitly opt out of Gittip by
locking this account. We don&rsquo;t allow new pledges to locked
accounts, and we&rsquo;ll set aside your username on Gittip to prevent
squatting.</p>

<a href="{{ github.oauth_url(website, u'lock', username) }}"
><button>Lock</button></a>

{% end %}
{% end %}
{% elif usertype == "organization" %}

<h2 class="first"><b>{{ name }}</b> is an {{ usertype }} on GitHub.</h2>
Expand Down
Loading

0 comments on commit 38a616d

Please sign in to comment.