Source: lib/gh-act.js

/**
 * @module  github-user-activity
 * @desc    A simple command line interface (CLI) utility to fetch and display the recent activities of a GitHub user using GitHub API.
 * @version 1.0.0
 * @author  Essam A. El-Sherif
 */

/* Import node.js core modules */
import fs                from 'node:fs';
import https             from 'node:https';
import os                from 'node:os';
import path              from 'node:path';
import process           from 'node:process';
import { fileURLToPath } from 'node:url';

/* Import from local modules */
import { parseCmdLine, getError } from './gh-act.cmd.js';
import { cmdOptions }             from './gh-act.cmd.js';
import { githubUser }             from './gh-act.cmd.js';
import { GitHubEvent }            from './gh-event.js';

/* Emulate commonJS __filename and __dirname constants */
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

/** @const {boolean} testMode - Run the program in test mode. */
const testMode = 'GH_ACT_TEST' in process.env;

/** @var {string} authToken - Authorization token required in test mode. */
let authToken = '';

/**
 *     function main
 * function processCmd(test)
 * function requestAPI(endpoint, etag)
 * function logRequestHeaders(res)
 * function logResponse(res)
 * function output(githubUserEvents)
 *
 * @name Main
 * @func
 * @desc The application entry point function
 */
(() => {

	const authTokenFile = path.join(__dirname, '../', cmdOptions.authTokenFile);

	// read the authorization token if exists
	try{
		authToken = fs.readFileSync(authTokenFile, 'utf8');
	}
	catch(e){}

	// run the CLI in test mode
	if('GH_ACT_TEST' in process.env){
		if(!authToken){
			process.stderr.write(`${getError(8)}\n`);
			process.exit(1);
		}
		else
		if(process.env['GH_ACT_TEST'] === 'parse'){
			parseCmdLine();
			process.stdout.write(JSON.stringify(cmdOptions));
		}
		else
		if(process.env['GH_ACT_TEST'] === 'fetchUser'){
			parseCmdLine();

			try{
				fs.mkdirSync(cmdOptions.cacheDir);
			}
			catch(e){}

			processCmd('fetchUser');
		}
		else
		if(process.env['GH_ACT_TEST'] === 'fetchAct'){
			parseCmdLine();

			try{
				fs.mkdirSync(cmdOptions.cacheDir);
			}
			catch(e){}

			processCmd('fetchAct');
		}
	}
	// run the CLI in normal mode
	else{
		parseCmdLine();

		try{
			fs.mkdirSync(cmdOptions.cacheDir);
		}
		catch(e){}

		processCmd().then(githubUserEvents => {
			output(githubUserEvents);
		});
	}
})('Main Function');

/**
 * function main
 *     function processCmd(test)
 * function requestAPI(endpoint, etag)
 * function logRequestHeaders(res)
 * function logResponse(res)
 * function output(githubUserEvents)
 *
 * @func  processCmd
 * @async
 * @param {string} test - Test descriptor.
 * @desc  Command processer function.
 */
