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.

331 lines
8.0 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. /**
  8. * Queries data from the github APi server and returns it as
  9. * a json object in a promise.
  10. *
  11. * This makes no attempt to cache
  12. *
  13. * @param {*} requestURL endpoint on githubapi: ex: /users/jrtechs/following
  14. */
  15. function queryGithubAPIRaw(requestURL)
  16. {
  17. return new Promise((resolve, reject)=>
  18. {
  19. var queryURL;
  20. if(requestURL.includes("?page="))
  21. {
  22. queryURL = GITHUB_API + requestURL + "&" + authenticate;
  23. }
  24. else
  25. {
  26. queryURL = GITHUB_API + requestURL + "?" + authenticate;
  27. }
  28. console.log(queryURL);
  29. got(queryURL, { json: true }).then(response =>
  30. {
  31. resolve(response.body);
  32. cache.put(requestURL, response.body);
  33. }).catch(error =>
  34. {
  35. resolve(error);
  36. cache.put(requestURL, error);
  37. });
  38. });
  39. }
  40. /**
  41. * Queries data from the github api server
  42. * and caches the results locally.
  43. *
  44. * @param {*} requestURL
  45. */
  46. function queryGitHubAPI(requestURL)
  47. {
  48. const apiData = cache.get(requestURL);
  49. return new Promise(function(resolve, reject)
  50. {
  51. if(apiData == null)
  52. {
  53. queryGithubAPIRaw(requestURL).then((dd)=>
  54. {
  55. resolve(dd);
  56. }).catch((err)=>
  57. {
  58. resolve(err);
  59. })
  60. }
  61. else
  62. {
  63. console.log("Fetched From Cache");
  64. resolve(apiData);
  65. }
  66. })
  67. }
  68. const API_FOLLOWING = "/following";
  69. const API_FOLLOWERS = "/followers";
  70. const API_USER_PATH = "/users/";
  71. const API_PAGINATION_SIZE = 100; // 100 is the max, 30 is the default
  72. // if this is too large, it would be infeasible to make graphs for people following popular people
  73. const API_MAX_PAGES = 2;
  74. const API_PAGINATION = "&per_page=" + API_PAGINATION_SIZE;
  75. /**
  76. * This will fetch all of the either followers or following of a
  77. * github user.
  78. *
  79. * This function is recursive to traverse all the pagination so we
  80. * can get a complete list of all the friends. The max amount of
  81. * followers/following you can get at once is 100.
  82. *
  83. * @param {*} username username of github client
  84. * @param {*} apiPath following or followers
  85. * @param {*} page current pagination page
  86. * @param {*} lst list we are building on
  87. */
  88. function fetchAllUsers(username, apiPath, page, lst)
  89. {
  90. return new Promise((resolve, reject)=>
  91. {
  92. queryGithubAPIRaw(API_USER_PATH + username + apiPath + "?page=" + page + API_PAGINATION).then((data)=>
  93. {
  94. if(data.hasOwnProperty("length"))
  95. {
  96. lst = lst.concat(data)
  97. if(page < API_MAX_PAGES && data.length === API_PAGINATION_SIZE)
  98. {
  99. fetchAllUsers(username, apiPath, page + 1, lst).then((l)=>
  100. {
  101. resolve(l);
  102. });
  103. }
  104. else
  105. {
  106. resolve(lst);
  107. }
  108. }
  109. else
  110. {
  111. console.log(data);
  112. reject("Malformed data");
  113. }
  114. }).catch((err)=>
  115. {
  116. reject("error with api request");
  117. });
  118. },
  119. (error)=>
  120. {
  121. if(error.hasOwnProperty("length"))
  122. {
  123. lst.concat(data);
  124. resolve(lst);
  125. }
  126. });
  127. }
  128. /**
  129. * Combines the list of friends and followers ignoring duplicates
  130. * that are already in the list. (person is both following and followed by someone)
  131. *
  132. * This also removes any unused properties like events_url and organizations_url
  133. *
  134. * @param {*} followingAndFollowers
  135. */
  136. function minimizeFriends(people)
  137. {
  138. var friendLst = [];
  139. var ids = new Set();
  140. for(var i = 0; i < people.length; i++)
  141. {
  142. if(!ids.has(people[i].id))
  143. {
  144. ids.add(people[i].id);
  145. friendLst.push({
  146. login: people[i].login,
  147. avatar_url: people[i].avatar_url
  148. });
  149. }
  150. }
  151. return friendLst;
  152. }
  153. /**
  154. * Fetches all the people that are either following or is followed
  155. * by a person on github. This will cache the results to make simultaneous
  156. * connections easier and less demanding on the github API.
  157. *
  158. * @param {*} user
  159. */
  160. function queryFriends(user)
  161. {
  162. const cacheHit = cache.get("/friends/" + user);
  163. return new Promise((resolve, reject)=>
  164. {
  165. if(cacheHit == null)
  166. {
  167. fetchAllUsers(user, API_FOLLOWERS, 1, []).then((followers)=>
  168. {
  169. fetchAllUsers(user, API_FOLLOWING, 1, []).then((following)=>
  170. {
  171. var fList = minimizeFriends(following.concat(followers));
  172. resolve(fList);
  173. cache.put("/friends/" + user, fList);
  174. }).catch((err)=>
  175. {
  176. console.log(err);
  177. reject("API ERROR");
  178. })
  179. }).catch((error)=>
  180. {
  181. console.log(error);
  182. resolve("API Error");
  183. })
  184. }
  185. else
  186. {
  187. console.log("Friends cache hit");
  188. resolve(cacheHit);
  189. }
  190. });
  191. }
  192. routes.get("/friends/:name", (request, result)=>
  193. {
  194. queryFriends(request.params.name).then(friends=>
  195. {
  196. result.json(friends)
  197. .end();
  198. }).catch(error=>
  199. {
  200. result.status(500)
  201. .json({error: 'API error fetching friends'})
  202. .end();
  203. });
  204. });
  205. function copyWithProperties(props, obj)
  206. {
  207. var newO = new Object();
  208. for(var i =0; i < props.length; i++)
  209. {
  210. newO[props[i]] = obj[props[i]];
  211. }
  212. return newO;
  213. }
  214. function minimizeRepositories(repositories)
  215. {
  216. var rList = [];
  217. for(var i = 0; i < repositories.length; i++)
  218. {
  219. rList.push(copyWithProperties(["name", "created_at", "homepage",
  220. "description", "language", "forks", "watchers",
  221. "open_issues_count", "license"],
  222. repositories[i]));
  223. }
  224. return rList;
  225. }
  226. const REPOS_PATH = "/repos";
  227. function queryRepositories(user)
  228. {
  229. const cacheHit = cache.get(user + REPOS_PATH);
  230. return new Promise((resolve, reject)=>
  231. {
  232. if(cacheHit == null)
  233. {
  234. fetchAllUsers(user, REPOS_PATH, 1, []).then((repos)=>
  235. {
  236. var minimized = minimizeRepositories(repos);
  237. resolve(minimized);
  238. cache.put(user + REPOS_PATH, minimized);
  239. });
  240. }
  241. else
  242. {
  243. console.log("Repositories cache hit");
  244. resolve(cacheHit);
  245. }
  246. });
  247. }
  248. routes.get("/repositories/:name", (request, result)=>
  249. {
  250. queryRepositories(request.params.name).then(repos=>
  251. {
  252. result.json(repos)
  253. .end();
  254. }).catch(error=>
  255. {
  256. result.status(500)
  257. .json({error: 'API error fetching friends'})
  258. .end();
  259. });
  260. });
  261. routes.get('/*', (request, result) =>
  262. {
  263. var gitHubAPIURL = request.url;
  264. result.setHeader('Content-Type', 'application/json');
  265. queryGitHubAPI(gitHubAPIURL).then((data)=>
  266. {
  267. if(data.hasOwnProperty("id") || data[0].hasOwnProperty("id"))
  268. {
  269. result.write(JSON.stringify(data));
  270. }
  271. else
  272. {
  273. result.write("[]");
  274. }
  275. result.end();
  276. }).catch((error)=>
  277. {
  278. try
  279. {
  280. if(error.hasOwnProperty("id") || error[0].hasOwnProperty("id"))
  281. {
  282. result.write(JSON.stringify(error));
  283. }
  284. else
  285. {
  286. result.write("[]");
  287. }
  288. }
  289. catch(error)
  290. {
  291. result.write("[]");
  292. };
  293. result.end();
  294. });
  295. if(cache.size() > 50000)
  296. {
  297. cache.clear();
  298. }
  299. });
  300. module.exports = routes;