-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
256 lines (224 loc) · 7.55 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
/*
This code is for syncing Google Contacts downloaded from the API to a Notion database.
It is a one-way sync and does not delete Notion items when the Google Contact
is deleted out of caution.
*/
const fs = require('fs');
const http = require('http');
const url = require('url');
const port = process.env.PORT || 3000;
/* Google */
const { google } = require('googleapis');
const people = google.people('v1');
const {
GNS_GOOGLE_CLIENT_ID,
GNS_GOOGLE_CLIENT_SECRET,
GNS_GOOGLE_REDIRECT_URL,
GNS_NOTION_TOKEN_SECRET,
GNS_NOTION_DATABASE_ID,
} = process.env;
// generate a url that asks permissions for the people scope
const GoogleScopes = [
'https://www.googleapis.com/auth/contacts',
'openid',
];
/* Notion setup */
const { Client, collectPaginatedAPI } = require('@notionhq/client');
const { log, getEnv } = require('./src/util');
const { constructContactItem, isGoogleConnectionValid } = require('./src/contacts');
const { calculateContactRequests } = require('./src/syncContacts');
const { DataStore } = require('./src/dataStore');
const notion = new Client({
auth: GNS_NOTION_TOKEN_SECRET,
});
function getOauthClient() {
const oauthClient = new google.auth.OAuth2(
GNS_GOOGLE_CLIENT_ID,
GNS_GOOGLE_CLIENT_SECRET,
GNS_GOOGLE_REDIRECT_URL,
);
return oauthClient;
}
const NoCacheOptions = {
'Cache-Control': 'private, no-cache, no-store, must-revalidate',
};
const datastore = new DataStore();
async function handleAuthCallback(req, res) {
log('handleAuthCallback');
// Handle the OAuth 2.0 server response
const q = url.parse(req.url, true).query;
if (q.error) { // An error response e.g. error=access_denied
log(`Error:${q.error}`);
res.writeHead(400, NoCacheOptions);
res.write(`Error:${q.error}`);
return;
}
// Get access and refresh tokens (if access_type is offline)
try {
const oauth2Client = getOauthClient();
const { tokens } = await oauth2Client.getToken(q.code);
oauth2Client.setCredentials(tokens);
const ticket = await oauth2Client.verifyIdToken({
idToken: tokens.id_token,
audience: GNS_GOOGLE_CLIENT_ID,
});
const payload = ticket.getPayload();
const gSub = payload.sub;
if (tokens.refresh_token !== undefined) {
// store the refresh token in the DB by Google's sub
// a persistent userId, therefore primary key
try {
datastore.storeToken(gSub, tokens.refresh_token);
res.writeHead(200, NoCacheOptions);
res.write('Credentials saved');
} catch (error) {
log(`Error saving refresh token: ${error.stack}`);
res.writeHead(500, NoCacheOptions);
res.write('Server error');
}
} else {
log('handleAuthCallback: No refresh token in response');
res.writeHead(200, NoCacheOptions);
res.write('No refresh token. Please un-authorize and re-authorize this app');
}
} catch (error) {
log('handleAuthCallback: Invalid token');
res.writeHead(400, NoCacheOptions);
res.write('Invalid token');
}
}
async function handleSyncContacts(req, res) {
const q = url.parse(req.url, true).query;
const gSub = q.sub;
if (!gSub) {
res.writeHead(400, NoCacheOptions);
res.write('sub is required');
return;
}
try {
const refreshToken = await datastore.getToken(gSub);
if (!refreshToken) {
log(`Refresh token not found for ${gSub}`);
res.writeHead(400, NoCacheOptions);
res.write('Credentials not found, please re-auth');
return;
}
const oauth2Client = getOauthClient();
oauth2Client.credentials.refresh_token = refreshToken;
await oauth2Client.getAccessToken();
const { data: { connections } } = await people.people.connections.list({
auth: oauth2Client,
personFields: ['names', 'emailAddresses', 'organizations'],
resourceName: 'people/me',
pageSize: 1000,
});
log(`\n\nDownloaded ${connections.length} Google Connections\n`);
// Fetch Notion Contact Pages
const notionPages = await collectPaginatedAPI(notion.databases.query, {
database_id: GNS_NOTION_DATABASE_ID,
filter: {
property: 'contactId',
// if an item doesn't have a contactId, it is a Notion item that wasn't synced from Google
// (for example, by a user clicking new). We want to skip these.
rich_text: { is_not_empty: true },
},
});
log(`Retrieved ${notionPages.length} Notion pages`);
// Calculate changes to sync to Notion
const googleContacts = connections
.filter((connect) => isGoogleConnectionValid(connect))
.map((connect) => constructContactItem(connect));
const notionContacts = notionPages.map((page) => constructContactItem(page));
log(`Calculating changes for ${googleContacts.length} Google Connections and ${notionContacts.length} Notion Pages`);
const changes = calculateContactRequests(googleContacts, notionContacts);
log(`Found ${changes.length} changes`);
// Send changes (creates and updates) to Notion
const responses = changes.map(async (change) => {
if (change.type === 'create') {
return notion.pages.create(
change.toNotionRequestData(GNS_NOTION_DATABASE_ID),
);
} if (change.type === 'update') {
return notion.pages.update(
change.toNotionRequestData(GNS_NOTION_DATABASE_ID),
);
}
throw Error('unknown change type');
});
log('Sync run complete.');
if (getEnv() === 'dev') {
console.log('Responses:', responses);
}
res.writeHead(200, NoCacheOptions);
res.write(`Sync complete with ${changes.length} updates`);
} catch (error) {
log(`
Error: ${error}
***
Stack:
${error.stack}
`);
if (error.message === 'invalid_grant') {
res.writeHead(403, NoCacheOptions);
res.write('Invalid token, please re-authorize');
// todo delete old token
} else {
res.writeHead(500, NoCacheOptions);
res.write('An error ocurred');
}
}
}
const html = fs.readFileSync('index.html');
log('creating server');
const server = http.createServer(async (req, res) => {
await datastore.init();
if (req.method === 'POST') {
let body = '';
req.on('data', (chunk) => {
body += chunk;
});
req.on('end', () => {
if (req.url === '/') {
log(`Received message: ${body}`);
} else if (req.url === '/scheduled') {
log(`Received task ${req.headers['x-aws-sqsd-taskname']} scheduled at ${req.headers['x-aws-sqsd-scheduled-at']}`);
}
res.writeHead(200, 'OK', { 'Content-Type': 'text/plain' });
res.end();
});
} else if (req.method === 'GET') {
if (req.url === '/') {
res.writeHead(200, NoCacheOptions);
res.write(html);
res.end();
} else if (req.url === '/auth') {
const oauth2Client = getOauthClient();
const authorizationUrl = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: GoogleScopes,
include_granted_scopes: true,
});
res.writeHead(301, {
Location: authorizationUrl, NoCacheOptions,
});
} else if (req.url.startsWith('/oauth2callback')) {
await handleAuthCallback(req, res);
} else if (req.url.startsWith('/synccontacts')) {
await handleSyncContacts(req, res);
} else {
res.writeHead(404);
res.write('Not found');
}
}
res.end();
});
server.listen(port);
log(`listening on port ${port}`);
// if (module === require.main) {
// run().catch(console.error);
// }
// async function runSync() {
// // todo get the list of users/refresh tokens to sync
// }
// exports.run = run;
// exports.runSync = runSync;