async function processCmd(test){

	const githubUserObjFile = path.join(cmdOptions.cacheDir, githubUser + '.user.json');
	const githubUserEventsFile =  path.join(cmdOptions.cacheDir, githubUser + '.events.json');

	let githubUserObj = null, githubUserEvents = null;

	// Retreive User JSON
	try{
		let res = fs.readFileSync(githubUserObjFile, 'utf8');
		let cacheUserObj = JSON.parse(res);

		try{
			githubUserObj = await requestAPI(`https://api.github.com/orgs/${githubUser}`, cacheUserObj.etag);

			if(githubUserObj.url){
				fs.writeFileSync(githubUserObjFile, JSON.stringify(githubUserObj, null, 0));
			}
			else{
				githubUserObj = cacheUserObj;
			}

			(test === 'fetchUser') && process.stdout.write(githubUserObj.url);
			(test === 'fetchUser') && process.exit(0);

			if(cmdOptions.user){
				('etag' in githubUserObj) && delete githubUserObj.etag;
				console.log(githubUserObj);
				process.exit(0);
			}
		}
		catch(e){
			try{
				githubUserObj = await requestAPI(`https://api.github.com/users/${githubUser}`, cacheUserObj.etag);

				if(githubUserObj.url){
					fs.writeFileSync(githubUserObjFile, JSON.stringify(githubUserObj, null, 0));
				}
				else{
					githubUserObj = cacheUserObj;
				}

				(test === 'fetchUser') && process.stdout.write(githubUserObj.url);
				(test === 'fetchUser') && process.exit(0);

				if(cmdOptions.user){
					('etag' in githubUserObj) && delete githubUserObj.etag;
					console.log(githubUserObj);
					process.exit(0);
				}
			}
			catch(e){
				fs.unlinkSync(githubUserObjFile);

				process.stderr.write(`${getError(3).replace('_', githubUser)}\n`);
				process.exit(1);
			}
		}
	}
	catch(e){
		try{
			githubUserObj = await requestAPI(`https://api.github.com/orgs/${githubUser}`);
			fs.writeFileSync(githubUserObjFile, JSON.stringify(githubUserObj, null, 0));

			(test === 'fetchUser') && process.stdout.write(githubUserObj.url);
			(test === 'fetchUser') && process.exit(0);

			if(cmdOptions.user){
				('etag' in githubUserObj) && delete githubUserObj.etag;
				console.log(githubUserObj);
				process.exit(0);
			}
		}
		catch(e){
			try{
				githubUserObj = await requestAPI(`https://api.github.com/users/${githubUser}`);
				fs.writeFileSync(githubUserObjFile, JSON.stringify(githubUserObj, null, 0));

				(test === 'fetchUser') && process.stdout.write(githubUserObj.url);
				(test === 'fetchUser') && process.exit(0);

				if(cmdOptions.user){
					('etag' in githubUserObj) && delete githubUserObj.etag;
					console.log(githubUserObj);
					process.exit(0);
				}
			}
			catch(e){
				process.stderr.write(`${getError(3).replace('_', githubUser)}\n`);
				process.exit(1);
			}
		}
	}

	// Retreive User Events JSON
	try{
		let res = fs.readFileSync(githubUserEventsFile, 'utf8');
		let cacheUserEvents = JSON.parse(res);

		try{
			let etag = cacheUserEvents.length ? cacheUserEvents[cacheUserEvents.length - 1].etag : undefined;
			githubUserEvents = await requestAPI(`${githubUserObj.events_url.replace(/\{.*\}/g, '')}`, etag);

			if(Array.isArray(githubUserEvents)){
				fs.writeFileSync(githubUserEventsFile, JSON.stringify(githubUserEvents, null, 0));
			}
			else{
				githubUserEvents = cacheUserEvents;
			}

			(test === 'fetchAct') && process.stdout.write(Object.prototype.toString.call(githubUserEvents));
			(test === 'fetchAct') && process.exit(0);
		}
		catch(e){
			fs.unlinkSync(githubUserEventsFile);

			process.stderr.write(`${getError(7).replace('_', githubUser)}\n`);
			process.exit(1);
		}
	}
	catch(e){
		try{
			githubUserEvents = await requestAPI(`${githubUserObj.events_url.replace(/\{.*\}/g, '')}`);
			fs.writeFileSync(githubUserEventsFile, JSON.stringify(githubUserEvents, null, 0));

			(test === 'fetchAct') && process.stdout.write(Object.prototype.toString.call(githubUserEvents));
			(test === 'fetchAct') && process.exit(0);
		}
		catch(e){
			process.stderr.write(`${getError(7).replace('_', githubUser)}\n`);
			process.exit(1);
		}
	}

	return Promise.resolve(githubUserEvents);
}

/**
 * function main
 * function processCmd(test)
 *     function requestAPI(endpoint, etag)
 * function logRequestHeaders(res)
 * function logResponse(res)
 * function output(githubUserEvents)
 *
 * @func   requestAPI
 * @param  {string} endpoint - GitHub endpoint url.
 * @param  {string} etag - Etag header used to make conditional request.
 * @return {Promise} Promise object that will fulfill upon processing the request.
 * @desc   Call to GitHub Rest API.
 */
function requestAPI(endpoint, etag){

	return new Promise((resolve, reject) => {

		const reqOpt = {};
		const req = https.request(endpoint, reqOpt);

		req.setHeader('User-Agent', cmdOptions.userAgent);
		req.setHeader('X-GitHub-Api-Version', cmdOptions.apiVersion);
		authToken && req.setHeader('Authorization', `token ${authToken}`);

		etag && req.setHeader('If-None-Match', `"${etag}"`);

		req.on('response', (res) => { resolve(res) });
		req.end();

	})
	.then((res) => new Promise((resolve, reject) => {

		cmdOptions.debug && logRequestHeaders(res);

		let etag = res.headers.etag;
		etag = etag && etag.replace(/[^a-fA-F0-9]/g, '');

		let resBody = '';
		res.on('data', (chunk) => { resBody += chunk });
		res.on('end', () => {

			res.resBody = resBody;
			cmdOptions.debug && logResponse(res);

			resBody = resBody ? JSON.parse(resBody) : {};

			if(etag){
				if(Array.isArray(resBody))
					resBody.push({'etag': etag});
				else
				if(typeof resBody === 'object')
					resBody.etag = etag;
			}

			if(res.statusCode >= 200 && res.statusCode < 400)
				resolve(resBody);
			else
				reject(resBody);
		});
	}));
}

