chore(functions): rebuild dist for the restore cleanup + alert-precision batches
dist/ is what firebase deploy ships (no predeploy hook), so the compiled output travels with the source it came from. Contains cleanupRestoreRequests, queueAndPush, and the excludeTokens seam. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
2dd09d5d0d
commit
b47be4e34c
|
|
@ -0,0 +1,159 @@
|
||||||
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||||
|
}
|
||||||
|
Object.defineProperty(o, k2, desc);
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
|
var __importStar = (this && this.__importStar) || (function () {
|
||||||
|
var ownKeys = function(o) {
|
||||||
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||||
|
var ar = [];
|
||||||
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||||
|
return ar;
|
||||||
|
};
|
||||||
|
return ownKeys(o);
|
||||||
|
};
|
||||||
|
return function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.cleanupExpiredRestoreRequests = exports.NOTIFY_WINDOW_MS = exports.FALLBACK_TTL_MS = exports.GRACE_MS = void 0;
|
||||||
|
exports.shouldReapRestoreRequest = shouldReapRestoreRequest;
|
||||||
|
exports.shouldNotifyExpiry = shouldNotifyExpiry;
|
||||||
|
exports.sweepExpiredRestoreRequests = sweepExpiredRestoreRequests;
|
||||||
|
const admin = __importStar(require("firebase-admin"));
|
||||||
|
const scheduler_1 = require("firebase-functions/v2/scheduler");
|
||||||
|
const queueAndPush_1 = require("../notifications/queueAndPush");
|
||||||
|
const log_1 = require("../log");
|
||||||
|
/**
|
||||||
|
* Cloud Function: cleanupExpiredRestoreRequests
|
||||||
|
*
|
||||||
|
* A `restore_requests` doc left behind (partner wrapped the couple key but the recipient never
|
||||||
|
* completed, or nobody ever answered) keeps its ECIES `keybox` — ciphertext sealed only to the
|
||||||
|
* recipient, useless to anyone else, but key material has no business lying around. Expiry is
|
||||||
|
* otherwise enforced only client-side (fulfil-time check, delete-before-re-request, delete-on-
|
||||||
|
* complete), so a request whose device disappeared lives forever.
|
||||||
|
*
|
||||||
|
* Hourly sweep: requests expire 30 minutes after creation, so this bounds a stranded keybox's life
|
||||||
|
* to ~1.5 h. Uses a collectionGroup query on purpose — it also reaps requests orphaned under
|
||||||
|
* already-deleted couple docs, which an iterate-the-couples sweep can never see (deleted parents
|
||||||
|
* don't list). Needs the COLLECTION_GROUP fieldOverride on `restore_requests.expiresAt` in
|
||||||
|
* `firestore.indexes.json`.
|
||||||
|
*
|
||||||
|
* Admin SDK bypasses the recipient-only delete rule, which is the point: nobody else CAN clean these.
|
||||||
|
*/
|
||||||
|
/** Never race a restore that is mid-completion: the client treats these as expired well before us. */
|
||||||
|
exports.GRACE_MS = 5 * 60 * 1000;
|
||||||
|
/** Reap docs with no usable `expiresAt` once they are unambiguously ancient. */
|
||||||
|
exports.FALLBACK_TTL_MS = 24 * 60 * 60 * 1000;
|
||||||
|
/**
|
||||||
|
* Only tell the requester about expiries that just happened (~2 sweep cycles). Without this bound,
|
||||||
|
* the first deploy would blast a notification for every ancient doc in the backlog at once.
|
||||||
|
*/
|
||||||
|
exports.NOTIFY_WINDOW_MS = 2 * 60 * 60 * 1000;
|
||||||
|
const asMillis = (v) => (typeof v === 'number' && v > 0 ? v : 0);
|
||||||
|
/**
|
||||||
|
* Pure reap decision, re-verified per doc even though the query already filtered — the query is an
|
||||||
|
* index scan, this is the contract. Deletes only on positive evidence of staleness:
|
||||||
|
* a real `expiresAt` past the grace window, or (defensively — the create rule doesn't validate field
|
||||||
|
* values) no usable `expiresAt` but a `createdAt` at least a day old. A doc with neither is left
|
||||||
|
* alone for a human to wonder about.
|
||||||
|
*/
|
||||||
|
function shouldReapRestoreRequest(data, nowMs) {
|
||||||
|
const expiresAt = asMillis(data.expiresAt);
|
||||||
|
if (expiresAt > 0)
|
||||||
|
return nowMs > expiresAt + exports.GRACE_MS;
|
||||||
|
const createdAt = asMillis(data.createdAt);
|
||||||
|
return createdAt > 0 && nowMs > createdAt + exports.FALLBACK_TTL_MS;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Pure notify decision: only for a request that expired *recently* while still waiting on someone
|
||||||
|
* (REQUESTED: partner never approved; READY: recipient never completed). Terminal states are debris —
|
||||||
|
* RESTORED already succeeded and DECLINED was answered; telling anyone again is noise.
|
||||||
|
*/
|
||||||
|
function shouldNotifyExpiry(data, nowMs) {
|
||||||
|
const expiresAt = asMillis(data.expiresAt);
|
||||||
|
if (expiresAt <= 0 || nowMs <= expiresAt + exports.GRACE_MS)
|
||||||
|
return false;
|
||||||
|
if (nowMs - expiresAt > exports.NOTIFY_WINDOW_MS)
|
||||||
|
return false;
|
||||||
|
return data.status === 'REQUESTED' || data.status === 'READY';
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* The sweep, with `db` injectable for tests. Per-doc failures are isolated (one bad doc can't stop
|
||||||
|
* the pass); the doc is deleted BEFORE the requester is notified so a notify failure costs a nudge,
|
||||||
|
* never a duplicate (the doc it would re-trigger on is already gone).
|
||||||
|
*/
|
||||||
|
async function sweepExpiredRestoreRequests(db, nowMs) {
|
||||||
|
const snap = await db
|
||||||
|
.collectionGroup('restore_requests')
|
||||||
|
.where('expiresAt', '<=', nowMs - exports.GRACE_MS)
|
||||||
|
.limit(200)
|
||||||
|
.get();
|
||||||
|
const counts = { scanned: snap.size, reaped: 0, notified: 0, skipped: 0, failed: 0 };
|
||||||
|
const results = await Promise.allSettled(snap.docs.map(async (doc) => {
|
||||||
|
var _a;
|
||||||
|
const data = ((_a = doc.data()) !== null && _a !== void 0 ? _a : {});
|
||||||
|
if (!shouldReapRestoreRequest(data, nowMs)) {
|
||||||
|
counts.skipped++;
|
||||||
|
log_1.logger.warn('[cleanupRestoreRequests] query hit failed re-verify; leaving doc', { path: doc.ref.path });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const notify = shouldNotifyExpiry(data, nowMs);
|
||||||
|
await doc.ref.delete();
|
||||||
|
counts.reaped++;
|
||||||
|
if (!notify)
|
||||||
|
return;
|
||||||
|
// Best-effort: a missed nudge is fine, a failed delete is not — hence delete-first above.
|
||||||
|
// Skip when the couple itself is gone (an orphan reap has nobody meaningful to notify).
|
||||||
|
try {
|
||||||
|
const coupleRef = doc.ref.parent.parent;
|
||||||
|
const recipientUid = typeof data.recipientUid === 'string' && data.recipientUid ? data.recipientUid : doc.id;
|
||||||
|
if (!coupleRef)
|
||||||
|
return;
|
||||||
|
const coupleDoc = await coupleRef.get();
|
||||||
|
if (!coupleDoc.exists)
|
||||||
|
return;
|
||||||
|
await (0, queueAndPush_1.queueAndPush)(db, recipientUid, {
|
||||||
|
type: 'restore_request_expired',
|
||||||
|
title: 'Your restore request expired',
|
||||||
|
body: 'No worries — start a new one whenever you’re ready.',
|
||||||
|
coupleId: coupleRef.id,
|
||||||
|
});
|
||||||
|
counts.notified++;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
log_1.logger.warn('[cleanupRestoreRequests] expiry notice failed', { path: doc.ref.path, error: String(e) });
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
counts.failed = results.filter((r) => r.status === 'rejected').length;
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
|
exports.cleanupExpiredRestoreRequests = (0, scheduler_1.onSchedule)({ schedule: 'every 1 hours', timeoutSeconds: 180 }, async () => {
|
||||||
|
try {
|
||||||
|
const counts = await sweepExpiredRestoreRequests(admin.firestore(), Date.now());
|
||||||
|
log_1.logger.log(`[cleanupRestoreRequests] scanned ${counts.scanned}; reaped ${counts.reaped}; ` +
|
||||||
|
`notified ${counts.notified}; skipped ${counts.skipped}; failed ${counts.failed}`);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
// Never rethrow: a scheduled-function failure retries in a storm, and the next hourly run IS the retry.
|
||||||
|
log_1.logger.error('[cleanupRestoreRequests] sweep failed', { error: String(e) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
//# sourceMappingURL=cleanupRestoreRequests.js.map
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"cleanupRestoreRequests.js","sourceRoot":"","sources":["../../src/backup/cleanupRestoreRequests.ts"],"names":[],"mappings":"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"}
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const cleanupRestoreRequests_1 = require("./cleanupRestoreRequests");
|
||||||
|
const queueAndPush_1 = require("../notifications/queueAndPush");
|
||||||
|
jest.mock('../notifications/queueAndPush', () => ({ queueAndPush: jest.fn().mockResolvedValue(undefined) }));
|
||||||
|
const NOW = 1800000000000;
|
||||||
|
describe('shouldReapRestoreRequest', () => {
|
||||||
|
it('keeps a fresh request', () => {
|
||||||
|
expect((0, cleanupRestoreRequests_1.shouldReapRestoreRequest)({ status: 'REQUESTED', expiresAt: NOW + 60000 }, NOW)).toBe(false);
|
||||||
|
});
|
||||||
|
it('keeps an expired request still inside the grace window', () => {
|
||||||
|
// The client already treats it as expired; we wait out the grace so we can never race a
|
||||||
|
// mid-completion restore.
|
||||||
|
expect((0, cleanupRestoreRequests_1.shouldReapRestoreRequest)({ status: 'READY', expiresAt: NOW - cleanupRestoreRequests_1.GRACE_MS + 1000 }, NOW)).toBe(false);
|
||||||
|
});
|
||||||
|
it('reaps expired requests regardless of status — DECLINED-after-READY still carries the keybox', () => {
|
||||||
|
const expiresAt = NOW - cleanupRestoreRequests_1.GRACE_MS - 1000;
|
||||||
|
for (const status of ['REQUESTED', 'READY', 'DECLINED', 'RESTORED', 'EXPIRED']) {
|
||||||
|
expect((0, cleanupRestoreRequests_1.shouldReapRestoreRequest)({ status, expiresAt }, NOW)).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('falls back to createdAt when expiresAt is unusable', () => {
|
||||||
|
expect((0, cleanupRestoreRequests_1.shouldReapRestoreRequest)({ createdAt: NOW - cleanupRestoreRequests_1.FALLBACK_TTL_MS - 1 }, NOW)).toBe(true);
|
||||||
|
expect((0, cleanupRestoreRequests_1.shouldReapRestoreRequest)({ expiresAt: 0, createdAt: NOW - cleanupRestoreRequests_1.FALLBACK_TTL_MS - 1 }, NOW)).toBe(true);
|
||||||
|
expect((0, cleanupRestoreRequests_1.shouldReapRestoreRequest)({ createdAt: NOW - 60000 }, NOW)).toBe(false);
|
||||||
|
});
|
||||||
|
it('leaves a doc with neither field — delete only on positive evidence', () => {
|
||||||
|
expect((0, cleanupRestoreRequests_1.shouldReapRestoreRequest)({}, NOW)).toBe(false);
|
||||||
|
expect((0, cleanupRestoreRequests_1.shouldReapRestoreRequest)({ expiresAt: 'soon', createdAt: null }, NOW)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('shouldNotifyExpiry', () => {
|
||||||
|
const justExpired = NOW - cleanupRestoreRequests_1.GRACE_MS - 60000;
|
||||||
|
it('notifies for a recently expired request still waiting on someone', () => {
|
||||||
|
expect((0, cleanupRestoreRequests_1.shouldNotifyExpiry)({ status: 'REQUESTED', expiresAt: justExpired }, NOW)).toBe(true);
|
||||||
|
expect((0, cleanupRestoreRequests_1.shouldNotifyExpiry)({ status: 'READY', expiresAt: justExpired }, NOW)).toBe(true);
|
||||||
|
});
|
||||||
|
it('stays silent for terminal states — RESTORED succeeded, DECLINED was answered', () => {
|
||||||
|
expect((0, cleanupRestoreRequests_1.shouldNotifyExpiry)({ status: 'RESTORED', expiresAt: justExpired }, NOW)).toBe(false);
|
||||||
|
expect((0, cleanupRestoreRequests_1.shouldNotifyExpiry)({ status: 'DECLINED', expiresAt: justExpired }, NOW)).toBe(false);
|
||||||
|
});
|
||||||
|
it('stays silent for backlog debris — first deploy must not blast ancient expiries', () => {
|
||||||
|
expect((0, cleanupRestoreRequests_1.shouldNotifyExpiry)({ status: 'REQUESTED', expiresAt: NOW - cleanupRestoreRequests_1.NOTIFY_WINDOW_MS - cleanupRestoreRequests_1.GRACE_MS - 1000 }, NOW)).toBe(false);
|
||||||
|
});
|
||||||
|
it('stays silent for the createdAt-fallback branch (no real expiry to report)', () => {
|
||||||
|
expect((0, cleanupRestoreRequests_1.shouldNotifyExpiry)({ status: 'REQUESTED', createdAt: NOW - cleanupRestoreRequests_1.FALLBACK_TTL_MS - 1 }, NOW)).toBe(false);
|
||||||
|
});
|
||||||
|
it('never notifies for something it would not reap', () => {
|
||||||
|
expect((0, cleanupRestoreRequests_1.shouldNotifyExpiry)({ status: 'REQUESTED', expiresAt: NOW + 60000 }, NOW)).toBe(false);
|
||||||
|
expect((0, cleanupRestoreRequests_1.shouldNotifyExpiry)({ status: 'REQUESTED', expiresAt: NOW - cleanupRestoreRequests_1.GRACE_MS + 1000 }, NOW)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('sweepExpiredRestoreRequests', () => {
|
||||||
|
const mockQueueAndPush = queueAndPush_1.queueAndPush;
|
||||||
|
function fakeDoc(opts) {
|
||||||
|
var _a;
|
||||||
|
const coupleRef = {
|
||||||
|
id: `couple_${opts.id}`,
|
||||||
|
get: jest.fn().mockResolvedValue({ exists: (_a = opts.coupleExists) !== null && _a !== void 0 ? _a : true }),
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
id: opts.id,
|
||||||
|
data: () => opts.data,
|
||||||
|
ref: {
|
||||||
|
path: `couples/couple_${opts.id}/restore_requests/${opts.id}`,
|
||||||
|
parent: { parent: coupleRef },
|
||||||
|
delete: opts.deleteError
|
||||||
|
? jest.fn().mockRejectedValue(opts.deleteError)
|
||||||
|
: jest.fn().mockResolvedValue(undefined),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function fakeDb(docs) {
|
||||||
|
return {
|
||||||
|
collectionGroup: jest.fn().mockReturnValue({
|
||||||
|
where: jest.fn().mockReturnValue({
|
||||||
|
limit: jest.fn().mockReturnValue({
|
||||||
|
get: jest.fn().mockResolvedValue({ size: docs.length, docs }),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
beforeEach(() => mockQueueAndPush.mockClear());
|
||||||
|
it('deletes the expired doc and nudges the requester', async () => {
|
||||||
|
const doc = fakeDoc({ id: 'uidA', data: { status: 'READY', recipientUid: 'uidA', expiresAt: NOW - cleanupRestoreRequests_1.GRACE_MS - 60000 } });
|
||||||
|
const counts = await (0, cleanupRestoreRequests_1.sweepExpiredRestoreRequests)(fakeDb([doc]), NOW);
|
||||||
|
expect(doc.ref.delete).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockQueueAndPush).toHaveBeenCalledWith(expect.anything(), 'uidA', expect.objectContaining({ type: 'restore_request_expired' }));
|
||||||
|
expect(counts).toMatchObject({ scanned: 1, reaped: 1, notified: 1, failed: 0 });
|
||||||
|
});
|
||||||
|
it('leaves a query hit that fails re-verify', async () => {
|
||||||
|
// The index says expired; the doc says otherwise (clock skew, weird data). The predicate wins.
|
||||||
|
const doc = fakeDoc({ id: 'uidB', data: { status: 'REQUESTED', expiresAt: NOW + 60000 } });
|
||||||
|
const counts = await (0, cleanupRestoreRequests_1.sweepExpiredRestoreRequests)(fakeDb([doc]), NOW);
|
||||||
|
expect(doc.ref.delete).not.toHaveBeenCalled();
|
||||||
|
expect(counts).toMatchObject({ reaped: 0, skipped: 1 });
|
||||||
|
});
|
||||||
|
it('skips the nudge for an orphan reap (couple gone) but still deletes', async () => {
|
||||||
|
const doc = fakeDoc({
|
||||||
|
id: 'uidC',
|
||||||
|
data: { status: 'READY', expiresAt: NOW - cleanupRestoreRequests_1.GRACE_MS - 60000 },
|
||||||
|
coupleExists: false,
|
||||||
|
});
|
||||||
|
const counts = await (0, cleanupRestoreRequests_1.sweepExpiredRestoreRequests)(fakeDb([doc]), NOW);
|
||||||
|
expect(doc.ref.delete).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockQueueAndPush).not.toHaveBeenCalled();
|
||||||
|
expect(counts).toMatchObject({ reaped: 1, notified: 0 });
|
||||||
|
});
|
||||||
|
it('one failing doc never stops the sweep', async () => {
|
||||||
|
const bad = fakeDoc({
|
||||||
|
id: 'uidD',
|
||||||
|
data: { status: 'READY', expiresAt: NOW - cleanupRestoreRequests_1.GRACE_MS - 60000 },
|
||||||
|
deleteError: new Error('firestore unavailable'),
|
||||||
|
});
|
||||||
|
const good = fakeDoc({ id: 'uidE', data: { status: 'REQUESTED', recipientUid: 'uidE', expiresAt: NOW - cleanupRestoreRequests_1.GRACE_MS - 60000 } });
|
||||||
|
const counts = await (0, cleanupRestoreRequests_1.sweepExpiredRestoreRequests)(fakeDb([bad, good]), NOW);
|
||||||
|
expect(good.ref.delete).toHaveBeenCalledTimes(1);
|
||||||
|
expect(counts).toMatchObject({ scanned: 2, reaped: 1, failed: 1 });
|
||||||
|
});
|
||||||
|
it('a notify failure costs the nudge, never the reap or the pass', async () => {
|
||||||
|
mockQueueAndPush.mockRejectedValueOnce(new Error('fcm down'));
|
||||||
|
const doc = fakeDoc({ id: 'uidF', data: { status: 'READY', recipientUid: 'uidF', expiresAt: NOW - cleanupRestoreRequests_1.GRACE_MS - 60000 } });
|
||||||
|
const counts = await (0, cleanupRestoreRequests_1.sweepExpiredRestoreRequests)(fakeDb([doc]), NOW);
|
||||||
|
expect(doc.ref.delete).toHaveBeenCalledTimes(1);
|
||||||
|
expect(counts).toMatchObject({ reaped: 1, notified: 0, failed: 0 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
//# sourceMappingURL=cleanupRestoreRequests.test.js.map
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -38,10 +38,8 @@ exports.selfAlertAllowed = selfAlertAllowed;
|
||||||
exports.isRestoreReadyTransition = isRestoreReadyTransition;
|
exports.isRestoreReadyTransition = isRestoreReadyTransition;
|
||||||
const admin = __importStar(require("firebase-admin"));
|
const admin = __importStar(require("firebase-admin"));
|
||||||
const firestore_1 = require("firebase-functions/v2/firestore");
|
const firestore_1 = require("firebase-functions/v2/firestore");
|
||||||
const quietHours_1 = require("../notifications/quietHours");
|
const queueAndPush_1 = require("../notifications/queueAndPush");
|
||||||
const push_1 = require("../notifications/push");
|
|
||||||
const log_1 = require("../log");
|
const log_1 = require("../log");
|
||||||
const CHANNEL = 'partner_activity';
|
|
||||||
// Suppress duplicate pushes when a request doc is rapidly deleted+recreated (a compromised account
|
// Suppress duplicate pushes when a request doc is rapidly deleted+recreated (a compromised account
|
||||||
// could loop that to spam both partners) and when an event is redelivered. The in-app queue entry is
|
// could loop that to spam both partners) and when an event is redelivered. The in-app queue entry is
|
||||||
// still written every time. A genuine re-request after the window still notifies — so this is a time
|
// still written every time. A genuine re-request after the window still notifies — so this is a time
|
||||||
|
|
@ -57,27 +55,6 @@ function selfAlertAllowed(lastAlertAt, now) {
|
||||||
function isRestoreReadyTransition(beforeStatus, afterStatus) {
|
function isRestoreReadyTransition(beforeStatus, afterStatus) {
|
||||||
return afterStatus === 'READY' && beforeStatus !== 'READY';
|
return afterStatus === 'READY' && beforeStatus !== 'READY';
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Write the durable in-app queue entry (always) and — unless quiet hours suppress it — push to every device.
|
|
||||||
* `bypassQuietHours` is for security signals (the "was this you?" self-alert) that must not be silenced.
|
|
||||||
*/
|
|
||||||
async function queueAndPush(db, uid, opts) {
|
|
||||||
const { type, title, body, coupleId, bypassQuietHours } = opts;
|
|
||||||
await db.collection('users').doc(uid).collection('notification_queue').add({
|
|
||||||
type, title, body, read: false, createdAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
||||||
});
|
|
||||||
const userDoc = await db.collection('users').doc(uid).get();
|
|
||||||
if (!userDoc.exists)
|
|
||||||
return;
|
|
||||||
const userData = userDoc.data();
|
|
||||||
if (!bypassQuietHours && (0, quietHours_1.recipientInQuietHours)(userData))
|
|
||||||
return;
|
|
||||||
await (0, push_1.sendPushToUser)(db, admin.messaging(), uid, {
|
|
||||||
notification: { title, body },
|
|
||||||
data: { type, couple_id: coupleId },
|
|
||||||
android: { notification: { channelId: CHANNEL } },
|
|
||||||
}, userData);
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Fires when a member starts a partner-assisted restore
|
* Fires when a member starts a partner-assisted restore
|
||||||
* (`couples/{coupleId}/restore_requests/{recipientUid}` created on a new/wiped device). Sends TWO
|
* (`couples/{coupleId}/restore_requests/{recipientUid}` created on a new/wiped device). Sends TWO
|
||||||
|
|
@ -88,14 +65,18 @@ async function queueAndPush(db, uid, opts) {
|
||||||
* No key material is read or logged — the request carries only a public key + a nonce.
|
* No key material is read or logged — the request carries only a public key + a nonce.
|
||||||
*/
|
*/
|
||||||
exports.onRestoreRequested = (0, firestore_1.onDocumentCreated)('couples/{coupleId}/restore_requests/{recipientUid}', async (event) => {
|
exports.onRestoreRequested = (0, firestore_1.onDocumentCreated)('couples/{coupleId}/restore_requests/{recipientUid}', async (event) => {
|
||||||
var _a, _b, _c, _d;
|
var _a, _b, _c, _d, _e, _f;
|
||||||
const { coupleId, recipientUid } = event.params;
|
const { coupleId, recipientUid } = event.params;
|
||||||
const db = admin.firestore();
|
const db = admin.firestore();
|
||||||
|
// The device that created the request identifies itself (create-only field, best-effort on the
|
||||||
|
// client). Used ONLY to spare that device its own "was this you?" — all other devices still get it.
|
||||||
|
const requesterToken = (_b = (_a = event.data) === null || _a === void 0 ? void 0 : _a.data()) === null || _b === void 0 ? void 0 : _b.requesterFcmToken;
|
||||||
|
const excludeTokens = typeof requesterToken === 'string' && requesterToken ? [requesterToken] : undefined;
|
||||||
const coupleRef = db.collection('couples').doc(coupleId);
|
const coupleRef = db.collection('couples').doc(coupleId);
|
||||||
const coupleDoc = await coupleRef.get();
|
const coupleDoc = await coupleRef.get();
|
||||||
if (!coupleDoc.exists)
|
if (!coupleDoc.exists)
|
||||||
return;
|
return;
|
||||||
const userIds = ((_b = (_a = coupleDoc.data()) === null || _a === void 0 ? void 0 : _a.userIds) !== null && _b !== void 0 ? _b : []);
|
const userIds = ((_d = (_c = coupleDoc.data()) === null || _c === void 0 ? void 0 : _c.userIds) !== null && _d !== void 0 ? _d : []);
|
||||||
// The recipient must be a member; notify the OTHER member (the partner who can help).
|
// The recipient must be a member; notify the OTHER member (the partner who can help).
|
||||||
if (!userIds.includes(recipientUid))
|
if (!userIds.includes(recipientUid))
|
||||||
return;
|
return;
|
||||||
|
|
@ -108,9 +89,9 @@ exports.onRestoreRequested = (0, firestore_1.onDocumentCreated)('couples/{couple
|
||||||
// request recreate-loops with the same window used for the self-alert.
|
// request recreate-loops with the same window used for the self-alert.
|
||||||
try {
|
try {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (selfAlertAllowed((_c = coupleDoc.data()) === null || _c === void 0 ? void 0 : _c.lastRestorePartnerAlertAt, now)) {
|
if (selfAlertAllowed((_e = coupleDoc.data()) === null || _e === void 0 ? void 0 : _e.lastRestorePartnerAlertAt, now)) {
|
||||||
await coupleRef.set({ lastRestorePartnerAlertAt: now }, { merge: true });
|
await coupleRef.set({ lastRestorePartnerAlertAt: now }, { merge: true });
|
||||||
await queueAndPush(db, partnerId, {
|
await (0, queueAndPush_1.queueAndPush)(db, partnerId, {
|
||||||
type: 'restore_requested',
|
type: 'restore_requested',
|
||||||
title: 'Help your partner restore 💜',
|
title: 'Help your partner restore 💜',
|
||||||
body: 'They’re setting up on a new device. Tap to help.',
|
body: 'They’re setting up on a new device. Tap to help.',
|
||||||
|
|
@ -124,14 +105,15 @@ exports.onRestoreRequested = (0, firestore_1.onDocumentCreated)('couples/{couple
|
||||||
// 2) Recipient self-alert — security signal, bypasses quiet hours, deduped against create-loops.
|
// 2) Recipient self-alert — security signal, bypasses quiet hours, deduped against create-loops.
|
||||||
try {
|
try {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (selfAlertAllowed((_d = coupleDoc.data()) === null || _d === void 0 ? void 0 : _d.lastRestoreSelfAlertAt, now)) {
|
if (selfAlertAllowed((_f = coupleDoc.data()) === null || _f === void 0 ? void 0 : _f.lastRestoreSelfAlertAt, now)) {
|
||||||
await coupleRef.set({ lastRestoreSelfAlertAt: now }, { merge: true });
|
await coupleRef.set({ lastRestoreSelfAlertAt: now }, { merge: true });
|
||||||
await queueAndPush(db, recipientUid, {
|
await (0, queueAndPush_1.queueAndPush)(db, recipientUid, {
|
||||||
type: 'restore_self_alert',
|
type: 'restore_self_alert',
|
||||||
title: 'New device is restoring your history',
|
title: 'New device is restoring your history',
|
||||||
body: 'If this wasn’t you, secure your account now.',
|
body: 'If this wasn’t you, secure your account now.',
|
||||||
coupleId,
|
coupleId,
|
||||||
bypassQuietHours: true,
|
bypassQuietHours: true,
|
||||||
|
excludeTokens,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -150,6 +132,8 @@ exports.onRestoreFulfilled = (0, firestore_1.onDocumentUpdated)('couples/{couple
|
||||||
const after = (_b = event.data) === null || _b === void 0 ? void 0 : _b.after.data();
|
const after = (_b = event.data) === null || _b === void 0 ? void 0 : _b.after.data();
|
||||||
if (!isRestoreReadyTransition(before === null || before === void 0 ? void 0 : before.status, after === null || after === void 0 ? void 0 : after.status))
|
if (!isRestoreReadyTransition(before === null || before === void 0 ? void 0 : before.status, after === null || after === void 0 ? void 0 : after.status))
|
||||||
return;
|
return;
|
||||||
|
const requesterToken = after === null || after === void 0 ? void 0 : after.requesterFcmToken;
|
||||||
|
const excludeTokens = typeof requesterToken === 'string' && requesterToken ? [requesterToken] : undefined;
|
||||||
const { coupleId, recipientUid } = event.params;
|
const { coupleId, recipientUid } = event.params;
|
||||||
const db = admin.firestore();
|
const db = admin.firestore();
|
||||||
const coupleDoc = await db.collection('couples').doc(coupleId).get();
|
const coupleDoc = await db.collection('couples').doc(coupleId).get();
|
||||||
|
|
@ -158,12 +142,13 @@ exports.onRestoreFulfilled = (0, firestore_1.onDocumentUpdated)('couples/{couple
|
||||||
return;
|
return;
|
||||||
log_1.logger.log(`[onRestoreFulfilled] couple=${coupleId} recipient=${recipientUid}`);
|
log_1.logger.log(`[onRestoreFulfilled] couple=${coupleId} recipient=${recipientUid}`);
|
||||||
try {
|
try {
|
||||||
await queueAndPush(db, recipientUid, {
|
await (0, queueAndPush_1.queueAndPush)(db, recipientUid, {
|
||||||
type: 'restore_self_alert',
|
type: 'restore_self_alert',
|
||||||
title: 'Your history was just restored',
|
title: 'Your history was just restored',
|
||||||
body: 'A new device now has access. If this wasn’t you, secure your account now.',
|
body: 'A new device now has access. If this wasn’t you, secure your account now.',
|
||||||
coupleId,
|
coupleId,
|
||||||
bypassQuietHours: true,
|
bypassQuietHours: true,
|
||||||
|
excludeTokens,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"version":3,"file":"onRestoreRequested.js","sourceRoot":"","sources":["../../src/backup/onRestoreRequested.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"onRestoreRequested.js","sourceRoot":"","sources":["../../src/backup/onRestoreRequested.ts"],"names":[],"mappings":"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"}
|
||||||
|
|
@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.wrapReleaseKeyCallable = exports.onDesireSyncPartFinished = exports.onHowWellPartFinished = exports.onWheelPartFinished = exports.onThisOrThatPartFinished = exports.onGameSessionUpdate = exports.onUserDelete = exports.scheduledOutcomesReminder = exports.aggregateOutcomeStats = exports.submitOutcomeCallable = exports.createInviteCallable = exports.acceptInviteCallable = exports.leaveCoupleCallable = exports.onCoupleLeave = exports.onMessageWritten = exports.onAnswerRevealed = exports.onAnswerWritten = exports.assignDailyQuestionCallable = exports.assignDailyQuestion = exports.onRestoreFulfilled = exports.onRestoreRequested = exports.onDateHistoryCreated = exports.onDateReflectionRevealed = exports.onDateReflectionWritten = exports.notifyOnDateMatch = exports.checkDeviceIntegrity = exports.sendReengagementReminder = exports.sendStreakReminder = exports.sendDailyQuestionProactiveReminder = exports.unlockDueMemoryCapsules = exports.sendChallengeDayReminders = exports.sendThinkingOfYouCallable = exports.sendGentleReminderCallable = exports.onEntitlementChanged = exports.syncEntitlement = exports.revenueCatWebhook = void 0;
|
exports.wrapReleaseKeyCallable = exports.onDesireSyncPartFinished = exports.onHowWellPartFinished = exports.onWheelPartFinished = exports.onThisOrThatPartFinished = exports.onGameSessionUpdate = exports.onUserDelete = exports.scheduledOutcomesReminder = exports.aggregateOutcomeStats = exports.submitOutcomeCallable = exports.createInviteCallable = exports.acceptInviteCallable = exports.leaveCoupleCallable = exports.onCoupleLeave = exports.onMessageWritten = exports.onAnswerRevealed = exports.onAnswerWritten = exports.assignDailyQuestionCallable = exports.assignDailyQuestion = exports.cleanupExpiredRestoreRequests = exports.onRestoreFulfilled = exports.onRestoreRequested = exports.onDateHistoryCreated = exports.onDateReflectionRevealed = exports.onDateReflectionWritten = exports.notifyOnDateMatch = exports.checkDeviceIntegrity = exports.sendReengagementReminder = exports.sendStreakReminder = exports.sendDailyQuestionProactiveReminder = exports.unlockDueMemoryCapsules = exports.sendChallengeDayReminders = exports.sendThinkingOfYouCallable = exports.sendGentleReminderCallable = exports.onEntitlementChanged = exports.syncEntitlement = exports.revenueCatWebhook = void 0;
|
||||||
// Global v2 options (region pin, instance cap) — MUST be imported before any function module so
|
// Global v2 options (region pin, instance cap) — MUST be imported before any function module so
|
||||||
// setGlobalOptions runs before the functions below are defined.
|
// setGlobalOptions runs before the functions below are defined.
|
||||||
require("./options");
|
require("./options");
|
||||||
|
|
@ -81,6 +81,8 @@ Object.defineProperty(exports, "onDateHistoryCreated", { enumerable: true, get:
|
||||||
var onRestoreRequested_1 = require("./backup/onRestoreRequested");
|
var onRestoreRequested_1 = require("./backup/onRestoreRequested");
|
||||||
Object.defineProperty(exports, "onRestoreRequested", { enumerable: true, get: function () { return onRestoreRequested_1.onRestoreRequested; } });
|
Object.defineProperty(exports, "onRestoreRequested", { enumerable: true, get: function () { return onRestoreRequested_1.onRestoreRequested; } });
|
||||||
Object.defineProperty(exports, "onRestoreFulfilled", { enumerable: true, get: function () { return onRestoreRequested_1.onRestoreFulfilled; } });
|
Object.defineProperty(exports, "onRestoreFulfilled", { enumerable: true, get: function () { return onRestoreRequested_1.onRestoreFulfilled; } });
|
||||||
|
var cleanupRestoreRequests_1 = require("./backup/cleanupRestoreRequests");
|
||||||
|
Object.defineProperty(exports, "cleanupExpiredRestoreRequests", { enumerable: true, get: function () { return cleanupRestoreRequests_1.cleanupExpiredRestoreRequests; } });
|
||||||
var assignDailyQuestion_1 = require("./questions/assignDailyQuestion");
|
var assignDailyQuestion_1 = require("./questions/assignDailyQuestion");
|
||||||
Object.defineProperty(exports, "assignDailyQuestion", { enumerable: true, get: function () { return assignDailyQuestion_1.assignDailyQuestion; } });
|
Object.defineProperty(exports, "assignDailyQuestion", { enumerable: true, get: function () { return assignDailyQuestion_1.assignDailyQuestion; } });
|
||||||
Object.defineProperty(exports, "assignDailyQuestionCallable", { enumerable: true, get: function () { return assignDailyQuestion_1.assignDailyQuestionCallable; } });
|
Object.defineProperty(exports, "assignDailyQuestionCallable", { enumerable: true, get: function () { return assignDailyQuestion_1.assignDailyQuestionCallable; } });
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,gGAAgG;AAChG,gEAAgE;AAChE,qBAAkB;AAClB,sDAAuC;AAEvC,qEAAqE;AACrE,8EAA8E;AAC9E,gFAAgF;AAChF,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;IAC5B,KAAK,CAAC,aAAa,EAAE,CAAA;AACvB,CAAC;AAED,mGAAmG;AACnG,8FAA8F;AAC9F,iGAAiG;AACjG,8CAA8C;AAC9C,6DAA6D;AAC7D,iEAA+D;AAAtD,sHAAA,iBAAiB,OAAA;AAC1B,6DAA2D;AAAlD,kHAAA,eAAe,OAAA;AACxB,uEAAqE;AAA5D,4HAAA,oBAAoB,OAAA;AAC7B,yFAAuF;AAA9E,wIAAA,0BAA0B,OAAA;AACnC,uFAAqF;AAA5E,sIAAA,yBAAyB,OAAA;AAClC,+DAGsC;AAFpC,0HAAA,yBAAyB,OAAA;AACzB,wHAAA,uBAAuB,OAAA;AAEzB,+EAA0F;AAAjF,2IAAA,kCAAkC,OAAA;AAC3C,iEAAmE;AAA1D,oHAAA,kBAAkB,OAAA;AAC3B,6DAAuE;AAA9D,wHAAA,wBAAwB,OAAA;AACjC,wEAAsE;AAA7D,4HAAA,oBAAoB,OAAA;AAC7B,2DAA2D;AAAlD,oHAAA,iBAAiB,OAAA;AAC1B,2EAAyE;AAAhE,kIAAA,uBAAuB,OAAA;AAChC,6EAA2E;AAAlE,oIAAA,wBAAwB,OAAA;AACjC,qEAAmE;AAA1D,4HAAA,oBAAoB,OAAA;AAC7B,kEAAoF;AAA3E,wHAAA,kBAAkB,OAAA;AAAE,wHAAA,kBAAkB,OAAA;AAC/C,uEAGwC;AAFtC,0HAAA,mBAAmB,OAAA;AACnB,kIAAA,2BAA2B,OAAA;AAE7B,+DAA6D;AAApD,kHAAA,eAAe,OAAA;AACxB,iEAA+D;AAAtD,oHAAA,gBAAgB,OAAA;AACzB,iEAA+D;AAAtD,oHAAA,gBAAgB,OAAA;AACzB,yDAAuD;AAA9C,8GAAA,aAAa,OAAA;AACtB,qEAAmE;AAA1D,0HAAA,mBAAmB,OAAA;AAC5B,uEAAqE;AAA5D,4HAAA,oBAAoB,OAAA;AAC7B,uEAAqE;AAA5D,4HAAA,oBAAoB,OAAA;AAC7B,yEAAuE;AAA9D,8HAAA,qBAAqB,OAAA;AAC9B,iEAAmE;AAA1D,0HAAA,qBAAqB,OAAA;AAC9B,iFAA+E;AAAtE,sIAAA,yBAAyB,OAAA;AAClC,qDAAmD;AAA1C,4GAAA,YAAY,OAAA;AACrB,mEAMoC;AALlC,0HAAA,mBAAmB,OAAA;AACnB,+HAAA,wBAAwB,OAAA;AACxB,0HAAA,mBAAmB,OAAA;AACnB,4HAAA,qBAAqB,OAAA;AACrB,+HAAA,wBAAwB,OAAA;AAG1B,8EAA4E;AAAnE,gIAAA,sBAAsB,OAAA;AAE/B,oFAAoF;AACpF,uEAAuE;AACvE,iFAAiF;AACjF,0DAA0D"}
|
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,gGAAgG;AAChG,gEAAgE;AAChE,qBAAkB;AAClB,sDAAuC;AAEvC,qEAAqE;AACrE,8EAA8E;AAC9E,gFAAgF;AAChF,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;IAC5B,KAAK,CAAC,aAAa,EAAE,CAAA;AACvB,CAAC;AAED,mGAAmG;AACnG,8FAA8F;AAC9F,iGAAiG;AACjG,8CAA8C;AAC9C,6DAA6D;AAC7D,iEAA+D;AAAtD,sHAAA,iBAAiB,OAAA;AAC1B,6DAA2D;AAAlD,kHAAA,eAAe,OAAA;AACxB,uEAAqE;AAA5D,4HAAA,oBAAoB,OAAA;AAC7B,yFAAuF;AAA9E,wIAAA,0BAA0B,OAAA;AACnC,uFAAqF;AAA5E,sIAAA,yBAAyB,OAAA;AAClC,+DAGsC;AAFpC,0HAAA,yBAAyB,OAAA;AACzB,wHAAA,uBAAuB,OAAA;AAEzB,+EAA0F;AAAjF,2IAAA,kCAAkC,OAAA;AAC3C,iEAAmE;AAA1D,oHAAA,kBAAkB,OAAA;AAC3B,6DAAuE;AAA9D,wHAAA,wBAAwB,OAAA;AACjC,wEAAsE;AAA7D,4HAAA,oBAAoB,OAAA;AAC7B,2DAA2D;AAAlD,oHAAA,iBAAiB,OAAA;AAC1B,2EAAyE;AAAhE,kIAAA,uBAAuB,OAAA;AAChC,6EAA2E;AAAlE,oIAAA,wBAAwB,OAAA;AACjC,qEAAmE;AAA1D,4HAAA,oBAAoB,OAAA;AAC7B,kEAAoF;AAA3E,wHAAA,kBAAkB,OAAA;AAAE,wHAAA,kBAAkB,OAAA;AAC/C,0EAA+E;AAAtE,uIAAA,6BAA6B,OAAA;AACtC,uEAGwC;AAFtC,0HAAA,mBAAmB,OAAA;AACnB,kIAAA,2BAA2B,OAAA;AAE7B,+DAA6D;AAApD,kHAAA,eAAe,OAAA;AACxB,iEAA+D;AAAtD,oHAAA,gBAAgB,OAAA;AACzB,iEAA+D;AAAtD,oHAAA,gBAAgB,OAAA;AACzB,yDAAuD;AAA9C,8GAAA,aAAa,OAAA;AACtB,qEAAmE;AAA1D,0HAAA,mBAAmB,OAAA;AAC5B,uEAAqE;AAA5D,4HAAA,oBAAoB,OAAA;AAC7B,uEAAqE;AAA5D,4HAAA,oBAAoB,OAAA;AAC7B,yEAAuE;AAA9D,8HAAA,qBAAqB,OAAA;AAC9B,iEAAmE;AAA1D,0HAAA,qBAAqB,OAAA;AAC9B,iFAA+E;AAAtE,sIAAA,yBAAyB,OAAA;AAClC,qDAAmD;AAA1C,4GAAA,YAAY,OAAA;AACrB,mEAMoC;AALlC,0HAAA,mBAAmB,OAAA;AACnB,+HAAA,wBAAwB,OAAA;AACxB,0HAAA,mBAAmB,OAAA;AACnB,4HAAA,qBAAqB,OAAA;AACrB,+HAAA,wBAAwB,OAAA;AAG1B,8EAA4E;AAAnE,gIAAA,sBAAsB,OAAA;AAE/B,oFAAoF;AACpF,uEAAuE;AACvE,iFAAiF;AACjF,0DAA0D"}
|
||||||
|
|
@ -47,9 +47,16 @@ function batchResponseToResults(batch) {
|
||||||
* round-trip, then prune any tokens FCM reports as permanently dead. Best-effort: never throws;
|
* round-trip, then prune any tokens FCM reports as permanently dead. Best-effort: never throws;
|
||||||
* returns a per-user summary. A whole-batch failure (auth/quota, not a per-token death) is logged
|
* returns a per-user summary. A whole-batch failure (auth/quota, not a per-token death) is logged
|
||||||
* and does not prune anything.
|
* and does not prune anything.
|
||||||
|
*
|
||||||
|
* `excludeTokens` skips specific devices — e.g. the restore self-alert must not go to the very
|
||||||
|
* device that requested the restore ("was this you?" to the asker is noise; their partner's and
|
||||||
|
* their other devices' copies are the signal). Excluding everything yields a clean zero no-op.
|
||||||
*/
|
*/
|
||||||
async function sendPushToUser(db, messaging, uid, content, userData) {
|
async function sendPushToUser(db, messaging, uid, content, userData, excludeTokens) {
|
||||||
const tokens = await getUserTokens(db, uid, userData);
|
let tokens = await getUserTokens(db, uid, userData);
|
||||||
|
if (excludeTokens && excludeTokens.length > 0) {
|
||||||
|
tokens = tokens.filter((t) => !excludeTokens.includes(t));
|
||||||
|
}
|
||||||
if (tokens.length === 0)
|
if (tokens.length === 0)
|
||||||
return { sent: 0, failed: 0, pruned: 0 };
|
return { sent: 0, failed: 0, pruned: 0 };
|
||||||
const message = Object.assign(Object.assign({ tokens, notification: content.notification }, (content.data ? { data: content.data } : {})), (content.android ? { android: content.android } : {}));
|
const message = Object.assign(Object.assign({ tokens, notification: content.notification }, (content.data ? { data: content.data } : {})), (content.android ? { android: content.android } : {}));
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"version":3,"file":"push.js","sourceRoot":"","sources":["../../src/notifications/push.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"push.js","sourceRoot":"","sources":["../../src/notifications/push.ts"],"names":[],"mappings":";;AAkBA,sCAeC;AAmBD,wDAQC;AAYD,wCAuCC;AA9GD,+CAA+C;AAC/C,gCAA4C;AAE5C;;;;;;;GAOG;AAEH;;;;GAIG;AACI,KAAK,UAAU,aAAa,CACjC,EAA6B,EAC7B,GAAW,EACX,QAAuC;IAEvC,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,MAAM,IAAI,GAAG,QAAQ,aAAR,QAAQ,cAAR,QAAQ,GAAI,CAAC,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;IAC7E,MAAM,MAAM,GAAG,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,QAAQ,CAAA;IAC7B,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IACxE,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,GAAG,EAAE,CAAA;IAChF,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE;;QACtB,MAAM,CAAC,GAAG,MAAA,CAAC,CAAC,IAAI,EAAE,0CAAE,KAAK,CAAA;QACzB,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;YAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAClF,CAAC,CAAC,CAAA;IACF,OAAO,MAAM,CAAA;AACf,CAAC;AAeD;;;GAGG;AACH,SAAgB,sBAAsB,CACpC,KAAoC;IAEpC,OAAO,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAC/B,CAAC,CAAC,OAAO;QACP,CAAC,CAAE,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,CAAC,SAAS,EAAsC;QAClF,CAAC,CAAE,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,CAAC,KAAK,EAA4B,CACvE,CAAA;AACH,CAAC;AAED;;;;;;;;;GASG;AACI,KAAK,UAAU,cAAc,CAClC,EAA6B,EAC7B,SAAoC,EACpC,GAAW,EACX,OAAoB,EACpB,QAAuC,EACvC,aAAwB;IAExB,IAAI,MAAM,GAAG,MAAM,aAAa,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAA;IACnD,IAAI,aAAa,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9C,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;IAC3D,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAA;IAEjE,MAAM,OAAO,iCACX,MAAM,EACN,YAAY,EAAE,OAAO,CAAC,YAAY,IAC/B,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,GAC5C,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CACzD,CAAA;IAED,IAAI,KAAoC,CAAA;IACxC,IAAI,CAAC;QACH,KAAK,GAAG,MAAM,SAAS,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAA;IACvD,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,YAAM,CAAC,IAAI,CAAC,8CAA8C,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;QACtF,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,CAAA;IACtD,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;;QAC/B,IAAI,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;YACf,YAAM,CAAC,IAAI,CAAC,8BAA8B,GAAG,UAAU,IAAA,iBAAW,EAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE;gBAC/E,IAAI,EAAE,MAAA,CAAC,CAAC,KAAK,0CAAE,IAAI;aACpB,CAAC,CAAA;QACJ,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,MAAM,MAAM,GAAG,MAAM,IAAA,6BAAe,EAAC,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,sBAAsB,CAAC,KAAK,CAAC,CAAC,CAAA;IACpF,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,CAAC,YAAY,EAAE,MAAM,EAAE,CAAA;AACzE,CAAC"}
|
||||||
|
|
@ -91,6 +91,42 @@ describe('batchResponseToResults', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('sendPushToUser', () => {
|
describe('sendPushToUser', () => {
|
||||||
|
it('skips excluded tokens — the requesting device must not get its own "was this you?"', async () => {
|
||||||
|
const { db, deleted } = makeFakeDb({}, ['A', 'B', 'C']);
|
||||||
|
const sent = [];
|
||||||
|
const messaging = {
|
||||||
|
sendEachForMulticast: async (msg) => {
|
||||||
|
sent.push(msg.tokens);
|
||||||
|
return batch(msg.tokens.map((_) => ({ success: true, messageId: 'm' })));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = await (0, push_1.sendPushToUser)(db, messaging, 'u1', { notification: { title: 't', body: 'b' } }, undefined, ['B']);
|
||||||
|
expect(sent).toEqual([['A', 'C']]);
|
||||||
|
expect(result).toEqual({ sent: 2, failed: 0, pruned: 0 });
|
||||||
|
expect(deleted).toEqual([]);
|
||||||
|
});
|
||||||
|
it('excluding every token is a clean zero no-op, never a throw', async () => {
|
||||||
|
const { db } = makeFakeDb({}, ['A']);
|
||||||
|
const messaging = {
|
||||||
|
sendEachForMulticast: async () => {
|
||||||
|
throw new Error('must not be called with zero tokens');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = await (0, push_1.sendPushToUser)(db, messaging, 'u1', { notification: { title: 't', body: 'b' } }, undefined, ['A']);
|
||||||
|
expect(result).toEqual({ sent: 0, failed: 0, pruned: 0 });
|
||||||
|
});
|
||||||
|
it('an absent exclude list changes nothing', async () => {
|
||||||
|
const { db } = makeFakeDb({}, ['A', 'B']);
|
||||||
|
const sent = [];
|
||||||
|
const messaging = {
|
||||||
|
sendEachForMulticast: async (msg) => {
|
||||||
|
sent.push(msg.tokens);
|
||||||
|
return batch(msg.tokens.map(() => ({ success: true, messageId: 'm' })));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await (0, push_1.sendPushToUser)(db, messaging, 'u1', { notification: { title: 't', body: 'b' } });
|
||||||
|
expect(sent).toEqual([['A', 'B']]);
|
||||||
|
});
|
||||||
it('sends to every token and prunes exactly the permanently-dead ones', async () => {
|
it('sends to every token and prunes exactly the permanently-dead ones', async () => {
|
||||||
const { db, deleted, commits } = makeFakeDb({ fcmToken: 'LEG' }, ['LEG', 'DEAD', 'GOOD']);
|
const { db, deleted, commits } = makeFakeDb({ fcmToken: 'LEG' }, ['LEG', 'DEAD', 'GOOD']);
|
||||||
const messaging = makeFakeMessaging(batch([
|
const messaging = makeFakeMessaging(batch([
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,65 @@
|
||||||
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||||
|
}
|
||||||
|
Object.defineProperty(o, k2, desc);
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
|
var __importStar = (this && this.__importStar) || (function () {
|
||||||
|
var ownKeys = function(o) {
|
||||||
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||||
|
var ar = [];
|
||||||
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||||
|
return ar;
|
||||||
|
};
|
||||||
|
return ownKeys(o);
|
||||||
|
};
|
||||||
|
return function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.queueAndPush = queueAndPush;
|
||||||
|
const admin = __importStar(require("firebase-admin"));
|
||||||
|
const quietHours_1 = require("./quietHours");
|
||||||
|
const push_1 = require("./push");
|
||||||
|
/**
|
||||||
|
* Write the durable in-app queue entry (always) and — unless quiet hours suppress it — push to every
|
||||||
|
* device. `bypassQuietHours` is for security signals (the "was this you?" self-alert class) that must
|
||||||
|
* not be silenced.
|
||||||
|
*
|
||||||
|
* Extracted from `backup/onRestoreRequested.ts` when the restore-request cleanup sweep needed the same
|
||||||
|
* behavior — one implementation, so queue-entry/quiet-hours/push semantics can't drift between callers.
|
||||||
|
*/
|
||||||
|
async function queueAndPush(db, uid, opts) {
|
||||||
|
const { type, title, body, coupleId, bypassQuietHours, channelId = 'partner_activity', excludeTokens } = opts;
|
||||||
|
await db.collection('users').doc(uid).collection('notification_queue').add({
|
||||||
|
type, title, body, read: false, createdAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
});
|
||||||
|
const userDoc = await db.collection('users').doc(uid).get();
|
||||||
|
if (!userDoc.exists)
|
||||||
|
return;
|
||||||
|
const userData = userDoc.data();
|
||||||
|
if (!bypassQuietHours && (0, quietHours_1.recipientInQuietHours)(userData))
|
||||||
|
return;
|
||||||
|
await (0, push_1.sendPushToUser)(db, admin.messaging(), uid, {
|
||||||
|
notification: { title, body },
|
||||||
|
data: { type, couple_id: coupleId },
|
||||||
|
android: { notification: { channelId } },
|
||||||
|
}, userData, excludeTokens);
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=queueAndPush.js.map
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"queueAndPush.js","sourceRoot":"","sources":["../../src/notifications/queueAndPush.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAYA,oCAoCC;AAhDD,sDAAuC;AACvC,6CAAoD;AACpD,iCAAuC;AAEvC;;;;;;;GAOG;AACI,KAAK,UAAU,YAAY,CAChC,EAA6B,EAC7B,GAAW,EACX,IASC;IAED,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,gBAAgB,EAAE,SAAS,GAAG,kBAAkB,EAAE,aAAa,EAAE,GAAG,IAAI,CAAA;IAC7G,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,oBAAoB,CAAC,CAAC,GAAG,CAAC;QACzE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC,eAAe,EAAE;KACxF,CAAC,CAAA;IAEF,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAA;IAC3D,IAAI,CAAC,OAAO,CAAC,MAAM;QAAE,OAAM;IAC3B,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,EAAE,CAAA;IAC/B,IAAI,CAAC,gBAAgB,IAAI,IAAA,kCAAqB,EAAC,QAAQ,CAAC;QAAE,OAAM;IAEhE,MAAM,IAAA,qBAAc,EAClB,EAAE,EACF,KAAK,CAAC,SAAS,EAAE,EACjB,GAAG,EACH;QACE,YAAY,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE;QAC7B,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE;QACnC,OAAO,EAAE,EAAE,YAAY,EAAE,EAAE,SAAS,EAAE,EAAE;KACzC,EACD,QAAQ,EACR,aAAa,CACd,CAAA;AACH,CAAC"}
|
||||||
|
|
@ -60,7 +60,7 @@ describe('wildcard-day cadence (mirrors client DailyModeResolver)', () => {
|
||||||
if ((0, assignDailyQuestion_1.isWildcardDay)(dateStr))
|
if ((0, assignDailyQuestion_1.isWildcardDay)(dateStr))
|
||||||
wild++;
|
wild++;
|
||||||
}
|
}
|
||||||
expect(wild).toBe(36); // doy ∈ {3,13,…,363} → 36 days
|
expect(wild).toBe(37); // doy ∈ {3,13,…,363} → 37 days (~10.1%)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
//# sourceMappingURL=assignDailyQuestion.test.js.map
|
//# sourceMappingURL=assignDailyQuestion.test.js.map
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"version":3,"file":"assignDailyQuestion.test.js","sourceRoot":"","sources":["../../src/questions/assignDailyQuestion.test.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"assignDailyQuestion.test.js","sourceRoot":"","sources":["../../src/questions/assignDailyQuestion.test.ts"],"names":[],"mappings":";;AAAA,+DAAmH;AAEnH,iFAAiF;AACjF,uEAAuE;AACvE,QAAQ,CAAC,iCAAiC,EAAE,GAAG,EAAE;IAC/C,EAAE,CAAC,kEAAkE,EAAE,GAAG,EAAE;QAC1E,gFAAgF;QAChF,uEAAuE;QACvE,MAAM,CAAC,IAAA,mCAAa,EAAC,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;QAC1E,oFAAoF;QACpF,gFAAgF;QAChF,MAAM,CAAC,IAAA,mCAAa,EAAC,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;IAC5E,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,4CAA4C;QAC5C,MAAM,CAAC,IAAA,mCAAa,EAAC,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;QAC1E,MAAM,CAAC,IAAA,mCAAa,EAAC,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;IAC5E,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,iCAAiC;QACjC,MAAM,CAAC,IAAA,kCAAY,EAAC,YAAY,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAA;QACjF,iCAAiC;QACjC,MAAM,CAAC,IAAA,kCAAY,EAAC,YAAY,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAA;IACnF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,8BAA8B;QAC9B,MAAM,CAAC,IAAA,uCAAiB,EAAC,YAAY,CAAC,CAAC,MAAM,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAA;QAC/F,uCAAuC;QACvC,MAAM,CAAC,IAAA,uCAAiB,EAAC,YAAY,CAAC,CAAC,MAAM,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAA;IACjG,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,iEAAiE,EAAE,GAAG,EAAE;QACzE,KAAK,MAAM,GAAG,IAAI,CAAC,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,CAAC,EAAE,CAAC;YACzF,MAAM,CAAC,IAAA,mCAAa,EAAC,IAAA,kCAAY,EAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QACpD,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,6FAA6F;AAC7F,6FAA6F;AAC7F,QAAQ,CAAC,yDAAyD,EAAE,GAAG,EAAE;IACvE,EAAE,CAAC,wDAAwD,EAAE,GAAG,EAAE;QAChE,MAAM,CAAC,IAAA,kCAAY,EAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QAC1C,MAAM,CAAC,IAAA,kCAAY,EAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QAC1C,MAAM,CAAC,IAAA,kCAAY,EAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAC3C,MAAM,CAAC,IAAA,kCAAY,EAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA,CAAC,0BAA0B;QACvE,MAAM,CAAC,IAAA,kCAAY,EAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA,CAAC,YAAY;IAC3D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAC9D,MAAM,CAAC,IAAA,mCAAa,EAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA,CAAC,QAAQ;QACvD,MAAM,CAAC,IAAA,mCAAa,EAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA,CAAC,SAAS;QACxD,MAAM,CAAC,IAAA,mCAAa,EAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA,CAAC,SAAS;QACzD,MAAM,CAAC,IAAA,mCAAa,EAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA,CAAC,QAAQ;QACxD,MAAM,CAAC,IAAA,mCAAa,EAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA,CAAC,QAAQ;IAC1D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,IAAI,IAAI,GAAG,CAAC,CAAA;QACZ,KAAK,IAAI,GAAG,GAAG,CAAC,EAAE,GAAG,IAAI,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC;YACpC,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;YAC1C,MAAM,OAAO,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;YAC5C,IAAI,IAAA,mCAAa,EAAC,OAAO,CAAC;gBAAE,IAAI,EAAE,CAAA;QACpC,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA,CAAC,wCAAwC;IAChE,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
||||||
Loading…
Reference in New Issue