diff --git a/api-renamed/index.js b/api-renamed/index.js index 098dfa5b..9a200708 100644 --- a/api-renamed/index.js +++ b/api-renamed/index.js @@ -38,6 +38,7 @@ export default async (req, res) => { disable_animations, border_radius, number_format, + role, border_color, rank_icon, show, @@ -102,6 +103,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-renamed/top-langs.js b/api-renamed/top-langs.js index fc8aef96..5b068836 100644 --- a/api-renamed/top-langs.js +++ b/api-renamed/top-langs.js @@ -31,6 +31,7 @@ export default async (req, res) => { locale, border_radius, border_color, + role, disable_animations, hide_progress, } = req.query; @@ -69,6 +70,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 474b4411..1e013933 100644 --- a/src/common/utils.js +++ b/src/common/utils.js @@ -4,6 +4,8 @@ import toEmoji from "emoji-name-map"; import wrap from "word-wrap"; import { themes } from "../../themes/index.js"; +const OWNER_AFFILIATIONS = ["OWNER", "COLLABORATOR", "ORGANIZATION_MEMBER"]; + const TRY_AGAIN_LATER = "Please try again later"; const SECONDARY_ERROR_MESSAGES = { @@ -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"; } /** @@ -598,6 +604,36 @@ const parseEmojis = (str) => { return toEmoji.get(emoji) || ""; }); }; +/** + * 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. @@ -633,11 +669,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 dc0003e4..8d649799 100644 --- a/src/fetchers/stats-fetcher.js +++ b/src/fetchers/stats-fetcher.js @@ -11,13 +11,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 @@ -33,15 +34,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, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!, $ownerAffiliations: [RepositoryAffiliation]) { user(login: $login) { name login @@ -110,6 +111,7 @@ const fetcher = (variables, token) => { * @param {boolean} variables.includeMergedPullRequests Include merged pull requests. * @param {boolean} variables.includeDiscussions Include discussions. * @param {boolean} variables.includeDiscussionsAnswers Include discussions answers. + * @param {string[]} ownerAffiliations The owner affiliations to filter by. Default: OWNER. * @returns {Promise} Axios response. * * @description This function supports multi-page fetching if the 'FETCH_MULTI_PAGE_STARS' environment variable is set to true. @@ -119,6 +121,7 @@ const statsFetcher = async ({ includeMergedPullRequests, includeDiscussions, includeDiscussionsAnswers, + ownerAffiliations, }) => { let stats; let hasNextPage = true; @@ -131,6 +134,7 @@ const statsFetcher = async ({ includeMergedPullRequests, includeDiscussions, includeDiscussionsAnswers, + ownerAffiliations: ownerAffiliations, }; let res = await retryer(fetcher, variables); if (res.data.errors) { @@ -282,6 +286,7 @@ const fetchRepoUserStats = async ( * @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. @@ -291,6 +296,7 @@ const fetchStats = async ( username, include_all_commits = false, exclude_repo = [], + ownerAffiliations = [], include_merged_pull_requests = false, include_discussions = false, include_discussions_answers = false, @@ -325,12 +331,14 @@ const fetchStats = async ( totalIssuesCommented: 0, rank: { level: "C", percentile: 100 }, }; + ownerAffiliations = parseOwnerAffiliations(ownerAffiliations); let res = await statsFetcher({ username, includeMergedPullRequests: include_merged_pull_requests, includeDiscussions: include_discussions, includeDiscussionsAnswers: include_discussions_answers, + ownerAffiliations, }); // Catch GraphQL errors. diff --git a/src/fetchers/top-languages-fetcher.js b/src/fetchers/top-languages-fetcher.js index 485cc8b7..7c87a95c 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}) { @@ -60,7 +61,8 @@ const fetcher = (variables, token) => { * Fetch top languages for a given username. * * @param {string} username GitHub username. - * @param {string[]} exclude_repo List of repositories to exclude. + * @param {string[]} exclude_repo List of repositories to exclude. Default: []. + * @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} 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);