import { postgrestLocalUrl, patreonVideoAccessBenefitId, giteaUrl } from './constants' import { type IPatron } from '@futureporn/types' import { PublicPatron } from '@futureporn/utils/patron.ts'; import { keycloakLocalUrl, patreonApiIdentityUrl } from "./constants"; import { Session } from 'next-auth'; import { JWT } from 'next-auth/jwt'; import { type UMAToken, getKeycloakIdFromTierId, getUMAToken, syncronizeKeycloakRoles } from './keycloak'; export type TiersList = string[] interface KeycloakToken { account: { access_token: string } } interface KeycloakIdpToken { access_token: string; expires_in: number; refresh_expires_in: number; refresh_token: string; token_type: string; "not-before-policy": number; scope: string; version: string; } interface PatreonResponse { data: UserData; included: IncludedItem[]; links: { self: string; }; } interface UserData { attributes: UserAttributes; id: string; relationships: { memberships: { data: Relationship[]; }; }; type: "user"; } interface UserAttributes { about: string | null; created: string; // ISO 8601 date string first_name: string; full_name: string; image_url: string; last_name: string; thumb_url: string; url: string; vanity: string | null; } interface Relationship { id: string; type: string; } type IncludedItem = Member | Tier | Benefit; interface Member { attributes: Record; id: string; relationships: { currently_entitled_tiers: { data: Relationship[]; }; }; type: "member"; } export interface Tier { attributes: Record; id: string; relationships: { benefits: { data: Relationship[]; }; }; type: "tier"; } interface Benefit { attributes: Record; id: string; type: "benefit"; } export interface ICampaign { pledgeSum: number; patronCount: number; } export interface IMarshalledCampaign { data: { attributes: { pledge_sum: number, patron_count: number } } } export const tiers = { everyone: '-1', free: '10620388', archiveSupporter: '8154170', stealthSupporter: '9561793', tuneItUp: '9184994', maxQ: '22529959', archiveCollector: '8154171', advancedArchiveSupporter: '8686045', quantumSupporter: '8694826', sneakyQuantumSupporter: '9560538', luberPlusPlus: '8686022' } export async function updateKeycloakUserPatreonEntitlements(token: JWT) { console.log(`updateKeycloakUserPatreonEntitlements() invoked.`) const userId = token?.sub if (!userId) { throw new Error(`failed to get userId from token.sub`); } const keycloakidpToken = await getKeycloakIdpToken(token.access_token) if (!keycloakidpToken) { throw new Error(`failed to get keycloakIdpToken; it was falsy.`) } const patreonTiersList = await getPatreonMemberships(keycloakidpToken) await syncronizeKeycloakRoles(userId, patreonTiersList) } export async function getKeycloakIdpToken(access_token: string): Promise { console.log(`getKeycloakIdpToken() using access_token=${access_token}`) // @todo check the access_token. if it is expired, use the refresh_token to get a new access_token. const res = await fetch(`https://keycloak.fp.sbtp.xyz/realms/futureporn/broker/patreon/token`, { headers: { 'Authorization': `Bearer ${access_token}` } }) if (!res.ok) { const bod = await res.text() const msg = `req.status=${res.status} req.statusText=${res.statusText} body=${bod}` console.log(msg) throw new Error(`Failed to getKeycloakIdpToken. ${msg}`) } const idpToken = await res.json() return idpToken } export function extractCurrentlyEntitledTiers(response: PatreonResponse): Relationship[] { return response.included .filter((item): item is Member => item.type === "member") .flatMap(member => member.relationships.currently_entitled_tiers.data); } export async function getPatreonMemberships(token: KeycloakIdpToken): Promise { // console.log(`getPatreonMemberships with keycloakidpToken as follows`) // console.log(token) const query = 'fields%5Buser%5D=about,created,email,first_name,full_name,image_url,last_name,thumb_url,url,vanity&include=memberships,memberships.currently_entitled_tiers,memberships.currently_entitled_tiers.benefits'; const res = await fetch(`${patreonApiIdentityUrl}?${query}`, { headers: { 'Authorization': `Bearer ${token.access_token}` } }) const data = await res.json() as PatreonResponse // from the currently_entitled_tiers, we only want the id. // we filter out any non-futureporn patreon tiers. return extractCurrentlyEntitledTiers(data) .map((rel) => rel.id) .filter((t) => Object.values(tiers).includes(t)) } export function isEntitledToPatronVideoAccess(session: Session): boolean { if (!session.user?.patreonBenefits) return false; const patreonBenefits = session.user.patreonBenefits return (patreonBenefits.includes(patreonVideoAccessBenefitId)) } export async function getPatrons(): Promise { let patrons: PublicPatron[] = [] try { const url = `${postgrestLocalUrl}/patrons` console.log(`GET requesting ${url}`) const res = await fetch(url); const data = await res.json(); if (!res.ok) throw new Error(`failed to get /patrons. res.status=${res.status}, res.statusText=${res.statusText}`); if (!data) throw new Error(`no patron data was available. ${JSON.stringify(data)}`); patrons = data } catch (e) { console.error('failed to get patrons~ list') console.error(e) return [] as PublicPatron[] } return patrons } export async function getCampaign(): Promise { const res = await fetch('https://www.patreon.com/api/campaigns/8012692', { headers: { accept: 'application/json' }, next: { revalidate: 43200 // 12 hour cache } }) const campaignData = await res.json(); const data = { patronCount: campaignData.data.attributes.patron_count, pledgeSum: campaignData.data.attributes.campaign_pledge_sum } return data }