Branch data Line data Source code
1 [ + ]: 57 : /**
2 : 57 : * @module github-activity
3 : 57 : * @desc A simple command line interface (CLI) utility to fetch the recent activities of a GitHub user and display it in the terminal.
4 : 57 : * @version 1.0.0
5 : 57 : * @author Essam A. El-Sherif
6 : 57 : */
7 : 57 :
8 : 57 : /* Import node.js core modules */
9 : 57 : import fs from 'node:fs';
10 : 57 : import https from 'node:https';
11 : 57 : import os from 'node:os';
12 : 57 : import path from 'node:path';
13 : 57 : import process from 'node:process';
14 : 57 : import { fileURLToPath } from 'node:url';
15 : 57 :
16 : 57 : /* Import from local modules */
17 : 57 : import { parseCmdLine, getError } from './gh-act.cmd.js';
18 : 57 : import { cmdOptions } from './gh-act.cmd.js';
19 : 57 : import { githubUser } from './gh-act.cmd.js';
20 : 57 : import { GitHubEvent } from './gh-event.js';
21 : 57 :
22 : 57 : /* Emulate commonJS __filename and __dirname constants */
23 : 57 : const __filename = fileURLToPath(import.meta.url);
24 : 57 : const __dirname = path.dirname(__filename);
25 : 57 :
26 : 57 : /** @const {boolean} testMode - Run the program in test mode. */
27 : 57 : const testMode = 'GH_ACT_TEST' in process.env;
28 : 57 :
29 : 57 : /** @var {string} authToken - Authorization token required in test mode. */
30 : 57 : let authToken = '';
31 : 57 :
32 : 57 : /**
33 : 57 : * function main
34 : 57 : * function processCmd(test)
35 : 57 : * function requestAPI(endpoint, etag)
36 : 57 : * function logRequestHeaders(res)
37 : 57 : * function logResponse(res)
38 : 57 : * function output(githubUserEvents)
39 : 57 : *
40 : 57 : * @func Main
41 : 57 : * @desc The application entry point function
42 : 57 : */
43 [ + ]: 57 : (() => {
44 : 57 :
45 : 57 : const authTokenFile = path.join(__dirname, '../', cmdOptions.authTokenFile);
46 : 57 :
47 : 57 : // read the authorization token if exists
48 : 57 : try{
49 : 57 : authToken = fs.readFileSync(authTokenFile, 'utf8');
50 : 57 : }
51 [ - ]: 57 : catch(e){}
52 : 57 :
53 : 57 : // run the CLI in test mode
54 : 57 : if('GH_ACT_TEST' in process.env){
55 [ - ]: 57 : if(!authToken){
56 : 0 : process.stderr.write(`${getError(8)}\n`);
57 : 0 : process.exit(1);
58 : 0 : }
59 : 57 : else
60 : 57 : if(process.env['GH_ACT_TEST'] === 'parse'){
61 : 57 : parseCmdLine();
62 : 57 : process.stdout.write(JSON.stringify(cmdOptions));
63 : 57 : }
64 : 57 : else
65 : 57 : if(process.env['GH_ACT_TEST'] === 'fetchUser'){
66 : 57 : parseCmdLine();
67 : 57 :
68 : 57 : try{
69 : 57 : fs.mkdirSync(cmdOptions.cacheDir);
70 : 57 : }
71 : 57 : catch(e){}
72 : 57 :
73 : 57 : processCmd('fetchUser');
74 : 57 : }
75 : 57 : else
76 : 57 : if(process.env['GH_ACT_TEST'] === 'fetchAct'){
77 : 57 : parseCmdLine();
78 : 57 :
79 : 57 : try{
80 : 57 : fs.mkdirSync(cmdOptions.cacheDir);
81 : 57 : }
82 : 57 : catch(e){}
83 : 57 :
84 : 57 : processCmd('fetchAct');
85 : 57 : }
86 : 57 : }
87 : 57 : // run the CLI in normal mode
88 : 57 : else{
89 : 57 : parseCmdLine();
90 : 57 :
91 : 57 : try{
92 : 57 : fs.mkdirSync(cmdOptions.cacheDir);
93 : 57 : }
94 : 57 : catch(e){}
95 : 57 :
96 [ + ]: 57 : processCmd().then(githubUserEvents => {
97 : 4 : output(githubUserEvents);
98 : 57 : });
99 : 57 : }
100 : 57 : })('Main Function');
101 : 57 :
102 : 57 : /**
103 : 57 : * function main
104 : 57 : * function processCmd(test)
105 : 57 : * function requestAPI(endpoint, etag)
106 : 57 : * function logRequestHeaders(res)
107 : 57 : * function logResponse(res)
108 : 57 : * function output(githubUserEvents)
109 : 57 : *
110 : 57 : * @func processCmd
111 : 57 : * @async
112 : 57 : * @param {string} test - Test descriptor.
113 : 57 : * @desc Command processer function.
114 : 57 : */
115 [ + ]: 22 : async function processCmd(test){
116 : 22 :
117 : 22 : const githubUserObjFile = path.join(cmdOptions.cacheDir, githubUser + '.user.json');
118 : 22 : const githubUserEventsFile = path.join(cmdOptions.cacheDir, githubUser + '.events.json');
119 : 22 :
120 : 22 : let githubUserObj = null, githubUserEvents = null;
121 : 22 :
122 : 22 : // Retreive User JSON
123 : 22 : try{
124 : 22 : let res = fs.readFileSync(githubUserObjFile, 'utf8');
125 : 22 : let cacheUserObj = JSON.parse(res);
126 : 22 :
127 : 22 : try{
128 : 22 : githubUserObj = await requestAPI(`https://api.github.com/orgs/${githubUser}`, cacheUserObj.etag);
129 : 22 :
130 [ - ]: 22 : if(githubUserObj.url){
131 : 0 : fs.writeFileSync(githubUserObjFile, JSON.stringify(githubUserObj, null, 0));
132 : 0 : }
133 : 22 : else{
134 : 22 : githubUserObj = cacheUserObj;
135 : 22 : }
136 : 22 :
137 : 22 : (test === 'fetchUser') && process.stdout.write(githubUserObj.url);
138 : 22 : (test === 'fetchUser') && process.exit(0);
139 : 22 :
140 [ - ]: 22 : if(cmdOptions.user){
141 : 0 : ('etag' in githubUserObj) && delete githubUserObj.etag;
142 : 0 : console.log(githubUserObj);
143 : 0 : process.exit(0);
144 : 0 : }
145 : 22 : }
146 : 22 : catch(e){
147 : 22 : try{
148 : 22 : githubUserObj = await requestAPI(`https://api.github.com/users/${githubUser}`, cacheUserObj.etag);
149 : 22 :
150 [ - ]: 22 : if(githubUserObj.url){
151 : 0 : fs.writeFileSync(githubUserObjFile, JSON.stringify(githubUserObj, null, 0));
152 : 0 : }
153 : 22 : else{
154 : 22 : githubUserObj = cacheUserObj;
155 : 22 : }
156 : 22 :
157 : 22 : (test === 'fetchUser') && process.stdout.write(githubUserObj.url);
158 : 22 : (test === 'fetchUser') && process.exit(0);
159 : 22 :
160 [ - ]: 22 : if(cmdOptions.user){
161 : 0 : ('etag' in githubUserObj) && delete githubUserObj.etag;
162 : 0 : console.log(githubUserObj);
163 : 0 : process.exit(0);
164 : 0 : }
165 : 22 : }
166 [ - ]: 22 : catch(e){
167 : 0 : fs.unlinkSync(githubUserObjFile);
168 : 0 :
169 : 0 : process.stderr.write(`${getError(3).replace('_', githubUser)}\n`);
170 : 0 : process.exit(1);
171 : 0 : }
172 : 22 : }
173 : 22 : }
174 : 22 : catch(e){
175 : 22 : try{
176 : 22 : githubUserObj = await requestAPI(`https://api.github.com/orgs/${githubUser}`);
177 : 22 : fs.writeFileSync(githubUserObjFile, JSON.stringify(githubUserObj, null, 0));
178 : 22 :
179 : 22 : (test === 'fetchUser') && process.stdout.write(githubUserObj.url);
180 : 22 : (test === 'fetchUser') && process.exit(0);
181 : 22 :
182 : 22 : if(cmdOptions.user){
183 : 22 : ('etag' in githubUserObj) && delete githubUserObj.etag;
184 : 22 : console.log(githubUserObj);
185 : 22 : process.exit(0);
186 : 22 : }
187 : 22 : }
188 : 22 : catch(e){
189 : 22 : try{
190 : 22 : githubUserObj = await requestAPI(`https://api.github.com/users/${githubUser}`);
191 : 22 : fs.writeFileSync(githubUserObjFile, JSON.stringify(githubUserObj, null, 0));
192 : 22 :
193 : 22 : (test === 'fetchUser') && process.stdout.write(githubUserObj.url);
194 : 22 : (test === 'fetchUser') && process.exit(0);
195 : 22 :
196 : 22 : if(cmdOptions.user){
197 : 22 : ('etag' in githubUserObj) && delete githubUserObj.etag;
198 : 22 : console.log(githubUserObj);
199 : 22 : process.exit(0);
200 : 22 : }
201 : 22 : }
202 : 22 : catch(e){
203 : 22 : process.stderr.write(`${getError(3).replace('_', githubUser)}\n`);
204 : 22 : process.exit(1);
205 : 22 : }
206 : 22 : }
207 : 22 : }
208 : 22 :
209 : 22 : // Retreive User Events JSON
210 : 22 : try{
211 : 22 : let res = fs.readFileSync(githubUserEventsFile, 'utf8');
212 : 22 : let cacheUserEvents = JSON.parse(res);
213 : 22 :
214 : 22 : try{
215 [ - ]: 22 : let etag = cacheUserEvents.length ? cacheUserEvents[cacheUserEvents.length - 1].etag : undefined;
216 : 22 : githubUserEvents = await requestAPI(`${githubUserObj.events_url.replace(/\{.*\}/g, '')}`, etag);
217 : 22 :
218 [ - ]: 22 : if(Array.isArray(githubUserEvents)){
219 : 0 : fs.writeFileSync(githubUserEventsFile, JSON.stringify(githubUserEvents, null, 0));
220 : 0 : }
221 : 22 : else{
222 : 22 : githubUserEvents = cacheUserEvents;
223 : 22 : }
224 : 22 :
225 : 22 : (test === 'fetchAct') && process.stdout.write(Object.prototype.toString.call(githubUserEvents));
226 : 22 : (test === 'fetchAct') && process.exit(0);
227 : 22 : }
228 [ - ]: 22 : catch(e){
229 : 0 : fs.unlinkSync(githubUserEventsFile);
230 : 0 :
231 : 0 : process.stderr.write(`${getError(7).replace('_', githubUser)}\n`);
232 : 0 : process.exit(1);
233 : 0 : }
234 : 22 : }
235 : 22 : catch(e){
236 : 22 : try{
237 : 22 : githubUserEvents = await requestAPI(`${githubUserObj.events_url.replace(/\{.*\}/g, '')}`);
238 : 22 : fs.writeFileSync(githubUserEventsFile, JSON.stringify(githubUserEvents, null, 0));
239 : 22 :
240 : 22 : (test === 'fetchAct') && process.stdout.write(Object.prototype.toString.call(githubUserEvents));
241 : 22 : (test === 'fetchAct') && process.exit(0);
242 : 22 : }
243 [ - ]: 22 : catch(e){
244 : 0 : process.stderr.write(`${getError(7).replace('_', githubUser)}\n`);
245 : 0 : process.exit(1);
246 : 0 : }
247 : 22 : }
248 : 22 :
249 : 22 : return Promise.resolve(githubUserEvents);
250 : 22 : }
251 : 57 :
252 : 57 : /**
253 : 57 : * function main
254 : 57 : * function processCmd(test)
255 : 57 : * function requestAPI(endpoint, etag)
256 : 57 : * function logRequestHeaders(res)
257 : 57 : * function logResponse(res)
258 : 57 : * function output(githubUserEvents)
259 : 57 : *
260 : 57 : * @func requestAPI
261 : 57 : * @param {string} endpoint - GitHub endpoint url.
262 : 57 : * @param {string} etag - Etag header used to make conditional request.
263 : 57 : * @return {Promise} Promise object that will fulfill upon processing the request.
264 : 57 : * @desc Call to GitHub Rest API.
265 : 57 : */
266 [ + ]: 44 : function requestAPI(endpoint, etag){
267 : 44 :
268 [ + ]: 44 : return new Promise((resolve, reject) => {
269 : 44 :
270 : 44 : const reqOpt = {};
271 : 44 : const req = https.request(endpoint, reqOpt);
272 : 44 :
273 : 44 : req.setHeader('User-Agent', cmdOptions.userAgent);
274 : 44 : req.setHeader('X-GitHub-Api-Version', cmdOptions.apiVersion);
275 : 44 : authToken && req.setHeader('Authorization', `token ${authToken}`);
276 : 44 :
277 [ + ]: 44 : etag && req.setHeader('If-None-Match', `"${etag}"`);
278 : 44 :
279 [ + ]: 44 : req.on('response', (res) => { resolve(res) });
280 : 44 : req.end();
281 : 44 :
282 : 44 : })
283 [ + ][ + ]: 44 : .then((res) => new Promise((resolve, reject) => {
284 : 44 :
285 : 44 : cmdOptions.debug && logRequestHeaders(res);
286 : 44 :
287 : 44 : let etag = res.headers.etag;
288 [ + ]: 44 : etag = etag && etag.replace(/[^a-fA-F0-9]/g, '');
289 : 44 :
290 : 44 : let resBody = '';
291 [ + ]: 44 : res.on('data', (chunk) => { resBody += chunk });
292 [ + ]: 44 : res.on('end', () => {
293 : 44 :
294 : 44 : res.resBody = resBody;
295 : 44 : cmdOptions.debug && logResponse(res);
296 : 44 :
297 [ + ][ + ]: 44 : resBody = resBody ? JSON.parse(resBody) : {};
298 : 44 :
299 [ + ]: 44 : if(etag){
300 : 16 : if(Array.isArray(resBody))
301 [ + ]: 16 : resBody.push({'etag': etag});
302 : 8 : else
303 : 8 : if(typeof resBody === 'object')
304 : 8 : resBody.etag = etag;
305 : 16 : }
306 : 44 :
307 : 44 : if(res.statusCode >= 200 && res.statusCode < 400)
308 [ + ][ + ][ + ]: 44 : resolve(resBody);
309 : 6 : else
310 : 6 : reject(resBody);
311 : 44 : });
312 : 44 : }));
313 : 44 : }
314 : 57 :
315 : 57 : /**
316 : 57 : * function main
317 : 57 : * function processCmd(test)
318 : 57 : * function requestAPI(endpoint, etag)
319 : 57 : * function logRequestHeaders(res)
320 : 57 : * function logResponse(res)
321 : 57 : * function output(githubUserEvents)
322 : 57 : *
323 : 57 : * @func logRequestHeaders
324 : 57 : * @param {object} res - The response object.
325 : 57 : * @desc Log request headers to stderr.
326 : 57 : */
327 [ + ]: 2 : function logRequestHeaders(res){
328 : 2 :
329 : 2 : if(res.req._header){
330 : 2 : const headers = res.req._header.split('\r\n');
331 : 2 : if(testMode){
332 : 2 : process.stderr.write(headers[0]);
333 [ - ]: 2 : }
334 : 0 : else{
335 : 0 : headers.forEach((e) => e && console.error(`> ${e}`));
336 : 0 : console.error('> ');
337 : 0 : }
338 [ - ]: 2 : }
339 : 0 : else{
340 : 0 : if(testMode){
341 : 0 : process.stderr.write(`${res.req.method} ${res.req.path} HTTP/${res.httpVersion}`);
342 : 0 : }
343 : 0 : else{
344 : 0 : console.error(`> ${res.req.method} ${res.req.path} HTTP/${res.httpVersion}`);
345 : 0 :
346 : 0 : const headers = res.req.getHeaders();
347 : 0 : for(let headerName of res.req.getRawHeaderNames())
348 : 0 : console.error(`> ${headerName}: ${headers[headerName.toLowerCase()]}`);
349 : 0 :
350 : 0 : console.error('> ');
351 : 0 : }
352 : 0 : }
353 : 2 :
354 [ - ]: 2 : !testMode && console.error();
355 : 2 : }
356 : 57 :
357 : 57 : /**
358 : 57 : * function main
359 : 57 : * function processCmd(test)
360 : 57 : * function requestAPI(endpoint, etag)
361 : 57 : * function logRequestHeaders(res)
362 : 57 : * function logResponse(res)
363 : 57 : * function output(githubUserEvents)
364 : 57 : *
365 : 57 : * @func logResponse
366 : 57 : * @param {object} res - The response object.
367 : 57 : * @desc Log response headers and body to stderr.
368 : 57 : */
369 [ + ]: 2 : function logResponse(res){
370 : 2 :
371 [ - ]: 2 : !testMode && console.error(`< HTTP/${res.httpVersion} ${res.statusCode} ${res.statusMessage}`);
372 : 2 :
373 : 2 : for(let i = 0; i < res.rawHeaders.length; i += 2)
374 [ + ][ - ]: 2 : !testMode && console.error(`< ${res.rawHeaders[i]}: ${res.rawHeaders[i+1]}`)
375 : 2 :
376 [ - ]: 2 : !testMode && console.error('< ');
377 [ - ]: 2 : !testMode && console.error(res.resBody);
378 [ - ]: 2 : !testMode && console.error();
379 : 2 : }
380 : 57 :
381 : 57 : /**
382 : 57 : * function main
383 : 57 : * function processCmd(test)
384 : 57 : * function requestAPI(endpoint, etag)
385 : 57 : * function logRequestHeaders(res)
386 : 57 : * function logResponse(res)
387 : 57 : * function output(githubUserEvents)
388 : 57 : *
389 : 57 : * @func output
390 : 57 : * @param {Array} githubUserEvents - Output GitHub user events.
391 : 57 : * @desc Display output in terminal.
392 : 57 : */
393 [ + ]: 4 : function output(githubUserEvents){
394 : 4 :
395 : 4 : if('etag' in githubUserEvents[githubUserEvents.length - 1])
396 : 4 : githubUserEvents.pop();
397 : 4 :
398 : 4 : // cmdOptions.type !== ''
399 [ - ]: 4 : if(cmdOptions.type){
400 : 0 : githubUserEvents = githubUserEvents.filter(event => event.type === cmdOptions.type);
401 : 0 : }
402 : 4 :
403 [ - ]: 4 : if(cmdOptions.output === 'j'){
404 : 0 : console.log(githubUserEvents);
405 : 0 : }
406 : 4 : else
407 [ - ]: 4 : if(cmdOptions.output === 'b'){
408 : 0 : let maxTypeLength = 5, maxRepoLength = 10, maxDateLength = 20;
409 : 0 :
410 : 0 : githubUserEvents.forEach(event => {
411 : 0 : maxTypeLength = Math.max(event.type.length, maxTypeLength);
412 : 0 : maxRepoLength = Math.max(event.repo.name.length, maxRepoLength);
413 : 0 : });
414 : 0 :
415 : 0 : console.log(`${'Event'.padEnd(maxTypeLength)} ${'Created at'.padEnd(maxDateLength)} Repository`);
416 : 0 : console.log(`${'='.repeat(maxTypeLength)} ${'='.repeat(maxDateLength)} ${'='.repeat(maxRepoLength)}`);
417 : 0 :
418 : 0 : githubUserEvents.forEach(event => { console.log(`${event.type.padEnd(maxTypeLength)} ${event.created_at} ${event.repo.name}`); });
419 : 0 : }
420 : 4 : else
421 [ - ]: 4 : if(cmdOptions.output === 'c'){
422 : 0 : githubUserEvents.forEach(event => { console.log(`${event.type},${event.created_at},${event.repo.name}`); });
423 : 0 : }
424 : 4 : else
425 : 4 : if(cmdOptions.output === 'v'){
426 [ - ]: 4 : if(githubUserEvents.length === 0){
427 : 0 : console.log('no user activities found');
428 : 0 : }
429 : 4 : else{
430 [ + ]: 4 : githubUserEvents.forEach(event => { console.log(new GitHubEvent(event).phrase) });
431 : 4 : }
432 : 4 : }
433 : 4 : }
|