Example #1
0
    def test_existing(self):
        resps = Table('responses')
        with LazyIntegrations() as itgs:
            itgs.write_cursor.execute(
                Query.into(resps).columns(
                    resps.name, resps.response_body, resps.description).insert(
                        *[Parameter('%s')
                          for _ in range(3)]).returning(resps.id).get_sql(),
                ('my_response', 'I like to {foo} the {bar}', 'Testing desc'))
            (respid, ) = itgs.write_cursor.fetchone()
            try:
                itgs.write_conn.commit()
                res: str = responses.get_response(itgs,
                                                  'my_response',
                                                  foo='open',
                                                  bar='door')
                self.assertEqual(res, 'I like to open the door')

                res: str = responses.get_response(itgs,
                                                  'my_response',
                                                  foo='eat',
                                                  buzz='bear')
                self.assertIsInstance(res, str)
                self.assertTrue(res.startswith('I like to eat the '), res)
                # it's not important how we choose to format the error, but it
                # needs the missing key or debugging will be a pain
                self.assertIn('bar', res)
            finally:
                itgs.write_conn.rollback()
                itgs.write_cursor.execute(
                    Query.from_(resps).delete().where(
                        resps.id == Parameter('%s')).get_sql(), (respid, ))
                itgs.write_conn.commit()
Example #2
0
    def handle_comment(self, itgs, comment, rpiden, rpversion):
        token_vals = PARSER.parse(comment['body'])
        target_username = token_vals[0]

        report = loan_format_helper.get_and_format_all_or_summary(itgs, target_username)
        formatted_response = get_response(
            itgs,
            'check',
            target_username=target_username,
            report=report
        )

        utils.reddit_proxy.send_request(
            itgs, rpiden, rpversion, 'post_comment',
            {
                'parent': comment['fullname'],
                'text': formatted_response
            }
        )
Example #3
0
    def suggest_loan_ids(self,
                         resp_name,
                         itgs,
                         comment_fullname,
                         lender_username,
                         loan_id,
                         amt,
                         rpiden,
                         rpversion,
                         loan=None):
        loans = Table('loans')
        lenders = Table('lenders')
        itgs.write_cursor.execute(
            loan_format_helper.create_loans_query().where(
                lenders.username == Parameter('%s')).where(
                    loans.repaid_at.isnull()).orderby(
                        loans.created_at, order=Order.desc).limit(7).get_sql(),
            (lender_username.lower(), ))

        loans = []
        row = itgs.write_cursor.fetchone()
        while row is not None:
            loans.append(loan_format_helper.fetch_loan(row))
            row = itgs.write_cursor.fetchone()

        suggested_loans = loan_format_helper.format_loan_table(loans,
                                                               include_id=True)

        formatted_response = get_response(
            itgs,
            resp_name,
            lender_username=lender_username,
            loan_id=loan_id,
            amount=str(amt),
            loan=('Loan Not Available' if loan is None
                  else loan_format_helper.format_loan_table([loan],
                                                            include_id=True)),
            suggested_loans=suggested_loans)
        utils.reddit_proxy.send_request(itgs, rpiden, rpversion,
                                        'post_comment', {
                                            'parent': comment_fullname,
                                            'text': formatted_response
                                        })
