godcrm/backend/scripts/restore-google-calendar-events.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

346 lines
12 KiB
JavaScript

/**
* Restore deleted Google Calendar events from trash
*
* Google Calendar keeps deleted events for 30 days with status "cancelled".
* This script finds them and restores by patching status back to "confirmed".
*
* Usage: node --experimental-modules scripts/restore-google-calendar-events.js [--dry-run]
*/
import { google } from 'googleapis';
import axios from 'axios';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import CryptoJS from 'crypto-js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Config paths (same as GoogleCalendarService)
const CALENDAR_CONFIG_PATH = path.resolve(__dirname, '../google-calendar-config.json');
const FALLBACK_CONFIG_PATH = path.resolve(__dirname, '../google-oauth-config.json');
const CONFIG_PATH = fs.existsSync(CALENDAR_CONFIG_PATH) ? CALENDAR_CONFIG_PATH : FALLBACK_CONFIG_PATH;
const TOKENS_PATH = path.resolve(__dirname, '../google-calendar-tokens.json');
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || process.env.JWT_SECRET || 'dev_jwt_secret_change_in_production';
const DRY_RUN = process.argv.includes('--dry-run');
// ── Helpers ──────────────────────────────────────────────────────────────────
function decrypt(ciphertext) {
const bytes = CryptoJS.AES.decrypt(ciphertext, ENCRYPTION_KEY);
return bytes.toString(CryptoJS.enc.Utf8);
}
function loadConfig() {
const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
const config = JSON.parse(raw);
let clientSecret = config.clientSecret;
if (clientSecret && clientSecret.startsWith('U2F')) {
clientSecret = decrypt(clientSecret);
}
return { clientId: config.clientId, clientSecret, redirectUri: config.redirectUri };
}
function loadTokens() {
const raw = fs.readFileSync(TOKENS_PATH, 'utf-8');
return JSON.parse(raw);
}
function saveTokens(tokens) {
fs.writeFileSync(TOKENS_PATH, JSON.stringify(tokens, null, 2), 'utf-8');
}
// ── Auth ─────────────────────────────────────────────────────────────────────
async function getAuthClient(userId) {
const config = loadConfig();
const tokens = loadTokens();
const userTokens = tokens[`user_${userId}`];
if (!userTokens) throw new Error(`No tokens for user ${userId}`);
const oauth2Client = new google.auth.OAuth2(config.clientId, config.clientSecret);
let accessToken = userTokens.access_token;
const refreshToken = userTokens.refresh_token ? decrypt(userTokens.refresh_token) : null;
// Always refresh to get a fresh token
if (refreshToken) {
console.log('🔄 Refreshing access token...');
const response = await axios.post('https://oauth2.googleapis.com/token', {
client_id: config.clientId,
client_secret: config.clientSecret,
refresh_token: refreshToken,
grant_type: 'refresh_token',
});
accessToken = response.data.access_token;
const newExpiry = new Date(Date.now() + (response.data.expires_in || 3600) * 1000).toISOString();
tokens[`user_${userId}`] = {
...userTokens,
access_token: accessToken,
token_expiry: newExpiry,
};
saveTokens(tokens);
console.log('✅ Token refreshed, expires:', newExpiry);
}
oauth2Client.setCredentials({ access_token: accessToken, refresh_token: refreshToken });
return oauth2Client;
}
// ── Main Logic ───────────────────────────────────────────────────────────────
async function findDeletedEvents(calendar, calendarId) {
const deletedEvents = [];
let pageToken = null;
// Look back 60 days to catch all recent deletions
const timeMin = new Date(Date.now() - 60 * 24 * 60 * 60 * 1000).toISOString();
do {
const params = {
calendarId,
showDeleted: true,
timeMin,
maxResults: 250,
singleEvents: false, // Get recurring event masters too
};
if (pageToken) params.pageToken = pageToken;
const res = await calendar.events.list(params);
const events = res.data.items || [];
for (const event of events) {
if (event.status === 'cancelled') {
deletedEvents.push(event);
}
}
pageToken = res.data.nextPageToken;
} while (pageToken);
return deletedEvents;
}
async function restoreEvent(calendar, calendarId, event) {
try {
// For recurring event instances (have recurringEventId), we need special handling
if (event.recurringEventId) {
// This is a cancelled instance of a recurring event
// We need to check if the parent recurring event still exists
try {
const parent = await calendar.events.get({
calendarId,
eventId: event.recurringEventId,
});
if (parent.data.status === 'cancelled') {
// Parent is also deleted - restore parent first
console.log(` ↳ Parent recurring event ${event.recurringEventId} also deleted, restoring parent...`);
if (!DRY_RUN) {
await calendar.events.patch({
calendarId,
eventId: event.recurringEventId,
requestBody: { status: 'confirmed' },
});
}
return { success: true, note: 'Restored via parent recurring event' };
} else {
// Parent exists and is confirmed - restore this instance
if (!DRY_RUN) {
await calendar.events.patch({
calendarId,
eventId: event.id,
requestBody: { status: 'confirmed' },
});
}
return { success: true, note: 'Restored recurring instance' };
}
} catch (parentErr) {
// Parent not found or error
console.log(` ↳ Could not access parent ${event.recurringEventId}: ${parentErr.message}`);
// Try direct restore anyway
if (!DRY_RUN) {
try {
await calendar.events.patch({
calendarId,
eventId: event.id,
requestBody: { status: 'confirmed' },
});
return { success: true, note: 'Direct restore of recurring instance' };
} catch (directErr) {
return { success: false, error: directErr.message };
}
}
return { success: true, note: 'Would attempt direct restore (dry-run)' };
}
}
// Regular (non-recurring) event - simple restore
if (!DRY_RUN) {
await calendar.events.patch({
calendarId,
eventId: event.id,
requestBody: { status: 'confirmed' },
});
}
return { success: true, note: 'Restored' };
} catch (err) {
return { success: false, error: err.message };
}
}
async function main() {
console.log('='.repeat(70));
console.log(' Google Calendar Event Restoration Tool');
console.log(DRY_RUN ? ' MODE: DRY RUN (no changes will be made)' : ' MODE: LIVE (will restore events!)');
console.log('='.repeat(70));
console.log();
const userId = 1;
const auth = await getAuthClient(userId);
const calendar = google.calendar({ version: 'v3', auth });
// Get list of calendars
const calListRes = await calendar.calendarList.list();
const calendars = calListRes.data.items || [];
console.log(`📅 Found ${calendars.length} calendars:\n`);
calendars.forEach(c => console.log(` - ${c.summary} (${c.id})`));
console.log();
// Target calendars (the ones that had deletions)
const targetCalendars = [
'bogolepovavikos@gmail.com',
'geramonnn@gmail.com',
];
let totalFound = 0;
let totalRestored = 0;
let totalFailed = 0;
const results = [];
for (const calId of targetCalendars) {
const calInfo = calendars.find(c => c.id === calId);
const calName = calInfo ? calInfo.summary : calId;
console.log(`\n${'─'.repeat(70)}`);
console.log(`📅 Calendar: ${calName} (${calId})`);
console.log('─'.repeat(70));
const deleted = await findDeletedEvents(calendar, calId);
console.log(`\n Found ${deleted.length} deleted events:\n`);
totalFound += deleted.length;
if (deleted.length === 0) {
console.log(' (none)');
continue;
}
// Group: recurring instances vs standalone
const standalone = deleted.filter(e => !e.recurringEventId);
const recurring = deleted.filter(e => !!e.recurringEventId);
// Track unique parent recurring events to avoid double-restoring
const restoredParents = new Set();
// Restore standalone events first
if (standalone.length > 0) {
console.log(` 📌 Standalone deleted events: ${standalone.length}`);
for (const event of standalone) {
const title = event.summary || '(no title)';
const start = event.start?.dateTime || event.start?.date || '?';
console.log(` → [${event.id}] "${title}" @ ${start}`);
const result = await restoreEvent(calendar, calId, event);
if (result.success) {
totalRestored++;
console.log(`${DRY_RUN ? 'Would restore' : 'Restored'}: ${result.note}`);
} else {
totalFailed++;
console.log(` ❌ Failed: ${result.error}`);
}
results.push({ calendar: calName, id: event.id, title, start, ...result });
}
}
// Restore recurring event instances
if (recurring.length > 0) {
console.log(`\n 🔁 Cancelled recurring instances: ${recurring.length}`);
// Group by parent recurring event
const byParent = {};
for (const event of recurring) {
if (!byParent[event.recurringEventId]) {
byParent[event.recurringEventId] = [];
}
byParent[event.recurringEventId].push(event);
}
for (const [parentId, instances] of Object.entries(byParent)) {
console.log(`\n Parent: ${parentId} (${instances.length} cancelled instances)`);
// First try restoring the parent if it's deleted
if (!restoredParents.has(parentId)) {
try {
const parentEvent = await calendar.events.get({
calendarId: calId,
eventId: parentId,
});
if (parentEvent.data.status === 'cancelled') {
console.log(` ↳ Parent event is also cancelled, restoring parent...`);
if (!DRY_RUN) {
await calendar.events.patch({
calendarId: calId,
eventId: parentId,
requestBody: { status: 'confirmed' },
});
}
console.log(` ✅ Parent restored`);
restoredParents.add(parentId);
}
} catch (err) {
console.log(` ⚠️ Could not check parent: ${err.message}`);
}
}
// Now restore each instance
for (const event of instances) {
const title = event.summary || '(from recurring)';
const start = event.originalStartTime?.dateTime || event.originalStartTime?.date || event.start?.dateTime || event.start?.date || '?';
console.log(` → Instance @ ${start}`);
const result = await restoreEvent(calendar, calId, event);
if (result.success) {
totalRestored++;
console.log(`${DRY_RUN ? 'Would restore' : 'Restored'}: ${result.note}`);
} else {
totalFailed++;
console.log(` ❌ Failed: ${result.error}`);
}
results.push({ calendar: calName, id: event.id, title, start, ...result });
}
}
}
}
// Summary
console.log(`\n${'='.repeat(70)}`);
console.log(' SUMMARY');
console.log('='.repeat(70));
console.log(` Total deleted events found: ${totalFound}`);
console.log(` ${DRY_RUN ? 'Would restore' : 'Restored'}: ${totalRestored}`);
console.log(` Failed: ${totalFailed}`);
console.log(DRY_RUN ? '\n ⚠️ This was a DRY RUN. Run without --dry-run to actually restore.' : '\n ✅ Restoration complete!');
console.log('='.repeat(70));
}
main().catch(err => {
console.error('❌ Fatal error:', err);
process.exit(1);
});