Skip to content

Commit

Permalink
Add initial version.
Browse files Browse the repository at this point in the history
  • Loading branch information
sleiss committed Apr 15, 2022
0 parents commit 0aedbdb
Show file tree
Hide file tree
Showing 8 changed files with 3,783 additions and 0 deletions.
73 changes: 73 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839


.idea/**

# Node
######################
/node/
node_tmp/
node_modules/
npm-debug.log.*
/.awcache/*
.node-gyp/
/.cache-loader/*

######################
# Intellij
######################
.idea/
*.iml
*.iws
*.ipr
*.ids
*.orig
classes/
.IntelliJIdea*/
.local/share/JetBrains/
.java/
out/

######################
# NPM
######################
*.npm/
*.cache/
*.config/
*.yarnrc
*.coverage/

#####
# ESLint
######################
.eslintcache
/out

# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out

# dependencies
*/node_modules

# profiling files
chrome-profiler-events*.json
speed-measure-plugin*.json

# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace


google_calendar_key.json
config.json

4 changes: 4 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.idea

config.json
google_calendar_key.json
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# google-calendar-sync

Sync your private google calendar with your work google calendar.
Details of the event will be left out - it will only be visible as 'Private event' in your work calendar.


## Setup
- Create a google cloud project (https://console.cloud.google.com/)
- Create a service account (https://console.cloud.google.com/iam-admin/serviceaccounts/create)
- Download a key for the service account (as JSON) and place it in `google_calendar_key.json`
- Create a file `config.json` and add the source and target calendar adresses
- Add the email of your service account (`client_email` within your downloaded key) to both the source and target calendar
- For the source calendar, read-only access is sufficient
- For the target calendar, write access is required (your administrator might have to enable this in the Google Admin console)
- Add a unique id to each source-target combination (this is used when multiple sources sync into the same target)
- `futureDays` defaults to `14`, `pastDays` to `7`.
- Example config:
```json
[
{
"id": "sync-id-1",
"sourceCalendar": "[email protected]",
"targetCalendar": "[email protected]",
"futureDays": 180,
"pastDays": 14
}
]
```
- Run it with `npx -y @cirqusde/google-calendar-sync`

## Publish to npm
- Bump version in `package.json`
- Run `npm install`
- Run `npm publish`
154 changes: 154 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
#!/usr/bin/node
import { calendar_v3, google } from 'googleapis';
import { JWT } from 'google-auth-library/build/src/auth/jwtclient';
import { readFileSync } from 'fs';
import { SourceTargetConfiguration } from './models';
import moment = require('moment');

const calendar: calendar_v3.Calendar = google.calendar('v3');

const key = JSON.parse(readFileSync('google_calendar_key.json', 'utf8'));


const scopes = ['https://www.googleapis.com/auth/calendar'];
const auth: JWT = new google.auth.JWT(key.client_email, undefined, key.private_key, scopes, undefined);

const syncCalendar = async (): Promise<{
createCounter: number,
updateCounter: number,
removeCounter: number,
}> => {
let createCounter = 0;
let updateCounter = 0;
let removeCounter = 0;

const config: SourceTargetConfiguration[] = JSON.parse(readFileSync('config.json', 'utf8'));

for (const configEntry of config) {
const pastDays = configEntry.pastDays || 7;
const futureDays = configEntry.futureDays || 14;

const earliestDate = moment(new Date()).subtract(pastDays, 'days');
const latestDate = moment(new Date()).add(futureDays, 'days');


const sourceEventsResponse = await calendar.events.list({
auth,
calendarId: configEntry.sourceCalendar,
timeMin: earliestDate.toISOString(),
timeMax: latestDate.toISOString(),
});
const sourceEvents = sourceEventsResponse.data.items || [];

const targetEventsResponse = await calendar.events.list({
auth,
calendarId: configEntry.targetCalendar,
timeMin: earliestDate.toISOString(),
timeMax: latestDate.toISOString(),
});
const targetEvents = targetEventsResponse.data.items;

let eventsToRemove = targetEvents?.filter(t => t.description?.includes(`google-sync-calendar-config-id: ${configEntry.id}`)) || [];

for (const sourceEvent of sourceEvents) {
const sourceEventId = sourceEvent.id || sourceEvent.iCalUID;
if (!sourceEventId) {
console.warn(`Skipping event ${sourceEvent.summary} because no id is available`);
continue;
}

if (sourceEvent.status === 'cancelled') {
// Will be removed later, if it was synced before
continue;
}

const targetEvent = targetEvents.filter(t => t.description?.includes(`google-sync-calender-source-id: ${sourceEventId}`))[0];
if (!targetEvent) {
try {
await createEvent(sourceEvent, sourceEventId, configEntry);
createCounter++;
} catch (e) {
console.error('Error while creating event', e);
}
continue;
}

// Event still exists -> Should not be removed
eventsToRemove = eventsToRemove.filter(t => t.description !== targetEvent.description);

if (areEventsEqual(sourceEvent, targetEvent)) {
// Nothing to do
continue;
}

// Something got changed
try {
await updateEvent(sourceEvent, sourceEventId, targetEvent, configEntry);
updateCounter++;
} catch (e) {
console.error('Error while updating event', e);
}
}

// Remove events which no longer exist in source
for (const eventToRemove of eventsToRemove) {
try {
await calendar.events.delete({
auth,
calendarId: configEntry.targetCalendar,
eventId: eventToRemove.id || eventToRemove.iCalUID,
});
removeCounter++;
} catch (e) {
console.error('Error while deleting event', e);
}
}
}

return {
createCounter,
updateCounter,
removeCounter,
};
}

const areEventsEqual = (a: calendar_v3.Schema$Event, b: calendar_v3.Schema$Event): boolean => {
return (a.start?.date === b.start?.date && a.start?.dateTime === b.start?.dateTime && a.start?.timeZone === b.start?.timeZone) &&
(a.end?.date === b.end?.date && a.end?.dateTime === b.end?.dateTime && a.end?.timeZone === b.end?.timeZone);
}

const createEvent = async (sourceEvent: calendar_v3.Schema$Event, sourceEventId: string, configEntry: SourceTargetConfiguration): Promise<void> => {
await calendar.events.insert({
auth,
calendarId: configEntry.targetCalendar,
requestBody: getRequestBody(sourceEvent, sourceEventId, configEntry),
});
}

const updateEvent = async (sourceEvent: calendar_v3.Schema$Event, sourceEventId: string, targetEvent: calendar_v3.Schema$Event, configEntry: SourceTargetConfiguration): Promise<void> => {
await calendar.events.update({
auth,
calendarId: configEntry.targetCalendar,
eventId: targetEvent.id || targetEvent.iCalUID,
requestBody: getRequestBody(sourceEvent, sourceEventId, configEntry),
});
}

const getRequestBody = (sourceEvent: calendar_v3.Schema$Event, sourceEventId: string, configEntry: SourceTargetConfiguration) => {
return {
summary: 'Private event',
description: `This is a private event synced by google-calendar-sync.
Do not update/remove this event manually - changes will be overwritten!
google-sync-calender-source-id: ${sourceEventId}
google-sync-calendar-config-id: ${configEntry.id}
`,
start: sourceEvent.start,
end: sourceEvent.end,
}
}

syncCalendar().then((res) => {
console.log(`Everything synced (${res.createCounter} created, ${res.updateCounter} updated, ${res.removeCounter} removed)`);
});
7 changes: 7 additions & 0 deletions models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface SourceTargetConfiguration {
id: string;
sourceCalendar: string;
targetCalendar: string;
futureDays?: number;
pastDays?: number;
}
Loading

0 comments on commit 0aedbdb

Please sign in to comment.