Skip to content

Commit

Permalink
feat: add liberapay support
Browse files Browse the repository at this point in the history
  • Loading branch information
Zaczero committed Dec 28, 2024
1 parent 71468ab commit 5bf2aae
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 1 deletion.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Supports:
- [**OpenCollective**](https://opencollective.com/)
- [**Afdian**](https://afdian.com/)
- [**Polar**](https://polar.sh/)
- [**Liberapay**](https://liberapay.com/)

## Usage

Expand Down Expand Up @@ -50,6 +51,10 @@ SPONSORKIT_AFDIAN_TOKEN=
SPONSORKIT_POLAR_TOKEN=
; The name of the organization to fetch sponsorships from.
SPONSORKIT_POLAR_ORGANIZATION=

; Liberapay provider.
; The name of the profile.
SPONSORKIT_LIBERAPAY_LOGIN=
```

> Only one provider is required to be configured.
Expand Down Expand Up @@ -87,6 +92,9 @@ export default defineConfig({
polar: {
// ...
},
liberapay: {
// ...
},

// Rendering configs
width: 800,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
},
"dependencies": {
"@antfu/utils": "^0.7.10",
"@fast-csv/parse": "^5.0.2",
"consola": "^3.2.3",
"d3-hierarchy": "^3.1.2",
"dotenv": "^16.4.5",
Expand Down
45 changes: 45 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/configs/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export function loadEnv(): Partial<SponsorkitConfig> {
token: process.env.SPONSORKIT_POLAR_TOKEN || process.env.POLAR_TOKEN,
organization: process.env.SPONSORKIT_POLAR_ORGANIZATION || process.env.POLAR_ORGANIZATION,
},
liberapay: {
login: process.env.SPONSORKIT_LIBERAPAY_LOGIN || process.env.LIBERAPAY_LOGIN,
},
outputDir: process.env.SPONSORKIT_DIR,
}

Expand Down
3 changes: 3 additions & 0 deletions src/processing/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export async function resolveAvatars(
}

const pngBuffer = await fetchImage(ship.sponsor.avatarUrl).catch((e) => {
// Liberapay avatar URLs can return 404 Not Found
if (ship.provider == 'liberapay' && e.toString().includes('404 Not Found') && fallbackAvatar)
return fallbackAvatar
t.error(`Failed to fetch avatar for ${ship.sponsor.login || ship.sponsor.name} [${ship.sponsor.avatarUrl}]`)
t.error(e)
if (fallbackAvatar)
Expand Down
5 changes: 5 additions & 0 deletions src/providers/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AfdianProvider } from './afdian'
import { GitHubProvider } from './github'
import { LiberapayProvider } from "./liberapay"
import { OpenCollectiveProvider } from './opencollective'
import { PatreonProvider } from './patreon'
import { PolarProvider } from './polar'
Expand All @@ -13,6 +14,7 @@ export const ProvidersMap = {
opencollective: OpenCollectiveProvider,
afdian: AfdianProvider,
polar: PolarProvider,
liberapay: LiberapayProvider,
}

export function guessProviders(config: SponsorkitConfig) {
Expand All @@ -32,6 +34,9 @@ export function guessProviders(config: SponsorkitConfig) {
if (config.polar && config.polar.token)
items.push('polar')

if (config.liberapay && config.liberapay.login)
items.push('liberapay')

// fallback
if (!items.length)
items.push('github')
Expand Down
86 changes: 86 additions & 0 deletions src/providers/liberapay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@

import { $fetch } from 'ofetch'
import { parseString } from '@fast-csv/parse'
import type { Provider, Sponsorship } from '../types'

export const LiberapayProvider: Provider = {
name: 'liberapay',
fetchSponsors(config) {
return fetchLiberapaySponsors(config.liberapay?.login)
},
}

interface LiberapayRow {
pledge_date: string
patron_id: string
patron_username: string
patron_public_name: string
donation_currency: string
weekly_amount: string
patron_avatar_url: string
}

interface ExchangeRate {
code: string
alphaCode: string
numericCode: string
name: string
rate: number
date: string
inverseRate: number
}

interface ExchangeRates {
[key: string]: ExchangeRate
}

export async function fetchLiberapaySponsors(login?: string): Promise<Sponsorship[]> {
if (!login)
throw new Error('Liberapay login is required')

// Fetch and parse public CSV data
const csvUrl = `https://liberapay.com/${login}/patrons/public.csv`
const csvResponse = await $fetch<string>(csvUrl)
const rows: LiberapayRow[] = []
await new Promise((resolve) => {
parseString(csvResponse, {
headers: true,
ignoreEmpty: true,
trim: true,
})
.on('data', (row) => rows.push(row))
.on('end', resolve)
})

// Only fetch exchange rates if we have patrons with non-USD currencies
const exchangeRates = rows.some(r => r.donation_currency !== 'USD')
? await $fetch<ExchangeRates>('https://www.floatrates.com/daily/usd.json')
: {}

return rows.map(row => ({
sponsor: {
type: 'User',
login: row.patron_username,
name: row.patron_public_name || row.patron_username,
avatarUrl: row.patron_avatar_url,
linkUrl: `https://liberapay.com/${row.patron_username}`,
},
monthlyDollars: getMonthlyDollarAmount(parseFloat(row.weekly_amount), row.donation_currency, exchangeRates),
privacyLevel: 'PUBLIC',
createdAt: new Date(row.pledge_date).toISOString(),
provider: 'liberapay',
}))
}

function getMonthlyDollarAmount(weeklyAmount: number, currency: string, exchangeRates: ExchangeRates): number {
const weeksPerMonth = 4.345
const monthlyAmount = weeklyAmount * weeksPerMonth

if (currency === 'USD')
return monthlyAmount

// Optionally exchange to USD
const currencyLower = currency.toLowerCase()
const inverseRate = exchangeRates[currencyLower]?.inverseRate ?? 1
return monthlyAmount * inverseRate
}
11 changes: 10 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export const outputFormats = ['svg', 'png', 'webp', 'json'] as const

export type OutputFormat = typeof outputFormats[number]

export type ProviderName = 'github' | 'patreon' | 'opencollective' | 'afdian' | 'polar'
export type ProviderName = 'github' | 'patreon' | 'opencollective' | 'afdian' | 'polar' | 'liberapay'

export type GitHubAccountType = 'user' | 'organization'

Expand Down Expand Up @@ -200,6 +200,15 @@ export interface ProvidersConfig {
*/
organization?: string
}

liberapay?: {
/**
* The name of the Liberapay profile.
*
* Will read from `SPONSORKIT_LIBERAPAY_LOGIN` environment variable if not set.
*/
login?: string;
};
}

export interface SponsorkitRenderOptions {
Expand Down

0 comments on commit 5bf2aae

Please sign in to comment.