@anmiles/google-api-wrapper 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +91 -0
- package/.gitlab-ci.yml +118 -0
- package/.vscode/settings.json +10 -0
- package/CHANGELOG.md +14 -0
- package/LICENSE.md +21 -0
- package/README.md +35 -0
- package/coverage.config.js +8 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/auth.d.ts +9 -0
- package/dist/lib/auth.js +27 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/data.d.ts +23 -0
- package/dist/lib/data.js +45 -0
- package/dist/lib/data.js.map +1 -0
- package/dist/lib/jsonLib.d.ts +14 -0
- package/dist/lib/jsonLib.js +48 -0
- package/dist/lib/jsonLib.js.map +1 -0
- package/dist/lib/logger.d.ts +12 -0
- package/dist/lib/logger.js +46 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/paths.d.ts +14 -0
- package/dist/lib/paths.js +42 -0
- package/dist/lib/paths.js.map +1 -0
- package/dist/lib/profiles.d.ts +10 -0
- package/dist/lib/profiles.js +34 -0
- package/dist/lib/profiles.js.map +1 -0
- package/dist/lib/secrets.d.ts +16 -0
- package/dist/lib/secrets.js +95 -0
- package/dist/lib/secrets.js.map +1 -0
- package/dist/lib/sleep.d.ts +6 -0
- package/dist/lib/sleep.js +11 -0
- package/dist/lib/sleep.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +18 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/secrets.d.ts +11 -0
- package/dist/types/secrets.js +3 -0
- package/dist/types/secrets.js.map +1 -0
- package/jest.config.js +22 -0
- package/package.json +50 -0
- package/src/index.ts +3 -0
- package/src/lib/__tests__/auth.test.ts +97 -0
- package/src/lib/__tests__/data.test.ts +154 -0
- package/src/lib/__tests__/jsonLib.test.ts +165 -0
- package/src/lib/__tests__/logger.test.ts +57 -0
- package/src/lib/__tests__/paths.test.ts +116 -0
- package/src/lib/__tests__/profiles.test.ts +117 -0
- package/src/lib/__tests__/secrets.test.ts +304 -0
- package/src/lib/__tests__/sleep.test.ts +17 -0
- package/src/lib/auth.ts +31 -0
- package/src/lib/data.ts +81 -0
- package/src/lib/jsonLib.ts +48 -0
- package/src/lib/logger.ts +21 -0
- package/src/lib/paths.ts +39 -0
- package/src/lib/profiles.ts +33 -0
- package/src/lib/secrets.ts +79 -0
- package/src/lib/sleep.ts +8 -0
- package/src/types/index.ts +1 -0
- package/src/types/secrets.ts +11 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import * as colorette from 'colorette';
|
|
4
|
+
import type GoogleApis from 'googleapis';
|
|
5
|
+
import jsonLib from '../jsonLib';
|
|
6
|
+
import logger from '../logger';
|
|
7
|
+
import type { Secrets } from '../../types';
|
|
8
|
+
|
|
9
|
+
import secrets from '../secrets';
|
|
10
|
+
const original = jest.requireActual('../secrets').default as typeof secrets;
|
|
11
|
+
jest.mock<typeof secrets>('../secrets', () => ({
|
|
12
|
+
getSecrets : jest.fn(),
|
|
13
|
+
getCredentials : jest.fn(),
|
|
14
|
+
createCredentials : jest.fn(),
|
|
15
|
+
checkSecrets : jest.fn(),
|
|
16
|
+
getSecretsError : jest.fn().mockImplementation(() => secretsError),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
jest.mock<Partial<typeof http>>('http', () => ({
|
|
20
|
+
createServer : jest.fn().mockImplementation((callback) => {
|
|
21
|
+
serverCallback = callback;
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
listen,
|
|
25
|
+
close,
|
|
26
|
+
};
|
|
27
|
+
}),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
jest.mock<Partial<typeof path>>('path', () => ({
|
|
31
|
+
join : jest.fn().mockImplementation((...args) => args.join('/')),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
jest.mock<Partial<typeof colorette>>('colorette', () => ({
|
|
35
|
+
yellow : jest.fn().mockImplementation((text) => `yellow:${text}`),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
jest.mock<Partial<typeof jsonLib>>('../jsonLib', () => ({
|
|
39
|
+
getJSON : jest.fn().mockImplementation(() => json),
|
|
40
|
+
getJSONAsync : jest.fn().mockImplementation(async () => json),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
jest.mock<Partial<typeof logger>>('../logger', () => ({
|
|
44
|
+
info : jest.fn(),
|
|
45
|
+
error : jest.fn().mockImplementation((error) => {
|
|
46
|
+
throw error;
|
|
47
|
+
}) as jest.Mock<never, any>,
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
const profile = 'username1';
|
|
51
|
+
const secretsFile = 'secrets/username1.json';
|
|
52
|
+
const credentialsFile = 'secrets/username1.credentials.json';
|
|
53
|
+
const wrongRedirectURI = 'wrong_redirect_uri';
|
|
54
|
+
|
|
55
|
+
const secretsError = 'secretsError';
|
|
56
|
+
|
|
57
|
+
const secretsJSON: Secrets = {
|
|
58
|
+
web : {
|
|
59
|
+
/* eslint-disable camelcase */
|
|
60
|
+
client_id : 'client_id.apps.googleusercontent.com',
|
|
61
|
+
project_id : 'project_id',
|
|
62
|
+
auth_uri : 'https://accounts.google.com/o/oauth2/auth',
|
|
63
|
+
token_uri : 'https://oauth2.googleapis.com/token',
|
|
64
|
+
auth_provider_x509_cert_url : 'https://www.googleapis.com/oauth2/v1/certs',
|
|
65
|
+
client_secret : 'client_secret',
|
|
66
|
+
redirect_uris : [ 'http://localhost:6006/oauthcallback' ],
|
|
67
|
+
/* eslint-enable camelcase */
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const credentialsJSON = {
|
|
72
|
+
token : {},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
let json: object;
|
|
76
|
+
|
|
77
|
+
const code = 'code';
|
|
78
|
+
const authUrl = 'https://authUrl';
|
|
79
|
+
const auth = {
|
|
80
|
+
generateAuthUrl : jest.fn().mockReturnValue(authUrl),
|
|
81
|
+
getToken : jest.fn().mockReturnValue({ tokens : credentialsJSON }),
|
|
82
|
+
} as unknown as GoogleApis.Common.OAuth2Client;
|
|
83
|
+
|
|
84
|
+
let request: http.IncomingMessage;
|
|
85
|
+
|
|
86
|
+
const response = {
|
|
87
|
+
end : jest.fn(),
|
|
88
|
+
} as unknown as http.ServerResponse;
|
|
89
|
+
|
|
90
|
+
let serverCallback: (
|
|
91
|
+
request: http.IncomingMessage,
|
|
92
|
+
response: http.ServerResponse
|
|
93
|
+
) => Promise<typeof credentialsJSON>;
|
|
94
|
+
|
|
95
|
+
let closedTime: number;
|
|
96
|
+
|
|
97
|
+
const listen = jest.fn();
|
|
98
|
+
const close = jest.fn().mockImplementation(() => {
|
|
99
|
+
closedTime = new Date().getTime();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('src/lib/secrets', () => {
|
|
103
|
+
describe('getSecrets', () => {
|
|
104
|
+
const getJSONSpy = jest.spyOn(jsonLib, 'getJSON');
|
|
105
|
+
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
json = secretsJSON;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should get json from secrets file', async () => {
|
|
111
|
+
await original.getSecrets(profile);
|
|
112
|
+
|
|
113
|
+
expect(getJSONSpy).toBeCalled();
|
|
114
|
+
expect(getJSONSpy.mock.calls[0][0]).toEqual(secretsFile);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should fallback to error', async () => {
|
|
118
|
+
await original.getSecrets(profile);
|
|
119
|
+
|
|
120
|
+
expect(getJSONSpy.mock.calls[0][1]).toThrowError(secretsError);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should check secrets', async () => {
|
|
124
|
+
await original.getSecrets(profile);
|
|
125
|
+
|
|
126
|
+
expect(secrets.checkSecrets).toBeCalledWith(profile, json, secretsFile);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should return secrets', async () => {
|
|
130
|
+
const result = await original.getSecrets(profile);
|
|
131
|
+
|
|
132
|
+
expect(result).toEqual(secretsJSON);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('getCredentials', () => {
|
|
137
|
+
const getJSONAsyncSpy = jest.spyOn(jsonLib, 'getJSONAsync');
|
|
138
|
+
|
|
139
|
+
beforeEach(() => {
|
|
140
|
+
json = credentialsJSON;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should get json from credentials file', async () => {
|
|
144
|
+
await original.getCredentials(profile, auth);
|
|
145
|
+
|
|
146
|
+
expect(getJSONAsyncSpy).toBeCalled();
|
|
147
|
+
expect(getJSONAsyncSpy.mock.calls[0][0]).toEqual(credentialsFile);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should fallback to createCredentials', async () => {
|
|
151
|
+
await original.getCredentials(profile, auth);
|
|
152
|
+
|
|
153
|
+
const fallback = getJSONAsyncSpy.mock.calls[0][1];
|
|
154
|
+
await fallback();
|
|
155
|
+
|
|
156
|
+
expect(secrets.createCredentials).toBeCalledWith(profile, auth);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should return credentials', async () => {
|
|
160
|
+
const result = await original.getCredentials(profile, auth);
|
|
161
|
+
|
|
162
|
+
expect(result).toEqual(credentialsJSON);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('createCredentials', () => {
|
|
167
|
+
function willOpen(request: http.IncomingMessage, timeout: number) {
|
|
168
|
+
setTimeout(async () => {
|
|
169
|
+
await serverCallback(request, response);
|
|
170
|
+
}, timeout);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
beforeEach(() => {
|
|
174
|
+
request = {
|
|
175
|
+
url : `/request.url?code=${code}`,
|
|
176
|
+
headers : {
|
|
177
|
+
host : 'localhost:6006',
|
|
178
|
+
},
|
|
179
|
+
} as http.IncomingMessage;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should generate authUrl', async () => {
|
|
183
|
+
willOpen(request, 100);
|
|
184
|
+
|
|
185
|
+
await original.createCredentials(profile, auth);
|
|
186
|
+
|
|
187
|
+
expect(auth.generateAuthUrl).toBeCalledWith({
|
|
188
|
+
// eslint-disable-next-line camelcase
|
|
189
|
+
access_type : 'offline',
|
|
190
|
+
scope : [ 'https://www.googleapis.com/auth/youtube.readonly' ],
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should create server on 6006 port', async () => {
|
|
195
|
+
willOpen(request, 100);
|
|
196
|
+
|
|
197
|
+
await original.createCredentials(profile, auth);
|
|
198
|
+
|
|
199
|
+
expect(http.createServer).toBeCalled();
|
|
200
|
+
expect(listen).toBeCalledWith(6006);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should ask to open browser page', async () => {
|
|
204
|
+
willOpen(request, 100);
|
|
205
|
+
|
|
206
|
+
await original.createCredentials(profile, auth);
|
|
207
|
+
|
|
208
|
+
expect(logger.info).toBeCalledWith(`Please open yellow:https://authUrl in your browser using google profile for yellow:${profile} and allow access to yellow:https://www.googleapis.com/auth/youtube.readonly`);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should ask to close webpage', async () => {
|
|
212
|
+
willOpen(request, 100);
|
|
213
|
+
|
|
214
|
+
await original.createCredentials(profile, auth);
|
|
215
|
+
|
|
216
|
+
expect(response.end).toBeCalledWith('<h1>Please close this page and return to application</h1>');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should close server if request.url is truthy', async () => {
|
|
220
|
+
willOpen(request, 100);
|
|
221
|
+
|
|
222
|
+
await original.createCredentials(profile, auth);
|
|
223
|
+
|
|
224
|
+
expect(close).toBeCalled();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should only resolve when request.url is truthy', async () => {
|
|
228
|
+
const emptyRequestTime = 100;
|
|
229
|
+
const requestTime = 200;
|
|
230
|
+
const emptyRequest = { ...request } as http.IncomingMessage;
|
|
231
|
+
emptyRequest.url = undefined;
|
|
232
|
+
|
|
233
|
+
const before = new Date().getTime();
|
|
234
|
+
willOpen(emptyRequest, emptyRequestTime);
|
|
235
|
+
willOpen(request, requestTime);
|
|
236
|
+
|
|
237
|
+
const result = await original.createCredentials(profile, auth);
|
|
238
|
+
const after = new Date().getTime();
|
|
239
|
+
|
|
240
|
+
expect(close).toBeCalledTimes(1);
|
|
241
|
+
expect(closedTime - before).toBeGreaterThanOrEqual(requestTime - 1);
|
|
242
|
+
expect(after - before).toBeGreaterThanOrEqual(requestTime - 1);
|
|
243
|
+
expect(result).toEqual(credentialsJSON);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should only resolve when request.url contains no code', async () => {
|
|
247
|
+
const noCodeRequestTime = 100;
|
|
248
|
+
const requestTime = 200;
|
|
249
|
+
const noCodeRequest = { ...request } as http.IncomingMessage;
|
|
250
|
+
noCodeRequest.url = '/request.url?param=value';
|
|
251
|
+
|
|
252
|
+
const before = new Date().getTime();
|
|
253
|
+
willOpen(noCodeRequest, noCodeRequestTime);
|
|
254
|
+
willOpen(request, requestTime);
|
|
255
|
+
|
|
256
|
+
const result = await original.createCredentials(profile, auth);
|
|
257
|
+
const after = new Date().getTime();
|
|
258
|
+
|
|
259
|
+
expect(close).toBeCalledTimes(1);
|
|
260
|
+
expect(closedTime - before).toBeGreaterThanOrEqual(requestTime - 1);
|
|
261
|
+
expect(after - before).toBeGreaterThanOrEqual(requestTime - 1);
|
|
262
|
+
expect(result).toEqual(credentialsJSON);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should return credentials JSON', async () => {
|
|
266
|
+
willOpen(request, 100);
|
|
267
|
+
|
|
268
|
+
const result = await original.createCredentials(profile, auth);
|
|
269
|
+
|
|
270
|
+
expect(result).toEqual(credentialsJSON);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe('checkSecrets', () => {
|
|
275
|
+
it('should return true if redirect_uri is correct', () => {
|
|
276
|
+
const result = original.checkSecrets(profile, secretsJSON, secretsFile);
|
|
277
|
+
|
|
278
|
+
expect(result).toBe(true);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should output error if redirect_uri is incorrect', () => {
|
|
282
|
+
const wrongSecretsJSON = { ...secretsJSON };
|
|
283
|
+
wrongSecretsJSON.web.redirect_uris[0] = wrongRedirectURI;
|
|
284
|
+
const func = () => original.checkSecrets(profile, wrongSecretsJSON, secretsFile);
|
|
285
|
+
|
|
286
|
+
expect(func).toThrowError('Error in credentials file: redirect URI should be http://localhost:6006/oauthcallback.\nsecretsError');
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe('getSecretsError', () => {
|
|
291
|
+
it('should return error message with instructions', () => {
|
|
292
|
+
const result = original.getSecretsError(profile, secretsFile);
|
|
293
|
+
expect(result).toEqual(`File ${secretsFile} not found!\n\
|
|
294
|
+
To obtain it, please create correct OAuth client ID:\n\
|
|
295
|
+
\tGo to https://console.cloud.google.com/apis/credentials/oauthclient\n\
|
|
296
|
+
\t[if applicable] Click "+ Create credentials" and choose "OAuth client ID\n\
|
|
297
|
+
\tSet application type "Web application"\n\
|
|
298
|
+
\tAdd authorized redirect URI: http://localhost:6006/oauthcallback\n\
|
|
299
|
+
\tClick "Create"\n\
|
|
300
|
+
\tClick "Download JSON" and download credentials to ./secrets/${profile}.json\n\
|
|
301
|
+
Then start this script again`);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import sleep from '../sleep';
|
|
2
|
+
|
|
3
|
+
const sleepMilliseconds = 300;
|
|
4
|
+
|
|
5
|
+
describe('src/lib/sleep', () => {
|
|
6
|
+
describe('sleep', () => {
|
|
7
|
+
|
|
8
|
+
it('should wait specified timeout', async () => {
|
|
9
|
+
const before = new Date().getTime();
|
|
10
|
+
|
|
11
|
+
await sleep.sleep(sleepMilliseconds);
|
|
12
|
+
|
|
13
|
+
const after = new Date().getTime();
|
|
14
|
+
expect(after - before).toBeGreaterThanOrEqual(sleepMilliseconds - 1);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
});
|
package/src/lib/auth.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
import type GoogleApis from 'googleapis';
|
|
3
|
+
import { getProfiles } from './profiles';
|
|
4
|
+
import { getCredentials, getSecrets } from './secrets';
|
|
5
|
+
|
|
6
|
+
import auth from './auth';
|
|
7
|
+
|
|
8
|
+
export { login, getAuth };
|
|
9
|
+
export default { login, getAuth };
|
|
10
|
+
|
|
11
|
+
async function login(profile?: string): Promise<void> {
|
|
12
|
+
const profiles = getProfiles().filter((p) => !profile || p === profile);
|
|
13
|
+
|
|
14
|
+
for (const profile of profiles) {
|
|
15
|
+
await auth.getAuth(profile);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function getAuth(profile: string): Promise<GoogleApis.Common.OAuth2Client> {
|
|
20
|
+
const secrets = await getSecrets(profile);
|
|
21
|
+
|
|
22
|
+
const googleAuth = new google.auth.OAuth2(
|
|
23
|
+
secrets.web.client_id,
|
|
24
|
+
secrets.web.client_secret,
|
|
25
|
+
secrets.web.redirect_uris[0],
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const tokens = await getCredentials(profile, googleAuth);
|
|
29
|
+
googleAuth.setCredentials(tokens);
|
|
30
|
+
return googleAuth;
|
|
31
|
+
}
|
package/src/lib/data.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
import type GoogleApis from 'googleapis';
|
|
3
|
+
import { getAuth } from './auth';
|
|
4
|
+
import data from './data';
|
|
5
|
+
import { log } from './logger';
|
|
6
|
+
import { sleep } from './sleep';
|
|
7
|
+
|
|
8
|
+
export { getItems, getCalendarAPI, getYoutubeAPI, getEvents, getVideos, setEvent };
|
|
9
|
+
export default { getItems, getCalendarAPI, getYoutubeAPI, getEvents, getVideos, setEvent };
|
|
10
|
+
|
|
11
|
+
type CommonApi<TArgs, TResponse> = {
|
|
12
|
+
list: (
|
|
13
|
+
params: TArgs & {pageToken: string | undefined},
|
|
14
|
+
options?: GoogleApis.Common.MethodOptions | undefined
|
|
15
|
+
) => Promise<GoogleApis.Common.GaxiosResponse<TResponse>>
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type CommonResponse<TItem> = {
|
|
19
|
+
items?: TItem[],
|
|
20
|
+
pageInfo?: {
|
|
21
|
+
totalResults?: number | null | undefined
|
|
22
|
+
},
|
|
23
|
+
nextPageToken?: string | null | undefined
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const requestInterval = 300;
|
|
27
|
+
|
|
28
|
+
async function getItems<
|
|
29
|
+
TApi extends CommonApi<TArgs, TResponse>,
|
|
30
|
+
TItem,
|
|
31
|
+
TArgs,
|
|
32
|
+
TResponse extends CommonResponse<TItem>
|
|
33
|
+
>(api: TApi, args: TArgs): Promise<TItem[]> {
|
|
34
|
+
const items: TItem[] = [];
|
|
35
|
+
|
|
36
|
+
let pageToken: string | null | undefined = undefined;
|
|
37
|
+
|
|
38
|
+
do {
|
|
39
|
+
const response: GoogleApis.Common.GaxiosResponse<TResponse> = await api.list({ ...args, pageToken });
|
|
40
|
+
response.data.items?.forEach((item) => items.push(item));
|
|
41
|
+
log(`Getting items (${items.length} of ${response.data.pageInfo?.totalResults || 'many'})...`);
|
|
42
|
+
pageToken = response.data.nextPageToken;
|
|
43
|
+
|
|
44
|
+
await sleep(requestInterval);
|
|
45
|
+
} while (pageToken);
|
|
46
|
+
|
|
47
|
+
return items;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function getCalendarAPI(profile: string): Promise<GoogleApis.calendar_v3.Calendar> {
|
|
51
|
+
const googleAuth = await getAuth(profile);
|
|
52
|
+
|
|
53
|
+
return google.calendar({
|
|
54
|
+
version : 'v3',
|
|
55
|
+
auth : googleAuth,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function getYoutubeAPI(profile: string): Promise<GoogleApis.youtube_v3.Youtube> {
|
|
60
|
+
const googleAuth = await getAuth(profile);
|
|
61
|
+
|
|
62
|
+
return google.youtube({
|
|
63
|
+
version : 'v3',
|
|
64
|
+
auth : googleAuth,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function getEvents(profile: string, args: GoogleApis.calendar_v3.Params$Resource$Events$List) {
|
|
69
|
+
const api = await data.getCalendarAPI(profile);
|
|
70
|
+
return getItems(api.events, args);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function getVideos(profile: string, args: GoogleApis.youtube_v3.Params$Resource$Playlistitems$List) {
|
|
74
|
+
const api = await data.getYoutubeAPI(profile);
|
|
75
|
+
return getItems(api.playlistItems, args);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function setEvent(profile: string, eventId: string | undefined, args: GoogleApis.calendar_v3.Params$Resource$Events$Update) {
|
|
79
|
+
const api = await data.getCalendarAPI(profile);
|
|
80
|
+
api.events.update({ eventId, ...args });
|
|
81
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { error } from './logger';
|
|
3
|
+
import { ensureFile } from './paths';
|
|
4
|
+
|
|
5
|
+
import jsonLib from './jsonLib';
|
|
6
|
+
|
|
7
|
+
export { getJSON, getJSONAsync, writeJSON };
|
|
8
|
+
export default { getJSON, getJSONAsync, writeJSON, readJSON, checkJSON };
|
|
9
|
+
|
|
10
|
+
function getJSON<T>(filename: string, createCallback: () => Exclude<T, Promise<any>>): T {
|
|
11
|
+
if (fs.existsSync(filename)) {
|
|
12
|
+
return jsonLib.readJSON(filename);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const json = createCallback();
|
|
16
|
+
jsonLib.checkJSON(filename, json);
|
|
17
|
+
ensureFile(filename);
|
|
18
|
+
jsonLib.writeJSON(filename, json);
|
|
19
|
+
return json;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function getJSONAsync<T>(filename: string, createCallbackAsync: () => Promise<T>): Promise<T> {
|
|
23
|
+
if (fs.existsSync(filename)) {
|
|
24
|
+
return jsonLib.readJSON(filename);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const json = await createCallbackAsync();
|
|
28
|
+
jsonLib.checkJSON(filename, json);
|
|
29
|
+
jsonLib.writeJSON(filename, json);
|
|
30
|
+
return json;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeJSON<T>(filename: string, json: T): void {
|
|
34
|
+
const jsonString = JSON.stringify(json, null, ' ');
|
|
35
|
+
fs.writeFileSync(filename, jsonString);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readJSON<T>(filename: string): T {
|
|
39
|
+
const jsonString = fs.readFileSync(filename).toString();
|
|
40
|
+
return JSON.parse(jsonString) as T;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function checkJSON<T>(filename: string, json: T): void {
|
|
44
|
+
if (json) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
error(`File ${filename} doesn't exist and should be created with initial data, but function createCallback returned nothing`);
|
|
48
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as colorette from 'colorette';
|
|
2
|
+
|
|
3
|
+
export { log, info, warn, error };
|
|
4
|
+
export default { log, info, warn, error };
|
|
5
|
+
|
|
6
|
+
function log(message: string): void {
|
|
7
|
+
console.log(message);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function info(message: string): void {
|
|
11
|
+
console.log(colorette.greenBright(message));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function warn(message: string): void {
|
|
15
|
+
console.warn(colorette.yellowBright(message));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function error(message: string): never {
|
|
19
|
+
console.error(`${colorette.redBright(message)}\n`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
package/src/lib/paths.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import paths from './paths';
|
|
4
|
+
|
|
5
|
+
export { ensureDir, ensureFile, getProfilesFile, getSecretsFile, getCredentialsFile };
|
|
6
|
+
export default { ensureDir, ensureFile, getProfilesFile, getSecretsFile, getCredentialsFile };
|
|
7
|
+
|
|
8
|
+
const dirPaths = {
|
|
9
|
+
input : 'input',
|
|
10
|
+
secrets : 'secrets',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function ensureDir(dirPath: string) {
|
|
14
|
+
if (!fs.existsSync(dirPath)) {
|
|
15
|
+
fs.mkdirSync(dirPath, { recursive : true });
|
|
16
|
+
}
|
|
17
|
+
return dirPath;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function ensureFile(filePath: string) {
|
|
21
|
+
paths.ensureDir(path.dirname(filePath));
|
|
22
|
+
|
|
23
|
+
if (!fs.existsSync(filePath)) {
|
|
24
|
+
fs.writeFileSync(filePath, '');
|
|
25
|
+
}
|
|
26
|
+
return filePath;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getProfilesFile() {
|
|
30
|
+
return path.join(dirPaths.input, 'profiles.json');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getSecretsFile(profile: string) {
|
|
34
|
+
return path.join(dirPaths.secrets, `${profile}.json`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getCredentialsFile(profile: string) {
|
|
38
|
+
return path.join(dirPaths.secrets, `${profile}.credentials.json`);
|
|
39
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getJSON, writeJSON } from './jsonLib';
|
|
2
|
+
import { error } from './logger';
|
|
3
|
+
import { getProfilesFile } from './paths';
|
|
4
|
+
|
|
5
|
+
import profiles from './profiles';
|
|
6
|
+
|
|
7
|
+
export { getProfiles, setProfiles, createProfile };
|
|
8
|
+
export default { getProfiles, setProfiles, createProfile };
|
|
9
|
+
|
|
10
|
+
function getProfiles(): string[] {
|
|
11
|
+
const profilesFile = getProfilesFile();
|
|
12
|
+
return getJSON(profilesFile, () => []);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function setProfiles(profiles: string[]): void {
|
|
16
|
+
const profilesFile = getProfilesFile();
|
|
17
|
+
writeJSON(profilesFile, profiles);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createProfile(profile: string): void {
|
|
21
|
+
if (!profile) {
|
|
22
|
+
error('Usage: `npm run create <profile>` where `profile` - is any profile name you want');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const existingProfiles = profiles.getProfiles();
|
|
26
|
+
|
|
27
|
+
if (existingProfiles.includes(profile)) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
existingProfiles.push(profile);
|
|
32
|
+
profiles.setProfiles(existingProfiles);
|
|
33
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import * as colorette from 'colorette';
|
|
3
|
+
import type GoogleApis from 'googleapis';
|
|
4
|
+
import type { Secrets } from '../types';
|
|
5
|
+
import { getJSON, getJSONAsync } from './jsonLib';
|
|
6
|
+
import { info, error } from './logger';
|
|
7
|
+
import { getSecretsFile, getCredentialsFile } from './paths';
|
|
8
|
+
|
|
9
|
+
import secrets from './secrets';
|
|
10
|
+
|
|
11
|
+
export { getSecrets, getCredentials };
|
|
12
|
+
export default { getSecrets, getCredentials, createCredentials, checkSecrets, getSecretsError };
|
|
13
|
+
|
|
14
|
+
const callbackPort = 6006;
|
|
15
|
+
const callbackURI = `http://localhost:${callbackPort}/oauthcallback`;
|
|
16
|
+
const scope = [ 'https://www.googleapis.com/auth/youtube.readonly' ];
|
|
17
|
+
|
|
18
|
+
async function getSecrets(profile: string): Promise<Secrets> {
|
|
19
|
+
const secretsFile = getSecretsFile(profile);
|
|
20
|
+
const secretsObject = getJSON<Secrets>(secretsFile, () => error(secrets.getSecretsError(profile, secretsFile)) as never);
|
|
21
|
+
secrets.checkSecrets(profile, secretsObject, secretsFile);
|
|
22
|
+
return secretsObject;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function getCredentials(profile: string, auth: GoogleApis.Common.OAuth2Client): Promise<GoogleApis.Auth.Credentials> {
|
|
26
|
+
const credentialsFile = getCredentialsFile(profile);
|
|
27
|
+
return getJSONAsync(credentialsFile, () => secrets.createCredentials(profile, auth));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function createCredentials(profile: string, auth: GoogleApis.Auth.OAuth2Client): Promise<GoogleApis.Auth.Credentials> {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
const authUrl = auth.generateAuthUrl({
|
|
33
|
+
// eslint-disable-next-line camelcase
|
|
34
|
+
access_type : 'offline',
|
|
35
|
+
scope,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const server = http.createServer(async (request, response) => {
|
|
39
|
+
response.end('<h1>Please close this page and return to application</h1>');
|
|
40
|
+
|
|
41
|
+
if (request.url) {
|
|
42
|
+
const url = new URL(`http://${request.headers.host}${request.url}`);
|
|
43
|
+
const code = url.searchParams.get('code');
|
|
44
|
+
|
|
45
|
+
if (!code) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
server.close();
|
|
50
|
+
const { tokens } = await auth.getToken(code);
|
|
51
|
+
resolve(tokens);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
server.listen(callbackPort);
|
|
56
|
+
info(`Please open ${colorette.yellow(authUrl)} in your browser using google profile for ${colorette.yellow(profile)} and allow access to ${colorette.yellow(scope.join(','))}`);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function checkSecrets(profile: string, secretsObject: Secrets, secretsFile: string): true | void {
|
|
61
|
+
if (secretsObject.web.redirect_uris[0] === callbackURI) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
error(`Error in credentials file: redirect URI should be ${callbackURI}.\n${secrets.getSecretsError(profile, secretsFile)}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getSecretsError(profile: string, secretsFile: string) {
|
|
68
|
+
return [
|
|
69
|
+
`File ${secretsFile} not found!`,
|
|
70
|
+
'To obtain it, please create correct OAuth client ID:',
|
|
71
|
+
'\tGo to https://console.cloud.google.com/apis/credentials/oauthclient',
|
|
72
|
+
'\t[if applicable] Click "+ Create credentials" and choose "OAuth client ID',
|
|
73
|
+
'\tSet application type "Web application"',
|
|
74
|
+
`\tAdd authorized redirect URI: ${callbackURI}`,
|
|
75
|
+
'\tClick "Create"',
|
|
76
|
+
`\tClick "Download JSON" and download credentials to ./secrets/${profile}.json`,
|
|
77
|
+
'Then start this script again',
|
|
78
|
+
].join('\n');
|
|
79
|
+
}
|
package/src/lib/sleep.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './secrets';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface Secrets {
|
|
2
|
+
web: {
|
|
3
|
+
client_id: `${string}.apps.googleusercontent.com`;
|
|
4
|
+
project_id: string;
|
|
5
|
+
auth_uri: 'https://accounts.google.com/o/oauth2/auth';
|
|
6
|
+
token_uri: 'https://oauth2.googleapis.com/token';
|
|
7
|
+
auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs';
|
|
8
|
+
client_secret: string;
|
|
9
|
+
redirect_uris: string[];
|
|
10
|
+
};
|
|
11
|
+
}
|