Example #4
0
    def handle_comment(self, itgs, comment, rpiden, rpversion):
        start_at = time.time()
        token_vals = PARSER.parse(comment['body'])
        borrower_username = comment['link_author']
        lender_username = comment['author']
        amount = token_vals[0]
        store_currency = token_vals[1] or amount.currency

        if amount.currency == store_currency:
            store_amount = amount
            rate = 1
        else:
            rate = convert.convert(itgs, amount.currency, store_currency)
            store_amount = money.Money(int(amount.minor * rate),
                                       store_currency)

        if store_currency == 'USD':
            usd_amount = store_amount
            usd_rate = 1
        else:
            # Where possible we want the source to be consistent rather than
            # the target as it allows us to reuse requests
            usd_rate = 1 / convert.convert(itgs, 'USD', store_currency)
            usd_amount = money.Money(int(store_amount.minor * usd_rate),
                                     'USD',
                                     exp=2,
                                     symbol='$',
                                     symbol_on_left=True)

        users = Table('users')
        currencies = Table('currencies')
        moneys = Table('moneys')
        loans = Table('loans')
        loan_creation_infos = Table('loan_creation_infos')
        (lender_user_id, ) = query_helper.find_or_create_or_find(
            itgs, (Query.from_(users).select(
                users.id).where(users.username == Parameter('%s')).get_sql(),
                   (lender_username.lower(), )),
            (Query.into(users).columns(users.username).insert(
                Parameter('%s')).returning(users.id).get_sql(),
             (lender_username.lower(), )))
        (borrower_user_id, ) = query_helper.find_or_create_or_find(
            itgs, (Query.from_(users).select(
                users.id).where(users.username == Parameter('%s')).get_sql(),
                   (borrower_username.lower(), )),
            (Query.into(users).columns(users.username).insert(
                Parameter('%s')).returning(users.id).get_sql(),
             (borrower_username.lower(), )))
        (db_store_currency_id, db_currency_symbol,
         db_currency_sym_on_left) = query_helper.find_or_create_or_find(
             itgs, (Query.from_(currencies).select(
                 currencies.id, currencies.symbol,
                 currencies.symbol_on_left).where(
                     currencies.code == Parameter('%s')).get_sql(),
                    (store_currency, )),
             (Query.into(currencies).columns(
                 currencies.code, currencies.symbol, currencies.symbol_on_left,
                 currencies.exponent).insert(
                     *[Parameter('%s') for _ in range(4)]).returning(
                         currencies.id, currencies.symbol,
                         currencies.symbol_on_left).get_sql(),
              (store_currency, ' ' + store_currency, False,
               money.ISO_CODES_TO_EXP[store_currency])))
        itgs.write_cursor.execute(
            Query.into(moneys).columns(
                moneys.currency_id, moneys.amount,
                moneys.amount_usd_cents).insert(
                    *[Parameter('%s')
                      for _ in range(3)]).returning(moneys.id).get_sql(),
            (db_store_currency_id, store_amount.minor, usd_amount.minor))
        (principal_id, ) = itgs.write_cursor.fetchone()
        itgs.write_cursor.execute(
            Query.into(moneys).columns(
                moneys.currency_id, moneys.amount,
                moneys.amount_usd_cents).insert(
                    *[Parameter('%s')
                      for _ in range(3)]).returning(moneys.id).get_sql(),
            (db_store_currency_id, 0, 0))
        (principal_repayment_id, ) = itgs.write_cursor.fetchone()
        itgs.write_cursor.execute(
            Query.into(loans).columns(
                loans.lender_id, loans.borrower_id, loans.principal_id,
                loans.principal_repayment_id, loans.created_at,
                loans.repaid_at, loans.unpaid_at,
                loans.deleted_at).insert(*[Parameter('%s')
                                           for _ in range(8)]).returning(
                                               loans.id).get_sql(),
            (lender_user_id, borrower_user_id, principal_id,
             principal_repayment_id,
             datetime.fromtimestamp(comment['created_utc']), None, None, None))
        (loan_id, ) = itgs.write_cursor.fetchone()

        itgs.write_cursor.execute(
            Query.into(loan_creation_infos).columns(
                loan_creation_infos.loan_id, loan_creation_infos.type,
                loan_creation_infos.parent_fullname,
                loan_creation_infos.comment_fullname,
                loan_creation_infos.mod_user_id).insert(
                    *[Parameter('%s') for _ in range(5)]).get_sql(),
            (loan_id, 0, comment['link_fullname'], comment['fullname'], None))
        itgs.write_conn.commit()

        store_amount.symbol = db_currency_symbol
        store_amount.symbol_on_left = db_currency_sym_on_left

        permalink = (
            'https://www.reddit.com/comments/{}/redditloans/{}'.format(
                comment['link_fullname'][3:], comment['fullname'][3:]))

        itgs.logger.print(Level.INFO,
                          '/u/{} just lent /u/{} {} - permalink: {}',
                          lender_username, borrower_username, store_amount,
                          permalink)

        itgs.channel.exchange_declare('events', 'topic')
        itgs.channel.basic_publish(
            'events', 'loans.create',
            json.dumps({
                'loan_id': loan_id,
                'comment': {
                    'link_fullname': comment['link_fullname'],
                    'fullname': comment['fullname'],
                    'subreddit': comment['subreddit']
                },
                'lender': {
                    'id': lender_user_id,
                    'username': lender_username
                },
                'borrower': {
                    'id': borrower_user_id,
                    'username': borrower_username
                },
                'amount': {
                    'minor': store_amount.minor,
                    'currency': store_amount.currency,
                    'exp': store_amount.exp,
                    'symbol': store_amount.symbol,
                    'symbol_on_left': store_amount.symbol_on_left
                },
                'permalink': permalink
            }))

        processing_time = time.time() - start_at
        formatted_response = get_response(
            itgs,
            'successful_loan',
            lender_username=lender_username,
            borrower_username=borrower_username,
            principal=str(store_amount),
            principal_explicit=repr(store_amount),
            loan_id=loan_id,
            processing_time=processing_time)

        utils.reddit_proxy.send_request(itgs, rpiden, rpversion,
                                        'post_comment', {
                                            'parent': comment['fullname'],
                                            'text': formatted_response
                                        })
