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.

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