示例#1
0
    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
示例#2
0
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
示例#3
0
        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)
示例#4
0
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
示例#5
0
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
示例#6
0
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
示例#7
0
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)
示例#8
0
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)
示例#9
0
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
示例#10
0
    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
示例#11
0
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
示例#12
0
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)
示例#13
0
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
示例#14
0
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
示例#15
0
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
示例#16
0
    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
示例#17
0
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 '...'
示例#18
0
    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?
示例#19
0
    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
示例#20
0
    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
示例#21
0
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)
示例#22
0
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
示例#23
0
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
示例#24
0
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)
示例#25
0
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
示例#26
0
    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
示例#27
0
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,))
示例#28
0
    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
示例#30
0
    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
示例#31
0
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 '...'
示例#32
0
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,))
示例#33
0
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)
示例#34
0
    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)
示例#35
0
    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
示例#36
0
    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
示例#37
0
    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
示例#38
0
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
示例#40
0
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 '...'
示例#41
0
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,))
示例#42
0
    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
示例#43
0
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 '...'
示例#44
0
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 "..."
示例#46
0
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
示例#47
0
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()
示例#48
0
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")
示例#49
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
示例#50
0
    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
                           )
示例#51
0
 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)
示例#53
0
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))
示例#54
0
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))
示例#55
0
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)
示例#56
0
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, ))
示例#57
0
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, ))
示例#58
0
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
示例#59
0
    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