From bef525164045ec10f7b164366fb63081b7debfd0 Mon Sep 17 00:00:00 2001 From: Changaco Date: Thu, 21 Sep 2023 08:47:38 +0200 Subject: [PATCH 1/5] don't fail when Stripe refuses to reverse a transfer --- liberapay/payin/stripe.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/liberapay/payin/stripe.py b/liberapay/payin/stripe.py index a54ab9084..5aec168ea 100644 --- a/liberapay/payin/stripe.py +++ b/liberapay/payin/stripe.py @@ -739,12 +739,19 @@ def settle_destination_charge( tr = stripe.Transfer.retrieve(charge.transfer) update_transfer_metadata(tr, pt) if tr.amount_reversed < bt.fee: - tr.reversals.create( - amount=bt.fee, - description="Stripe fee", - metadata={'payin_id': payin.id}, - idempotency_key='payin_fee_%i' % payin.id, - ) + try: + tr.reversals.create( + amount=bt.fee, + description="Stripe fee", + metadata={'payin_id': payin.id}, + idempotency_key='payin_fee_%i' % payin.id, + ) + except stripe.error.StripeError as e: + # In some cases Stripe can refuse to create a reversal. This is + # a serious problem, it means that Liberapay is losing money, + # but it can't be properly resolved automatically, so here the + # error is merely sent to Sentry. + website.tell_sentry(e) elif tr.amount_reversed > bt.fee: reversed_amount = int_to_Money(tr.amount_reversed, tr.currency) - fee record_reversals(db, pt, tr) From f0eac97489ff1b833ccee7d58666617944cb8c0c Mon Sep 17 00:00:00 2001 From: Changaco Date: Thu, 21 Sep 2023 13:42:42 +0200 Subject: [PATCH 2/5] automatically free up usernames taken by fraudsters --- liberapay/main.py | 4 +++- liberapay/models/participant.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/liberapay/main.py b/liberapay/main.py index 175d440e1..0a54b2a27 100644 --- a/liberapay/main.py +++ b/liberapay/main.py @@ -36,7 +36,8 @@ from liberapay.models.account_elsewhere import refetch_elsewhere_data from liberapay.models.community import Community from liberapay.models.participant import ( - Participant, clean_up_closed_accounts, send_account_disabled_notifications, + Participant, clean_up_closed_accounts, free_up_usernames, + send_account_disabled_notifications, generate_profile_description_missing_notifications ) from liberapay.models.repository import refetch_repos @@ -185,6 +186,7 @@ def default_body_parser(body_bytes, headers): cron(Daily(hour=17), paypal.sync_all_pending_payments, True) cron(Daily(hour=18), Payday.update_cached_amounts, True) cron(Daily(hour=19), Participant.delete_old_feedback, True) + cron(Daily(hour=20), free_up_usernames, True) cron(intervals.get('notify_patrons', 1200), Participant.notify_patrons, True) if conf.ses_feedback_queue_url: cron(intervals.get('fetch_email_bounces', 60), handle_email_bounces, True) diff --git a/liberapay/models/participant.py b/liberapay/models/participant.py index ee11979cc..268042257 100644 --- a/liberapay/models/participant.py +++ b/liberapay/models/participant.py @@ -3747,6 +3747,28 @@ def clean_up_closed_accounts(): return len(participants) +def free_up_usernames(): + n = website.db.one(""" + WITH updated AS ( + UPDATE participants + SET username = '~' || id::text + WHERE username NOT LIKE '~%' + AND marked_as IN ('fraud', 'spam') + AND kind IN ('individual', 'organization') + AND ( + SELECT e.ts + FROM events e + WHERE e.participant = participants.id + AND e.type = 'flags_changed' + ORDER BY e.ts DESC + LIMIT 1 + ) < (current_timestamp - interval '3 weeks') + RETURNING id + ) SELECT count(*) FROM updated; + """) + print(f"Freed up {n} username{'s' if n > 1 else ''}.") + + def send_account_disabled_notifications(): """Notify the owners of accounts that have been flagged as fraud or spam. From 0be6677fd39bec86fbebff5bcea8398ce01b1820 Mon Sep 17 00:00:00 2001 From: Changaco Date: Thu, 21 Sep 2023 13:55:06 +0200 Subject: [PATCH 3/5] =?UTF-8?q?don't=20count=20suspended=20accounts=20as?= =?UTF-8?q?=20=E2=80=9Copen=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- liberapay/billing/payday.py | 1 + 1 file changed, 1 insertion(+) diff --git a/liberapay/billing/payday.py b/liberapay/billing/payday.py index b4dc44cc4..07509b32a 100644 --- a/liberapay/billing/payday.py +++ b/liberapay/billing/payday.py @@ -715,6 +715,7 @@ def update_stats(cls, payday_id): FROM participants p WHERE p.kind IN ('individual', 'organization') AND p.join_time < %(ts_start)s + AND p.is_suspended IS NOT true AND COALESCE(( SELECT payload::text FROM events e From 62f2b4fa6f7af928425586356731a199639fd1ad Mon Sep 17 00:00:00 2001 From: Changaco Date: Thu, 21 Sep 2023 14:45:11 +0200 Subject: [PATCH 4/5] honor custom schedule in `get_tips_awaiting_payment` --- liberapay/models/participant.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/liberapay/models/participant.py b/liberapay/models/participant.py index 268042257..a44b66005 100644 --- a/liberapay/models/participant.py +++ b/liberapay/models/participant.py @@ -3199,10 +3199,17 @@ def get_tips_awaiting_payment(self, weeks_early=3, exclude_recipients_of=None): SELECT t.*, p AS tippee_p FROM current_tips t JOIN participants p ON p.id = t.tippee + LEFT JOIN scheduled_payins sp ON sp.payer = t.tipper + AND sp.payin IS NULL + AND t.tippee::text IN ( + SELECT tr->>'tippee_id' + FROM json_array_elements(sp.transfers) tr + ) WHERE t.tipper = %(tipper_id)s AND t.renewal_mode > 0 AND ( t.paid_in_advance IS NULL OR - t.paid_in_advance < (t.amount * %(weeks_early)s) + t.paid_in_advance < (t.amount * %(weeks_early)s) OR + sp.execution_date <= (current_date + interval '%(weeks_early)s weeks') ) AND p.status = 'active' AND ( p.goal IS NULL OR p.goal >= 0 ) From 7740c2cbc73b8730677daa42434bb282a1130493 Mon Sep 17 00:00:00 2001 From: Changaco Date: Thu, 21 Sep 2023 14:51:59 +0200 Subject: [PATCH 5/5] further restrict the range of `weeks_early` --- www/%username/giving/pay/%payment_id.spt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/%username/giving/pay/%payment_id.spt b/www/%username/giving/pay/%payment_id.spt index 1c44d1c1c..57212f539 100644 --- a/www/%username/giving/pay/%payment_id.spt +++ b/www/%username/giving/pay/%payment_id.spt @@ -50,7 +50,7 @@ elif payin_id: raise response.error(404, "unknown payin ID in URL path") response.redirect(payer.path('giving/pay/stripe/%i' % payin.id)) -weeks_early = request.qs.get_int('weeks_early', default=3) +weeks_early = request.qs.get_int('weeks_early', default=3, minimum=1, maximum=520) donation_groups, n_fundable = payer.get_tips_awaiting_payment(weeks_early) donations_not_fundable = ( donation_groups['no_provider'] +