Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,13 +225,14 @@ import { fetchSponsors } from '@lizardbyte/contribkit'

const sponsors = await fetchSponsors({
github: {
token,
login,
},
// ...
})
```

Set credentials with environment variables such as `CONTRIBKIT_GITHUB_TOKEN`.

Check the type definition or source code for more utils available.

### Renderers
Expand Down
83 changes: 83 additions & 0 deletions src/configs/credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type { ContribkitConfig } from '../types'
import process from 'node:process'
import dotenv from 'dotenv'

export interface EnvCredentials {
github?: {
token?: string
}
patreon?: {
token?: string
}
opencollective?: {
key?: string
}
afdian?: {
token?: string
}
polar?: {
token?: string
}
githubContributors?: {
token?: string
}
gitlabContributors?: {
token?: string
}
githubContributions?: {
token?: string
}
}

interface ConfigWithCredentials extends ContribkitConfig {
credentials?: EnvCredentials
}

export function pruneUndefined<T>(value: T): T {
if (Array.isArray(value))
return value.map(item => pruneUndefined(item)) as T

if (!value || typeof value !== 'object')
return value

return Object.fromEntries(
Object.entries(value)
.filter(([, item]) => item !== undefined)
.map(([key, item]) => [key, pruneUndefined(item)]),
) as T
}

export function loadEnvCredentials(): EnvCredentials {
dotenv.config({ quiet: true })

return pruneUndefined({
github: {
token: process.env.CONTRIBKIT_GITHUB_TOKEN || process.env.GITHUB_TOKEN,
},
patreon: {
token: process.env.CONTRIBKIT_PATREON_TOKEN || process.env.PATREON_TOKEN,
},
opencollective: {
key: process.env.CONTRIBKIT_OPENCOLLECTIVE_KEY || process.env.OPENCOLLECTIVE_KEY,
},
afdian: {
token: process.env.CONTRIBKIT_AFDIAN_TOKEN || process.env.AFDIAN_TOKEN,
},
polar: {
token: process.env.CONTRIBKIT_POLAR_TOKEN || process.env.POLAR_TOKEN,
},
githubContributors: {
token: process.env.CONTRIBKIT_GITHUB_CONTRIBUTORS_TOKEN,
},
gitlabContributors: {
token: process.env.CONTRIBKIT_GITLAB_CONTRIBUTORS_TOKEN,
},
githubContributions: {
token: process.env.CONTRIBKIT_GITHUB_CONTRIBUTIONS_TOKEN,
},
})
}

export function getCredentials(config: ContribkitConfig): EnvCredentials {
return (config as ConfigWithCredentials).credentials ?? loadEnvCredentials()
}
26 changes: 9 additions & 17 deletions src/configs/env.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,33 @@
import type { ContribkitConfig, GitHubAccountType } from '../types'
import process from 'node:process'
import dotenv from 'dotenv'
import { loadEnvCredentials, pruneUndefined } from './credentials'

function getDeprecatedEnv(name: string, replacement: string) {
const value = process.env[name]
if (value)
console.warn(`[contribkit] env.${name} is deprecated, use env.${replacement} instead`)
return value
export interface EnvConfig {
config: Partial<ContribkitConfig>
credentials: ReturnType<typeof loadEnvCredentials>
}

export function loadEnv(): Partial<ContribkitConfig> {
export function loadEnv(): EnvConfig {
dotenv.config({ quiet: true })

const config: Partial<ContribkitConfig> = {
mode: process.env.CONTRIBKIT_MODE as ContribkitConfig['mode'] | undefined,
github: {
login: process.env.CONTRIBKIT_GITHUB_LOGIN || process.env.GITHUB_LOGIN,
token: process.env.CONTRIBKIT_GITHUB_TOKEN || process.env.GITHUB_TOKEN,
type: (process.env.CONTRIBKIT_GITHUB_TYPE || process.env.GITHUB_TYPE) as GitHubAccountType | undefined,
},
patreon: {
token: process.env.CONTRIBKIT_PATREON_TOKEN || process.env.PATREON_TOKEN,
},
opencollective: {
key: process.env.CONTRIBKIT_OPENCOLLECTIVE_KEY || process.env.OPENCOLLECTIVE_KEY,
id: process.env.CONTRIBKIT_OPENCOLLECTIVE_ID || process.env.OPENCOLLECTIVE_ID,
slug: process.env.CONTRIBKIT_OPENCOLLECTIVE_SLUG || process.env.OPENCOLLECTIVE_SLUG,
githubHandle: process.env.CONTRIBKIT_OPENCOLLECTIVE_GH_HANDLE || process.env.OPENCOLLECTIVE_GH_HANDLE,
type: process.env.CONTRIBKIT_OPENCOLLECTIVE_TYPE || process.env.OPENCOLLECTIVE_TYPE,
},
afdian: {
userId: process.env.CONTRIBKIT_AFDIAN_USER_ID || process.env.AFDIAN_USER_ID,
token: process.env.CONTRIBKIT_AFDIAN_TOKEN || process.env.AFDIAN_TOKEN,
exchangeRate: Number.parseFloat(process.env.CONTRIBKIT_AFDIAN_EXCHANGE_RATE || process.env.AFDIAN_EXCHANGE_RATE || '0') || undefined,
},
polar: {
token: process.env.CONTRIBKIT_POLAR_TOKEN || process.env.POLAR_TOKEN,
organization: process.env.CONTRIBKIT_POLAR_ORGANIZATION || process.env.POLAR_ORGANIZATION,
},
liberapay: {
Expand All @@ -44,12 +36,10 @@ export function loadEnv(): Partial<ContribkitConfig> {
outputDir: process.env.CONTRIBKIT_DIR,
githubContributors: {
login: process.env.CONTRIBKIT_GITHUB_CONTRIBUTORS_LOGIN,
token: process.env.CONTRIBKIT_GITHUB_CONTRIBUTORS_TOKEN,
minContributions: Number(process.env.CONTRIBKIT_GITHUB_CONTRIBUTORS_MIN) || 1,
repo: process.env.CONTRIBKIT_GITHUB_CONTRIBUTORS_REPO,
},
gitlabContributors: {
token: process.env.CONTRIBKIT_GITLAB_CONTRIBUTORS_TOKEN,
minContributions: Number(process.env.CONTRIBKIT_GITLAB_CONTRIBUTORS_MIN) || 1,
repoId: Number(process.env.CONTRIBKIT_GITLAB_CONTRIBUTORS_REPO_ID),
},
Expand All @@ -60,12 +50,14 @@ export function loadEnv(): Partial<ContribkitConfig> {
},
githubContributions: {
login: process.env.CONTRIBKIT_GITHUB_CONTRIBUTIONS_LOGIN,
token: process.env.CONTRIBKIT_GITHUB_CONTRIBUTIONS_TOKEN,
maxContributions: Number(process.env.CONTRIBKIT_GITHUB_CONTRIBUTIONS_MAX) || undefined,
logarithmicScaling: process.env.CONTRIBKIT_GITHUB_CONTRIBUTIONS_LOGARITHMIC === 'true',
},
}

// remove undefined keys
return JSON.parse(JSON.stringify(config))
return {
config: pruneUndefined(config),
credentials: loadEnvCredentials(),
}
}
14 changes: 8 additions & 6 deletions src/configs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export function defineConfig(config: ContribkitConfig): ContribkitConfig {

export async function loadConfig(inlineConfig: ContribkitConfig = {}): Promise<Required<ContribkitConfig>> {
const env = loadEnv()
const envConfig = env.config

const { config = {} } = await _loadConfig<ContribkitConfig>({
sources: [
Expand All @@ -39,35 +40,36 @@ export async function loadConfig(inlineConfig: ContribkitConfig = {}): Promise<R
fallbackAvatar: FALLBACK_AVATAR,
includePastSponsors: hasNegativeTier,
...defaultConfig,
...env,
...envConfig,
...config,
...inlineConfig,
github: {
...env.github,
...envConfig.github,
...config.github,
...inlineConfig.github,
},
patreon: {
...env.patreon,
...envConfig.patreon,
...config.patreon,
...inlineConfig.patreon,
},
opencollective: {
...env.opencollective,
...envConfig.opencollective,
...config.opencollective,
...inlineConfig.opencollective,
},
afdian: {
...env.afdian,
...envConfig.afdian,
...config.afdian,
...inlineConfig.afdian,
},
credentials: env.credentials,
} as Required<ContribkitConfig>

if (!['sponsors', 'sponsees'].includes(resolved.mode))
throw new Error(`Invalid mode: ${resolved.mode}. Expected "sponsors" or "sponsees".`)

resolved.name = inlineConfig.name || config.name || env.name || resolved.mode
resolved.name = inlineConfig.name || config.name || envConfig.name || resolved.mode

return resolved
}
Expand Down
6 changes: 3 additions & 3 deletions src/providers/afdian.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ContribkitConfig, Provider, Sponsorship } from '../types'
import { createHash } from 'node:crypto'
import { $fetch } from 'ofetch'
import { getCredentials } from '../configs/credentials'

// afdian api docs https://afdian.net/p/9c65d9cc617011ed81c352540025c377

Expand All @@ -12,14 +13,13 @@ export const AfdianProvider: Provider = {
return Promise.resolve([])
}

return fetchAfdianSponsors(config.afdian)
return fetchAfdianSponsors(config.afdian, getCredentials(config).afdian?.token)
},
}

export async function fetchAfdianSponsors(options: ContribkitConfig['afdian'] = {}): Promise<Sponsorship[]> {
export async function fetchAfdianSponsors(options: ContribkitConfig['afdian'] = {}, token = getCredentials({}).afdian?.token): Promise<Sponsorship[]> {
const {
userId,
token,
exchangeRate = 6.5,
includePurchases = true,
purchaseEffectivity = 30,
Expand Down
4 changes: 2 additions & 2 deletions src/providers/crowdinContributors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ export const CrowdinContributorsProvider: Provider = {
name: 'crowdinContributors',
fetchSponsors(config) {
return fetchCrowdinContributors(
config.crowdinContributors?.token || config.token!,
config.crowdinContributors?.token,
config.crowdinContributors?.projectId || 0,
config.crowdinContributors?.minTranslations || 1,
)
},
}

export async function fetchCrowdinContributors(
token: string,
token: string | undefined,
projectId: number,
minTranslations = 1,
): Promise<Sponsorship[]> {
Expand Down
22 changes: 14 additions & 8 deletions src/providers/github.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ContribkitConfig, GitHubAccountType, Provider, Sponsorship, Tier } from '../types'
import { $fetch } from 'ofetch'
import { getCredentials } from '../configs/credentials'
import { normalizeUrl } from '../utils'

function getMonthDifference(startDate: Date, endDate: Date) {
Expand Down Expand Up @@ -42,24 +43,24 @@ export const GitHubProvider: Provider = {
fetchSponsors(config) {
if (config.mode === 'sponsees') {
return fetchGitHubSponsoringAsSponsorships(
config.github?.token || config.token!,
config.github?.login || config.login!,
getCredentials(config).github?.token,
config.github?.login,
config.github?.type || 'user',
)
}

return fetchGitHubSponsors(
config.github?.token || config.token!,
config.github?.login || config.login!,
getCredentials(config).github?.token,
config.github?.login,
config.github?.type || 'user',
config,
)
},
}

export async function fetchGitHubSponsors(
token: string,
login: string,
token: string | undefined,
login: string | undefined,
type: GitHubAccountType,
config: ContribkitConfig,
): Promise<Sponsorship[]> {
Expand Down Expand Up @@ -360,10 +361,15 @@ export async function fetchGitHubSponsoring(
}

export async function fetchGitHubSponsoringAsSponsorships(
token: string,
login: string,
token: string | undefined,
login: string | undefined,
type: GitHubAccountType,
): Promise<Sponsorship[]> {
if (!token)
throw new Error('GitHub token is required')
if (!login)
throw new Error('GitHub login is required')

// Sponsees mode always loads full history regardless of active status.
const records = await fetchGitHubSponsoring(token, login, type, false)
const recordsBySponsorable = groupSponsoringRecordsByLogin(records)
Expand Down
5 changes: 3 additions & 2 deletions src/providers/githubContributions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Provider, Sponsorship } from '../types'
import { $fetch } from 'ofetch'
import { getCredentials } from '../configs/credentials'

export const GitHubContributionsProvider: Provider = {
name: 'githubContributions',
Expand All @@ -8,7 +9,7 @@ export const GitHubContributionsProvider: Provider = {
throw new Error('GitHub login is required for githubContributions provider')

return fetchGitHubContributions(
config.githubContributions?.token || config.token!,
getCredentials(config).githubContributions?.token,
config.githubContributions.login,
config.githubContributions.maxContributions,
config.githubContributions.logarithmicScaling,
Expand Down Expand Up @@ -304,7 +305,7 @@ function convertToSponsorships(
}

export async function fetchGitHubContributions(
token: string,
token: string | undefined,
login: string,
maxContributions?: number,
logarithmicScaling?: boolean,
Expand Down
9 changes: 5 additions & 4 deletions src/providers/githubContributors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Provider, Sponsorship } from '../types'
import { $fetch } from 'ofetch'
import { getCredentials } from '../configs/credentials'

export const GitHubContributorsProvider: Provider = {
name: 'githubContributors',
Expand All @@ -8,17 +9,17 @@ export const GitHubContributorsProvider: Provider = {
throw new Error('GitHub repository is required')

return fetchGitHubContributors(
config.githubContributors?.token || config.token!,
config.githubContributors?.login || config.login!,
getCredentials(config).githubContributors?.token,
config.githubContributors.login,
config.githubContributors.repo,
config.githubContributors?.minContributions,
)
},
}

export async function fetchGitHubContributors(
token: string,
login: string,
token: string | undefined,
login: string | undefined,
repo: string,
minContributions = 1,
): Promise<Sponsorship[]> {
Expand Down
Loading