Example #5
0
def handle_loan_create(version, event):
    """Handle a loan create event from the events queue.

    Arguments:
        version (any): The version to pass to the reddit proxy
        event (dict): Describes the loan
            loan_id (int): The id of the loan that was generated
            comment (dict): The comment that generated the loan.
                link_fullname (str): The fullname of the link the comment is in
                fullname (str): The fullname of the comment
            lender (dict): The lender
                id (int): The id of the user in our database
                username (str): The username for the lender
            borrower (dict): The borrower
                id (int): The id of the user in our database
                username (str): The username for the borrower
            amount (dict): The amount of money transfered. Has the same keys as
                the Money object has attributes.
                minor (int)
                currency (int)
                exp (int)
                symbol (str, None)
                symbol_on_left (bool)
            permalink (str): A permanent link to the loan.
    """
    with LazyIntegrations(
            logger_iden='runners/new_lender.py#handle_loan_create') as itgs:
        itgs.logger.print(Level.TRACE, 'Detected loan from /u/{} to /u/{}',
                          event['lender']['username'],
                          event['borrower']['username'])
        amount = Money(**event['amount'])

        loans = Table('loans')
        itgs.read_cursor.execute(
            Query.from_(loans).select(
                Count('*')).where(loans.lender_id == Parameter('%s')).where(
                    loans.id < Parameter('%s')).get_sql(),
            (event['lender']['id'], event['loan_id']))
        (num_previous_loans, ) = itgs.read_cursor.fetchone()

        if num_previous_loans > 0:
            itgs.logger.print(
                Level.TRACE,
                ('Ignoring the loan by /u/{} to /u/{} - /u/{} has {} ' +
                 'previous loans, so they are not new'),
                event['lender']['username'], event['borrower']['username'],
                event['lender']['username'], num_previous_loans)
            return

        itgs.logger.print(
            Level.INFO,
            '/u/{} just made his first loan as lender. Messaging the mods.',
            event['lender']['username'])

        formatted_body = get_response(
            itgs,
            'new_lender',
            lender_username=event['lender']['username'],
            borrower_username=event['borrower']['username'],
            amount=amount,
            permalink=event['permalink'])

        utils.reddit_proxy.send_request(
            itgs, 'new_lender', version, 'compose', {
                'recipient': '/r/borrow',
                'subject': 'New Lender: /u/{}'.format(
                    event['lender']['username']),
                'body': formatted_body
            })
Example #6
0
def handle_loan_unpaid(version, body):
    time.sleep(5)  # give the transaction some time to complete
    loan_unpaid_event_id = body['loan_unpaid_event_id']

    with LazyIntegrations(logger_iden=LOGGER_IDEN) as itgs:
        itgs.logger.print(Level.DEBUG, 'Detected loan unpaid event: {}', loan_unpaid_event_id)
        loan_unpaid_events = Table('loan_unpaid_events')
        loans = Table('loans')
        usrs = Table('users')
        borrowers = usrs.as_('borrowers')
        lenders = usrs.as_('lenders')

        itgs.read_cursor.execute(
            Query.from_(loan_unpaid_events)
            .join(loans).on(loans.id == loan_unpaid_events.loan_id)
            .join(borrowers).on(borrowers.id == loans.borrower_id)
            .join(lenders).on(lenders.id == loans.lender_id)
            .select(borrowers.username, lenders.username)
            .where(loan_unpaid_events.id == Parameter('%s'))
            .get_sql(),
            (loan_unpaid_event_id,)
        )
        row = itgs.read_cursor.fetchone()
        if row is None:
            itgs.logger.print(
                Level.WARN, 'Loan unpaid event {} did not exist!', loan_unpaid_event_id)
            return
        (username, lender_username) = row
        itgs.logger.print(
            Level.TRACE,
            'Ensuring /u/{} is moderator or banned from unpaid event {}',
            username, loan_unpaid_event_id
        )
        info = perms.manager.fetch_info(itgs, username, RPIDEN, version)
        if info is None:
            itgs.logger.print(
                Level.INFO,
                '/u/{} defaulted on a loan then deleted their account.',
                username
            )
            return
        if info['borrow_banned']:
            itgs.logger.print(
                Level.DEBUG,
                '/u/{} defaulted on a loan but they are already banned.',
                username
            )
            return
        if info['borrow_moderator']:
            itgs.logger.print(
                Level.INFO,
                '/u/{} defaulted on a loan but is a moderator - no ban',
                username
            )
            return
        if info['borrow_approved_submitter']:
            itgs.logger.print(
                Level.INFO,
                '/u/{} defaulted on a loan but is an approved submitter - no ban',
                username
            )
            # easy to forget about approved submitters
            utils.reddit_proxy.send_request(
                itgs, RPIDEN, version, 'compose',
                {
                    'recipient': '/r/borrow',
                    'subject': 'Approved Submitter Unpaid Loan',
                    'body': (
                        '/u/{} defaulted on a loan but did not get banned since they are '
                        + 'an approved submitter.'
                    ).format(username)
                }
            )
            return

        itgs.logger.print(
            Level.TRACE,
            'Banning /u/{} because they defaulted on a loan',
            username
        )

        substitutions = {
            'borrower_username': username,
            'lender_username': lender_username
        }

        utils.reddit_proxy.send_request(
            itgs, RPIDEN, version, 'ban_user',
            {
                'subreddit': 'borrow',
                'username': username,
                'message': get_response(itgs, 'unpaid_ban_message', **substitutions),
                'note': get_response(itgs, 'unpaid_ban_note', **substitutions)
            }
        )
        itgs.logger.print(
            Level.INFO,
            'Banned /u/{} on /r/borrow - failed to repay loan with /u/{}',
            username,
            lender_username
        )
        perms.manager.flush_cache(itgs, username)
