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.

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