All files / src/utils system-pm.js

100% Statements 33/33
100% Branches 10/10
100% Functions 5/5
100% Lines 31/31

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 10635x 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 };