mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-21 20:18:11 +02:00
added google meeting notes to calendar
This commit is contained in:
parent
2eed7bce1c
commit
6b592df5d4
1 changed files with 113 additions and 19 deletions
|
|
@ -3,17 +3,28 @@ import path from 'path';
|
||||||
import { google } from 'googleapis';
|
import { google } from 'googleapis';
|
||||||
import { authenticate } from '@google-cloud/local-auth';
|
import { authenticate } from '@google-cloud/local-auth';
|
||||||
import { OAuth2Client } from 'google-auth-library';
|
import { OAuth2Client } from 'google-auth-library';
|
||||||
|
import TurndownService from 'turndown';
|
||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
const CREDENTIALS_PATH = path.join(process.cwd(), 'credentials.json');
|
const CREDENTIALS_PATH = path.join(process.cwd(), 'credentials.json');
|
||||||
const TOKEN_PATH = path.join(process.cwd(), 'token_calendar.json');
|
const TOKEN_PATH = path.join(process.cwd(), 'token_calendar_notes.json'); // Changed to force re-auth with new scopes
|
||||||
const SYNC_INTERVAL_MS = 60 * 1000;
|
const SYNC_INTERVAL_MS = 60 * 1000;
|
||||||
const SCOPES = ['https://www.googleapis.com/auth/calendar.readonly'];
|
const SCOPES = [
|
||||||
|
'https://www.googleapis.com/auth/calendar.readonly',
|
||||||
|
'https://www.googleapis.com/auth/drive.readonly'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Initialize Turndown
|
||||||
|
const turndownService = new TurndownService({
|
||||||
|
headingStyle: 'atx',
|
||||||
|
codeBlockStyle: 'fenced'
|
||||||
|
});
|
||||||
|
|
||||||
// --- Auth Functions ---
|
// --- Auth Functions ---
|
||||||
|
|
||||||
async function loadSavedCredentialsIfExist(): Promise<OAuth2Client | null> {
|
async function loadSavedCredentialsIfExist(): Promise<OAuth2Client | null> {
|
||||||
try {
|
try {
|
||||||
|
if (!fs.existsSync(TOKEN_PATH)) return null;
|
||||||
const tokenContent = fs.readFileSync(TOKEN_PATH, 'utf-8');
|
const tokenContent = fs.readFileSync(TOKEN_PATH, 'utf-8');
|
||||||
const tokenData = JSON.parse(tokenContent);
|
const tokenData = JSON.parse(tokenContent);
|
||||||
|
|
||||||
|
|
@ -21,17 +32,22 @@ async function loadSavedCredentialsIfExist(): Promise<OAuth2Client | null> {
|
||||||
const keys = JSON.parse(credsContent);
|
const keys = JSON.parse(credsContent);
|
||||||
const key = keys.installed || keys.web;
|
const key = keys.installed || keys.web;
|
||||||
|
|
||||||
const credentials = {
|
const client = new google.auth.OAuth2(
|
||||||
type: 'authorized_user',
|
key.client_id,
|
||||||
client_id: key.client_id,
|
key.client_secret,
|
||||||
client_secret: key.client_secret,
|
key.redirect_uris ? key.redirect_uris[0] : 'http://localhost'
|
||||||
|
);
|
||||||
|
|
||||||
|
client.setCredentials({
|
||||||
refresh_token: tokenData.refresh_token || tokenData.refreshToken,
|
refresh_token: tokenData.refresh_token || tokenData.refreshToken,
|
||||||
access_token: tokenData.token || tokenData.access_token,
|
access_token: tokenData.token || tokenData.access_token,
|
||||||
expiry_date: tokenData.expiry || tokenData.expiry_date
|
expiry_date: tokenData.expiry || tokenData.expiry_date,
|
||||||
};
|
scope: tokenData.scope
|
||||||
return google.auth.fromJSON(credentials) as OAuth2Client;
|
});
|
||||||
|
|
||||||
|
return client;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// console.error("Error loading saved credentials:", err); // Optional: silence if just not found
|
console.error("Error loading saved credentials:", err);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -81,6 +97,12 @@ async function authorize(): Promise<OAuth2Client> {
|
||||||
return client!;
|
return client!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Helper Functions ---
|
||||||
|
|
||||||
|
function cleanFilename(name: string): string {
|
||||||
|
return name.replace(/[\\/*?:\"<>|]/g, "").replace(/\s+/g, "_").substring(0, 100).trim();
|
||||||
|
}
|
||||||
|
|
||||||
// --- Sync Logic ---
|
// --- Sync Logic ---
|
||||||
|
|
||||||
function cleanUpOldFiles(currentEventIds: Set<string>, syncDir: string) {
|
function cleanUpOldFiles(currentEventIds: Set<string>, syncDir: string) {
|
||||||
|
|
@ -88,13 +110,31 @@ function cleanUpOldFiles(currentEventIds: Set<string>, syncDir: string) {
|
||||||
|
|
||||||
const files = fs.readdirSync(syncDir);
|
const files = fs.readdirSync(syncDir);
|
||||||
for (const filename of files) {
|
for (const filename of files) {
|
||||||
if (!filename.endsWith('.json') || filename === 'sync_state.json') continue;
|
if (filename === 'sync_state.json') continue;
|
||||||
|
|
||||||
const eventId = filename.replace('.json', '');
|
// We expect files like:
|
||||||
if (!currentEventIds.has(eventId)) {
|
// {eventId}.json
|
||||||
|
// {eventId}_doc_{docId}.md
|
||||||
|
|
||||||
|
let eventId: string | null = null;
|
||||||
|
|
||||||
|
if (filename.endsWith('.json')) {
|
||||||
|
eventId = filename.replace('.json', '');
|
||||||
|
} else if (filename.endsWith('.md')) {
|
||||||
|
// Try to extract eventId from prefix
|
||||||
|
// Assuming eventId doesn't contain underscores usually, but if it does, this split might be fragile.
|
||||||
|
// Google Calendar IDs are usually alphanumeric.
|
||||||
|
// Let's rely on the delimiter we use: "_doc_"
|
||||||
|
const parts = filename.split('_doc_');
|
||||||
|
if (parts.length > 1) {
|
||||||
|
eventId = parts[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventId && !currentEventIds.has(eventId)) {
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync(path.join(syncDir, filename));
|
fs.unlinkSync(path.join(syncDir, filename));
|
||||||
console.log(`Removed old/out-of-window event: ${eventId}`);
|
console.log(`Removed old/out-of-window file: ${filename}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Error deleting file ${filename}:`, e);
|
console.error(`Error deleting file ${filename}:`, e);
|
||||||
}
|
}
|
||||||
|
|
@ -117,11 +157,63 @@ async function saveEvent(event: any, syncDir: string): Promise<boolean> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function processAttachments(drive: any, event: any, syncDir: string) {
|
||||||
|
if (!event.attachments || event.attachments.length === 0) return;
|
||||||
|
|
||||||
|
const eventId = event.id;
|
||||||
|
const eventTitle = event.summary || 'Untitled';
|
||||||
|
const eventDate = event.start?.dateTime || event.start?.date || 'Unknown';
|
||||||
|
const organizer = event.organizer?.email || 'Unknown';
|
||||||
|
|
||||||
|
for (const att of event.attachments) {
|
||||||
|
// We only care about Google Docs
|
||||||
|
if (att.mimeType === 'application/vnd.google-apps.document') {
|
||||||
|
const fileId = att.fileId;
|
||||||
|
const safeTitle = cleanFilename(att.title);
|
||||||
|
// Unique filename linked to event
|
||||||
|
const filename = `${eventId}_doc_${safeTitle}.md`;
|
||||||
|
const filePath = path.join(syncDir, filename);
|
||||||
|
|
||||||
|
// Simple cache check: if file exists, skip.
|
||||||
|
// Ideally we check modifiedTime, but that requires an extra API call per file.
|
||||||
|
// Given the loop interval, we can just check existence to save quota.
|
||||||
|
// If user updates notes, they might want them re-synced.
|
||||||
|
// For now, let's just check existence. To be smarter, we'd need a state file or check API.
|
||||||
|
if (fs.existsSync(filePath)) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await drive.files.export({
|
||||||
|
fileId: fileId,
|
||||||
|
mimeType: 'text/html'
|
||||||
|
});
|
||||||
|
|
||||||
|
const html = res.data;
|
||||||
|
const md = turndownService.turndown(html);
|
||||||
|
|
||||||
|
const frontmatter = [
|
||||||
|
`# ${att.title}`,
|
||||||
|
`**Event:** ${eventTitle}`,
|
||||||
|
`**Date:** ${eventDate}`,
|
||||||
|
`**Organizer:** ${organizer}`,
|
||||||
|
`**Link:** ${att.fileUrl}`,
|
||||||
|
`---`,
|
||||||
|
``
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, frontmatter + md);
|
||||||
|
console.log(`Synced Note: ${att.title} for event ${eventTitle}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to download note ${att.title}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function syncCalendarWindow(auth: OAuth2Client, syncDir: string, lookbackDays: number) {
|
async function syncCalendarWindow(auth: OAuth2Client, syncDir: string, lookbackDays: number) {
|
||||||
// Calculate window: -lookbackDays to +2 weeks
|
// Calculate window
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const lookbackMs = lookbackDays * 24 * 60 * 60 * 1000;
|
const lookbackMs = lookbackDays * 24 * 60 * 60 * 1000;
|
||||||
const twoWeeksForwardMs = 14 * 24 * 60 * 60 * 1000; // Remaining constant as per original python script
|
const twoWeeksForwardMs = 14 * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
const timeMin = new Date(now.getTime() - lookbackMs).toISOString();
|
const timeMin = new Date(now.getTime() - lookbackMs).toISOString();
|
||||||
const timeMax = new Date(now.getTime() + twoWeeksForwardMs).toISOString();
|
const timeMax = new Date(now.getTime() + twoWeeksForwardMs).toISOString();
|
||||||
|
|
@ -129,6 +221,7 @@ async function syncCalendarWindow(auth: OAuth2Client, syncDir: string, lookbackD
|
||||||
console.log(`Syncing calendar from ${timeMin} to ${timeMax} (lookback: ${lookbackDays} days)...`);
|
console.log(`Syncing calendar from ${timeMin} to ${timeMax} (lookback: ${lookbackDays} days)...`);
|
||||||
|
|
||||||
const calendar = google.calendar({ version: 'v3', auth });
|
const calendar = google.calendar({ version: 'v3', auth });
|
||||||
|
const drive = google.drive({ version: 'v3', auth });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await calendar.events.list({
|
const res = await calendar.events.list({
|
||||||
|
|
@ -149,6 +242,7 @@ async function syncCalendarWindow(auth: OAuth2Client, syncDir: string, lookbackD
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
if (event.id) {
|
if (event.id) {
|
||||||
await saveEvent(event, syncDir);
|
await saveEvent(event, syncDir);
|
||||||
|
await processAttachments(drive, event, syncDir);
|
||||||
currentEventIds.add(event.id);
|
currentEventIds.add(event.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -162,13 +256,13 @@ async function syncCalendarWindow(auth: OAuth2Client, syncDir: string, lookbackD
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
console.log("Starting Google Calendar Sync (TS)...");
|
console.log("Starting Google Calendar & Notes Sync (TS)...");
|
||||||
|
|
||||||
const syncDirArg = process.argv[2];
|
const syncDirArg = process.argv[2];
|
||||||
const lookbackDaysArg = process.argv[3];
|
const lookbackDaysArg = process.argv[3];
|
||||||
|
|
||||||
const SYNC_DIR = syncDirArg || 'synced_calendar_events';
|
const SYNC_DIR = syncDirArg || 'synced_calendar_events';
|
||||||
const LOOKBACK_DAYS = lookbackDaysArg ? parseInt(lookbackDaysArg, 10) : 14; // Default to 14 days
|
const LOOKBACK_DAYS = lookbackDaysArg ? parseInt(lookbackDaysArg, 10) : 14;
|
||||||
|
|
||||||
if (isNaN(LOOKBACK_DAYS) || LOOKBACK_DAYS <= 0) {
|
if (isNaN(LOOKBACK_DAYS) || LOOKBACK_DAYS <= 0) {
|
||||||
console.error("Error: Lookback days must be a positive number.");
|
console.error("Error: Lookback days must be a positive number.");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue