diff --git a/api/index.js b/api/index.js index c42bc04891234..b897d57e45dd3 100644 --- a/api/index.js +++ b/api/index.js @@ -38,6 +38,7 @@ export default async (req, res) => { border_color, rank_icon, show, + role, } = req.query; res.setHeader("Content-Type", "image/svg+xml"); @@ -71,6 +72,7 @@ export default async (req, res) => { username, parseBoolean(include_all_commits), parseArray(exclude_repo), + parseArray(role), showStats.includes("prs_merged") || showStats.includes("prs_merged_percentage"), showStats.includes("discussions_started"), diff --git a/api/top-langs.js b/api/top-langs.js index c5bed634c1eab..de594ef8a78e0 100644 --- a/api/top-langs.js +++ b/api/top-langs.js @@ -32,6 +32,7 @@ export default async (req, res) => { border_color, disable_animations, hide_progress, + role, } = req.query; res.setHeader("Content-Type", "image/svg+xml"); @@ -67,6 +68,7 @@ export default async (req, res) => { parseArray(exclude_repo), size_weight, count_weight, + parseArray(role), ); let cacheSeconds = parseInt( diff --git a/src/common/utils.js b/src/common/utils.js index b780657c1c244..438e4a4c2af2d 100644 --- a/src/common/utils.js +++ b/src/common/utils.js @@ -6,6 +6,8 @@ import { themes } from "../../themes/index.js"; const TRY_AGAIN_LATER = "Please try again later"; +const OWNER_AFFILIATIONS = ["OWNER", "COLLABORATOR", "ORGANIZATION_MEMBER"]; + const SECONDARY_ERROR_MESSAGES = { MAX_RETRY: "You can deploy own instance or wait until public will be no longer limited", @@ -15,6 +17,9 @@ const SECONDARY_ERROR_MESSAGES = { GRAPHQL_ERROR: TRY_AGAIN_LATER, GITHUB_REST_API_ERROR: TRY_AGAIN_LATER, WAKATIME_USER_NOT_FOUND: "Make sure you have a public WakaTime profile", + INVALID_AFFILIATION: `Invalid owner affiliations. Valid values are: ${OWNER_AFFILIATIONS.join( + ", ", + )}`, }; /** @@ -37,6 +42,7 @@ class CustomError extends Error { static GRAPHQL_ERROR = "GRAPHQL_ERROR"; static GITHUB_REST_API_ERROR = "GITHUB_REST_API_ERROR"; static WAKATIME_ERROR = "WAKATIME_ERROR"; + static INVALID_AFFILIATION = "INVALID_AFFILIATION"; } /** @@ -585,6 +591,37 @@ const parseEmojis = (str) => { }); }; +/** + * Parse owner affiliations. + * + * @param {string[]} affiliations + * @returns {string[]} Parsed affiliations. + * + * @throws {CustomError} If affiliations contains invalid values. + */ +const parseOwnerAffiliations = (affiliations) => { + // Set default value for ownerAffiliations. + // NOTE: Done here since parseArray() will always return an empty array even nothing + //was specified. + affiliations = + affiliations && affiliations.length > 0 + ? affiliations.map((affiliation) => affiliation.toUpperCase()) + : ["OWNER"]; + + // Check if ownerAffiliations contains valid values. + if ( + affiliations.some( + (affiliation) => !OWNER_AFFILIATIONS.includes(affiliation), + ) + ) { + throw new CustomError( + "Invalid query parameter", + CustomError.INVALID_AFFILIATION, + ); + } + return affiliations; +}; + /** * Get diff in minutes between two dates. * @@ -618,11 +655,13 @@ export { wrapTextMultiline, logger, CONSTANTS, + OWNER_AFFILIATIONS, CustomError, MissingParamError, measureText, lowercaseTrim, chunkArray, parseEmojis, + parseOwnerAffiliations, dateDiff, }; diff --git a/src/fetchers/stats-fetcher.js b/src/fetchers/stats-fetcher.js index 115cd50a51564..47f6739fa5f2b 100644 --- a/src/fetchers/stats-fetcher.js +++ b/src/fetchers/stats-fetcher.js @@ -10,13 +10,14 @@ import { MissingParamError, request, wrapTextMultiline, + parseOwnerAffiliations, } from "../common/utils.js"; dotenv.config(); // GraphQL queries. const GRAPHQL_REPOS_FIELD = ` - repositories(first: 100, ownerAffiliations: OWNER, orderBy: {direction: DESC, field: STARGAZERS}, after: $after) { + repositories(first: 100, after: $after, ownerAffiliations: $ownerAffiliations, orderBy: {direction: DESC, field: STARGAZERS}) { totalCount nodes { name @@ -32,15 +33,15 @@ const GRAPHQL_REPOS_FIELD = ` `; const GRAPHQL_REPOS_QUERY = ` - query userInfo($login: String!, $after: String) { - user(login: $login) { + query userInfo($login: String!, $after: String, $ownerAffiliations: [RepositoryAffiliation]) { + user(login: $login, ownerAffiliations: $ownerAffiliations) { ${GRAPHQL_REPOS_FIELD} } } `; const GRAPHQL_STATS_QUERY = ` - query userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!) { + query userInfo($login: String!, $after: String, $ownerAffiliations: [RepositoryAffiliation], $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!) { user(login: $login) { name login @@ -106,6 +107,7 @@ const fetcher = (variables, token) => { * * @param {object} variables Fetcher variables. * @param {string} variables.username Github username. + * @param {string[]} variables.ownerAffiliations The owner affiliations to filter by. Default: OWNER. * @param {boolean} variables.includeMergedPullRequests Include merged pull requests. * @param {boolean} variables.includeDiscussions Include discussions. * @param {boolean} variables.includeDiscussionsAnswers Include discussions answers. @@ -115,6 +117,7 @@ const fetcher = (variables, token) => { */ const statsFetcher = async ({ username, + ownerAffiliations, includeMergedPullRequests, includeDiscussions, includeDiscussionsAnswers, @@ -127,6 +130,7 @@ const statsFetcher = async ({ login: username, first: 100, after: endCursor, + ownerAffiliations: ownerAffiliations, includeMergedPullRequests, includeDiscussions, includeDiscussionsAnswers, @@ -214,6 +218,7 @@ const totalCommitsFetcher = async (username) => { * @param {string} username GitHub username. * @param {boolean} include_all_commits Include all commits. * @param {string[]} exclude_repo Repositories to exclude. + * @param {string[]} ownerAffiliations Owner affiliations. Default: OWNER. * @param {boolean} include_merged_pull_requests Include merged pull requests. * @param {boolean} include_discussions Include discussions. * @param {boolean} include_discussions_answers Include discussions answers. @@ -223,6 +228,7 @@ const fetchStats = async ( username, include_all_commits = false, exclude_repo = [], + ownerAffiliations = [], include_merged_pull_requests = false, include_discussions = false, include_discussions_answers = false, @@ -245,9 +251,11 @@ const fetchStats = async ( contributedTo: 0, rank: { level: "C", percentile: 100 }, }; + ownerAffiliations = parseOwnerAffiliations(ownerAffiliations); let res = await statsFetcher({ username, + ownerAffiliations, includeMergedPullRequests: include_merged_pull_requests, includeDiscussions: include_discussions, includeDiscussionsAnswers: include_discussions_answers, diff --git a/src/fetchers/top-languages-fetcher.js b/src/fetchers/top-languages-fetcher.js index 485cc8b75de8a..9809d9ce87bad 100644 --- a/src/fetchers/top-languages-fetcher.js +++ b/src/fetchers/top-languages-fetcher.js @@ -6,6 +6,7 @@ import { MissingParamError, request, wrapTextMultiline, + parseOwnerAffiliations, } from "../common/utils.js"; /** @@ -24,10 +25,10 @@ const fetcher = (variables, token) => { return request( { query: ` - query userInfo($login: String!) { + query userInfo($login: String!, $ownerAffiliations: [RepositoryAffiliation]) { user(login: $login) { - # fetch only owner repos & not forks - repositories(ownerAffiliations: OWNER, isFork: false, first: 100) { + # do not fetch forks + repositories(ownerAffiliations: $ownerAffiliations, isFork: false, first: 100) { nodes { name languages(first: 10, orderBy: {field: SIZE, direction: DESC}) { @@ -61,6 +62,7 @@ const fetcher = (variables, token) => { * * @param {string} username GitHub username. * @param {string[]} exclude_repo List of repositories to exclude. + * @param {string[]} ownerAffiliations The owner affiliations to filter by. Default: OWNER. * @param {number} size_weight Weightage to be given to size. * @param {number} count_weight Weightage to be given to count. * @returns {Promise<TopLangData>} Top languages data. @@ -70,12 +72,14 @@ const fetchTopLanguages = async ( exclude_repo = [], size_weight = 1, count_weight = 0, + ownerAffiliations = [], ) => { if (!username) { throw new MissingParamError(["username"]); } + ownerAffiliations = parseOwnerAffiliations(ownerAffiliations); - const res = await retryer(fetcher, { login: username }); + const res = await retryer(fetcher, { login: username, ownerAffiliations }); if (res.data.errors) { logger.error(res.data.errors);