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()
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 } )
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 })
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 })
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 })
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)
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 })
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)
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)
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 })
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 } )
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 })
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)
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 })