def _get_renderer_factory(self, media_type, renderer): """Given two bytestrings, return a renderer factory or None. """ typecheck(media_type, str, renderer, str) if renderer_re.match(renderer) is None: possible = ', '.join(sorted( self.website.renderer_factories.keys())) msg = ("Malformed renderer %s. It must match %s. Possible " "renderers (might need third-party libs): %s.") raise SyntaxError(msg % (renderer, renderer_re.pattern, possible)) renderer = renderer[2:] # strip off the hashbang renderer = renderer.decode('US-ASCII') factories = self.website.renderer_factories make_renderer = factories.get(renderer, None) if isinstance(make_renderer, ImportError): raise make_renderer elif make_renderer is None: possible = [] want_legend = False for k, v in sorted(factories.iteritems()): if isinstance(v, ImportError): k = '*' + k want_legend = True possible.append(k) possible = ', '.join(possible) if want_legend: legend = " (starred are missing third-party libraries)" else: legend = '' raise ValueError("Unknown renderer for %s: %s. Possible " "renderers%s: %s." % (media_type, renderer, legend, possible)) return make_renderer
def authorize(participant_id, pmt): """Given two unicodes, return a dict. This function attempts to authorize the credit card details referenced by pmt. If the attempt succeeds we cancel the transaction. If it fails we log the failure. Even for failure we keep the payment_method_token, we don't reset it to None/NULL. It's useful for loading the previous (bad) credit card info from Samurai in order to prepopulate the form. """ typecheck(pmt, unicode, participant_id, unicode) transaction = Processor.authorize(pmt, '1.00', custom=participant_id) if transaction.errors: last_bill_result = json.dumps(transaction.errors) out = dict(transaction.errors) else: transaction.reverse() last_bill_result = '' out = {} STANDING = """\ UPDATE participants SET payment_method_token=%s , last_bill_result=%s WHERE id=%s """ db.execute(STANDING, (pmt, last_bill_result, participant_id)) return out
def genparticipants(for_payday): """Closure generator to yield participants with tips and total. We re-fetch participants each time, because the second time through we want to use the total obligations they have for next week, and if we pass a non-False for_payday to get_tips_and_total then we only get unfulfilled tips from prior to that timestamp, which is none of them by definition. If someone changes tips after payout starts, and we crash during payout, then their new tips_and_total will be used on the re-run. That's okay. Note that we take ts_start from the outer scope when we pass it to get_participants, but we pass in for_payday, because we might want it to be False (per the definition of git_tips_and_total). """ for participant in self.get_participants(ts_start): tips, total = get_tips_and_total( participant['id'] , for_payday=for_payday , db=self.db ) typecheck(total, Decimal) yield(participant, tips, total)
def upcharge(amount): """Given an amount, return a higher amount and the difference. """ typecheck(amount, Decimal) charge_amount = (amount + FEE_CHARGE[0]) / (1 - FEE_CHARGE[1]) charge_amount = charge_amount.quantize(FEE_CHARGE[0], rounding=ROUND_UP) return charge_amount, charge_amount - amount
def transfer(tipper, tippee, amount): """Given two unicodes and a Decimal, return a boolean indicating success. If the tipper doesn't have enough in their Gittip account then we return False. Otherwise we decrement tipper's balance and increment tippee's *pending* balance by amount. """ typecheck(tipper, unicode, tippee, unicode, amount, decimal.Decimal) with db.get_connection() as conn: cursor = conn.cursor() try: debit_participant(cursor, tipper, amount) except ValueError: return False credit_participant(cursor, tippee, amount) record_transfer(cursor, tipper, tippee, amount) increment_payday(cursor, amount) # Success. # ======== conn.commit() return True
def capture_card_hold(db, participant, amount, hold): """Capture the previously created hold on the participant's credit card. """ typecheck( hold, balanced.CardHold , amount, Decimal ) username = participant.username assert participant.id == int(hold.meta['participant_id']) route = ExchangeRoute.from_address(participant, 'balanced-cc', hold.card_href) assert isinstance(route, ExchangeRoute) cents, amount_str, charge_amount, fee = _prep_hit(amount) amount = charge_amount - fee # account for possible rounding e_id = record_exchange(db, route, amount, fee, participant, 'pre') meta = dict(participant_id=participant.id, exchange_id=e_id) try: hold.capture(amount=cents, description=username, meta=meta) record_exchange_result(db, e_id, 'succeeded', None, participant) except Exception as e: error = repr_exception(e) record_exchange_result(db, e_id, 'failed', error, participant) raise hold.meta['state'] = 'captured' hold.save() log("Captured " + amount_str + " on Balanced for " + username)
def capture_card_hold(db, participant, amount, hold): """Capture the previously created hold on the participant's credit card. """ typecheck( hold, braintree.Transaction , amount, Decimal ) username = participant.username assert participant.id == int(hold.custom_fields['participant_id']) route = ExchangeRoute.from_address(participant, 'braintree-cc', hold.credit_card['token']) assert isinstance(route, ExchangeRoute) cents, amount_str, charge_amount, fee = _prep_hit(amount) amount = charge_amount - fee # account for possible rounding e_id = record_exchange(db, route, amount, fee, participant, 'pre') # TODO: Find a way to link transactions and corresponding exchanges # meta = dict(participant_id=participant.id, exchange_id=e_id) error = '' try: result = braintree.Transaction.submit_for_settlement(hold.id, str(cents/100.00)) assert result.is_success if result.transaction.status != 'submitted_for_settlement': error = result.transaction.status except Exception as e: error = repr_exception(e) if error == '': record_exchange_result(db, e_id, 'succeeded', None, participant) log("Captured " + amount_str + " on Braintree for " + username) else: record_exchange_result(db, e_id, 'failed', error, participant) raise Exception(error)
def get_user_info(screen_name): """Given a unicode, return a dict. """ typecheck(screen_name, unicode) try: rec = gittip.db.one( "SELECT user_info FROM elsewhere " "WHERE platform='twitter' " "AND user_info->'screen_name' = %s" , (screen_name,) ) except TooFew: rec = None if rec is not None: user_info = rec['user_info'] else: # Updated using Twython as a point of reference: # https://github.com/ryanmcgrath/twython/blob/master/twython/twython.py#L76 oauth = OAuth1( # we do not have access to the website obj, # so let's grab the details from the env environ['TWITTER_CONSUMER_KEY'], environ['TWITTER_CONSUMER_SECRET'], environ['TWITTER_ACCESS_TOKEN'], environ['TWITTER_ACCESS_TOKEN_SECRET'], ) url = "https://api.twitter.com/1.1/users/show.json?screen_name=%s" user_info = requests.get(url % screen_name, auth=oauth) # Keep an eye on our Twitter usage. # ================================= rate_limit = user_info.headers['X-Rate-Limit-Limit'] rate_limit_remaining = user_info.headers['X-Rate-Limit-Remaining'] rate_limit_reset = user_info.headers['X-Rate-Limit-Reset'] try: rate_limit = int(rate_limit) rate_limit_remaining = int(rate_limit_remaining) rate_limit_reset = int(rate_limit_reset) except (TypeError, ValueError): log( "Got weird rate headers from Twitter: %s %s %s" % (rate_limit, rate_limit_remaining, rate_limit_reset) ) else: reset = datetime.datetime.fromtimestamp(rate_limit_reset, tz=utc) reset = to_age(reset) log( "Twitter API calls used: %d / %d. Resets %s." % (rate_limit - rate_limit_remaining, rate_limit, reset) ) if user_info.status_code == 200: user_info = json.loads(user_info.text) else: log("Twitter lookup failed with %d." % user_info.status_code) raise Response(404) return user_info
def charge(self, participant_id, balanced_account_uri, stripe_customer_id, amount): """Given three unicodes and a Decimal, return a boolean. This is the only place where we actually charge credit cards. Amount should be the nominal amount. We'll compute Gittip's fee below this function and add it to amount to end up with charge_amount. """ typecheck(participant_id, unicode, balanced_account_uri, (unicode, None), amount, Decimal) if balanced_account_uri is None and stripe_customer_id is None: self.mark_missing_funding() return False if balanced_account_uri is not None: things = self.charge_on_balanced(participant_id, balanced_account_uri, amount) charge_amount, fee, error = things else: assert stripe_customer_id is not None things = self.charge_on_stripe(participant_id, stripe_customer_id, amount) charge_amount, fee, error = things amount = charge_amount - fee # account for possible rounding under # charge_on_* self.record_charge(amount, charge_amount, fee, error, participant_id) return not bool(error) # True indicates success
def get_balanced_account(username, balanced_account_uri): """Find or create a balanced.Account. """ typecheck(username, unicode, balanced_account_uri, (unicode, None)) # XXX Balanced requires an email address # https://github.com/balanced/balanced-api/issues/20 # quote to work around https://github.com/gittip/www.gittip.com/issues/781 email_address = '{}@gittip.com'.format(quote(username)) if balanced_account_uri is None: try: account = \ balanced.Account.query.filter(email_address=email_address).one() except balanced.exc.NoResultFound: account = balanced.Account(email_address=email_address).save() BALANCED_ACCOUNT = """\ UPDATE participants SET balanced_account_uri=%s WHERE username=%s """ gittip.db.run(BALANCED_ACCOUNT, (account.uri, username)) account.meta['username'] = username account.save() # HTTP call under here else: account = balanced.Account.find(balanced_account_uri) return account
def capture_card_hold(db, participant, amount, hold): """Capture the previously created hold on the participant's credit card. """ typecheck(hold, balanced.CardHold, amount, Decimal) username = participant.username assert participant.id == int(hold.meta['participant_id']) cents, amount_str, charge_amount, fee = _prep_hit(amount) amount = charge_amount - fee # account for possible rounding e_id = record_exchange(db, 'bill', amount, fee, participant, 'pre') meta = dict(participant_id=participant.id, exchange_id=e_id) try: hold.capture(amount=cents, description=username, meta=meta) record_exchange_result(db, e_id, 'succeeded', None, participant) except Exception as e: error = repr_exception(e) record_exchange_result(db, e_id, 'failed', error, participant) raise hold.meta['state'] = 'captured' hold.save() log("Captured " + amount_str + " on Balanced for " + username)
def get_user_info(login): """Get the given user's information from the DB or failing that, github. :param login: A unicode string representing a username in github. :returns: A dictionary containing github specific information for the user. """ typecheck(login, unicode) rec = gittip.db.fetchone( "SELECT user_info FROM elsewhere " "WHERE platform='github' " "AND user_info->'login' = %s" , (login,) ) if rec is not None: user_info = rec['user_info'] else: url = "https://api.github.com/users/%s" user_info = requests.get(url % login, params={ 'client_id': os.environ.get('GITHUB_CLIENT_ID'), 'client_secret': os.environ.get('GITHUB_CLIENT_SECRET') }) status = user_info.status_code content = user_info.text # Calculate how much of our ratelimit we have consumed remaining = int(user_info.headers['x-ratelimit-remaining']) limit = int(user_info.headers['x-ratelimit-limit']) # thanks to from __future__ import division this is a float percent_remaining = remaining/limit log_msg = '' log_lvl = None # We want anything 50% or over if 0.5 <= percent_remaining: log_msg = ("{0}% of GitHub's ratelimit has been consumed. {1}" " requests remaining.").format(percent_remaining * 100, remaining) if 0.5 <= percent_remaining < 0.8: log_lvl = logging.WARNING elif 0.8 <= percent_remaining < 0.95: log_lvl = logging.ERROR elif 0.95 <= percent_remaining: log_lvl = logging.CRITICAL if log_msg and log_lvl: log(log_msg, log_lvl) if status == 200: user_info = json.loads(content) elif status == 404: raise Response(404, "GitHub identity '{0}' not found.".format(login)) else: log("Github api responded with {0}: {1}".format(status, content), level=logging.WARNING) raise Response(502, "GitHub lookup failed with %d." % status) return user_info
def get_balanced_account(participant_id, balanced_account_uri): """Find or create a balanced.Account. """ typecheck(participant_id, unicode, balanced_account_uri, (unicode, None)) # XXX Balanced requires an email address # https://github.com/balanced/balanced-api/issues/20 email_address = '{}@gittip.com'.format(participant_id) if balanced_account_uri is None: try: account = \ balanced.Account.query.filter(email_address=email_address).one() except balanced.exc.NoResultFound: account = balanced.Account(email_address=email_address).save() BALANCED_ACCOUNT = """\ UPDATE participants SET balanced_account_uri=%s WHERE id=%s """ gittip.db.execute(BALANCED_ACCOUNT, (account.uri, participant_id)) account.meta['participant_id'] = participant_id account.save() # HTTP call under here else: account = balanced.Account.find(balanced_account_uri) return account
def __init__(self, db, user_id, user_info=None, existing_record=None): """Either: - Takes a user_id and user_info, and updates the database. Or: - Takes a user_id and existing_record, and constructs a "model" object out of the record """ typecheck(user_id, (int, unicode, long), user_info, (None, dict)) self.user_id = unicode(user_id) self.db = db if user_info is not None: a, b, c, d = self.upsert(user_info) self.participant = a self.is_claimed = b self.is_locked = c self.balance = d self.user_info = user_info # hack to make this into a weird pseudo-model that can share convenience methods elif existing_record is not None: self.participant = existing_record.participant self.is_claimed, self.is_locked, self.balance = self.get_misc_info(self.participant) self.user_info = existing_record.user_info self.record = existing_record
def wrap(u): """Given a unicode, return a unicode. """ typecheck(u, unicode) linkified = linkify(u) # Do this first, because it calls xthml_escape. out = linkified.replace(u'\r\n', u'<br />\r\n').replace(u'\n', u'<br />\n') return out if out else '...'
def __init__(self, d): """Takes headers as a dict or str. """ typecheck(d, (dict, str)) if isinstance(d, str): from aspen.exceptions import MalformedHeader def genheaders(): for line in d.splitlines(): if b':' not in line: # no colon separator in header raise MalformedHeader(line) k, v = line.split(b':', 1) if k != k.strip(): # disallowed leading or trailing whitspace # (per http://tools.ietf.org/html/rfc7230#section-3.2.4) raise MalformedHeader(line) yield k, v.strip() else: genheaders = d.iteritems CaseInsensitiveMapping.__init__(self, genheaders) # Cookie # ====== self.cookie = SimpleCookie() try: self.cookie.load(self.get('Cookie', b'')) except CookieError: pass # XXX really?
def get_img_src(self, size=128): """Return a value for <img src="..." />. Until we have our own profile pics, delegate. XXX Is this an attack vector? Can someone inject this value? Don't think so, but if you make it happen, let me know, eh? Thanks. :) https://www.gittip.com/security.txt """ typecheck(size, int) src = '/assets/%s/avatar-default.gif' % os.environ['__VERSION__'] github, twitter, bitbucket, bountysource = self.get_accounts_elsewhere() if github is not None: # GitHub -> Gravatar: http://en.gravatar.com/site/implement/images/ if 'gravatar_id' in github.user_info: gravatar_hash = github.user_info['gravatar_id'] src = "https://www.gravatar.com/avatar/%s.jpg?s=%s" src %= (gravatar_hash, size) elif twitter is not None: # https://dev.twitter.com/docs/api/1.1/get/users/show if 'profile_image_url_https' in twitter.user_info: src = twitter.user_info['profile_image_url_https'] # For Twitter, we don't have good control over size. The # biggest option is 73px(?!), but that's too small. Let's go # with the original: even though it may be huge, that's # preferrable to guaranteed blurriness. :-/ src = src.replace('_normal.', '.') return src
def transfer(self, tipper, tippee, amount, pachinko=False): """Given two unicodes, a Decimal, and a boolean, return a boolean. If the tipper doesn't have enough in their Gittip account then we return False. Otherwise we decrement tipper's balance and increment tippee's *pending* balance by amount. """ typecheck( tipper, unicode , tippee, unicode , amount, Decimal , pachinko, bool ) with self.db.get_cursor() as cursor: try: self.debit_participant(cursor, tipper, amount) except NegativeBalance: return False self.credit_participant(cursor, tippee, amount) context = 'take' if pachinko else 'tip' self.record_transfer(cursor, tipper, tippee, amount, context) return True
def get_balanced_account(username, balanced_account_uri): """Find or create a balanced.Account. """ typecheck( username, unicode , balanced_account_uri, (unicode, None) ) # XXX Balanced requires an email address # https://github.com/balanced/balanced-api/issues/20 # quote to work around https://github.com/gittip/www.gittip.com/issues/781 email_address = '{}@gittip.com'.format(quote(username)) if balanced_account_uri is None: try: account = \ balanced.Account.query.filter(email_address=email_address).one() except balanced.exc.NoResultFound: account = balanced.Account(email_address=email_address).save() BALANCED_ACCOUNT = """\ UPDATE participants SET balanced_account_uri=%s WHERE username=%s """ gittip.db.run(BALANCED_ACCOUNT, (account.uri, username)) account.meta['username'] = username account.save() # HTTP call under here else: account = balanced.Account.find(balanced_account_uri) return account
def get_user_info(db, username): """Get the given user's information from the DB or failing that, bitbucket. :param username: A unicode string representing a username in bitbucket. :returns: A dictionary containing bitbucket specific information for the user. """ typecheck(username, (unicode, PathPart)) rec = db.one( """ SELECT user_info FROM elsewhere WHERE platform='bitbucket' AND user_info->'username' = %s """, (username,), ) if rec is not None: user_info = rec else: url = "%s/users/%s?pagelen=100" user_info = requests.get(url % (BASE_API_URL, username)) status = user_info.status_code content = user_info.content if status == 200: user_info = json.loads(content)["user"] elif status == 404: raise Response(404, "Bitbucket identity '{0}' not found.".format(username)) else: log("Bitbucket api responded with {0}: {1}".format(status, content), level=logging.WARNING) raise Response(502, "Bitbucket lookup failed with %d." % status) return user_info
def clear(db, thing, username, balanced_customer_href): typecheck( thing, unicode , username, unicode , balanced_customer_href, (unicode, str) ) invalidate_on_balanced(thing, balanced_customer_href) store_result(db, thing, username, None)
def associate(db, thing, participant, balanced_account, balanced_thing_uri): """Given four unicodes, return a unicode. This function attempts to associate the credit card or bank account details referenced by balanced_thing_uri with a Balanced Account. If it fails we log and return a unicode describing the failure. Even for failure we keep balanced_customer_href; we don't reset it to None/NULL. It's useful for loading the previous (bad) info from Balanced in order to prepopulate the form. """ typecheck(participant, Participant, balanced_account, balanced.Customer, balanced_thing_uri, unicode, thing, unicode) invalidate_on_balanced(thing, balanced_account) try: if thing == "credit card": obj = balanced.Card.fetch(balanced_thing_uri) else: assert thing == "bank account", thing # sanity check obj = balanced.BankAccount.fetch(balanced_thing_uri) obj.associate_to_customer(balanced_account) except balanced.exc.HTTPError as err: error = err.message.message.decode('UTF-8') # XXX UTF-8? else: error = '' typecheck(error, unicode) store_result(db, thing, participant, error) return error
def hit_balanced(self, participant_id, balanced_account_uri, amount): """We have a purported balanced_account_uri. Try to use it. """ typecheck( participant_id, unicode , balanced_account_uri, unicode , amount, Decimal ) try_charge_amount = (amount + FEE[0]) * FEE[1] try_charge_amount = try_charge_amount.quantize( FEE[0] , rounding=ROUND_UP ) charge_amount = try_charge_amount also_log = '' if charge_amount < MINIMUM: charge_amount = MINIMUM # per Balanced also_log = ', rounded up to $%s' % charge_amount fee = try_charge_amount - amount cents = int(charge_amount * 100) msg = "Charging %s %d cents ($%s + $%s fee = $%s%s) ... " msg %= participant_id, cents, amount, fee, try_charge_amount, also_log try: customer = balanced.Account.find(balanced_account_uri) customer.debit(cents, description=participant_id) log(msg + "succeeded.") except balanced.exc.HTTPError as err: log(msg + "failed: %s" % err.message) return charge_amount, fee, err.message return charge_amount, fee, None
def clear(thing, participant_id, balanced_account_uri): typecheck( thing, unicode , participant_id, unicode , balanced_account_uri, unicode ) assert thing in ("credit card", "bank account"), thing # XXX Things in balanced cannot be deleted at the moment. # ======================================================= # Instead we mark all valid cards as invalid which will restrict against # anyone being able to issue charges against them in the future. # # See: https://github.com/balanced/balanced-api/issues/22 account = balanced.Account.find(balanced_account_uri) things = account.cards if thing == "credit card" else account.bank_accounts for _thing in things: if _thing.is_valid: _thing.is_valid = False _thing.save() CLEAR = """\ UPDATE participants SET last_%s_result=NULL WHERE id=%%s """ % ("bill" if thing == "credit card" else "ach") db.execute(CLEAR, (participant_id,))
def transfer(self, tipper, tippee, amount, pachinko=False): """Given two unicodes, a Decimal, and a boolean, return a boolean. If the tipper doesn't have enough in their Gittip account then we return False. Otherwise we decrement tipper's balance and increment tippee's *pending* balance by amount. """ typecheck( tipper, unicode , tippee, unicode , amount, Decimal , pachinko, bool ) with self.db.get_connection() as conn: cursor = conn.cursor() try: self.debit_participant(cursor, tipper, amount) except IntegrityError: return False self.credit_participant(cursor, tippee, amount) self.record_transfer(cursor, tipper, tippee, amount) if pachinko: self.mark_pachinko(cursor, amount) else: self.mark_transfer(cursor, amount) conn.commit() return True
def get_user_info(db, username, osm_api_url): """Get the given user's information from the DB or failing that, openstreetmap. :param username: A unicode string representing a username in OpenStreetMap. :param osm_api_url: URL of OpenStreetMap API. :returns: A dictionary containing OpenStreetMap specific information for the user. """ typecheck(username, (unicode, PathPart)) rec = db.one(""" SELECT user_info FROM elsewhere WHERE platform='openstreetmap' AND user_info->'username' = %s """, (username,)) if rec is not None: user_info = rec else: osm_user = requests.get("%s/user/%s" % (osm_api_url, username)) if osm_user.status_code == 200: log("User %s found in OpenStreetMap but not in gittip." % username) user_info = None elif osm_user.status_code == 404: raise Response(404, "OpenStreetMap identity '{0}' not found.".format(username)) else: log("OpenStreetMap api responded with {0}: {1}".format(status, content), level=logging.WARNING) raise Response(502, "OpenStreetMap lookup failed with %d." % status) return user_info
def transfer(self, tipper, tippee, amount, pachinko=False): """Given two unicodes, a Decimal, and a boolean, return a boolean. If the tipper doesn't have enough in their Gittip account then we return False. Otherwise we decrement tipper's balance and increment tippee's *pending* balance by amount. """ typecheck(tipper, unicode, tippee, unicode, amount, Decimal, pachinko, bool) with self.db.get_cursor() as cursor: try: self.debit_participant(cursor, tipper, amount) except IntegrityError: return False self.credit_participant(cursor, tippee, amount) self.record_transfer(cursor, tipper, tippee, amount) if pachinko: self.mark_pachinko(cursor, amount) else: self.mark_transfer(cursor, amount) return True
def wrap(u): """Given a unicode, return a unicode. """ typecheck(u, unicode) u = linkify(u) # Do this first, because it calls xthml_escape. u = u.replace(u'\r\n', u'<br />\r\n').replace(u'\n', u'<br />\n') return u if u else '...'
def clear(participant_id, stripe_customer_id): typecheck(participant_id, unicode, stripe_customer_id, unicode) # "Unlike other objects, deleted customers can still be retrieved through # the API, in order to be able to track the history of customers while # still removing their credit card details and preventing any further # operations to be performed" https://stripe.com/docs/api#delete_customer # # Hmm ... should we protect against that in associate (above)? # # What this means though is (I think?) that we'll continue to be able to # search for customers in the Stripe management UI by participant_id (which # is stored as description in associate) even after the association is lost # in our own database. This should be helpful for customer support. customer = stripe.Customer.retrieve(stripe_customer_id) customer.delete() CLEAR = """\ UPDATE participants SET stripe_customer_id=NULL , last_bill_result=NULL WHERE id=%s """ db.execute(CLEAR, (participant_id,))
def charge(self, participant, amount): """Given dict and Decimal, return None. This is the only place where we actually charge credit cards. Amount should be the nominal amount. We'll compute Gittip's fee below this function and add it to amount to end up with charge_amount. """ typecheck(participant, Participant, amount, Decimal) username = participant.username balanced_customer_href = participant.balanced_customer_href typecheck(username, unicode, balanced_customer_href, (unicode, None)) # Perform some last-minute checks. # ================================ if balanced_customer_href is None: self.mark_missing_funding() return # Participant has no funding source. if not is_whitelisted(participant): return # Participant not trusted. # Go to Balanced. # =============== things = self.charge_on_balanced(username, balanced_customer_href, amount) charge_amount, fee, error = things amount = charge_amount - fee # account for possible rounding under # charge_on_* self.record_charge(amount, charge_amount, fee, error, username)
def get_img_src(self, size=128): """Return a value for <img src="..." />. Until we have our own profile pics, delegate. XXX Is this an attack vector? Can someone inject this value? Don't think so, but if you make it happen, let me know, eh? Thanks. :) https://www.gittip.com/security.txt """ typecheck(size, int) src = '/assets/%s/avatar-default.gif' % os.environ['__VERSION__'] github, twitter = self.get_accounts_elsewhere() if github is not None: # GitHub -> Gravatar: http://en.gravatar.com/site/implement/images/ if 'gravatar_id' in github.user_info: gravatar_hash = github.user_info['gravatar_id'] src = "https://www.gravatar.com/avatar/%s.jpg?s=%s" src %= (gravatar_hash, size) elif twitter is not None: # https://dev.twitter.com/docs/api/1/get/users/profile_image/%3Ascreen_name if 'profile_image_url_https' in twitter.user_info: src = twitter.user_info['profile_image_url_https'] # For Twitter, we don't have good control over size. We don't # want the original, cause that can be huge. The next option is # 73px(?!). src = src.replace('_normal.', '_bigger.') return src
def get_img_src(self, size=128): """Return a value for <img src="..." />. Until we have our own profile pics, delegate. XXX Is this an attack vector? Can someone inject this value? Don't think so, but if you make it happen, let me know, eh? Thanks. :) https://www.gittip.com/security.txt """ typecheck(size, int) src = '/assets/%s/avatar-default.gif' % os.environ['__VERSION__'] github, twitter, bitbucket = self.get_accounts_elsewhere() if github is not None: # GitHub -> Gravatar: http://en.gravatar.com/site/implement/images/ if 'gravatar_id' in github.user_info: gravatar_hash = github.user_info['gravatar_id'] src = "https://www.gravatar.com/avatar/%s.jpg?s=%s" src %= (gravatar_hash, size) elif twitter is not None: # https://dev.twitter.com/docs/api/1/get/users/profile_image/%3Ascreen_name if 'profile_image_url_https' in twitter.user_info: src = twitter.user_info['profile_image_url_https'] # For Twitter, we don't have good control over size. We don't # want the original, cause that can be huge. The next option is # 73px(?!). src = src.replace('_normal.', '_bigger.') return src
def __init__(self, headers, fp, server_software): """Takes a str, a file-like object, and another str. If the Mapping API is used (in/one/all/has), then the iterable will be read and parsed as media of type application/x-www-form-urlencoded or multipart/form-data, according to content_type. """ typecheck(headers, Headers, server_software, str) raw_len = int(headers.get('Content-length', '') or '0') self.raw = self._read_raw(server_software, fp, raw_len) # XXX lazy! parsed = self._parse(headers, self.raw) if parsed is None: # There was no content-type. Use self.raw. pass else: for k in parsed.keys(): v = parsed[k] if isinstance(v, cgi.MiniFieldStorage): v = v.value.decode("UTF-8") # XXX Really? Always UTF-8? else: assert isinstance(v, cgi.FieldStorage), v if v.filename is None: v = v.value.decode("UTF-8") self[k] = v
def get_user_info(login): """Get the given user's information from the DB or failing that, github. :param login: A unicode string representing a username in github. :returns: A dictionary containing github specific information for the user. """ typecheck(login, unicode) rec = gittip.db.fetchone( "SELECT user_info FROM elsewhere " "WHERE platform='github' " "AND user_info->'login' = %s" , (login,) ) if rec is not None: user_info = rec['user_info'] else: url = "https://api.github.com/users/%s" user_info = requests.get(url % login) status = user_info.status_code content = user_info.text if status == 200: user_info = json.loads(content) elif status == 404: raise Response(404, "GitHub identity '{0}' not found.".format(login)) else: log("Github api responded with {0}: {1}".format(status, content), level=logging.WARNING) raise Response(502, "GitHub lookup failed with %d." % status) return user_info
def _get_renderer_factory(self, media_type, renderer): """Given two bytestrings, return a renderer factory or None. """ typecheck(media_type, str, renderer, str) if renderer_re.match(renderer) is None: possible =', '.join(sorted(self.website.renderer_factories.keys())) msg = ("Malformed renderer %s. It must match %s. Possible " "renderers (might need third-party libs): %s.") raise SyntaxError(msg % (renderer, renderer_re.pattern, possible)) renderer = renderer.decode('US-ASCII') factories = self.website.renderer_factories make_renderer = factories.get(renderer, None) if isinstance(make_renderer, ImportError): raise make_renderer elif make_renderer is None: possible = [] want_legend = False for k, v in sorted(factories.iteritems()): if isinstance(v, ImportError): k = '*' + k want_legend = True possible.append(k) possible = ', '.join(possible) if want_legend: legend = " (starred are missing third-party libraries)" else: legend = '' raise ValueError("Unknown renderer for %s: %s. Possible " "renderers%s: %s." % (media_type, renderer, legend, possible)) return make_renderer
def clear(thing, participant_id, balanced_account_uri): typecheck( thing, unicode , participant_id, unicode , balanced_account_uri, unicode ) assert thing in ("credit card", "bank account"), thing # XXX Things in balanced cannot be deleted at the moment. # ======================================================= # Instead we mark all valid cards as invalid which will restrict against # anyone being able to issue charges against them in the future. # # See: https://github.com/balanced/balanced-api/issues/22 account = balanced.Account.find(balanced_account_uri) things = account.cards if thing == "credit card" else account.bank_accounts for _thing in things: if _thing.is_valid: _thing.is_valid = False _thing.save() CLEAR = """\ UPDATE participants SET last_%s_result=NULL WHERE id=%%s """ % ("bill" if thing == "credit card" else "ach") gittip.db.execute(CLEAR, (participant_id,))
def charge_on_balanced(self, username, balanced_customer_href, amount): """We have a purported balanced_customer_href. Try to use it. """ typecheck( username, unicode , balanced_customer_href, unicode , amount, Decimal ) cents, msg, charge_amount, fee = self._prep_hit(amount) msg = msg % (username, "Balanced") try: customer = balanced.Customer.fetch(balanced_customer_href) customer.cards.one().debit(amount=cents, description=username) log(msg + "succeeded.") error = "" except balanced.exc.HTTPError as err: error = err.message.message except: error = repr(sys.exc_info()[1]) if error: log(msg + "failed: %s" % error) return charge_amount, fee, error
def get_user_info(username): """Get the given user's information from the DB or failing that, bitbucket. :param username: A unicode string representing a username in bitbucket. :returns: A dictionary containing bitbucket specific information for the user. """ typecheck(username, unicode) rec = gittip.db.fetchone( "SELECT user_info FROM elsewhere " "WHERE platform='bitbucket' " "AND user_info->'username' = %s", (username, )) if rec is not None: user_info = rec['user_info'] else: url = "%s/users/%s?pagelen=100" user_info = requests.get(url % (BASE_API_URL, username)) status = user_info.status_code content = user_info.content if status == 200: user_info = json.loads(content)['user'] elif status == 404: raise Response( 404, "Bitbucket identity '{0}' not found.".format(username)) else: log("Bitbucket api responded with {0}: {1}".format( status, content), level=logging.WARNING) raise Response(502, "Bitbucket lookup failed with %d." % status) return user_info
def wrap(u): """Given a unicode, return a unicode. """ typecheck(u, unicode) u = linkify(u) # Do this first, because it calls xthml_escape. u = u.replace(u"\r\n", u"<br />\r\n").replace(u"\n", u"<br />\n") return u if u else "..."
def get_balanced_account(participant_id, balanced_account_uri): """Find or create a balanced.Account. """ typecheck( participant_id, unicode , balanced_account_uri, (unicode, None) ) # XXX Balanced requires an email address # https://github.com/balanced/balanced-api/issues/20 email_address = '{}@gittip.com'.format(participant_id) if balanced_account_uri is None: try: account = \ balanced.Account.query.filter(email_address=email_address).one() except balanced.exc.NoResultFound: account = balanced.Account(email_address=email_address).save() BALANCED_ACCOUNT = """\ UPDATE participants SET balanced_account_uri=%s WHERE id=%s """ gittip.db.execute(BALANCED_ACCOUNT, (account.uri, participant_id)) account.meta['participant_id'] = participant_id account.save() # HTTP call under here else: account = balanced.Account.find(balanced_account_uri) return account
def redact_pmt(pmt): """Given a unicode, redact it with Samurai. """ typecheck(pmt, (unicode, None)) if pmt is not None: pm = PaymentMethod(pmt) if pm['payment_method_token']: pm._payment_method.redact()
def yes_no(s): typecheck(s, unicode) s = s.lower() if s in [u'yes', u'true', u'1']: return True if s in [u'no', u'false', u'0']: return False raise ValueError("must be either yes/true/1 or no/false/0")
def get_user_info(screen_name): """Given a unicode, return a dict. """ typecheck(screen_name, (unicode, UnicodeWithParams)) rec = gittip.db.one( "SELECT user_info FROM elsewhere " "WHERE platform='twitter' " "AND user_info->'screen_name' = %s" , (screen_name,) ) if rec is not None: user_info = rec else: # Updated using Twython as a point of reference: # https://github.com/ryanmcgrath/twython/blob/master/twython/twython.py#L76 oauth = OAuth1( # we do not have access to the website obj, # so let's grab the details from the env environ['TWITTER_CONSUMER_KEY'], environ['TWITTER_CONSUMER_SECRET'], environ['TWITTER_ACCESS_TOKEN'], environ['TWITTER_ACCESS_TOKEN_SECRET'], ) url = "https://api.twitter.com/1.1/users/show.json?screen_name=%s" user_info = requests.get(url % screen_name, auth=oauth) # Keep an eye on our Twitter usage. # ================================= rate_limit = user_info.headers['X-Rate-Limit-Limit'] rate_limit_remaining = user_info.headers['X-Rate-Limit-Remaining'] rate_limit_reset = user_info.headers['X-Rate-Limit-Reset'] try: rate_limit = int(rate_limit) rate_limit_remaining = int(rate_limit_remaining) rate_limit_reset = int(rate_limit_reset) except (TypeError, ValueError): log( "Got weird rate headers from Twitter: %s %s %s" % (rate_limit, rate_limit_remaining, rate_limit_reset) ) else: reset = datetime.datetime.fromtimestamp(rate_limit_reset, tz=utc) reset = to_age(reset) log( "Twitter API calls used: %d / %d. Resets %s." % (rate_limit - rate_limit_remaining, rate_limit, reset) ) if user_info.status_code == 200: user_info = json.loads(user_info.text) else: log("Twitter lookup failed with %d." % user_info.status_code) raise Response(404) return user_info
def charge(self, participant, amount): """Given dict and Decimal, return None. This is the only place where we actually charge credit cards. Amount should be the nominal amount. We'll compute Gittip's fee below this function and add it to amount to end up with charge_amount. """ typecheck(participant, RealDictRow, amount, Decimal) participant_id = participant['id'] balanced_account_uri = participant['balanced_account_uri'] stripe_customer_id = participant['stripe_customer_id'] typecheck( participant_id, unicode , balanced_account_uri, (unicode, None) , stripe_customer_id, (unicode, None) ) # Perform some last-minute checks. # ================================ if balanced_account_uri is None and stripe_customer_id is None: self.mark_missing_funding() return # Participant has no funding source. if not is_whitelisted(participant): return # Participant not trusted. # Go to Balanced or Stripe. # ========================= if balanced_account_uri is not None: things = self.charge_on_balanced( participant_id , balanced_account_uri , amount ) charge_amount, fee, error = things else: assert stripe_customer_id is not None things = self.charge_on_stripe( participant_id , stripe_customer_id , amount ) charge_amount, fee, error = things amount = charge_amount - fee # account for possible rounding under # charge_on_* self.record_charge( amount , charge_amount , fee , error , participant_id )
def update_goal(self, goal): typecheck(goal, (Decimal, None)) with self.db.get_cursor() as c: tmp = goal if goal is None else unicode(goal) add_event(c, 'participant', dict(id=self.id, action='set', values=dict(goal=tmp))) c.one( "UPDATE participants SET goal=%s WHERE username=%s RETURNING id", (goal, self.username)) self.set_attributes(goal=goal)
def prep(self, amount): """Given a dollar amount as a string, return a 3-tuple. The return tuple is like the one returned from _prep_hit, but with the second value, a log message, removed. """ typecheck(amount, unicode) out = list(_prep_hit(D(amount))) out = [out[0]] + out[2:] return tuple(out)
def store_error(thing, participant_id, msg): typecheck(thing, unicode, participant_id, unicode, msg, unicode) assert thing in ("credit card", "bank account"), thing ERROR = """\ UPDATE participants SET last_%s_result=%%s WHERE id=%%s """ % ("bill" if thing == "credit card" else "ach") gittip.db.execute(ERROR, (msg, participant_id))
def store_error(db, thing, username, msg): typecheck(thing, unicode, username, unicode, msg, unicode) assert thing in ("credit card", "bank account"), thing ERROR = """\ UPDATE participants SET last_%s_result=%%s WHERE username=%%s """ % ("bill" if thing == "credit card" else "ach") db.run(ERROR, (msg, username))
def prep(amount): """Given a dollar amount as a string, return a 3-tuple. The return tuple is like the one returned from _prep_hit, but with the second value, a log message, removed. """ typecheck(amount, unicode) payday = Payday(gittip.db) out = list(payday._prep_hit(Decimal(amount))) out = [out[0]] + out[2:] return tuple(out)
def clear(thing, username, balanced_account_uri): typecheck(thing, unicode, username, unicode, balanced_account_uri, unicode) assert thing in ("credit card", "bank account"), thing invalidate_on_balanced(thing, balanced_account_uri) CLEAR = """\ UPDATE participants SET last_%s_result=NULL WHERE username=%%s """ % ("bill" if thing == "credit card" else "ach") gittip.db.execute(CLEAR, (username, ))
def clear(db, thing, username, balanced_customer_href): typecheck(thing, unicode, username, unicode, balanced_customer_href, (unicode, str)) assert thing in ("credit card", "bank account"), thing invalidate_on_balanced(thing, balanced_customer_href) CLEAR = """\ UPDATE participants SET last_%s_result=NULL WHERE username=%%s """ % ("bill" if thing == "credit card" else "ach") db.run(CLEAR, (username, ))
def _typecast(key, value): """Given two unicodes, return a unicode, and an int or unicode. """ typecheck(key, (unicode, PathPart), value, (unicode, PathPart)) debug(lambda: "typecasting " + key + ", " + value) if key.endswith('.int'): # you can typecast to int key = key[:-4] try: value = int(value) except ValueError: raise Response(404) debug(lambda: "typecasted " + key + ", " + repr(value)) return key, value
def __init__(self, user_id, user_info=None): """Takes a user_id and user_info, and updates the database. """ typecheck(user_id, (int, unicode), user_info, (None, dict)) self.user_id = unicode(user_id) if user_info is not None: a, b, c, d = self.upsert(user_info) self.participant_id = a self.is_claimed = b self.is_locked = c self.balance = d