/**
 * function main
 * function processCmd(test)
 * function requestAPI(endpoint, etag)
 *     function logRequestHeaders(res)
 * function logResponse(res)
 * function output(githubUserEvents)
 *
 * @func   logRequestHeaders
 * @param  {object} res - The response object.
 * @desc   Log request headers to stderr.
 */
function logRequestHeaders(res){

	if(res.req._header){
		const headers = res.req._header.split('\r\n');
		if(testMode){
			process.stderr.write(headers[0]);
		}
		else{
			headers.forEach((e) => e && console.error(`> ${e}`));
			console.error('> ');
		}
	}
	else{
		if(testMode){
			process.stderr.write(`${res.req.method} ${res.req.path} HTTP/${res.httpVersion}`);
		}
		else{
			console.error(`> ${res.req.method} ${res.req.path} HTTP/${res.httpVersion}`);

			const headers = res.req.getHeaders();
			for(let headerName of res.req.getRawHeaderNames())
				console.error(`> ${headerName}: ${headers[headerName.toLowerCase()]}`);

			console.error('> ');
		}
	}

	!testMode && console.error();
}

/**
 * function main
 * function processCmd(test)
 * function requestAPI(endpoint, etag)
 * function logRequestHeaders(res)
 *     function logResponse(res)
 * function output(githubUserEvents)
 *
 * @func   logResponse
 * @param  {object} res - The response object.
 * @desc   Log response headers and body to stderr.
 */
function logResponse(res){

	!testMode && console.error(`< HTTP/${res.httpVersion} ${res.statusCode} ${res.statusMessage}`);

	for(let i = 0; i < res.rawHeaders.length; i += 2)
		!testMode && console.error(`< ${res.rawHeaders[i]}: ${res.rawHeaders[i+1]}`)

	!testMode && console.error('< ');
	!testMode && console.error(res.resBody);
	!testMode && console.error();
}

/**
 * function main
 * function processCmd(test)
 * function requestAPI(endpoint, etag)
 * function logRequestHeaders(res)
 * function logResponse(res)
 *     function output(githubUserEvents)
 *
 * @func  output
 * @param {Array} githubUserEvents - Output GitHub user events.
 * @desc  Display output in terminal.
 */
function output(githubUserEvents){

	if('etag' in githubUserEvents[githubUserEvents.length - 1])
		githubUserEvents.pop();

	// cmdOptions.type !== ''
	if(cmdOptions.type){
		githubUserEvents = githubUserEvents.filter(event => event.type === cmdOptions.type);
	}

	if(cmdOptions.output === 'j'){
		console.log(githubUserEvents);
	}
	else
	if(cmdOptions.output === 'b'){
		let maxTypeLength = 5, maxRepoLength = 10, maxDateLength = 20;

		githubUserEvents.forEach(event => {
			maxTypeLength = Math.max(event.type.length, maxTypeLength);
			maxRepoLength = Math.max(event.repo.name.length, maxRepoLength);
		});

		console.log(`${'Event'.padEnd(maxTypeLength)}    ${'Created at'.padEnd(maxDateLength)}    Repository`);
		console.log(`${'='.repeat(maxTypeLength)}    ${'='.repeat(maxDateLength)}    ${'='.repeat(maxRepoLength)}`);

		githubUserEvents.forEach(event => { console.log(`${event.type.padEnd(maxTypeLength)}    ${event.created_at}    ${event.repo.name}`); });
	}
	else
	if(cmdOptions.output === 'c'){
		githubUserEvents.forEach(event => { console.log(`${event.type},${event.created_at},${event.repo.name}`); });
	}
	else
	if(cmdOptions.output === 'v'){
		if(githubUserEvents.length === 0){
			console.log('no user activities found');
		}
		else{
			githubUserEvents.forEach(event => { console.log(new GitHubEvent(event).phrase) });
		}
	}
}