diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..f6ffbc1 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +# .npmrc +engine-strict=true diff --git a/README.md b/README.md index 34ae639..0470310 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # SplitScreen.Me Hub 📦 + SplitScreen.Me Logo ![CI/CD](https://github.com/SplitScreen-Me/splitscreenme-hub/workflows/CI/badge.svg) @@ -14,64 +15,80 @@ Feel free to [contribute](#contribute) and help us build the most amazing **hub ## Basic API 🔥 Handler research: -``` + +```json url: "api/v1/handlers/:search_text", httpMethod: "get" ``` Get all handlers (up to 500): -``` + +```json url: "api/v1/allhandlers", httpMethod: "get" ``` Specific handler infos: -``` + +```json url: "api/v1/handler/:handler_id", httpMethod: "get" ``` Get available packages for one handler: -``` + +```json url: "api/v1/packages/:handler_id", httpMethod: "get" ``` Get package info: -``` + +```json url: "api/v1/packages/:package_id", httpMethod: "get" ``` - + Get comments done by users about a handler: -``` + +```json url: "api/v1/comments/:handler_id", httpMethod: "get" ``` - + Download a package from it's ID: -``` + +```json url: /cdn/storage/packages/:package_id/original/handler-{handler_id}-v{version_of_handler}.nc?download=true httpMethod: "get" ``` Get IGDB screenshots for a handler: -``` + +```json url: "api/v1/screenshots/:handler_id", httpMethod: "get" ``` ## Contribute + ### Prerequisites + 1. IDE or text editor. for example [WebStorm](https://www.jetbrains.com/webstorm/) or [VSCode](https://code.visualstudio.com/) 2. IDE for MongoDB, we recommend [NoSQLBooster](https://nosqlbooster.com/) + #### Installation + You must use Node v12 and not a higher version. + +```sh +npm install ``` -$ npm install -``` + #### Local Development + +```sh +npm run dev ``` -$ npm run dev -``` + This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server. diff --git a/imports/api/Handlers/server/github-methods.js b/imports/api/Handlers/server/github-methods.js new file mode 100644 index 0000000..eba9cf8 --- /dev/null +++ b/imports/api/Handlers/server/github-methods.js @@ -0,0 +1,85 @@ +import { Meteor } from 'meteor/meteor'; +import axios from 'axios'; + +// authenticate with GitHub API +Meteor.startup(() => { + Meteor.setInterval(() => { + axios + .get('https://api.github.com/rate_limit') + .then(response => { + if (response.data.resources.core.remaining < 10) { + console.log('GitHub API rate limit reached, refreshing token.'); + axios.defaults.headers.common.Authorization = `token ${Meteor.settings.private.GITHUB_API_TOKEN}`; + } + }) + .catch(error => { + console.log(error); + }); + }, 1000 * 60 * 60 * 24 * 10); +}); + +/** Get the total download count of all release assets in a GitHub repository */ +export const getGitHubDownloads = async (owner, repo) => { + let downloadCount = 0; + let page = 1; + let hasMore = true; + + try { + while (hasMore) { + const response = await axios.get(`https://api.github.com/repos/${owner}/${repo}/releases`, { + params: { + page, + per_page: 100, // Max items per page (GitHub's limit) + }, + headers: { + 'User-Agent': 'Your-App-Name', // GitHub requires this + Accept: 'application/vnd.github.v3+json', + // Uncomment for authenticated requests (recommended): + // Authorization: `token ${process.env.GITHUB_TOKEN}` + }, + }); + + const releases = response.data; + + // No more releases (empty array) + if (releases.length === 0) { + hasMore = false; + break; + } + + // Sum downloads from all assets + for (const release of releases) { + for (const asset of release.assets) { + downloadCount += asset.download_count; + } + } + + // Check for pagination (GitHub uses Link headers) + const linkHeader = response.headers.link; + hasMore = linkHeader?.includes('rel="next"'); + page++; + } + + return downloadCount; + } catch (error) { + // Handle rate limits (403) and other errors + if (error.response?.status === 403) { + const resetTime = new Date(error.response.headers['x-ratelimit-reset'] * 1000); + console.error(`GitHub API rate limited. Resets at: ${resetTime}`); + } else { + console.error('GitHub API error:', error.message); + } + return 0; // Fallback value + } +}; + +/** Get the star count of a GitHub repository */ +export const getGitHubStars = async (owner, repo) => { + try { + const data = await axios.get(`https://api.github.com/repos/${owner}/${repo}`); + return data.data.stargazers_count; + } catch (e) { + console.log(e); + return 0; + } +}; diff --git a/imports/api/Handlers/server/publications.js b/imports/api/Handlers/server/publications.js index 10ed271..f1fbe01a 100644 --- a/imports/api/Handlers/server/publications.js +++ b/imports/api/Handlers/server/publications.js @@ -4,8 +4,10 @@ import Handlers from '../Handlers'; import Comments from '../../Comments/Comments'; import escapeRegExp from '../../../modules/regexescaper'; import Packages from '../../Packages/server/ServerPackages'; -import axios from "axios"; -import { bearerToken } from "./igdb-methods"; +import axios from 'axios'; +import { bearerToken } from './igdb-methods'; +import { getGitHubStars, getGitHubDownloads } from './github-methods'; +import formatNumbers from '../../../modules/formatNumbers'; Meteor.publish( 'handlers', @@ -16,7 +18,6 @@ Meteor.publish( limit = 18, localHandlerIds = [], ) { - const isSearchFromArray = localHandlerIds.length > 0; let sortObject = { trendScore: handlerSortOrder === 'up' ? 1 : -1 }; @@ -36,7 +37,8 @@ Meteor.publish( if (handlerOptionSearch === 'alphabetical') { sortObject = { gameName: handlerSortOrder === 'up' ? -1 : 1 }; } - const searchInArraySelectorCondition = isSearchFromArray > 0 ? {_id: { $in: localHandlerIds }} : {}; + const searchInArraySelectorCondition = + isSearchFromArray > 0 ? { _id: { $in: localHandlerIds } } : {}; return Handlers.find( { @@ -87,21 +89,20 @@ Meteor.publish( }, ); - /* This code is a PoC, its dirty */ const screenshotsCache = {}; WebApp.connectHandlers.use('/api/v1/screenshots', async (req, res, next) => { res.writeHead(200); - const handlerId = req.url.split("/")[1]; - if(handlerId.length > 0){ - const handler = await Handlers.findOne({ _id: handlerId }, {fields: {gameId: 1}}); - if(!handler?.gameId) { - res.end(JSON.stringify({error: 'Incorrect handlerId'})); + const handlerId = req.url.split('/')[1]; + if (handlerId.length > 0) { + const handler = await Handlers.findOne({ _id: handlerId }, { fields: { gameId: 1 } }); + if (!handler?.gameId) { + res.end(JSON.stringify({ error: 'Incorrect handlerId' })); return; } - if(!screenshotsCache[handler.gameId]){ + if (!screenshotsCache[handler.gameId]) { const igdbApi = axios.create({ baseURL: 'https://api.igdb.com/v4/', timeout: 2500, @@ -112,49 +113,68 @@ WebApp.connectHandlers.use('/api/v1/screenshots', async (req, res, next) => { Accept: 'application/json', }, }); - const igdbAnswer = await igdbApi.post('screenshots', `fields *;where game = ${handler.gameId};`); + const igdbAnswer = await igdbApi.post( + 'screenshots', + `fields *;where game = ${handler.gameId};`, + ); screenshotsCache[handler.gameId] = igdbAnswer.data; } - res.end(JSON.stringify({screenshots: screenshotsCache[handler.gameId]})); - }else{ - res.end(JSON.stringify({error: 'No handler ID provided'})) + res.end(JSON.stringify({ screenshots: screenshotsCache[handler.gameId] })); + } else { + res.end(JSON.stringify({ error: 'No handler ID provided' })); } - res.end(JSON.stringify({error: 'Unknown error'})) -}) + res.end(JSON.stringify({ error: 'Unknown error' })); +}); /* End of dirty PoC */ WebApp.connectHandlers.use('/api/v1/hubstats', async (req, res, next) => { res.writeHead(200); let downloadsSum = 0; let hotnessSum = 0; - let handlerCount = 0; - let usersCount = 0; + // TODO: Store these in a database and update it every few hours + const totalNucleusCoopGitHubStars = + (await getGitHubStars('SplitScreen-Me', 'splitscreenme-nucleus')) + + (await getGitHubStars('ZeroFox5866', 'nucleuscoop')) + + (await getGitHubStars('nucleuscoop', 'nucleuscoop')) + + (await getGitHubStars('distrohelena', 'nucleuscoop')); + const totalNucleusCoopGitHubDownloads = + (await getGitHubDownloads('SplitScreen-Me', 'splitscreenme-nucleus')) + + (await getGitHubDownloads('ZeroFox5866', 'nucleuscoop')) + + (await getGitHubDownloads('nucleuscoop', 'nucleuscoop')) + + (await getGitHubDownloads('distrohelena', 'nucleuscoop')); + const allPackages = Packages.collection.find({}).fetch(); - allPackages.forEach(pkg => { + for (pkg of allPackages) { if (pkg.meta.downloads > 0) { downloadsSum = downloadsSum + pkg.meta.downloads; } - }); + } const allHandlers = Handlers.find({ private: false }).fetch(); - allHandlers.forEach(hndl => { + for (const hndl of allHandlers) { if (hndl.stars > 0) { hotnessSum = hotnessSum + hndl.stars; } - }); - handlerCount = allHandlers.length; + } const allUsers = Meteor.users.find({}).fetch(); - usersCount = allUsers.length; + const usersCount = allUsers.length; + + const usersWithHandlersCount = Meteor.users + .find({ 'profile.handlerId': { $exists: true } }) + .fetch().length; const allComments = Comments.find({}).fetch(); const commentsCount = allComments.length; res.end( - `Total downloads: ${downloadsSum}` + - `\nTotal hotness: ${hotnessSum}` + - `\nTotal handlers: ${handlerCount}` + - `\nTotal users: ${usersCount}` + - `\nTotal comments ${commentsCount}` + `Total downloads: ${formatNumbers(downloadsSum)}` + + `\nTotal hotness: ${formatNumbers(hotnessSum)}` + + `\nTotal handlers: ${formatNumbers(allHandlers.length)}` + + `\nTotal users: ${formatNumbers(usersCount)}` + + `\nTotal handler authors: ${formatNumbers(usersWithHandlersCount)}` + + `\nTotal comments ${formatNumbers(commentsCount)}` + + `\nTotal Nucleus Co-Op GitHub stars: ${formatNumbers(totalNucleusCoopGitHubStars)}` + + `\nTotal Nucleus Co-Op GitHub downloads: ${formatNumbers(totalNucleusCoopGitHubDownloads)}`, ); }); @@ -185,7 +205,8 @@ Meteor.publish( function handlersFull() { return Handlers.find( { - private: false, publicAuthorized: true + private: false, + publicAuthorized: true, }, { sort: { stars: -1 }, diff --git a/imports/modules/formatNumbers.js b/imports/modules/formatNumbers.js new file mode 100644 index 0000000..3e924aa --- /dev/null +++ b/imports/modules/formatNumbers.js @@ -0,0 +1,4 @@ +/** Format numbers with spaces*/ +export default number => { + return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' '); +}; diff --git a/package.json b/package.json index 4b8ba43..d69d7a3 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,9 @@ "dev": "meteor --settings settings-development.json", "lint": "" }, + "engines": { + "node": "<=13.0.0" + }, "dependencies": { "@babel/runtime": "^7.14.8", "@cleverbeagle/seeder": "^1.3.1", diff --git a/settings-development.json b/settings-development.json index f0fda16..b41bf75 100644 --- a/settings-development.json +++ b/settings-development.json @@ -9,6 +9,7 @@ }, "private": { "IGDB_API_KEY": "", + "GITHUB_PERSONAL_ACCESS_TOKEN": "", "MAIL_URL": "", "DISCORD_BOT_SECRET_TOKEN": "", "DISCORD_ADMIN_LOGGING_WEBHOOK": "",