Skip to content

Add group tip notifications and restrict /atip to DMs#8

Open
reubenyap wants to merge 6 commits intomasterfrom
claude/group-tip-notifications-BbvlM
Open

Add group tip notifications and restrict /atip to DMs#8
reubenyap wants to merge 6 commits intomasterfrom
claude/group-tip-notifications-BbvlM

Conversation

@reubenyap
Copy link
Copy Markdown
Member

@reubenyap reubenyap commented Mar 9, 2026

Summary

  • When a tip is sent in a group chat, post a notification so members can see it
  • Restrict /atip (anonymous tips) to DMs only — using it in a group defeats the purpose
  • When /atip is attempted in a group, the bot deletes the command and privately tells the user to use DMs

Test plan

  • Verify /tip in a group shows a notification message in the group chat
  • Verify /tip in a DM does not post a group notification
  • Verify /atip in a group deletes the command and sends a DM to the sender
  • Verify /atip in a DM works normally with anonymous sender name

Note

High Risk
High risk because it changes how balances are stored/updated (float→Decimal128 with new atomic $inc/find_one_and_update paths) and introduces a one-time database migration that affects core funds accounting.

Overview
Monetary precision + safer accounting: Updates the bot to use Decimal/MongoDB Decimal128 for balances and amounts, adds a custom PyMongo codec, and replaces balance math with atomic $inc/find_one_and_update guards to reduce race conditions in deposits, withdrawals, tips, and envelope claims.

Behavior changes: Adds a one-time migrate_to_decimal128.py script (dry-run by default) to convert existing users, envelopes, and tip_logs monetary fields; introduces automatic red-envelope expiry with refunds after 24h; posts a group notification when a tip is sent in a group; and restricts /atip to DMs by deleting group attempts and DM-ing the sender.

Written by Cursor Bugbot for commit 68a597b. This will update automatically on new commits. Configure here.

claude added 6 commits March 9, 2026 05:47
- Use atomic find_one_and_update with $gte guards for tips, withdrawals,
  and red envelope creation to prevent spending more than available balance
- Use $inc instead of $set for all balance mutations to prevent race
  conditions between concurrent operations
- Use Decimal arithmetic for all FIRO calculations to prevent floating
  point precision errors (stored as float in MongoDB for backward
  compatibility with existing database)
- Fix withdrawal fee logic: atomic deduct full amount, rollback exact
  amount on RPC failure
- Fix catch_envelope to atomically deduct from envelope remains with
  $gte guard, preventing over-distribution
- Fix update_balance withdrawal handler to properly handle locked amount
  reduction without leaving stale locked values
- Store MongoClient as self.client for transaction session access
- Replace print() with structured logging throughout
- Replace deprecated insert() calls with insert_one()

https://claude.ai/code/session_01K7FrkbpYpqJAoJfcx67osm
Replace float storage with BSON Decimal128 via a custom pymongo codec
that transparently converts between Python Decimal and Decimal128.
This eliminates floating-point drift from accumulated $inc operations.

The codec is registered on the database connection so all collections
automatically serialize Decimal values as Decimal128 and deserialize
them back. The to_decimal() helper handles reading both legacy float
values (pre-migration) and Decimal128 values (post-migration).

Add migrate_to_decimal128.py script to batch-convert existing float
Balance/Locked fields to Decimal128. Supports dry-run mode by default
and logs every change. The bot works correctly both before and after
migration (graceful degradation).

https://claude.ai/code/session_01K7FrkbpYpqJAoJfcx67osm
Red envelopes now expire after 24 hours. Unclaimed funds are
automatically returned to the creator's balance with a DM notification.
The group message button is cleaned up on expiry.

- Add expire_envelopes() background task running every 60 seconds
- Add expiry guard in catch_envelope() to reject catches on expired envelopes
- Add "expired" field to envelope documents for explicit state tracking
- Atomic find_one_and_update prevents double-refund races

https://claude.ai/code/session_01K7FrkbpYpqJAoJfcx67osm
Ensures envelopes that expired while the bot was offline are
immediately processed before the polling loop begins, closing
the 60-second gap before the first scheduled run.

https://claude.ai/code/session_01K7FrkbpYpqJAoJfcx67osm
When a tip is made in a public channel, post a notification in that
channel so other members can see it. Anonymous tips show "Anonymous"
as the sender. DM notifications remain unchanged.

https://claude.ai/code/session_01K7FrkbpYpqJAoJfcx67osm
/atip in a group chat defeats its purpose since the command message
reveals the sender. Now the bot deletes the command from the group,
replies privately telling the user to use DMs, and returns early.
The unreachable anonymous branch in the group notification is removed.

https://claude.ai/code/session_01K7FrkbpYpqJAoJfcx67osm
@reubenyap
Copy link
Copy Markdown
Member Author

bugbot run

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Stale remains causes over-crediting in envelope expiry
    • Changed ReturnDocument.AFTER to ReturnDocument.BEFORE and now derive the refund amount from the returned document's actual remains value at update time, preventing the race condition with catch_envelope.

Create PR

Or push these changes by commenting:

@cursor push 700c0a9c0e
Preview (700c0a9c0e)
diff --git a/tipbot.py b/tipbot.py
--- a/tipbot.py
+++ b/tipbot.py
@@ -558,9 +558,8 @@
                 if remains <= 0:
                     continue
 
-                store_remains = decimal_to_store(remains)
-
                 # Atomically zero out the envelope remains and mark as expired
+                # Use BEFORE to get the actual remains at update time (avoids race with catch_envelope)
                 updated = self.col_envelopes.find_one_and_update(
                     {
                         "_id": envelope['_id'],
@@ -574,11 +573,17 @@
                             "expired_at": int(datetime.datetime.now().timestamp())
                         }
                     },
-                    return_document=ReturnDocument.AFTER
+                    return_document=ReturnDocument.BEFORE
                 )
                 if not updated:
                     continue
 
+                # Use the actual remains from the document at update time
+                actual_remains = to_decimal(updated['remains'])
+                if actual_remains <= 0:
+                    continue
+                store_remains = decimal_to_store(actual_remains)
+
                 # Credit the creator
                 self.col_users.update_one(
                     {"_id": envelope['creator_id']},
@@ -586,7 +591,7 @@
                 )
 
                 logger.info("Envelope %s expired: refunded %s FIRO to user %s",
-                            envelope['_id'], remains, envelope['creator_id'])
+                            envelope['_id'], actual_remains, envelope['creator_id'])
 
                 # Notify the creator
                 try:
@@ -594,7 +599,7 @@
                         envelope['creator_id'],
                         "<b>Your red envelope expired.</b>\n"
                         "<b>%s FIRO</b> unclaimed funds have been returned to your balance." %
-                        "{0:.8f}".format(remains),
+                        "{0:.8f}".format(actual_remains),
                         parse_mode='HTML'
                     )
                 except Exception as exc:
This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Comment thread tipbot.py
self.col_users.update_one(
{"_id": envelope['creator_id']},
{"$inc": {"Balance": store_remains}}
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale remains causes over-crediting in envelope expiry

High Severity

expire_envelopes runs in the background scheduler thread while catch_envelope runs in the main polling thread. The store_remains used to credit the creator is derived from the initial find cursor result, but between that read and the find_one_and_update, a concurrent catch_envelope call can reduce remains. The find_one_and_update uses ReturnDocument.AFTER (where remains is already zeroed), so the stale store_remains is used for the refund. This over-credits the creator, effectively creating funds out of thin air.

Additional Locations (1)

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants