Add Google Calendar integration and Claude docs
- New server/google_calendar.mjs wrapping googleapis v3 with auto token refresh - Four new actions: calendar-list-events (auto-accept), calendar-create/update/delete-event (queue) - bin/ccc-gcal-auth.mjs one-time OAuth2 consent flow helper - config.example.json updated with google_calendar block - server/config.mjs, index.mjs wired up following the same pattern as SMTP/mailer - Bump version to 1.2.0 - Add CLAUDE.md and claude-info/ with architecture reference, feature plans, and contributing checklist Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -120,4 +120,68 @@ export const actions = {
|
||||
return { permissions: target_user ? all.filter(e => e.user === target_user) : all };
|
||||
},
|
||||
},
|
||||
|
||||
'calendar-list-events': {
|
||||
description: 'List upcoming calendar events',
|
||||
params: [
|
||||
{ name: 'calendar_id', required: false, type: 'string' },
|
||||
{ name: 'time_min', required: false, type: 'string' },
|
||||
{ name: 'time_max', required: false, type: 'string' },
|
||||
{ name: 'max_results', required: false, type: 'number' },
|
||||
],
|
||||
policy: 'auto-accept',
|
||||
handler: async ({ calendar_id, time_min, time_max, max_results }, { calendar }) => {
|
||||
if (!calendar) { throw new Error('Google Calendar not configured'); }
|
||||
const events = await calendar.list_events({ calendar_id, time_min, time_max, max_results });
|
||||
return { events };
|
||||
},
|
||||
},
|
||||
|
||||
'calendar-create-event': {
|
||||
description: 'Create a new calendar event',
|
||||
params: [
|
||||
{ name: 'summary', required: true, type: 'string' },
|
||||
{ name: 'start', required: true, type: 'string' },
|
||||
{ name: 'end', required: true, type: 'string' },
|
||||
{ name: 'description', required: false, type: 'string' },
|
||||
{ name: 'calendar_id', required: false, type: 'string' },
|
||||
],
|
||||
policy: 'queue',
|
||||
handler: async ({ summary, start, end, description, calendar_id }, { calendar }) => {
|
||||
if (!calendar) { throw new Error('Google Calendar not configured'); }
|
||||
const event = await calendar.create_event({ calendar_id, summary, start, end, description });
|
||||
return { event };
|
||||
},
|
||||
},
|
||||
|
||||
'calendar-update-event': {
|
||||
description: 'Update fields on an existing calendar event',
|
||||
params: [
|
||||
{ name: 'event_id', required: true, type: 'string' },
|
||||
{ name: 'calendar_id', required: false, type: 'string' },
|
||||
{ name: 'summary', required: false, type: 'string' },
|
||||
{ name: 'start', required: false, type: 'string' },
|
||||
{ name: 'end', required: false, type: 'string' },
|
||||
{ name: 'description', required: false, type: 'string' },
|
||||
],
|
||||
policy: 'queue',
|
||||
handler: async ({ event_id, calendar_id, summary, start, end, description }, { calendar }) => {
|
||||
if (!calendar) { throw new Error('Google Calendar not configured'); }
|
||||
const event = await calendar.update_event({ calendar_id, event_id, summary, start, end, description });
|
||||
return { event };
|
||||
},
|
||||
},
|
||||
|
||||
'calendar-delete-event': {
|
||||
description: 'Delete a calendar event',
|
||||
params: [
|
||||
{ name: 'event_id', required: true, type: 'string' },
|
||||
{ name: 'calendar_id', required: false, type: 'string' },
|
||||
],
|
||||
policy: 'queue',
|
||||
handler: async ({ event_id, calendar_id }, { calendar }) => {
|
||||
if (!calendar) { throw new Error('Google Calendar not configured'); }
|
||||
return calendar.delete_event({ calendar_id, event_id });
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -46,10 +46,23 @@ export function load_config(file_path) {
|
||||
? resolve(config_dir, parsed.mail_perms)
|
||||
: null;
|
||||
|
||||
let google_calendar = null;
|
||||
if (parsed.google_calendar) {
|
||||
const { credentials, token } = parsed.google_calendar;
|
||||
if (!credentials || !token) {
|
||||
throw new Error("Config 'google_calendar' must have 'credentials' and 'token' fields");
|
||||
}
|
||||
google_calendar = {
|
||||
credentials_path: resolve(config_dir, credentials),
|
||||
token_path: resolve(config_dir, token),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
users: secrets.users,
|
||||
smtp: parsed.smtp ?? null,
|
||||
mail_perms_path,
|
||||
google_calendar,
|
||||
bind: parsed.bind ?? null,
|
||||
port: parsed.port ?? null,
|
||||
};
|
||||
|
||||
76
server/google_calendar.mjs
Normal file
76
server/google_calendar.mjs
Normal file
@@ -0,0 +1,76 @@
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { google } from 'googleapis';
|
||||
|
||||
// Returns a calendar client wrapping the Google Calendar API v3.
|
||||
// cfg = { credentials_path, token_path }
|
||||
export function create_calendar_client({ credentials_path, token_path }) {
|
||||
const credentials = JSON.parse(readFileSync(credentials_path, 'utf8'));
|
||||
const { client_id, client_secret, redirect_uris } = credentials.installed ?? credentials.web;
|
||||
|
||||
const oauth2_client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
|
||||
|
||||
const token = JSON.parse(readFileSync(token_path, 'utf8'));
|
||||
oauth2_client.setCredentials(token);
|
||||
|
||||
// Persist refreshed tokens automatically
|
||||
oauth2_client.on('tokens', (new_tokens) => {
|
||||
let current = {};
|
||||
try {
|
||||
current = JSON.parse(readFileSync(token_path, 'utf8'));
|
||||
} catch (_) {}
|
||||
const merged = { ...current, ...new_tokens };
|
||||
writeFileSync(token_path, JSON.stringify(merged, null, '\t') + '\n', 'utf8');
|
||||
});
|
||||
|
||||
const cal = google.calendar({ version: 'v3', auth: oauth2_client });
|
||||
|
||||
async function list_events({ calendar_id = 'primary', time_min, time_max, max_results = 10 } = {}) {
|
||||
const res = await cal.events.list({
|
||||
calendarId: calendar_id,
|
||||
timeMin: time_min,
|
||||
timeMax: time_max,
|
||||
maxResults: max_results,
|
||||
singleEvents: true,
|
||||
orderBy: 'startTime',
|
||||
});
|
||||
return res.data.items ?? [];
|
||||
}
|
||||
|
||||
async function create_event({ calendar_id = 'primary', summary, start, end, description }) {
|
||||
const res = await cal.events.insert({
|
||||
calendarId: calendar_id,
|
||||
requestBody: {
|
||||
summary,
|
||||
description,
|
||||
start: { dateTime: start },
|
||||
end: { dateTime: end },
|
||||
},
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async function update_event({ calendar_id = 'primary', event_id, summary, start, end, description }) {
|
||||
const patch = {};
|
||||
if (summary !== undefined) { patch.summary = summary; }
|
||||
if (description !== undefined) { patch.description = description; }
|
||||
if (start !== undefined) { patch.start = { dateTime: start }; }
|
||||
if (end !== undefined) { patch.end = { dateTime: end }; }
|
||||
|
||||
const res = await cal.events.patch({
|
||||
calendarId: calendar_id,
|
||||
eventId: event_id,
|
||||
requestBody: patch,
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async function delete_event({ calendar_id = 'primary', event_id }) {
|
||||
await cal.events.delete({
|
||||
calendarId: calendar_id,
|
||||
eventId: event_id,
|
||||
});
|
||||
return { deleted: true, event_id };
|
||||
}
|
||||
|
||||
return { list_events, create_event, update_event, delete_event };
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { create_auth_middleware, check_can_approve } from './auth.mjs';
|
||||
import { create_mailer } from './mailer.mjs';
|
||||
import { exec as real_exec } from './helpers.mjs';
|
||||
import { load_mail_perms } from './mail_perms.mjs';
|
||||
import { create_calendar_client } from './google_calendar.mjs';
|
||||
|
||||
function get_arg(argv, flag) {
|
||||
const i = argv.indexOf(flag);
|
||||
@@ -29,7 +30,7 @@ try {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { users, smtp, mail_perms_path } = cfg;
|
||||
const { users, smtp, mail_perms_path, google_calendar: gcal_cfg } = cfg;
|
||||
const PORT = process.env.CONDUIT_PORT || cfg.port || 3015;
|
||||
const BIND = process.env.CONDUIT_BIND || cfg.bind || '127.0.0.1';
|
||||
|
||||
@@ -47,6 +48,16 @@ if (!mail_perms_path) {
|
||||
|
||||
const real_mailer_send = create_mailer(smtp);
|
||||
|
||||
let calendar = null;
|
||||
if (gcal_cfg) {
|
||||
try {
|
||||
calendar = create_calendar_client(gcal_cfg);
|
||||
console.log('Google Calendar client initialized');
|
||||
} catch (err) {
|
||||
console.error(`Warning: Failed to initialize Google Calendar client: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const app = express();
|
||||
app.use(express.json({
|
||||
verify: (req, _res, buf) => {
|
||||
@@ -70,7 +81,7 @@ function make_ctx(caller) {
|
||||
const mailer_send = DRY_RUN
|
||||
? async (to, subject) => console.log(`[${ts()}] [DRY-RUN] send-mail: to=${to} subject=${JSON.stringify(subject)}`)
|
||||
: real_mailer_send;
|
||||
return { caller, users, mail_perm_store, exec, mailer_send };
|
||||
return { caller, users, mail_perm_store, exec, mailer_send, calendar };
|
||||
}
|
||||
|
||||
async function run_action(def, params, ctx) {
|
||||
|
||||
Reference in New Issue
Block a user