Example #7
0
def _handle_self_post(itgs, version, post):
    """Handles a post on a relevant subreddit which involves writing a markdown
    body. This assumes we have not already responded.

    Arguments:
        - itgs (LazyIntegrations): The integrations to use when connecting with
            networked components.
        - version (any): The version of this daemon that we're running, which
            we use to identify with the reddit proxy.
        - post (dict): The self-post that we are handling.
    """
    author = post['author']
    subreddit = post['subreddit']
    title = post['title']

    if not can_interact(itgs, author, 'links', version):
        if author.lower() not in IGNORED_USERS:
            itgs.logger.print(
                Level.INFO,
                'Using no summons for selfpost by /u/{} to /r/{}; insufficient access',
                author, subreddit)
        return

    if '[req]' not in title.lower():
        # This doesn't appear to be a request post. We allow users to opt out
        # of receiving a response to non-request posts.

        users = Table('users')
        itgs.read_cursor.execute(
            Query.from_(users).select(
                users.id).where(users.username == Parameter('%s')).get_sql(),
            (author.lower(), ))
        row = itgs.read_cursor.fetchone()
        if row is not None:
            (user_id, ) = row
            settings = user_settings.get_settings(itgs, user_id)

            if settings.non_req_response_opt_out:
                itgs.logger.print(
                    Level.DEBUG,
                    '/u/{} made a non-request post (title: {}); ignoring it because '
                    +
                    'they have opted out of receiving a check for non-request posts.',
                    author, title)
                return
    else:
        request = utils.req_post_interpreter.interpret(title)
        itgs.channel.exchange_declare('events', 'topic')
        itgs.channel.basic_publish(
            'events', 'loans.request',
            json.dumps({
                'post': post,
                'request': request.dict()
            }))

    itgs.logger.print(
        Level.INFO,
        '/u/{} made a post to /r/{}: "{}"; they are receiving a check.',
        author, subreddit, title)

    report = loan_format_helper.get_and_format_all_or_summary(itgs, author)
    formatted_response = get_response(itgs,
                                      'check',
                                      target_username=author,
                                      report=report)

    utils.reddit_proxy.send_request(itgs, 'links', version, 'post_comment', {
        'parent': post['fullname'],
        'text': formatted_response
    })
Example #8
0
 def test_missing(self):
     with LazyIntegrations() as itgs:
         res = responses.get_response(itgs, 'my_missing_key')
         self.assertIsInstance(res, str)
         self.assertIn('my_missing_key', res)
Example #9
0
def send_messages(version):
    with LazyIntegrations(logger_iden=LOGGER_IDEN) as itgs:
        itgs.logger.print(Level.TRACE,
                          'Sending moderator onboarding messages...')

        mod_onboarding_messages = Table('mod_onboarding_messages')
        itgs.read_cursor.execute(
            Query.from_(mod_onboarding_messages).select(
                Max(mod_onboarding_messages.msg_order)).get_sql())
        (max_msg_order, ) = itgs.read_cursor.fetchone()
        if max_msg_order is None:
            itgs.logger.print(Level.DEBUG,
                              'There are no moderator onboarding messages.')
            return

        mod_onboarding_progress = Table('mod_onboarding_progress')
        moderators = Table('moderators')
        users = Table('users')
        itgs.read_cursor.execute(
            Query.from_(moderators).join(users).on(
                users.id == moderators.user_id).
            left_join(mod_onboarding_progress).on(
                mod_onboarding_progress.moderator_id == moderators.id).select(
                    users.id, moderators.id, users.username,
                    mod_onboarding_progress.msg_order).where(
                        mod_onboarding_progress.msg_order.isnull()
                        | (mod_onboarding_progress.msg_order < Parameter('%s'))
                    ).get_sql(), (max_msg_order, ))
        rows = itgs.read_cursor.fetchall()

        responses = Table('responses')
        titles = responses.as_('titles')
        bodies = responses.as_('bodies')
        for (user_id, mod_id, username, cur_msg_order) in rows:
            itgs.read_cursor.execute(
                Query.from_(mod_onboarding_messages).join(titles).on(
                    titles.id == mod_onboarding_messages.title_id).join(bodies)
                .on(bodies.id == mod_onboarding_messages.body_id).select(
                    mod_onboarding_messages.msg_order, titles.id, titles.name,
                    bodies.id, bodies.name).where(
                        Parameter('%s').isnull()
                        | (mod_onboarding_messages.msg_order > Parameter('%s'))
                    ).orderby(mod_onboarding_messages.msg_order,
                              order=Order.asc).limit(1).get_sql(), (
                                  cur_msg_order,
                                  cur_msg_order,
                              ))
            (new_msg_order, title_id, title_name, body_id,
             body_name) = itgs.read_cursor.fetchone()
            title_formatted = get_response(itgs, title_name, username=username)
            body_formatted = get_response(itgs, body_name, username=username)
            utils.reddit_proxy.send_request(
                itgs, 'mod_onboarding_messages', version, 'compose', {
                    'recipient': username,
                    'subject': title_formatted,
                    'body': body_formatted
                })
            utils.mod_onboarding_utils.store_letter_message_with_id_and_names(
                itgs, user_id, title_id, title_name, body_id, body_name)
            if cur_msg_order is None:
                itgs.write_cursor.execute(
                    Query.into(mod_onboarding_progress).columns(
                        mod_onboarding_progress.moderator_id,
                        mod_onboarding_progress.msg_order).insert(
                            *(Parameter('%s') for _ in range(2))).get_sql(),
                    (mod_id, new_msg_order))
            else:
                itgs.write_cursor.execute(
                    Query.update(mod_onboarding_progress).set(
                        mod_onboarding_progress.msg_order,
                        Parameter('%s')).set(
                            mod_onboarding_progress.updated_at,
                            CurTimestamp()).where(
                                mod_onboarding_progress.moderator_id ==
                                Parameter('%s')).get_sql(),
                    (new_msg_order, mod_id))
            itgs.write_conn.commit()
            itgs.logger.print(
                Level.INFO,
                'Successfully sent moderator onboarding message (msg_order={}) to /u/{}',
                new_msg_order, username)
Example #10
0
    def handle_comment(self, itgs, comment, rpiden, rpversion):
        token_vals = PARSER.parse(comment['body'])
        borrower_username = comment['author']
        lender_username = token_vals[0]
        amt = token_vals[1]

        usd_amount = None
        if amt.currency == 'USD':
            usd_amount = amt
        else:
            # We prefer the source is stable so we get the inverted rate and invert
            usd_rate = 1 / convert(itgs, 'USD', amt.currency)
            usd_amount = Money(int(amt.minor * usd_rate),
                               'USD',
                               exp=2,
                               symbol='$',
                               symbol_on_left=True)

        loans = Table('loans')
        users = Table('users')
        lenders = users.as_('lenders')
        borrowers = users.as_('borrowers')
        loan_creation_infos = Table('loan_creation_infos')
        moneys = Table('moneys')
        principals = moneys.as_('principals')
        currencies = Table('currencies')
        principal_currencies = currencies.as_('principal_currencies')
        principal_repayments = moneys.as_('principal_repayments')

        itgs.read_cursor.execute(
            Query.from_(loans).select(
                loans.id, loan_creation_infos.parent_fullname,
                loan_creation_infos.comment_fullname).join(loan_creation_infos)
            .on(loan_creation_infos.loan_id == loans.id).join(lenders).on(
                lenders.id == loans.lender_id).join(borrowers).on(
                    borrowers.id == loans.borrower_id).join(principals).on(
                        principals.id ==
                        loans.principal_id).join(principal_currencies).on(
                            principal_currencies.id == principals.currency_id).
            join(principal_repayments).on(
                principal_repayments.id == loans.principal_repayment_id).where(
                    lenders.username == Parameter('%s')).where(
                        borrowers.username == Parameter('%s')).where(
                            principal_repayments.amount == 0).where(
                                loans.unpaid_at.isnull()).where(
                                    loans.deleted_at.isnull()).
            where(((principal_currencies.code == amt.currency)
                   & (principals.amount == amt.minor)) |
                  ((principal_currencies.code != amt.currency)
                   & (principals.amount <= (usd_amount.minor + 100)))).orderby(
                       loans.created_at, order=Order.desc).limit(1).get_sql(),
            (lender_username.lower(), borrower_username.lower()))
        row = itgs.read_cursor.fetchone()

        if row is None:
            formatted_response = get_response(
                itgs,
                'confirm_no_loan',
                borrower_username=borrower_username,
                lender_username=lender_username,
                amount=amt,
                usd_amount=usd_amount)
        else:
            (loan_id, parent_fullname, comment_fullname) = row
            formatted_response = get_response(
                itgs,
                'confirm',
                borrower_username=borrower_username,
                lender_username=lender_username,
                amount=amt,
                usd_amount=usd_amount,
                loan_permalink=(
                    'https://www.reddit.com' if parent_fullname is None else
                    'https://www.reddit.com/comments/{}/redditloans/{}'.format(
                        parent_fullname[3:], comment_fullname[3:])),
                loan_id=loan_id)

        itgs.logger.print(
            Level.INFO,
            '/u/{} confirmed /u/{} sent him {} (matched loan: {}). Permalink: {}',
            borrower_username, lender_username, amt,
            'no' if row is None else f'yes, loan {loan_id}',
            'https://www.reddit.com/comments/{}/redditloans/{}'.format(
                comment['link_fullname'][3:], comment['fullname'][3:]))
        utils.reddit_proxy.send_request(itgs, rpiden, rpversion,
                                        'post_comment', {
                                            'parent': comment['fullname'],
                                            'text': formatted_response
                                        })
Example #11
0
    def handle_comment(self, itgs, comment, rpiden, rpversion):
        token_vals = PARSER.parse(comment['body'])
        lender_username = comment['author']
        borrower_username = token_vals[0]
        amt = token_vals[1]

        comment_permalink = 'https://www.reddit.com/comments/{}/redditloans/{}'.format(
            comment['link_fullname'][3:], comment['fullname'][3:])

        loans = Table('loans')
        lenders = Table('lenders')
        borrowers = Table('borrowers')

        effected_loans_pre = []
        effected_loans_post = []
        remaining = amt
        while remaining.minor > 0:
            itgs.write_cursor.execute(
                loan_format_helper.create_loans_query().where(
                    lenders.username == Parameter('%s')).where(
                        borrowers.username == Parameter('%s')).where(
                            loans.repaid_at.isnull()).orderby(
                                loans.created_at,
                                order=Order.asc).limit(1).get_sql(),
                (lender_username.lower(), borrower_username.lower()))
            row = itgs.write_cursor.fetchone()
            if row is None:
                break
            loan_pre = loan_format_helper.fetch_loan(row)
            old_minor = remaining.minor
            (_, _, remaining) = utils.paid_utils.apply_repayment(
                itgs, loan_pre.id, remaining)
            itgs.write_cursor.execute(
                loan_format_helper.create_loans_query().where(
                    loans.id == Parameter('%s')).get_sql(), (loan_pre.id, ))
            row = itgs.write_cursor.fetchone()
            if row is None:
                itgs.logger.print(
                    Level.WARN,
                    'Somehow, while handling the paid summon by /u/{} at {}, '
                    +
                    'the loan was deleted while applying repayment. We stopped '
                    +
                    'propagating the loan early. If nobody was deleting loans this '
                    + 'is definitely developer error.', lender_username,
                    comment_permalink)
                effected_loans_pre.append(loan_pre)
                break
            loan_post = loan_format_helper.fetch_loan(row)
            if old_minor <= remaining.minor:
                # Sanity check to prevent loops
                break
            effected_loans_pre.append(loan_pre)
            effected_loans_post.append(loan_post)

        itgs.logger.print(
            Level.INFO, '/u/{} was repaid by /u/{} by {} over {} loan{} at {}',
            lender_username, borrower_username, amt, len(effected_loans_pre),
            's' if len(effected_loans_pre) != 1 else '', comment_permalink)

        formatted_response = get_response(
            itgs,
            'paid',
            lender_username=lender_username,
            borrower_username=borrower_username,
            loans_before=loan_format_helper.format_loan_table(
                effected_loans_pre),
            loans_after=loan_format_helper.format_loan_table(
                effected_loans_post),
            num_loans_affected=len(effected_loans_pre),
            amount=str(amt),
            remaining=str(remaining))

        utils.reddit_proxy.send_request(itgs, rpiden, rpversion,
                                        'post_comment', {
                                            'parent': comment['fullname'],
                                            'text': formatted_response
                                        })
def handle_loan_request(version, event):
    """Handle a loan request event from the events queue.

    Arguments:
        version (any): The version to pass to the reddit proxy
        event (dict): Describes the request
            post (dict):
                A self post from reddit-proxy "subreddit_links" (Documented
                at reddit-proxy/src/handlers/links.py)
            request (dict):
                A dictified utils.req_post_interpreter.LoanRequest
    """
    post = event['post']
    with LazyIntegrations(logger_iden='runners/borrower_request.py#handle_loan_request') as itgs:
        itgs.logger.print(
            Level.TRACE,
            'Detected loan request from /u/{}',
            post['author']
        )

        users = Table('users')
        itgs.read_cursor.execute(
            users.select(users.id)
            .where(users.username == Parameter('%s'))
            .get_sql(),
            (post['author'].lower(),)
        )
        row = itgs.read_cursor.fetchone()
        if row is None:
            itgs.logger.print(
                Level.TRACE,
                'Ignoring loan request from /u/{} - they do not have any '
                + 'outstanding loans (no history)',
                post['author']
            )
            return
        (author_user_id,) = row

        loans = Table('loans')
        itgs.read_cursor.execute(
            loan_format_helper.create_loans_query()
            .select(loans.lender_id)
            .where(loans.borrower_id == Parameter('%s'))
            .where(loans.repaid_at.isnull())
            .where(loans.unpaid_at.isnull())
            .get_sql(),
            (author_user_id,)
        )
        row = itgs.read_cursor.fetchone()
        outstanding_borrowed_loans = []
        while row is not None:
            outstanding_borrowed_loans.append({
                'pretty': loan_format_helper.fetch_loan(row[:-1]),
                'lender_id': row[-1]
            })
            row = itgs.read_cursor.fetchone()

        if not outstanding_borrowed_loans:
            itgs.logger.print(
                Level.TRACE,
                'Ignoring loan request from /u/{} - no outstanding loans',
                post['author']
            )
            return

        unique_lenders = frozenset(loan['lender_id'] for loan in outstanding_borrowed_loans)
        itgs.logger.print(
            Level.INFO,
            '/u/{} made a loan request while they have {} open loans from '
            + '{} unique lenders: {}. Going to inform each lender which has not '
            + 'opted out of borrower request pms.',
            post['author'], len(outstanding_borrowed_loans), len(unique_lenders),
            unique_lenders
        )

        for lender_id in unique_lenders:
            lender_settings = get_settings(itgs, lender_id)
            if lender_settings.borrower_req_pm_opt_out:
                itgs.logger.print(
                    Level.TRACE,
                    'Not sending an alert to user {} - opted out',
                    lender_id
                )
                continue

            pretty_loans = [
                loan['pretty']
                for loan in outstanding_borrowed_loans
                if loan['lender_id'] == lender_id
            ]

            formatted_body = get_response(
                itgs,
                'borrower_request',
                lender_username=pretty_loans[0].lender,
                borrower_username=post['author'],
                thread='https://www.reddit.com/r/{}/comments/{}/redditloans'.format(
                    post['subreddit'], post['fullname'][3:]
                ),
                loans=loan_format_helper.format_loan_table(pretty_loans, include_id=True)
            )

            utils.reddit_proxy.send_request(
                itgs, 'borrower_request', version, 'compose',
                {
                    'recipient': pretty_loans[0].lender,
                    'subject': '/u/{} has made a request thread'.format(post['author']),
                    'body': formatted_body
                }
            )
