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
44 changes: 38 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,45 @@ This is a fork of [sponsorkit](https://github.com/antfu-collective/sponsorkit) t

Supports:

- [**GitHub Sponsors**](https://github.com/sponsors)
- [**Patreon**](https://www.patreon.com/)
- [**OpenCollective**](https://opencollective.com/)
- [**Afdian**](https://afdian.com/)
- [**Polar**](https://polar.sh/)
- [**Liberapay**](https://liberapay.com/)
- Contributors:
- [**CrowdIn**](https://crowdin.com)
- [**GitHub**](https://github.com)
- [**Gitlab**](https://gitlab.com)
- Sponsors:
- [**GitHub Sponsors**](https://github.com/sponsors)
- [**Patreon**](https://www.patreon.com/)
- [**OpenCollective**](https://opencollective.com/)
- [**Afdian**](https://afdian.com/)
- [**Polar**](https://polar.sh/)
- [**Liberapay**](https://liberapay.com/)

## Usage

Create `.env` file with:

```ini
;; Contributors

; CrowdInContributors provider.
CONTRIBKIT_CROWDIN_TOKEN=
CONTRIBKIT_CROWDIN_PROJECT_ID=
CONTRIBKIT_CROWDIN_MIN_TRANSLATIONS=1

; GitHubContributors provider.
; Token requires the `public_repo` and `read:user` scopes.
CONTRIBKIT_GITHUB_CONTRIBUTORS_TOKEN=
CONTRIBKIT_GITHUB_CONTRIBUTORS_LOGIN=
CONTRIBKIT_GITHUB_CONTRIBUTORS_MIN=1
CONTRIBKIT_GITHUB_CONTRIBUTORS_REPO=

; GitlabContributors provider.
; Token requires the `read_api` and `read_user` scopes.
CONTRIBKIT_GITLAB_CONTRIBUTORS_TOKEN=
CONTRIBKIT_GITLAB_CONTRIBUTORS_MIN=1
CONTRIBKIT_GITLAB_CONTRIBUTORS_REPO_ID=

;; Sponsors

; GitHub provider.
; Token requires the `read:user` and `read:org` scopes.
CONTRIBKIT_GITHUB_TOKEN=
Expand Down Expand Up @@ -64,6 +91,11 @@ CONTRIBKIT_LIBERAPAY_LOGIN=

> Only one provider is required to be configured.

> ![NOTE]
> The contributor providers are intended to be separated from each other, unlike the sponsor providers.
> This will require different env variables to be set for each provider, and to be created from separate
> commands.

Run:

```base
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"release": "bumpp && pnpm publish"
},
"dependencies": {
"@crowdin/crowdin-api-client": "^1.41.2",
"ansis": "^3.17.0",
"cac": "^6.7.14",
"consola": "^3.4.0",
Expand Down
16 changes: 16 additions & 0 deletions src/configs/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,22 @@ export function loadEnv(): Partial<ContribkitConfig> {
login: process.env.CONTRIBKIT_LIBERAPAY_LOGIN || process.env.LIBERAPAY_LOGIN,
},
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),
},
crowdinContributors: {
token: process.env.CONTRIBKIT_CROWDIN_TOKEN,
projectId: Number(process.env.CONTRIBKIT_CROWDIN_PROJECT_ID),
minTranslations: Number(process.env.CONTRIBKIT_CROWDIN_MIN_TRANSLATIONS) || 1,
},
}

// remove undefined keys
Expand Down
8 changes: 7 additions & 1 deletion src/configs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,17 @@ export async function loadConfig(inlineConfig: ContribkitConfig = {}): Promise<R
const { config = {} } = await _loadConfig<ContribkitConfig>({
sources: [
{
files: 'sponsorkit.config',
files: 'contrib.config',
},
{
files: 'contribkit.config',
},
{
files: 'sponsor.config',
},
{
files: 'sponsorkit.config',
},
],
merge: true,
})
Expand Down
94 changes: 94 additions & 0 deletions src/providers/crowdinContributors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import type { Credentials, ReportsModel } from '@crowdin/crowdin-api-client'
import type { Provider, Sponsorship } from '../types'
import { ProjectsGroups, Reports } from '@crowdin/crowdin-api-client'

interface Member {
id: number
username: string
fullName: string
avatarUrl: string
joinedAt: string
}

export const CrowdinContributorsProvider: Provider = {
name: 'crowdinContributors',
fetchSponsors(config) {
return fetchCrowdinContributors(
config.crowdinContributors?.token || config.token!,
config.crowdinContributors?.projectId || 0,
config.crowdinContributors?.minTranslations || 1,
)
},
}

export async function fetchCrowdinContributors(
token: string,
projectId: number,
minTranslations = 1,
): Promise<Sponsorship[]> {
if (!token)
throw new Error('Crowdin token is required')
if (!projectId)
throw new Error('Crowdin project ID is required')

const credentials: Credentials = {
token,
}

// get the project
const projectsGroups: ProjectsGroups = new ProjectsGroups(credentials)
const project = await projectsGroups.getProject(projectId)

// get top members report
const reports: Reports = new Reports(credentials)

// today's date in ISO 8601 format
const dateTo = new Date().toISOString()
const dateFrom = project.data.createdAt

const createReportRequestBody: ReportsModel.GenerateReportRequest = {
name: 'top-members',
schema: {
unit: 'words',
format: 'json',
dateFrom,
dateTo,
},
}

const createReport = await reports.generateReport(projectId, createReportRequestBody)

// get the report
// sleep for 5 seconds
await new Promise(resolve => setTimeout(resolve, 5000))
const report = await reports.downloadReport(projectId, createReport.data.identifier)

// build contributors object from looping over the report data
const reportRaw = await fetch(report.data.url)
const reportData = await reportRaw.json() as { data: { user: Member, translated: number }[] }

const contributors = reportData.data
.filter((entry: { user: Member, translated: number }) => entry.translated > minTranslations)
.map((entry: { user: Member, translated: number }) => ({
member: entry.user,
translations: entry.translated,
}))

return contributors
.filter(Boolean)
.map(({ member, translations }: { member: Member, translations: number }) => ({
sponsor: {
type: 'User',
login: member.username,
name: member.username, // fullName is also available
avatarUrl: member.avatarUrl,
linkUrl: `https://crowdin.com/profile/${member.username}`,
},
isOneTime: false,
monthlyDollars: translations,
privacyLevel: 'PUBLIC',
tierName: 'Translator',
createdAt: member.joinedAt,
provider: 'crowdinContributors',
}))
}
90 changes: 90 additions & 0 deletions src/providers/githubContributors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type { Provider, Sponsorship } from '../types'
import { $fetch } from 'ofetch'

export const GitHubContributorsProvider: Provider = {
name: 'githubContributors',
fetchSponsors(config) {
if (!config.githubContributors?.repo)
throw new Error('GitHub repository is required')

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

export async function fetchGitHubContributors(
token: string,
login: string,
repo: string,
minContributions = 1,
): Promise<Sponsorship[]> {
if (!token)
throw new Error('GitHub token is required')

if (!login)
throw new Error('GitHub login is required')

if (!repo)
throw new Error('GitHub repository is required')

const allContributors: Array<{
login: string
contributions: number
type: string
url: string
avatar_url: string
}> = []

let page = 1
let hasNextPage = true

while (hasNextPage) {
const response = await $fetch<typeof allContributors>(
`https://api.github.com/repos/${login}/${repo}/contributors`,
{
query: {
page: String(page),
per_page: '100',
},
headers: {
Authorization: `bearer ${token}`,
Accept: 'application/vnd.github.v3+json',
},
},
)

if (!response || !response.length)
break

allContributors.push(...response)

// GitHub returns exactly 100 items when there are more pages
hasNextPage = response.length === 100
page++
}

return allContributors
.filter(contributor =>
contributor.type === 'User'
&& contributor.contributions >= minContributions,
)
.map(contributor => ({
sponsor: {
type: 'User',
login: contributor.login,
name: contributor.login,
avatarUrl: contributor.avatar_url,
linkUrl: contributor.url,
},
isOneTime: false,
monthlyDollars: contributor.contributions,
privacyLevel: 'PUBLIC',
tierName: 'Contributor',
createdAt: new Date().toISOString(),
provider: 'githubContributors',
}))
}
Loading