mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-04-24 15:08:44 -05:00
* Kysely initial * Badges initial * Badge routes migrated * Badges migrated * Calendar work * Fix one type problem * Calendar work * findResultsByUserId work * Calendar reworking finished * PlusSuggestions work * Migrated suggestions * Builds progress * Migrated builds * Admin migrated * Migrate articles * User search * Faster getUser * Selectable/insertable as global * Refresh prod db script + patronTier index * identifierToUserId * updateProfile * findByIdentifier * More indexes * User upsert * upsertLite * findAllPlusMembers * updateResultHighlights * updateMany * User finished migration * Fix types * Fix PlusVotingResult typing * PlusVotingRepository WIP * Migrated resultsByMonthYear * Migrated plusVotes (done with db. related migrations) * Plus code to features folder * Fix TODOs * Export * Fix range * Migrate some user pages * Move rest user routes * Move /play * Map list generator * Front page * Move map list generation logic * Move plus voting logic * Info * API * Adjust TODOs * theme * Auth * Remove TODO
145 lines
3.9 KiB
TypeScript
145 lines
3.9 KiB
TypeScript
import type { z } from "zod";
|
|
import { dateToDatabaseTimestamp } from "~/utils/dates";
|
|
import { fetchWithTimeout } from "~/utils/fetch";
|
|
import type { Unpacked } from "~/utils/types";
|
|
import {
|
|
PATREON_INITIAL_URL,
|
|
TIER_1_ID,
|
|
TIER_2_ID,
|
|
TIER_3_ID,
|
|
TIER_4_ID,
|
|
} from "./constants";
|
|
import { patronResponseSchema } from "./schema";
|
|
import * as UserRepository from "~/features/user-page/UserRepository.server";
|
|
|
|
interface NoDiscordConnectionUser {
|
|
email: string;
|
|
name: string;
|
|
}
|
|
|
|
export async function updatePatreonData(): Promise<void> {
|
|
const patrons: UserRepository.UpdatePatronDataArgs = [];
|
|
const noDiscordConnected: Array<NoDiscordConnectionUser> = [];
|
|
const noDataIds: Array<string> = [];
|
|
let nextUrlToFetchWith = PATREON_INITIAL_URL;
|
|
|
|
while (nextUrlToFetchWith) {
|
|
const patronData = await fetchPatronData(nextUrlToFetchWith);
|
|
|
|
const parsed = parsePatronData(patronData);
|
|
patrons.push(...parsed.patrons);
|
|
noDiscordConnected.push(...parsed.noDiscordConnection);
|
|
noDataIds.push(...parsed.noDataIds);
|
|
|
|
// TS freaks out if we don't keep nextUrlToFetchWith string so that's why this weird thing here
|
|
nextUrlToFetchWith = patronData.links.next ?? "";
|
|
}
|
|
|
|
await UserRepository.updatePatronData(patrons);
|
|
|
|
// eslint-disable-next-line no-console
|
|
console.log(
|
|
`Added ${patrons.length} patrons. ${
|
|
noDiscordConnected.length
|
|
} patrons had no Discord connected. No full data for following Patreon ID's: ${noDataIds.join(
|
|
", ",
|
|
)}`,
|
|
);
|
|
}
|
|
|
|
async function fetchPatronData(urlToFetch: string) {
|
|
if (!process.env["PATREON_ACCESS_TOKEN"]) {
|
|
throw new Error("Missing Patreon access token");
|
|
}
|
|
|
|
const response = await fetchWithTimeout(
|
|
urlToFetch,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${process.env["PATREON_ACCESS_TOKEN"]}`,
|
|
},
|
|
},
|
|
30_000,
|
|
);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
`Patreon response not succesful. Status code was: ${response.status}`,
|
|
);
|
|
}
|
|
|
|
return patronResponseSchema.parse(await response.json());
|
|
}
|
|
|
|
function parsePatronData({
|
|
data,
|
|
included,
|
|
}: z.infer<typeof patronResponseSchema>) {
|
|
const patronsWithIds: Array<
|
|
{
|
|
patreonId: string;
|
|
} & Omit<Unpacked<UserRepository.UpdatePatronDataArgs>, "discordId">
|
|
> = [];
|
|
|
|
for (const patron of data) {
|
|
// from Patreon:
|
|
// "declined_since indicates the date of the most recent payment if it failed, or `null` if the most recent payment succeeded.
|
|
// A pledge with a non-null declined_since should be treated as invalid."
|
|
if (patron.attributes.declined_since) {
|
|
continue;
|
|
}
|
|
|
|
patronsWithIds.push({
|
|
patreonId: patron.relationships.patron.data.id,
|
|
patronSince: dateToDatabaseTimestamp(
|
|
new Date(patron.attributes.created_at),
|
|
),
|
|
patronTier: idToTier(patron.relationships.reward.data.id),
|
|
});
|
|
}
|
|
|
|
const result: {
|
|
patrons: UserRepository.UpdatePatronDataArgs;
|
|
noDiscordConnection: Array<NoDiscordConnectionUser>;
|
|
noDataIds: string[];
|
|
} = {
|
|
patrons: [],
|
|
noDiscordConnection: [],
|
|
noDataIds: [],
|
|
};
|
|
for (const extraData of included) {
|
|
if (extraData.type !== "user") continue;
|
|
|
|
const patronData = patronsWithIds.find((p) => p.patreonId === extraData.id);
|
|
if (!patronData) {
|
|
result.noDataIds.push(extraData.id);
|
|
continue;
|
|
}
|
|
|
|
const discordId = extraData.attributes.social_connections.discord?.user_id;
|
|
if (!discordId) {
|
|
result.noDiscordConnection.push({
|
|
email: extraData.attributes.email,
|
|
name: extraData.attributes.full_name,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
result.patrons.push({
|
|
patronSince: patronData.patronSince,
|
|
discordId,
|
|
patronTier: patronData.patronTier,
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function idToTier(id: string) {
|
|
const tier = [null, TIER_1_ID, TIER_2_ID, TIER_3_ID, TIER_4_ID].indexOf(id);
|
|
|
|
if (tier === -1) throw new Error(`Invalid tier id: ${id}`);
|
|
|
|
return tier;
|
|
}
|