Skip to content

Commit

Permalink
Merge pull request #618 from cynicaloptimist/development
Browse files Browse the repository at this point in the history
Use v2 Patreon API to support Free Trials
  • Loading branch information
cynicaloptimist authored Sep 14, 2023
2 parents 7c0f5ad + db0b7e7 commit dfea95e
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 41 deletions.
8 changes: 6 additions & 2 deletions client/Utility/Metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ export class Metrics {
}

if (typeof gtag == "function") {
gtag("event", name, eventData);
try {
gtag("event", name, eventData);
} catch (e) {}
}

if (!env.SendMetrics) {
Expand Down Expand Up @@ -77,7 +79,9 @@ export class Metrics {
}

if (typeof gtag == "function") {
gtag("event", name, eventData);
try {
gtag("event", name, eventData);
} catch (e) {}
}

if (!env.SendMetrics) {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "improved-initiative",
"version": "3.7.7",
"version": "3.7.8",
"description": "Combat tracker for Dungeons and Dragons (D&D) 5th Edition",
"license": "MIT",
"repository": {
Expand Down
101 changes: 66 additions & 35 deletions server/patreon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import * as crypto from "crypto";
import * as express from "express";

import * as _ from "lodash";
import * as patreon from "patreon";
import axios from "axios";
import * as querystring from "querystring";

import * as request from "request";

Expand All @@ -25,10 +26,10 @@ const tiersWithAccountSyncEntitled = [

const tiersWithEpicEntitled = ["1937132", "8749940"];

const baseUrl = process.env.BASE_URL,
patreonClientId = process.env.PATREON_CLIENT_ID,
patreonClientSecret = process.env.PATREON_CLIENT_SECRET,
patreonUrl = process.env.PATREON_URL;
const baseUrl = process.env.BASE_URL;
const patreonClientId = process.env.PATREON_CLIENT_ID;
const patreonClientSecret = process.env.PATREON_CLIENT_SECRET;
const patreonUrl = process.env.PATREON_URL;

interface Post {
attributes: {
Expand Down Expand Up @@ -60,17 +61,22 @@ export function configureLoginRedirect(app: express.Application): void {

app.get(redirectPath, async (req: Req, res: Res) => {
try {
console.log("req.query >" + JSON.stringify(req.query));
console.log("req.body >" + JSON.stringify(req.body));
const code = req.query.code;

const OAuthClient = patreon.oauth(patreonClientId, patreonClientSecret);

const tokens = await OAuthClient.getTokens(code, redirectUri);

const APIClient = patreon.patreon(tokens.access_token);
const { rawJson } = await APIClient(`/current_user`);
await handleCurrentUser(req, res, rawJson);
const code = req.query.code as string;

const tokens = await getTokens(code, redirectUri);

const userResponse = await axios.get(
`https://www.patreon.com/api/oauth2/v2/identity` +
`?${encodeURIComponent("fields[user]")}=email` +
`&include=memberships.currently_entitled_tiers`,
{
headers: {
authorization: "Bearer " + tokens.access_token
}
}
);

await handleCurrentUser(req, res, userResponse.data);
} catch (err) {
console.error("Patreon login flow failed:", JSON.stringify(err));
res
Expand All @@ -82,6 +88,23 @@ export function configureLoginRedirect(app: express.Application): void {
});
}

async function getTokens(code: string, redirectUri: string) {
const tokensResponse = await axios.post(
"https://www.patreon.com/api/oauth2/token",
querystring.stringify({
code: code,
grant_type: "authorization_code",
client_id: patreonClientId,
client_secret: patreonClientSecret,
redirect_uri: redirectUri
}),
{ headers: { "content-type": "application/x-www-form-urlencoded" } }
);

const tokens = tokensResponse.data;
return tokens;
}

export async function handleCurrentUser(
req: Req,
res: Res,
Expand All @@ -92,16 +115,10 @@ export async function handleCurrentUser(
encounterId = (req.query.state as string).replace(/['"]/g, "");
}

const pledges = (apiResponse.included || []).filter(
item => item.type == "pledge" && item.attributes.declined_since == null
);

const userRewards = pledges.map((r: Pledge) =>
_.get(r, "relationships.reward.data.id", "none")
);
const entitledTierIds = getEntitledTierIds(apiResponse);

const userId = apiResponse.data.id;
const standing = getUserAccountLevel(userId, userRewards);
const standing = getUserAccountLevel(userId, entitledTierIds);
const emailAddress = _.get(apiResponse, "data.attributes.email", "");
console.log(
`User login: ${emailAddress}, API response: ${JSON.stringify(
Expand All @@ -124,13 +141,20 @@ export async function handleCurrentUser(
res.redirect(`/e/${encounterId}?login=patreon`);
}

export function updateSessionAccountFeatures(
session: Express.Session,
standing: AccountStatus
): void {
session.hasStorage = standing == "pledge" || standing == "epic";
session.hasEpicInitiative = standing == "epic";
session.isLoggedIn = true;
function getEntitledTierIds(apiResponse: Record<string, any>) {
const memberships = apiResponse.included?.filter(i => i.type === "member");
if (!memberships) {
return [];
}

const entitledTierIds = _.flatMap(
memberships,
m => m.relationships?.currently_entitled_tiers?.data
)
.filter(d => d?.type === "tier")
.map(d => d.id);

return entitledTierIds;
}

function getUserAccountLevel(
Expand Down Expand Up @@ -158,6 +182,15 @@ function getUserAccountLevel(
return standing;
}

export function updateSessionAccountFeatures(
session: Express.Session,
standing: AccountStatus
): void {
session.hasStorage = standing == "pledge" || standing == "epic";
session.hasEpicInitiative = standing == "epic";
session.isLoggedIn = true;
}

export function configureLogout(app: express.Application): void {
const logoutPath = "/logout";
app.get(logoutPath, (req: Req, res: Res) => {
Expand Down Expand Up @@ -270,21 +303,19 @@ async function handleWebhook(req: Req, res: Res) {
}

function verifySender(req: Req, res: Res, next) {
console.log(req.rawBody);

const webhookSecret = process.env.PATREON_WEBHOOK_SECRET;
if (!webhookSecret) {
return res.status(501).send("Webhook not configured");
}

const signature = req.header("X-Patreon-Signature");
if (!signature) {
console.log("Signature not found.");
console.warn("Signature not found.");
return res.status(401).send("Signature not found.");
}

if (!verifySignature(signature, webhookSecret, req.rawBody)) {
console.log("Signature mismatch with provided signature: " + signature);
console.warn("Signature mismatch with provided signature: " + signature);
return res.status(401).send("Signature mismatch.");
}

Expand Down
3 changes: 2 additions & 1 deletion server/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ const getClientOptions = (session: Express.Session) => {
"http://www.patreon.com/oauth2/authorize" +
`?response_type=code&client_id=${patreonClientId}` +
`&redirect_uri=${baseUrl}/r/patreon` +
`&scope=users pledges-to-me` +
`&scope=` +
encodeURIComponent(`identity identity.memberships identity[email]`) +
`&state=${encounterId}`;

const environment: ClientEnvironment = {
Expand Down

0 comments on commit dfea95e

Please sign in to comment.