This repository was archived by the owner on Apr 2, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 14
/
Copy pathsetup.js
executable file
·340 lines (302 loc) · 11.8 KB
/
setup.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
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
/**
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
const util = require('util');
const exec = util.promisify(require('child_process').exec);
const spawn = require('child_process').spawn;
var fs = require('fs');
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
const serviceAccountName = "canvasquiz";
const keyFileName = "canvasquizkey.json";
const keyPath = "deploy/functions/modules/auth/";
const agentZipName = "canvas-quiz-starter-agent.zip";
function installDeps() {
return new Promise((resolve, reject) => {
const child = spawn('npm', ['run', 'installall'], { stdio: 'inherit' });
child.on('exit', function (code) {
if (code === 0) {
msg('Dependencies installed successfully');
} else {
error(`There was an error installing dependencies. You probably want to look into it.`)
}
resolve();
});
})
}
async function setup() {
msg(`This script will help set up your Canvas Quiz project.`)
msg(`\x1b[1mIT IS IMPORTANT YOU DO THESE THINGS FIRST:`);
msg(`1. Create a new Firebase project.`);
msg(`2. Enable Blaze billing for your project.`);
msg(`3. Install the Google Cloud SDK: https://cloud.google.com/sdk/docs/#install_the_latest_cloud_tools_version_cloudsdk_current_version`);
msg(`4. Create a new Dialogflow agent connected to your Firebase project.`);
const installConfirm = await prompt('Do you want to install all dependencies with npm? [y/n]:', true);
if (installConfirm) {
msg('Installing dependencies...');
await installDeps();
} else {
msg('Skipping dependency installation...');
}
msg('Getting current Google Cloud project...')
let gcloudProject;
try {
gcloudProject = await getProject();
} catch (e) {
error(`Error getting project`);
error(`More info from gcloud:`);
error(`----------------------`);
error(e);
readline.close();
return;
}
msg('Using project ' + gcloudProject);
let serviceAccountEmail;
let matchingAccounts;
try {
matchingAccounts = await getMatchingServiceAccounts(serviceAccountName, gcloudProject);
} catch (e) {
error(`Error getting service accounts. Have you run "gcloud auth login?:`);
error(`More info from gcloud:`);
error(`----------------------`);
error(e);
readline.close();
return;
}
if (matchingAccounts) {
msg("This project already has service accounts that look like they're for Canvas Quiz:");
for (const account of matchingAccounts) {
msg(account.email);
serviceAccountEmail = account.email;
}
} else {
try {
msg(`Creating service account...`);
serviceAccountEmail = await createServiceAccount(serviceAccountName, gcloudProject);
} catch (e) {
error('Error creating serivce account:');
error(JSON.stringify(e));
readline.close();
return;
}
}
if (!fs.existsSync("./" + keyPath + keyFileName)) {
msg(`Creating key...`)
try {
await createKey(serviceAccountName, gcloudProject, keyFileName);
} catch (e) {
error('Error creating key:');
error(JSON.stringify(e));
}
} else {
const keyContents = fs.readFileSync('./' + keyPath + keyFileName, 'utf8');
const key = JSON.parse(keyContents);
if (key.client_email.indexOf(`${serviceAccountName}@${gcloudProject}`) != 0) {
msg(`It looks like the downloaded key file doesn't match your service account.`);
msg(`Service account for current key file: ${key.client_email}`);
const confirm = await prompt('Download a new key?', true);
if (confirm) {
msg(`Creating key...`)
try {
await createKey(serviceAccountName, gcloudProject, keyFileName);
} catch (e) {
error('Error creating key:');
error(JSON.stringify(e));
}
} else {
msg(`Keeping current key. If you need to get another key,` +
`delete the one in deploy/functions/modules/auth and ` +
`run this again.`);
}
} else {
msg(`Keeping current key. If you need to get another key,` +
`delete the one in deploy/functions/modules/auth and ` +
`run this again.`);
}
}
msg('Enabling Google Sheets API...');
await exec('gcloud services enable sheets.googleapis.com');
msg('Please make a copy of the Google Sheet at https://docs.google.com/spreadsheets/d/1Nk6ZedoaNutKK4aJb1mD_MsKOdyUwFBeay_6cVOplBk/edit.');
msg(`Now enter your sheet's ID:`);
const sheetID = await getSheetID();
msg('Please share that sheet with the service account you just created:');
msg(serviceAccountEmail)
await prompt('Enter any character to continue:');
msg('Updating config variables...');
await replaceInFile('./deploy/functions/modules/config.js', [
{ term: '$PROJECTID', replacement: gcloudProject },
{ term: '$SHEETID', replacement: sheetID },
{ term: '$AUTHKEY', replacement: `/auth/${keyFileName}` }
])
await replaceInFile('./deploy/.firebaserc', [
{ term: '$PROJECTID', replacement: gcloudProject }
])
msg('Enabling Dialogflow API...')
await exec('gcloud services enable dialogflow.googleapis.com');
process.env.GOOGLE_APPLICATION_CREDENTIALS = __dirname + `/${keyPath}${keyFileName}`;
msg('HEADS UP! This script is about to restore the Dialogflow Agent. If you already set up an agent, this will overwrite everything. There is no undo.');
const confirm = await prompt('Are you sure? [y/n]:', true);
if (confirm) {
msg('Restoring Dialogflow agent...');
try {
await restoreDialogflowAgent(agentZipName, gcloudProject);
} catch (e) {
error(`Error restoring Dialogflow agent. Make sure you've created an agent and set the Google Proejct ID. More details:`);
error(JSON.stringify(e));
}
} else {
msg('Skipping agent restore. If you skipped this by accident, just run the script again.');
}
msg(`Make sure you update your fulfillment URL in the Dialogflow console to your Firebase functions URL`);
msg('All done.');
readline.close();
return;
}
function replaceInFile(filename, replacements) {
return new Promise((resolve, reject) => {
const fileContents = fs.readFileSync(filename, 'utf8');
let edited = fileContents;
for (var pair of replacements) {
const escapedTerm = pair.term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
edited = edited.replace(new RegExp(escapedTerm, 'g'), pair.replacement);
}
fs.writeFileSync(filename, edited, 'utf8');
resolve();
})
}
function createServiceAccount(name, project) {
return new Promise(async (resolve, reject) => {
try {
const { stdout1 } = await exec(`gcloud iam service-accounts create ${name} --format=json`);
const { stdout2 } = await exec(`gcloud projects add-iam-policy-binding ${project} --member "serviceAccount:${name}@${project}.iam.gserviceaccount.com" --role "roles/owner"`);
const email = `${name}@${project}.iam.gserviceaccount.com`;
resolve(email);
} catch (e) {
reject(e);
}
})
}
function createKey(name, project, keyFileName) {
return new Promise(async (resolve, reject) => {
try {
const { stdout } = await exec(`gcloud iam service-accounts keys create ${keyFileName} --iam-account ${name}@${project}.iam.gserviceaccount.com`);
if (!fs.existsSync(`./${keyPath}`)){
fs.mkdirSync(`./${keyPath}`);
}
fs.rename(keyFileName, `./${keyPath}${keyFileName}`, (err) => {
if (err){
reject(err);
} else{
resolve();
}
});
} catch (e) {
reject(e);
}
})
}
function getMatchingServiceAccounts(emailPart, project) {
return new Promise(async (resolve, reject) => {
try {
const { stdout } = await exec(`gcloud iam service-accounts list --filter="EMAIL:${emailPart}@*" --format=json --project="${project}"`);
const accounts = JSON.parse(stdout);
if (accounts.length === 0) {
resolve(false);
} else {
resolve(accounts);
}
} catch (e) {
reject(e);
}
})
}
async function getProject() {
return new Promise(async (resolve, reject) => {
try {
const { stdout, stderror } = await exec('gcloud config get-value project --format=json');
const project = JSON.parse(stdout);
msg("Currently using project " + project);
const result = await prompt('Does that look right? [y/n]:', true);
if (result) {
resolve(project);
} else {
const newProject = await prompt('Please enter your project name:');
await exec(`gcloud config set project ${newProject}`);
resolve(newProject);
}
} catch (e) {
console.log(JSON.stringify(e));
reject();
}
})
}
function getSheetID() {
return new Promise(async (resolve, reject) => {
readline.question('Please enter the id of your Google Sheet:', (input) => {
resolve(input);
})
})
}
function prompt(question, yesNo) {
return new Promise((resolve, reject) => {
readline.question(question, (input) => {
if (yesNo) {
if (input.toLowerCase() == "y") {
resolve(true);
} else {
resolve(false);
}
} else {
resolve(input);
}
})
})
}
function restoreDialogflowAgent(filename, project) {
return new Promise((resolve, reject) => {
const dialogflow = require('dialogflow');
const client = new dialogflow.v2.AgentsClient();
const formattedParent = client.projectPath(project);
fs.readFile(filename, function (err, data) {
const base64encoded = Buffer.from(data).toString('base64');
client.restoreAgent({
parent: formattedParent,
agentContent: base64encoded
}).then(responses => {
const [operation, initialApiResponse] = responses;
// Operation#promise starts polling for the completion of the LRO.
return operation.promise();
}).then(responses => {
const result = responses[0];
const metadata = responses[1];
const finalApiResponse = responses[2];
console.log(JSON.stringify(responses));
resolve(true);
})
.catch(err => {
reject(err);
});
})
})
}
function msg(body) {
console.log(`\x1b[96m${body}\x1b[0m`);
}
function error(body) {
console.log(`\x1b[31m${body}\x1b[0m`);
}
setup();