diff --git a/.gitignore b/.gitignore index 6f87a777..3e3da891 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,7 @@ ios_encrypt.md closer-app-22014-firebase-adminsdk-fbsvc-ed20bf6003.json DAILY_FUN_IMPLEMENTATION_BATCH_PLAN.md gitleaks-after.json +docs/img/qa.jpeg +docs/img/sam.jpg +ClaudeReport.md +ClaudeReport.md diff --git a/app/src/main/java/app/closer/notifications/NotificationRateLimiter.kt b/app/src/main/java/app/closer/notifications/NotificationRateLimiter.kt index f0179325..30e203f9 100644 --- a/app/src/main/java/app/closer/notifications/NotificationRateLimiter.kt +++ b/app/src/main/java/app/closer/notifications/NotificationRateLimiter.kt @@ -8,10 +8,15 @@ import java.util.concurrent.TimeUnit /** * Persisted rate limiter for partner-trigger and reminder notifications. * - * Limits: - * - 2 partner-trigger notifications per day - * - 1 reminder notification per day - * - 4 total notifications per week + * Limits (sized for a couples app where partner activity is the core loop — the old 2/day + + * 4/week caps suppressed legitimate game start/finish and partner-action notifications after a + * single game; these are anti-runaway ceilings, not gentle-nudge throttles): + * - 20 partner-trigger notifications per day + * - 1 reminder notification per day (proactive nudges stay gentle) + * - 100 total notifications per week + * + * Note: chat messages are NOT throttled here — foreground messages show the in-app bubble and + * backgrounded ones are displayed by the OS from the FCM notification block, both bypassing this. * * Counts are stored in [SharedPreferences] and reset when a new day or week starts. */ @@ -30,9 +35,9 @@ class NotificationRateLimiter(context: Context) { private const val KEY_REMINDER_COUNT = "reminder_count" private const val KEY_TOTAL_COUNT = "total_count" - const val MAX_PARTNER_PER_DAY = 2 + const val MAX_PARTNER_PER_DAY = 20 const val MAX_REMINDER_PER_DAY = 1 - const val MAX_TOTAL_PER_WEEK = 4 + const val MAX_TOTAL_PER_WEEK = 100 } /** diff --git a/firestore.rules b/firestore.rules index a20ce2c1..be87eeea 100644 --- a/firestore.rules +++ b/firestore.rules @@ -376,11 +376,20 @@ service cloud.firestore { // Discussion messages: any couple member can read, but only the author can write/update/delete match /messages/{messageId} { allow read: if isCouplesMember(coupleId); + // Text messages carry ciphertext in `text`; image messages carry only a `mediaUrl` + // pointing at the encrypted bytes in Storage (the photo itself is E2E-encrypted). allow create: if isCouplesMember(coupleId) && coupleEncryptionEnabled(coupleId) && request.resource.data.authorUserId == request.auth.uid - && request.resource.data.keys().hasOnly(['authorUserId', 'text', 'createdAt']) - && isCiphertext(request.resource.data.text); + && request.resource.data.keys().hasOnly(['authorUserId', 'text', 'createdAt', 'type', 'mediaUrl']) + && ( + (request.resource.data.get('type', 'text') == 'image' + && request.resource.data.mediaUrl is string + && request.resource.data.mediaUrl.size() > 0) + || + (request.resource.data.get('type', 'text') == 'text' + && isCiphertext(request.resource.data.text)) + ); allow update: if isCouplesMember(coupleId) && coupleEncryptionEnabled(coupleId) && resource.data.authorUserId == request.auth.uid diff --git a/storage.rules b/storage.rules index 0d24e27f..0815bd36 100644 --- a/storage.rules +++ b/storage.rules @@ -20,6 +20,16 @@ service firebase.storage { allow read: if request.auth != null && request.auth.uid == uid; } + // Encrypted chat media: the author writes under their own path (already E2E-encrypted + // ciphertext, so Storage never holds anything readable). The partner reads via the tokenized + // download URL, which bypasses these rules — same model as profile photos. 15 MB cap. + match /users/{uid}/chat_media/{file} { + allow write: if request.auth != null + && request.auth.uid == uid + && request.resource.size < 15 * 1024 * 1024; + allow read: if request.auth != null && request.auth.uid == uid; + } + // Deny all other paths by default. match /{allPaths=**} { allow read, write: if false;