Skip to content

Commit c47bc0c

Browse files
feat(contributors): add contributor providers (#3)
1 parent b58e43d commit c47bc0c

File tree

9 files changed

+450
-8
lines changed

9 files changed

+450
-8
lines changed

README.md

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,45 @@ This is a fork of [sponsorkit](https://github.com/antfu-collective/sponsorkit) t
1111

1212
Supports:
1313

14-
- [**GitHub Sponsors**](https://github.com/sponsors)
15-
- [**Patreon**](https://www.patreon.com/)
16-
- [**OpenCollective**](https://opencollective.com/)
17-
- [**Afdian**](https://afdian.com/)
18-
- [**Polar**](https://polar.sh/)
19-
- [**Liberapay**](https://liberapay.com/)
14+
- Contributors:
15+
- [**CrowdIn**](https://crowdin.com)
16+
- [**GitHub**](https://github.com)
17+
- [**Gitlab**](https://gitlab.com)
18+
- Sponsors:
19+
- [**GitHub Sponsors**](https://github.com/sponsors)
20+
- [**Patreon**](https://www.patreon.com/)
21+
- [**OpenCollective**](https://opencollective.com/)
22+
- [**Afdian**](https://afdian.com/)
23+
- [**Polar**](https://polar.sh/)
24+
- [**Liberapay**](https://liberapay.com/)
2025

2126
## Usage
2227

2328
Create `.env` file with:
2429

2530
```ini
31+
;; Contributors
32+
33+
; CrowdInContributors provider.
34+
CONTRIBKIT_CROWDIN_TOKEN=
35+
CONTRIBKIT_CROWDIN_PROJECT_ID=
36+
CONTRIBKIT_CROWDIN_MIN_TRANSLATIONS=1
37+
38+
; GitHubContributors provider.
39+
; Token requires the `public_repo` and `read:user` scopes.
40+
CONTRIBKIT_GITHUB_CONTRIBUTORS_TOKEN=
41+
CONTRIBKIT_GITHUB_CONTRIBUTORS_LOGIN=
42+
CONTRIBKIT_GITHUB_CONTRIBUTORS_MIN=1
43+
CONTRIBKIT_GITHUB_CONTRIBUTORS_REPO=
44+
45+
; GitlabContributors provider.
46+
; Token requires the `read_api` and `read_user` scopes.
47+
CONTRIBKIT_GITLAB_CONTRIBUTORS_TOKEN=
48+
CONTRIBKIT_GITLAB_CONTRIBUTORS_MIN=1
49+
CONTRIBKIT_GITLAB_CONTRIBUTORS_REPO_ID=
50+
51+
;; Sponsors
52+
2653
; GitHub provider.
2754
; Token requires the `read:user` and `read:org` scopes.
2855
CONTRIBKIT_GITHUB_TOKEN=
@@ -64,6 +91,11 @@ CONTRIBKIT_LIBERAPAY_LOGIN=
6491

6592
> Only one provider is required to be configured.
6693
94+
> ![NOTE]
95+
> The contributor providers are intended to be separated from each other, unlike the sponsor providers.
96+
> This will require different env variables to be set for each provider, and to be created from separate
97+
> commands.
98+
6799
Run:
68100

69101
```base

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"release": "bumpp && pnpm publish"
4747
},
4848
"dependencies": {
49+
"@crowdin/crowdin-api-client": "^1.41.2",
4950
"ansis": "^3.17.0",
5051
"cac": "^6.7.14",
5152
"consola": "^3.4.0",

src/configs/env.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,22 @@ export function loadEnv(): Partial<ContribkitConfig> {
4141
login: process.env.CONTRIBKIT_LIBERAPAY_LOGIN || process.env.LIBERAPAY_LOGIN,
4242
},
4343
outputDir: process.env.CONTRIBKIT_DIR,
44+
githubContributors: {
45+
login: process.env.CONTRIBKIT_GITHUB_CONTRIBUTORS_LOGIN,
46+
token: process.env.CONTRIBKIT_GITHUB_CONTRIBUTORS_TOKEN,
47+
minContributions: Number(process.env.CONTRIBKIT_GITHUB_CONTRIBUTORS_MIN) || 1,
48+
repo: process.env.CONTRIBKIT_GITHUB_CONTRIBUTORS_REPO,
49+
},
50+
gitlabContributors: {
51+
token: process.env.CONTRIBKIT_GITLAB_CONTRIBUTORS_TOKEN,
52+
minContributions: Number(process.env.CONTRIBKIT_GITLAB_CONTRIBUTORS_MIN) || 1,
53+
repoId: Number(process.env.CONTRIBKIT_GITLAB_CONTRIBUTORS_REPO_ID),
54+
},
55+
crowdinContributors: {
56+
token: process.env.CONTRIBKIT_CROWDIN_TOKEN,
57+
projectId: Number(process.env.CONTRIBKIT_CROWDIN_PROJECT_ID),
58+
minTranslations: Number(process.env.CONTRIBKIT_CROWDIN_MIN_TRANSLATIONS) || 1,
59+
},
4460
}
4561

4662
// remove undefined keys

src/configs/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,17 @@ export async function loadConfig(inlineConfig: ContribkitConfig = {}): Promise<R
1818
const { config = {} } = await _loadConfig<ContribkitConfig>({
1919
sources: [
2020
{
21-
files: 'sponsorkit.config',
21+
files: 'contrib.config',
2222
},
2323
{
2424
files: 'contribkit.config',
2525
},
26+
{
27+
files: 'sponsor.config',
28+
},
29+
{
30+
files: 'sponsorkit.config',
31+
},
2632
],
2733
merge: true,
2834
})

src/providers/crowdinContributors.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import type { Credentials, ReportsModel } from '@crowdin/crowdin-api-client'
2+
import type { Provider, Sponsorship } from '../types'
3+
import { ProjectsGroups, Reports } from '@crowdin/crowdin-api-client'
4+
5+
interface Member {
6+
id: number
7+
username: string
8+
fullName: string
9+
avatarUrl: string
10+
joinedAt: string
11+
}
12+
13+
export const CrowdinContributorsProvider: Provider = {
14+
name: 'crowdinContributors',
15+
fetchSponsors(config) {
16+
return fetchCrowdinContributors(
17+
config.crowdinContributors?.token || config.token!,
18+
config.crowdinContributors?.projectId || 0,
19+
config.crowdinContributors?.minTranslations || 1,
20+
)
21+
},
22+
}
23+
24+
export async function fetchCrowdinContributors(
25+
token: string,
26+
projectId: number,
27+
minTranslations = 1,
28+
): Promise<Sponsorship[]> {
29+
if (!token)
30+
throw new Error('Crowdin token is required')
31+
if (!projectId)
32+
throw new Error('Crowdin project ID is required')
33+
34+
const credentials: Credentials = {
35+
token,
36+
}
37+
38+
// get the project
39+
const projectsGroups: ProjectsGroups = new ProjectsGroups(credentials)
40+
const project = await projectsGroups.getProject(projectId)
41+
42+
// get top members report
43+
const reports: Reports = new Reports(credentials)
44+
45+
// today's date in ISO 8601 format
46+
const dateTo = new Date().toISOString()
47+
const dateFrom = project.data.createdAt
48+
49+
const createReportRequestBody: ReportsModel.GenerateReportRequest = {
50+
name: 'top-members',
51+
schema: {
52+
unit: 'words',
53+
format: 'json',
54+
dateFrom,
55+
dateTo,
56+
},
57+
}
58+
59+
const createReport = await reports.generateReport(projectId, createReportRequestBody)
60+
61+
// get the report
62+
// sleep for 5 seconds
63+
await new Promise(resolve => setTimeout(resolve, 5000))
64+
const report = await reports.downloadReport(projectId, createReport.data.identifier)
65+
66+
// build contributors object from looping over the report data
67+
const reportRaw = await fetch(report.data.url)
68+
const reportData = await reportRaw.json() as { data: { user: Member, translated: number }[] }
69+
70+
const contributors = reportData.data
71+
.filter((entry: { user: Member, translated: number }) => entry.translated > minTranslations)
72+
.map((entry: { user: Member, translated: number }) => ({
73+
member: entry.user,
74+
translations: entry.translated,
75+
}))
76+
77+
return contributors
78+
.filter(Boolean)
79+
.map(({ member, translations }: { member: Member, translations: number }) => ({
80+
sponsor: {
81+
type: 'User',
82+
login: member.username,
83+
name: member.username, // fullName is also available
84+
avatarUrl: member.avatarUrl,
85+
linkUrl: `https://crowdin.com/profile/${member.username}`,
86+
},
87+
isOneTime: false,
88+
monthlyDollars: translations,
89+
privacyLevel: 'PUBLIC',
90+
tierName: 'Translator',
91+
createdAt: member.joinedAt,
92+
provider: 'crowdinContributors',
93+
}))
94+
}

src/providers/githubContributors.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import type { Provider, Sponsorship } from '../types'
2+
import { $fetch } from 'ofetch'
3+
4+
export const GitHubContributorsProvider: Provider = {
5+
name: 'githubContributors',
6+
fetchSponsors(config) {
7+
if (!config.githubContributors?.repo)
8+
throw new Error('GitHub repository is required')
9+
10+
return fetchGitHubContributors(
11+
config.githubContributors?.token || config.token!,
12+
config.githubContributors?.login || config.login!,
13+
config.githubContributors.repo,
14+
config.githubContributors?.minContributions,
15+
)
16+
},
17+
}
18+
19+
export async function fetchGitHubContributors(
20+
token: string,
21+
login: string,
22+
repo: string,
23+
minContributions = 1,
24+
): Promise<Sponsorship[]> {
25+
if (!token)
26+
throw new Error('GitHub token is required')
27+
28+
if (!login)
29+
throw new Error('GitHub login is required')
30+
31+
if (!repo)
32+
throw new Error('GitHub repository is required')
33+
34+
const allContributors: Array<{
35+
login: string
36+
contributions: number
37+
type: string
38+
url: string
39+
avatar_url: string
40+
}> = []
41+
42+
let page = 1
43+
let hasNextPage = true
44+
45+
while (hasNextPage) {
46+
const response = await $fetch<typeof allContributors>(
47+
`https://api.github.com/repos/${login}/${repo}/contributors`,
48+
{
49+
query: {
50+
page: String(page),
51+
per_page: '100',
52+
},
53+
headers: {
54+
Authorization: `bearer ${token}`,
55+
Accept: 'application/vnd.github.v3+json',
56+
},
57+
},
58+
)
59+
60+
if (!response || !response.length)
61+
break
62+
63+
allContributors.push(...response)
64+
65+
// GitHub returns exactly 100 items when there are more pages
66+
hasNextPage = response.length === 100
67+
page++
68+
}
69+
70+
return allContributors
71+
.filter(contributor =>
72+
contributor.type === 'User'
73+
&& contributor.contributions >= minContributions,
74+
)
75+
.map(contributor => ({
76+
sponsor: {
77+
type: 'User',
78+
login: contributor.login,
79+
name: contributor.login,
80+
avatarUrl: contributor.avatar_url,
81+
linkUrl: contributor.url,
82+
},
83+
isOneTime: false,
84+
monthlyDollars: contributor.contributions,
85+
privacyLevel: 'PUBLIC',
86+
tierName: 'Contributor',
87+
createdAt: new Date().toISOString(),
88+
provider: 'githubContributors',
89+
}))
90+
}

0 commit comments

Comments
 (0)