Website for visualizing a persons github network.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

426 lines
10 KiB

5 years ago
  1. const routes = require('express').Router();
  2. const got = require("got");
  3. const cache = require('memory-cache');
  4. const dotenv = require("dotenv").config();
  5. const GITHUB_API = "https://api.github.com";
  6. const authenticate = `client_id=${process.env.CLIENT_ID}&client_secret=${process.env.CLIENT_SECRET}`;
  7. const API_FOLLOWING = "/following";
  8. const API_FOLLOWERS = "/followers";
  9. const API_USER_PATH = "/users/";
  10. const API_ORGS_PATH = "/orgs/";
  11. const API_PAGINATION_SIZE = 100; // 100 is the max, 30 is the default
  12. // if this is too large, it would be infeasible to make graphs for people following popular people
  13. const API_MAX_PAGES = 2;
  14. const API_PAGINATION = "&per_page=" + API_PAGINATION_SIZE;
  15. const REPOS_PATH = "/repos";
  16. const got_options = {
  17. json: true,
  18. username : process.env.CLIENT_ID,
  19. password : process.env.CLIENT_SECRET
  20. };
  21. /**
  22. * Queries data from the github APi server and returns it as
  23. * a json object in a promise.
  24. *
  25. * This makes no attempt to cache
  26. *
  27. * @param {*} requestURL endpoint on githubapi: ex: /users/jrtechs/following
  28. */
  29. function queryGithubAPIRaw(requestURL)
  30. {
  31. return new Promise((resolve, reject)=>
  32. {
  33. var queryURL;
  34. if(requestURL.includes("?page="))
  35. {
  36. queryURL = GITHUB_API + requestURL + "&" + authenticate;
  37. }
  38. else
  39. {
  40. queryURL = GITHUB_API + requestURL + "?" + authenticate;
  41. }
  42. console.log(queryURL);
  43. got(queryURL, got_options).then(response =>
  44. {
  45. resolve(response.body);
  46. cache.put(requestURL, response.body);
  47. }).catch(error =>
  48. {
  49. resolve(error);
  50. cache.put(requestURL, error);
  51. });
  52. });
  53. }
  54. /**
  55. * Queries data from the github api server
  56. * and caches the results locally.
  57. *
  58. * @param {*} requestURL
  59. */
  60. function queryGitHubAPI(requestURL)
  61. {
  62. const apiData = cache.get(requestURL);
  63. return new Promise(function(resolve, reject)
  64. {
  65. if(apiData == null)
  66. {
  67. queryGithubAPIRaw(requestURL).then((dd)=>
  68. {
  69. resolve(dd);
  70. }).catch((err)=>
  71. {
  72. resolve(err);
  73. })
  74. }
  75. else
  76. {
  77. console.log("Fetched From Cache");
  78. resolve(apiData);
  79. }
  80. })
  81. }
  82. /**
  83. * Fetches all content from a particular github api endpoint
  84. * using their pagination schema.
  85. *
  86. * @param {*} username username of github client
  87. * @param {*} apiPath following or followers
  88. * @param {*} page current pagination page
  89. * @param {*} lst list we are building on
  90. */
  91. function fetchAllWithPagination(apiPath, page, lst)
  92. {
  93. return new Promise((resolve, reject)=>
  94. {
  95. queryGithubAPIRaw(apiPath + "?page=" + page + API_PAGINATION).then((data)=>
  96. {
  97. if(data.hasOwnProperty("length"))
  98. {
  99. lst = lst.concat(data)
  100. if(page < API_MAX_PAGES && data.length === API_PAGINATION_SIZE)
  101. {
  102. fetchAllWithPagination(apiPath, page + 1, lst).then((l)=>
  103. {
  104. resolve(l);
  105. });
  106. }
  107. else
  108. {
  109. resolve(lst);
  110. }
  111. }
  112. else
  113. {
  114. console.log(data);
  115. reject("Malformed data");
  116. }
  117. }).catch((err)=>
  118. {
  119. reject("error with api request");
  120. });
  121. },
  122. (error)=>
  123. {
  124. if(error.hasOwnProperty("length"))
  125. {
  126. lst.concat(data);
  127. resolve(lst);
  128. }
  129. });
  130. }
  131. /**
  132. * Makes a copy of a JS object with certain properties
  133. *
  134. * @param {*} props
  135. * @param {*} obj
  136. */
  137. function copyWithProperties(props, obj)
  138. {
  139. var newO = new Object();
  140. for(var i =0; i < props.length; i++)
  141. {
  142. newO[props[i]] = obj[props[i]];
  143. }
  144. return newO;
  145. }
  146. /**
  147. * Combines the list of friends and followers ignoring duplicates
  148. * that are already in the list. (person is both following and followed by someone)
  149. *
  150. * This also removes any unused properties like events_url and organizations_url
  151. *
  152. * @param {*} followingAndFollowers
  153. */
  154. function minimizeFriends(people)
  155. {
  156. var friendLst = [];
  157. var ids = new Set();
  158. for(var i = 0; i < people.length; i++)
  159. {
  160. if(!ids.has(people[i].id))
  161. {
  162. ids.add(people[i].id);
  163. friendLst.push({
  164. login: people[i].login,
  165. avatar_url: people[i].avatar_url
  166. });
  167. }
  168. }
  169. return friendLst;
  170. }
  171. /**
  172. * Fetches all the people that are either following or is followed
  173. * by a person on github. This will cache the results to make simultaneous
  174. * connections easier and less demanding on the github API.
  175. *
  176. * @param {*} user
  177. */
  178. function queryFriends(user)
  179. {
  180. const cacheHit = cache.get("/friends/" + user);
  181. return new Promise((resolve, reject)=>
  182. {
  183. if(cacheHit == null)
  184. {
  185. fetchAllWithPagination(API_USER_PATH + user + API_FOLLOWERS, 1, []).then((followers)=>
  186. {
  187. fetchAllWithPagination(API_USER_PATH + user + API_FOLLOWING, 1, []).then((following)=>
  188. {
  189. var fList = minimizeFriends(following.concat(followers));
  190. resolve(fList);
  191. cache.put("/friends/" + user, fList);
  192. }).catch((err)=>
  193. {
  194. console.log(err);
  195. reject("API ERROR");
  196. })
  197. }).catch((error)=>
  198. {
  199. console.log(error);
  200. resolve("API Error");
  201. })
  202. }
  203. else
  204. {
  205. console.log("Friends cache hit");
  206. resolve(cacheHit);
  207. }
  208. });
  209. }
  210. /**
  211. *
  212. * Fetches all of the members of an organization from the
  213. * API or cache
  214. *
  215. * /orgs/RITlug/members?page=1
  216. *
  217. * @param {*} orgName
  218. */
  219. function getOrganizationMembers(orgName)
  220. {
  221. const cacheHit = cache.get("/org/users/" + orgName);
  222. return new Promise((resolve, reject)=>
  223. {
  224. if(cacheHit == null)
  225. {
  226. fetchAllWithPagination(API_ORGS_PATH + orgName + "/members", 1, []).then((mems)=>
  227. {
  228. var minimized = minimizeFriends(mems);
  229. resolve(minimized);
  230. cache.put("/org/users/" + orgName, minimized);
  231. }).catch((err)=>
  232. {
  233. console.log(err)
  234. })
  235. }
  236. else
  237. {
  238. console.log("Org members cache hit");
  239. resolve(cacheHit);
  240. }
  241. });
  242. }
  243. /**
  244. * Minimizes the JSON for a list of repositories
  245. *
  246. * @param {*} repositories
  247. */
  248. function minimizeRepositories(repositories)
  249. {
  250. var rList = [];
  251. for(var i = 0; i < repositories.length; i++)
  252. {
  253. rList.push(copyWithProperties(["name", "created_at", "homepage",
  254. "description", "language", "forks", "watchers",
  255. "open_issues_count", "license", "html_url"],
  256. repositories[i]));
  257. }
  258. return rList;
  259. }
  260. /**
  261. * Fetches all repositories from the API
  262. *
  263. * @param {*} user name of org/user
  264. * @param {*} orgsOrUsers either /users/ or /orgs/
  265. */
  266. function queryRepositories(user, orgsOrUsers)
  267. {
  268. const cacheHit = cache.get(user + REPOS_PATH);
  269. return new Promise((resolve, reject)=>
  270. {
  271. if(cacheHit == null)
  272. {
  273. fetchAllWithPagination(orgsOrUsers + user + REPOS_PATH, 1, []).then((repos)=>
  274. {
  275. var minimized = minimizeRepositories(repos);
  276. resolve(minimized);
  277. cache.put(user + REPOS_PATH, minimized);
  278. }).catch((err)=>
  279. {
  280. console.log(err)
  281. console.log("bad things went down");
  282. })
  283. }
  284. else
  285. {
  286. console.log("Repositories cache hit");
  287. resolve(cacheHit);
  288. }
  289. });
  290. }
  291. /**
  292. * /users/name/following/followers
  293. */
  294. routes.get("/friends/:name", (request, result)=>
  295. {
  296. queryFriends(request.params.name).then(friends=>
  297. {
  298. result.json(friends)
  299. .end();
  300. }).catch(error=>
  301. {
  302. result.status(500)
  303. .json({error: 'API error fetching friends'})
  304. .end();
  305. });
  306. });
  307. routes.get("/org/users/:name", (request, result)=>
  308. {
  309. getOrganizationMembers(request.params.name).then(friends=>
  310. {
  311. result.json(friends)
  312. .end();
  313. }).catch(error=>
  314. {
  315. result.status(500)
  316. .json({error: 'API error fetching friends'})
  317. .end();
  318. });
  319. });
  320. routes.get("/repositories/:name", (request, result)=>
  321. {
  322. queryRepositories(request.params.name, API_USER_PATH).then(repos=>
  323. {
  324. result.json(repos)
  325. .end();
  326. }).catch(error=>
  327. {
  328. result.status(500)
  329. .json({error: 'API error fetching friends'})
  330. .end();
  331. });
  332. });
  333. routes.get("/org/repositories/:name", (request, result)=>
  334. {
  335. queryRepositories(request.params.name, API_ORGS_PATH).then(repos=>
  336. {
  337. result.json(repos)
  338. .end();
  339. }).catch(error=>
  340. {
  341. result.status(500)
  342. .json({error: 'API error fetching friends'})
  343. .end();
  344. });
  345. });
  346. routes.get('/*', (request, result) =>
  347. {
  348. var gitHubAPIURL = request.url;
  349. result.setHeader('Content-Type', 'application/json');
  350. queryGitHubAPI(gitHubAPIURL).then((data)=>
  351. {
  352. if(data.hasOwnProperty("id") || data[0].hasOwnProperty("id"))
  353. {
  354. result.write(JSON.stringify(data));
  355. }
  356. else
  357. {
  358. result.write("[]");
  359. }
  360. result.end();
  361. }).catch((error)=>
  362. {
  363. try
  364. {
  365. if(error.hasOwnProperty("id") || error[0].hasOwnProperty("id"))
  366. {
  367. result.write(JSON.stringify(error));
  368. }
  369. else
  370. {
  371. result.write("[]");
  372. }
  373. }
  374. catch(error)
  375. {
  376. result.write("[]");
  377. };
  378. result.end();
  379. });
  380. if(cache.size() > 50000)
  381. {
  382. cache.clear();
  383. }
  384. });
  385. module.exports = routes;