Skip to content

Commit

Permalink
feat(api): add sponsors endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonsaldan committed Jan 12, 2025
1 parent 9d2d0cc commit 209a53a
Show file tree
Hide file tree
Showing 3 changed files with 216 additions and 1 deletion.
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
KOFI_SPONSORS=Tanner,Navi,nono9k,Maxine,Vonnieboo,uktexan,Yungguap,Cbb,
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ web_modules/
.yarn-integrity

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
Expand Down
215 changes: 215 additions & 0 deletions src/app/api/v1/sponsors/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import { NextResponse } from 'next/server'

const repos = ['usenocturne/nocturne-ui', 'usenocturne/nocturne-image']
const manualContributors = ['Jenner Gray']

interface BuyMeACoffeeSponsor {
payer_name: string
amount: number
support_created_on: string
}

interface BuyMeACoffeeResponse {
data: BuyMeACoffeeSponsor[]
current_page: number
last_page: number
}

interface SponsorsResponse {
developers: string[]
contributors: string[]
sponsors: string[]
}

interface GitHubPullRequest {
user: {
login: string
}
merged_at: string | null
}

async function fetchGitHubContributors(): Promise<string[]> {
try {
const allPRs = await Promise.all(
repos.map(async (repo) => {
const response = await fetch(
`https://api.github.com/repos/${repo}/pulls?state=closed&per_page=100`,
{
headers: {
Accept: 'application/vnd.github.v3+json',
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
},
},
)

if (!response.ok) {
throw new Error(
`Failed to fetch PRs from ${repo}: ${response.status}`,
)
}

const pulls: GitHubPullRequest[] = await response.json()
return pulls
.filter((pr) => pr.merged_at !== null)
.map((pr) => pr.user.login)
}),
)

const excludedUsernames = [
'68p',
'BBaoVanC',
'itsnebulalol',
'brandonsaldan',
'bot',
'dependabot',
'bbaovanc',
'DominicFrye',
'shadow',
]

const allContributors = [
...Array.from(new Set(allPRs.flat())),
...manualContributors,
]

const uniqueContributors = Array.from(new Set(allContributors))
.filter(
(name) =>
!excludedUsernames.some(
(excluded) => name.toLowerCase() === excluded.toLowerCase(),
) && !name.toLowerCase().includes('bot'),
)
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))

return uniqueContributors
} catch (error: unknown) {
if (error instanceof Error) {
console.error('Error fetching GitHub contributors:', error.message)
}

return manualContributors.sort((a, b) =>
a.toLowerCase().localeCompare(b.toLowerCase()),
)
}
}

async function fetchBuyMeACoffeePage(
page: number,
): Promise<BuyMeACoffeeResponse> {
const response = await fetch(
`https://developers.buymeacoffee.com/api/v1/supporters?page=${page}`,
{
headers: {
Authorization: `Bearer ${process.env.BUYMEACOFFEE_ACCESS_TOKEN}`,
},
},
)

if (!response.ok) {
throw new Error(
`Failed to fetch from Buy Me a Coffee API: ${response.status}`,
)
}

return response.json()
}

async function fetchBuyMeACoffeeSponsors(): Promise<string[]> {
try {
const firstPage = await fetchBuyMeACoffeePage(1)

const allPages = await Promise.all(
Array.from({ length: firstPage.last_page }, (_, i: number) => {
const pageNum = i + 1
return fetchBuyMeACoffeePage(pageNum)
}),
)

const allSponsors: string[] = allPages.flatMap((page) =>
page.data
.map((supporter: BuyMeACoffeeSponsor) => supporter.payer_name)
.filter((name: string) => {
const unwantedNames = ['Someone', 'Anonymous', '']
return (
!unwantedNames.includes(name) &&
name.trim() !== '' &&
name.toLowerCase() !== 'anonymous'
)
}),
)

return Array.from(new Set(allSponsors))
} catch (error: unknown) {
if (error instanceof Error) {
console.error('Error fetching from Buy Me a Coffee:', error.message)
}
return []
}
}

function getKofiSponsors(): string[] {
const kofiSponsorsEnv = process.env.KOFI_SPONSORS
if (!kofiSponsorsEnv) {
return []
}

return kofiSponsorsEnv
.split(',')
.map((name: string) => name.trim())
.filter((name: string) => name !== '')
}

export async function GET(): Promise<NextResponse<SponsorsResponse>> {
try {
const developers = ['Brandon Saldan', 'bbaovanc', 'Dominic Frye', 'shadow']

const [buyMeACoffeeSponsors, kofiSponsors, contributors] =
await Promise.all([
fetchBuyMeACoffeeSponsors(),
Promise.resolve(getKofiSponsors()),
fetchGitHubContributors(),
])

const allSponsors = Array.from(
new Set(
[...buyMeACoffeeSponsors, ...kofiSponsors].map((name) =>
name.toLowerCase(),
),
),
)
.map((lowerName) => {
const original = [...buyMeACoffeeSponsors, ...kofiSponsors].find(
(name) => name.toLowerCase() === lowerName,
)
return original || lowerName
})
.sort((a: string, b: string) => a.localeCompare(b))

const response: SponsorsResponse = {
developers,
contributors,
sponsors: allSponsors,
}

return new NextResponse(JSON.stringify(response), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=7200',
},
})
} catch (error: unknown) {
return NextResponse.json(
{
developers: [],
contributors: [],
sponsors: [],
},
{
status: 500,
headers: {
'Cache-Control': 'no-store',
},
},
)
}
}

0 comments on commit 209a53a

Please sign in to comment.