Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | 35x 35x 35x 35x 35x 35x 27x 14x 14x 9x 9x 14x 17x 38x 27x 27x 27x 27x 27x 26x 26x 19x 19x 26x 26x 26x 26x 26x 26x 8x 35x | const { db, rtdb, FieldValue } = require('./firebase');
const { generateId, now } = require('./helpers');
const log = require('./log');
const SYSTEM_UID = 'SHYTALK_SYSTEM';
const SYSTEM_DISPLAY_NAME = 'ShyTalk System';
// Process-lifetime cache: the system-user doc is created at most once per
// process lifetime. Without this, every `sendSystemPm` call (and there are
// many — every age-verification decision, mod warning, admin device action)
// burns one Firestore read for a no-op existence check on Spark-tier quota.
// Reset on cold-start; first call after process restart re-validates.
let systemUserEnsured = false;
async function ensureSystemUser() {
if (systemUserEnsured) return;
const snap = await db.doc(`users/${SYSTEM_UID}`).get();
if (!snap.exists) {
const timestamp = now();
await db.doc(`users/${SYSTEM_UID}`).set({
id: SYSTEM_UID,
displayName: SYSTEM_DISPLAY_NAME,
userType: 'SYSTEM',
createdAt: timestamp,
lastSeenAt: timestamp,
});
}
systemUserEnsured = true;
}
// Test-only — reset the cache between unit-test runs so each test starts
// with a clean slate. Production callers should never invoke this.
function _resetSystemUserCache() {
systemUserEnsured = false;
}
function systemConversationId(recipientUid) {
return [recipientUid, SYSTEM_UID].sort((a, b) => String(a).localeCompare(String(b))).join('_');
}
async function sendSystemPm(recipientUid, text) {
await ensureSystemUser();
const convId = systemConversationId(recipientUid);
const timestamp = now();
const msgId = generateId();
const convSnap = await db.doc(`conversations/${convId}`).get();
const convData = {
id: convId,
isGroup: false,
participantIds: convSnap.exists
? convSnap.data().participantIds || [recipientUid, SYSTEM_UID]
: [recipientUid, SYSTEM_UID],
lastMessage: {
text,
senderId: SYSTEM_UID,
senderName: SYSTEM_DISPLAY_NAME,
type: 'SYSTEM',
createdAt: timestamp,
},
lastMessageAt: timestamp,
};
if (!convSnap.exists) {
convData.createdAt = timestamp;
// SHY-0132 — stamp false so the system-PM thread matches the conversations
// segregation filter `where('crossCohortAtMigration','==', false)`. A system PM
// (user ↔ SYSTEM_UID) is never cross-cohort, so false is always correct; set it
// only on create so a hypothetical existing value is never overwritten.
convData.crossCohortAtMigration = false;
}
await db.doc(`conversations/${convId}`).set(convData, { merge: true });
await db.doc(`conversations/${convId}/messages/${msgId}`).set({
id: msgId,
conversationId: convId,
senderId: SYSTEM_UID,
senderName: SYSTEM_DISPLAY_NAME,
text,
type: 'SYSTEM',
createdAt: timestamp,
});
const settingsPath = `conversations/${convId}/userSettings/${recipientUid}`;
await db.doc(settingsPath).set(
{
userId: recipientUid,
conversationId: convId,
unreadCount: FieldValue.increment(1),
isHidden: false,
},
{ merge: true },
);
try {
await rtdb.ref(`conversations/${convId}/events/lastEvent`).set({
type: 'new_message',
ts: Date.now(),
});
} catch (err) {
log.warn('system-pm', 'Failed to write RTDB event', { convId, error: err.message });
}
}
module.exports = { sendSystemPm, SYSTEM_UID, systemConversationId, _resetSystemUserCache };
|