Example #13
0
    def handle_comment(self, itgs, comment, rpiden, rpversion):
        token_vals = PARSER.parse(comment['body'])
        lender_username = comment['author']
        borrower_username = token_vals[0]

        comment_permalink = 'https://www.reddit.com/comments/{}/redditloans/{}'.format(
            comment['link_fullname'][3:], comment['fullname'][3:])

        loans = Table('loans')
        lenders = Table('lenders')
        borrowers = Table('borrowers')
        itgs.write_cursor.execute(
            loan_format_helper.create_loans_query().where(
                lenders.username == Parameter('%s')).where(
                    borrowers.username == Parameter('%s')).where(
                        loans.unpaid_at.isnull()).where(
                            loans.repaid_at.isnull()).get_sql(),
            (lender_username.lower(), borrower_username.lower()))
        row = itgs.write_cursor.fetchone()

        affected_pre = []
        while row is not None:
            affected_pre.append(loan_format_helper.fetch_loan(row))
            row = itgs.write_cursor.fetchone()

        if affected_pre:
            itgs.write_cursor.execute(
                Query.update(loans).set(loans.unpaid_at, Now()).where(
                    loans.id.isin([Parameter('%s')
                                   for _ in affected_pre])).get_sql(),
                tuple(loan.id for loan in affected_pre))

            loan_unpaid_events = Table('loan_unpaid_events')
            itgs.write_cursor.execute(
                Query.into(loan_unpaid_events).columns(
                    loan_unpaid_events.loan_id,
                    loan_unpaid_events.unpaid).insert(
                        *[(Parameter('%s'), True)
                          for _ in affected_pre]).returning(
                              loan_unpaid_events.id).get_sql(),
                tuple(loan.id for loan in affected_pre))
            itgs.channel.exchange_declare('events', 'topic')
            row = itgs.write_cursor.fetchone()
            while row is not None:
                itgs.channel.basic_publish(
                    'events', 'loans.unpaid',
                    json.dumps({"loan_unpaid_event_id": row[0]}))
                row = itgs.write_cursor.fetchone()

            itgs.write_cursor.execute(
                loan_format_helper.create_loans_query().where(
                    loans.id.isin([Parameter('%s')
                                   for _ in affected_pre])).get_sql(),
                tuple(loan.id for loan in affected_pre))
            row = itgs.write_cursor.fetchone()
            affected_post = []
            while row is not None:
                affected_post.append(loan_format_helper.fetch_loan(row))
                row = itgs.write_cursor.fetchone()
        else:
            affected_post = []

        itgs.logger.print(Level.INFO,
                          '/u/{} marked {} loan{} sent to /u/{} unpaid at {}',
                          lender_username, len(affected_pre),
                          's' if len(affected_pre) != 1 else '',
                          borrower_username, comment_permalink)

        borrower_summary = loan_format_helper.get_and_format_all_or_summary(
            itgs, borrower_username)

        formatted_response = get_response(
            itgs,
            'unpaid',
            lender_username=lender_username,
            borrower_username=borrower_username,
            loans_before=loan_format_helper.format_loan_table(affected_pre),
            loans_after=loan_format_helper.format_loan_table(affected_post),
            borrower_summary=borrower_summary)

        utils.reddit_proxy.send_request(itgs, rpiden, rpversion,
                                        'post_comment', {
                                            'parent': comment['fullname'],
                                            'text': formatted_response
                                        })
Example #14
0
def handle_loan_created(version, body):
    """Called whenever we detect that a loan was just created.

    Arguments:
    - `version (float)`: The version for communicating with the reddit-proxy
    - `body (dict)`: The body of the loans.create event
    """
    with LazyIntegrations(logger_iden=LOGGER_IDEN) as itgs:
        lender_username = body['lender']['username']
        borrower_username = body['borrower']['username']
        borrower_id = body['borrower']['id']
        itgs.logger.print(Level.TRACE,
                          'Detected that /u/{} received a loan from /u/{}',
                          borrower_username, lender_username)

        loans = Table('loans')
        itgs.read_cursor.execute(
            Query.from_(loans).select(Count(Star())).where(
                loans.deleted_at.isnull()).where(
                    loans.lender_id == Parameter('%s')).get_sql(),
            (borrower_id, ))
        (num_as_lender, ) = itgs.read_cursor.fetchone()

        if num_as_lender == 0:
            itgs.logger.print(Level.TRACE,
                              'Nothing to do - /u/{} has no loans as lender',
                              borrower_username)
            return

        substitutions = {
            'lender_username':
            lender_username,
            'borrower_username':
            borrower_username,
            'loan_id':
            body['loan_id'],
            'loans_table':
            loan_format_helper.get_and_format_all_or_summary(
                itgs, borrower_username)
        }

        info = perms.manager.fetch_info(itgs, borrower_username, RPIDEN,
                                        version)
        if info['borrow_moderator']:
            itgs.logger.print(
                Level.DEBUG,
                'Ignoring that moderator /u/{} received a loan as lender',
                borrower_username)
            return

        if info['borrow_approved_submitter']:
            itgs.logger.print(
                Level.DEBUG,
                '/u/{} - who previously acted as lender - received a loan, '
                'but they are on the approved submitter list. Sending a pm but '
                'not taking any other action.', borrower_username)
            utils.reddit_proxy.send_request(
                itgs, RPIDEN, version, 'compose', {
                    'recipient':
                    '/r/borrow',
                    'subject':
                    get_response(
                        itgs, 'approved_lender_received_loan_modmail_pm_title',
                        **substitutions),
                    'body':
                    get_response(
                        itgs, 'approved_lender_received_loan_modmail_pm_body',
                        **substitutions)
                })
            return

        itgs.logger.print(
            Level.DEBUG,
            '/u/{} - who has previously acted as a lender - received a loan. '
            'Messaging moderators and ensuring they are not in /r/lenderscamp',
            borrower_username)

        utils.reddit_proxy.send_request(
            itgs, RPIDEN, version, 'compose', {
                'recipient':
                '/r/borrow',
                'subject':
                get_response(itgs, 'lender_received_loan_modmail_pm_title', **
                             substitutions),
                'body':
                get_response(itgs, 'lender_received_loan_modmail_pm_body', **
                             substitutions)
            })

        is_approved = utils.reddit_proxy.send_request(
            itgs, RPIDEN, version, 'user_is_approved', {
                'subreddit': 'lenderscamp',
                'username': borrower_username
            })
        is_moderator = utils.reddit_proxy.send_request(
            itgs, RPIDEN, version, 'user_is_moderator', {
                'subreddit': 'lenderscamp',
                'username': borrower_username
            })
        if is_moderator:
            itgs.logger.print(
                Level.DEBUG,
                'Removing /u/{} as contributor on /r/lenderscamp suppressed - they are a mod there',
                borrower_username)
            return

        if is_approved:
            utils.reddit_proxy.send_request(itgs, RPIDEN, version,
                                            'disapprove_user', {
                                                'subreddit': 'lenderscamp',
                                                'username': borrower_username
                                            })
            itgs.logger.print(
                Level.INFO,
                'Finished alerting about lender-gone-borrower /u/{} and removing from lenderscamp',
                borrower_username)
        else:
            itgs.logger.print(
                Level.INFO,
                'Alerted /r/borrow about /u/{} receiving a loan. They were '
                'already not a contributor to /r/lenderscamp.',
                borrower_username)
Example #15
0
    def handle_comment(self, itgs, comment, rpiden, rpversion):
        token_vals = PARSER.parse(comment['body'])
        lender_username = comment['author']
        loan_id = token_vals[0]
        amt = token_vals[1]

        comment_permalink = 'https://www.reddit.com/comments/{}/redditloans/{}'.format(
            comment['link_fullname'][3:], comment['fullname'][3:])

        loans = Table('loans')
        itgs.write_cursor.execute(
            loan_format_helper.create_loans_query().where(
                loans.id == Parameter('%s')).get_sql(), (loan_id, ))
        row = itgs.write_cursor.fetchone()
        if row is None:
            itgs.logger.print(
                Level.INFO,
                '/u/{} tried to mark non-existent loan {} as paid at {}',
                lender_username, loan_id, comment_permalink)
            self.suggest_loan_ids('paid_with_id_not_found', itgs,
                                  comment['fullname'], lender_username,
                                  loan_id, amt, rpiden, rpversion)
            return

        loan = loan_format_helper.fetch_loan(row)
        if loan.lender.lower() != lender_username.lower():
            itgs.logger.print(
                Level.INFO,
                '/u/{} tried to mark loan {} (lender: {}, borrower: {}) as paid at {}',
                lender_username, loan_id, loan.lender, loan.borrower,
                comment_permalink)
            self.suggest_loan_ids('paid_with_id_wrong_lender',
                                  itgs,
                                  comment['fullname'],
                                  lender_username,
                                  loan_id,
                                  amt,
                                  rpiden,
                                  rpversion,
                                  loan=loan)
            return

        if loan.repaid_at is not None:
            itgs.logger.print(
                Level.INFO,
                '/u/{} tried to mark loan {} (already repaid) as paid at {}',
                lender_username, loan_id, comment_permalink)
            self.suggest_loan_ids('paid_with_id_already_repaid',
                                  itgs,
                                  comment['fullname'],
                                  lender_username,
                                  loan_id,
                                  amt,
                                  rpiden,
                                  rpversion,
                                  loan=loan)
            return

        (_, applied,
         remaining) = utils.paid_utils.apply_repayment(itgs, loan_id, amt)

        itgs.write_cursor.execute(
            loan_format_helper.create_loans_query().where(
                loans.id == Parameter('%s')).get_sql(), (loan_id, ))
        loan_after = loan_format_helper.fetch_loan(
            itgs.write_cursor.fetchone())

        itgs.logger.print(
            Level.INFO,
            '/u/{} repaid /u/{} {} ({} ignored) toward loan {} - permalink: {}',
            loan.borrower, lender_username, applied, remaining, loan_id,
            comment_permalink)

        formatted_response = get_response(
            itgs,
            'paid_with_id',
            loan_id=loan.id,
            lender_username=lender_username,
            borrower_username=loan.borrower,
            loan_before=loan_format_helper.format_loan_table([loan],
                                                             include_id=True),
            loan_after=loan_format_helper.format_loan_table([loan_after],
                                                            include_id=True),
            amount=str(amt),
            applied=str(applied),
            remaining=str(remaining))

        utils.reddit_proxy.send_request(itgs, rpiden, rpversion,
                                        'post_comment', {
                                            'parent': comment['fullname'],
                                            'text': formatted_response
                                        })