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.

416 lines
11 KiB

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