Remove Patreon functionality (#129)

* Remove Patreon links, supper status checking
* Add warnings to admin panel and README
This commit is contained in:
Eliot Partridge
2019-09-17 17:55:21 -05:00
committed by GitHub
parent 4def56d96c
commit cb5e757fd3
24 changed files with 18 additions and 1127 deletions
-30
View File
@@ -32,13 +32,11 @@ import {
import { accountStatus, accountAround, getServer, getLoginServer, RemovedDocument, accountHidden } from './internal';
import { create } from './reporter';
import { system } from './logger';
import { updatePatreonData, updatePastSupporters } from './polling';
import { AdminService } from './services/adminService';
import { getOriginStats, clearOrigins, removeAllOrigins, removeOrigins, addOrigin, clearOriginsForAccounts } from './api/origins';
import { getDuplicateEntries, getAllDuplicatesQuickInfo, getAllDuplicatesWithInfo } from './api/duplicates';
import { splitAccounts } from './api/merge';
import { removeAuth, assignAuth } from './api/admin-auths';
import { getLastPatreonData } from './patreon';
import { removeFriend, addFriend } from './accountUtils';
@Socket({
@@ -542,34 +540,6 @@ export class AdminServerActions implements IAdminServerActions, SocketServer {
async getDuplicateEntries(force: boolean) {
return await getDuplicateEntries(this.adminService.accounts.items, force);
}
// patreon
@Method({ promise: true })
async updatePatreon() {
await updatePatreonData(this.server, this.settings);
}
@Method({ promise: true })
async resetSupporter(accountId: string) {
await Account.updateOne(
{ _id: accountId },
{ $unset: { supporter: 1, patreon: 1, supporterDeclinedSince: 1 } }).exec();
}
@Method({ promise: true })
async getLastPatreonData() {
const data = await getLastPatreonData();
// if (data) {
// data.pledges.forEach(pledge => {
// const auth = this.adminService.auths.items.find(a => a.openId === pledge.user);
// pledge.account = auth && auth.account;
// });
// }
return data;
}
@Method({ promise: true })
async updatePastSupporters() {
await updatePastSupporters();
}
// other
@Method({ promise: true })
async getTimings(serverId: string) {
-237
View File
@@ -1,237 +0,0 @@
import * as Bluebird from 'bluebird';
import { toPairs, groupBy, max } from 'lodash';
import { patreon, PatronData } from 'patreon';
import { IAuth, IAccount, UpdateAccounts, UpdateAccount, QueryAuths, QueryAccounts, UpdateAuth } from './db';
import { PatreonFlags, PatreonPledge, PatreonData, PatreonReward } from '../common/adminInterfaces';
import { Dict } from '../common/interfaces';
import { DAY, SECOND, MONTH, rewardLevel1, rewardLevel2, rewardLevel3 } from '../common/constants';
import { LogMessage, LogAccountMessage } from './serverInterfaces';
import { delay, fromNow } from '../common/utils';
import { logPatreon } from './logger';
export const declinedDayLimit = 14;
export const declinedTimeLimit = declinedDayLimit * DAY;
export const supporterLogLimit = 10;
export const SUPPORTER_REWARD_IDS: Dict<PatreonFlags> = {
[rewardLevel1]: PatreonFlags.Supporter1,
[rewardLevel2]: PatreonFlags.Supporter2,
[rewardLevel3]: PatreonFlags.Supporter3,
};
export type RemoveOldSupporters = ReturnType<typeof createRemoveOldSupporters>;
export type UpdateSupporters = ReturnType<typeof createUpdateSupporters>;
export type AddTotalPledged = ReturnType<typeof createAddTotalPledged>;
let lastPatreonData: PatreonData | undefined = undefined;
export function getLastPatreonData() {
return lastPatreonData;
}
/* istanbul ignore next */
export function createPatreonClient(accessToken: string): (path: string) => Promise<PatronData> {
const timeoutLimit = 10 * SECOND;
const client = patreon(accessToken);
client.setStore({ sync() { } });
return (path: string) => Promise.race([
delay(timeoutLimit).then(() => { throw new Error('Patreon request timed out'); }),
client(path),
]);
}
export async function fetchPatreonData(client: (path: string) => Promise<PatronData>, log: LogMessage): Promise<PatreonData> {
const campaignData = await client('/current_user/campaigns');
const rewards = campaignData.rawJson.included
.filter(x => x.type === 'reward')
.map<PatreonReward>(x => ({
id: x.id,
title: x.attributes.title || '',
description: x.attributes.description || '',
}));
const campaignId = campaignData.rawJson.data[0].id;
const pledges: PatreonPledge[] = [];
const queryParams = '&include=patron.null,reward.null&fields%5Bpledge%5D=total_historical_amount_cents,declined_since';
const query = 'page%5Bcount%5D=100&sort=created';
let url = `/campaigns/${campaignId}/pledges?${query}`;
let pages = 0;
do {
const pledgeData = await client(`${url}${queryParams}`);
const pledgeItems = pledgeData.rawJson.data
.filter(x => x.relationships.patron.data && x.relationships.reward.data)
.map<PatreonPledge>(x => ({
user: x.relationships.patron.data.id,
reward: x.relationships.reward.data.id,
total: x.attributes.total_historical_amount_cents || 0,
declinedSince: x.attributes.declined_since || undefined,
}));
pledges.push(...pledgeItems);
url = (pledgeData.rawJson.links.next || '').replace('https://www.patreon.com/api/oauth2/api', '');
pages++;
if (pages > 100) {
throw new Error('Exceeded 100 pages of patreon data');
}
} while (url);
log(`fetched patreon data (pages: ${pages}, pledges: ${pledges.length}, rewards: ${rewards.length})`);
return lastPatreonData = { pledges, rewards };
}
export const createUpdatePatreonInfo =
(
queryAuths: QueryAuths, queryAccounts: QueryAccounts, removeOldSupporters: RemoveOldSupporters,
updateSupporters: UpdateSupporters, updateTotalPledged: AddTotalPledged
) =>
async ({ pledges }: PatreonData, now: Date) => {
const ids = pledges.map(p => p.user);
const query = {
provider: 'patreon',
openId: { $in: ids },
account: { $exists: true },
banned: { $ne: true },
disabled: { $ne: true },
};
const patreonAuths = await queryAuths(query, '_id account openId pledged');
const accountsWithPatreon = await queryAccounts({ patreon: { $exists: true, $ne: 0 } }, '_id patreon supporterDeclinedSince');
// removes support from accounts without any non-banned patreon auth
await removeOldSupporters(patreonAuths, accountsWithPatreon);
await updateSupporters(patreonAuths, accountsWithPatreon, pledges, now);
await updateTotalPledged(patreonAuths, pledges);
};
export const createRemoveOldSupporters =
(updateAccounts: UpdateAccounts, log: LogAccountMessage) =>
async (auths: IAuth[], accounts: IAccount[]) => {
const clear = accounts
.filter(account => auths.every(auth => !auth.account || !account._id.equals(auth.account)))
.map(account => account._id);
clear.forEach(id => log(`${id}`, `removed supporter`));
await updateAccounts({ _id: { $in: clear } }, {
$unset: { patreon: 1, supporterDeclinedSince: 1 },
$push: {
supporterLog: {
$each: [{ date: new Date(), message: 'removed supporter' }],
$slice: -supporterLogLimit,
},
},
});
await updateAccounts(
{ supporterDeclinedSince: { $exists: true, $lt: fromNow(-2 * MONTH) } },
{ $unset: { supporterDeclinedSince: 1 } });
};
export const createUpdateSupporters =
(updateAccount: UpdateAccount, log: LogAccountMessage) =>
async (auths: IAuth[], accountsWithPatreon: IAccount[], pledges: PatreonPledge[], now: Date) => {
const start = Date.now();
const pledgesMap = new Map<string, PatreonPledge>();
const accountsWithPatreonMap = new Map<string, IAccount>();
for (const pledge of pledges) {
pledgesMap.set(pledge.user, pledge);
}
for (const account of accountsWithPatreon) {
accountsWithPatreonMap.set(account._id.toString(), account);
}
const setup = auths
.filter(auth => auth.account)
.map(auth => {
const accountId = auth.account!.toString();
const pledge = auth.openId && pledgesMap.get(auth.openId);
const pledgeFlags = pledge && SUPPORTER_REWARD_IDS[pledge.reward] || PatreonFlags.None;
const declinedSince = (pledge && pledge.declinedSince) ? new Date(pledge.declinedSince) : undefined;
const account = accountsWithPatreonMap.get(accountId!);
const declined = isDeclined(declinedSince, now);
const patreon = declined ? PatreonFlags.None : pledgeFlags;
const current = account && account.patreon || 0;
const declinedChanged = !!account && !datesEqual(account.supporterDeclinedSince, declinedSince);
const hadPatreon = !!account;
return {
account: accountId, patreon, declinedSince, declinedChanged, declined, current, hadPatreon
};
});
const grouped = toPairs(groupBy(setup, x => x.account))
.map(([account, items]) => {
const current = max(items.map(i => i.current))!;
const patreon = max(items.map(i => i.patreon))!;
return {
account,
changed: items.some(i => !i.hadPatreon) || current !== patreon,
declinedChanged: items.some(i => i.declinedChanged),
patreon,
declinedSince: items.map(i => i.declinedSince).find(x => !!x),
declined: items.some(i => i.declined),
hadPatreon: items.some(i => i.hadPatreon),
};
})
.filter(({ changed, declinedChanged }) => changed || declinedChanged);
grouped
.filter(g => g.changed)
.map(g => ({ account: g.account, message: supporterMessage(g.patreon, g.declined, g.hadPatreon) }))
.filter(({ message }) => !!message)
.forEach(({ account, message }) => log(`${account}`, message!));
logPatreon(`update supporters (${Date.now() - start}ms) ` +
`[auths: ${auths.length}, grouped: ${grouped.length}, pledges: ${pledges.length}, ` +
`accountsWithPatreon: ${accountsWithPatreon.length}]`);
await Bluebird.map(grouped, ({ account, patreon, declinedSince, changed, declined, hadPatreon }) => {
const message = changed ? supporterMessage(patreon, declined, hadPatreon) : undefined;
return updateAccount(account, {
supporterDeclinedSince: declinedSince,
...(changed ? { patreon } : {}),
...(message ? {
$push: {
supporterLog: {
$each: [{ date: new Date(), message }],
$slice: -supporterLogLimit,
},
}
} : {}),
});
}, { concurrency: 4 });
};
function isDeclined(declinedSince: Date | undefined, now: Date): boolean {
return !!declinedSince && (
now.getDate() > declinedDayLimit ||
(now.getTime() - declinedSince.getTime()) > declinedTimeLimit);
}
function supporterMessage(patreon: PatreonFlags, declined: boolean, hadPatreon: boolean) {
return patreon ?
`added supporter (${patreon})` :
(hadPatreon ? `removed supporter${declined ? ' (declined)' : ''}` : undefined);
}
function datesEqual(a: Date | undefined, b: Date | undefined) {
return (!a && !b) || (a && b && a.getTime() === b.getTime());
}
export const createAddTotalPledged =
(updateAuth: UpdateAuth) =>
async (auths: IAuth[], pledges: PatreonPledge[]) => {
const setup = auths
.map(auth => ({ auth, pledge: pledges.find(p => p.user === auth.openId) }))
.filter(({ auth, pledge }) => pledge && pledge.total !== auth.pledged);
await Bluebird.map(setup, ({ auth, pledge }) =>
updateAuth(auth._id, { pledged: pledge!.total }), { concurrency: 4 });
};
+3 -44
View File
@@ -1,55 +1,18 @@
import * as fs from 'fs';
import * as Bluebird from 'bluebird';
import {
InternalGameServerState, BannedMuted, Settings, ServerConfig, InternalLoginServerState, SupporterFlags
InternalGameServerState, BannedMuted, InternalLoginServerState, SupporterFlags
} from '../common/adminInterfaces';
import { fromNow, delay } from '../common/utils';
import { logger, logPatreon, system, logPerformance } from './logger';
import { Auth, updateAccounts, updateAccount, queryAuths, queryAccounts, updateAuth, Account, SupporterInvite } from './db';
import { logPerformance } from './logger';
import { Auth, updateAccounts, queryAuths, Account, SupporterInvite } from './db';
import { getDiskSpace, getCertificateExpirationDate, getMemoryUsage } from './serverUtils';
import { HOUR, MINUTE, DAY, SECOND, YEAR } from '../common/constants';
import {
fetchPatreonData, createPatreonClient, createUpdatePatreonInfo, createRemoveOldSupporters,
createUpdateSupporters, createAddTotalPledged
} from './patreon';
import { create } from './reporter';
import { servers, serverStatus, loginServers, RemovedDocument } from './internal';
import * as paths from './paths';
import { updateSupporterInvites } from './services/supporterInvites';
import { AdminService } from './services/adminService';
import { clearOrigins } from './api/origins';
import { config } from './config';
let updatingPatreonPromise: Promise<void> | undefined;
async function updatePatreonDataInternal(server: ServerConfig, accessToken: string) {
try {
const removeOldSupporters = createRemoveOldSupporters(updateAccounts, system);
const updateSupporters = createUpdateSupporters(updateAccount, system);
const addTotalPledged = createAddTotalPledged(updateAuth);
const updatePatreonInfo = createUpdatePatreonInfo(
queryAuths, queryAccounts, removeOldSupporters, updateSupporters, addTotalPledged);
const client = createPatreonClient(accessToken);
const data = await fetchPatreonData(client, logPatreon);
await updatePatreonInfo(data, new Date());
serverStatus.lastPatreonUpdate = (new Date()).toISOString();
} catch (e) {
const message = e.error ? (e.error.message || e.error.statusText || `${e}`) : e.message;
const stack = (e.error ? e.error.stack : e.stack) || '';
create(server).danger('Patreon update failed', `${message}\n${stack}`.trim());
logger.error(e);
} finally {
updatingPatreonPromise = undefined;
}
}
export async function updatePatreonData(server: ServerConfig, { patreonToken }: Settings) {
if (patreonToken && config.supporterLink) {
return updatingPatreonPromise = updatingPatreonPromise || updatePatreonDataInternal(server, patreonToken);
}
}
async function clearOldIgnores() {
const start = Date.now();
@@ -200,10 +163,6 @@ export function pollServers() {
return poll(() => Promise.all([...loginServers, ...servers].map(updateServerState)), 1 * SECOND);
}
export function pollPatreon(server: ServerConfig, settings: Settings) {
return poll(() => updatePatreonData(server, settings), 10 * MINUTE);
}
export const pollDiskSpace = () => pollImmediate(() =>
getDiskSpace().then(value => serverStatus.diskSpace = value), HOUR);
export const pollMemoryUsage = () => pollImmediate(() =>
-1
View File
@@ -99,7 +99,6 @@ export function createIndex(assetsPath: string, adminAssetsPath: string) {
host: config.host,
title: config.title,
twitterLink: config.twitterLink,
supporterLink: config.supporterLink,
discordLink: config.discordLink,
email: config.contactEmail,
logo: `${config.host}${getRevImageURL('logo-120.png')}`,
+2 -4
View File
@@ -39,9 +39,9 @@ import { start } from './start';
import { createServerActionsFactory } from './serverActionsManager';
import { init, createRemovedDocument } from './internal';
import {
pollServers, pollDiskSpace, pollCertificateExpirationDate, pollPatreon, startBansCleanup, pollMemoryUsage,
pollServers, pollDiskSpace, pollCertificateExpirationDate, startBansCleanup, pollMemoryUsage,
startMergesCleanup, startStrayAuthsCleanup, startClearOldIgnores, startCollectingUsersVisitedCount,
startSupporterInvitesCleanup, startPotentialDuplicatesCleanup, startAccountAlertsCleanup, startUpdatePastSupporters,
startSupporterInvitesCleanup, startPotentialDuplicatesCleanup, startAccountAlertsCleanup,
startClearTo10Origns, startClearVeryOldOrigns
} from './polling';
import { pathTo } from './paths';
@@ -393,10 +393,8 @@ reloadSettings().then(() => {
startSupporterInvitesCleanup();
startPotentialDuplicatesCleanup(adminService!);
startAccountAlertsCleanup();
startUpdatePastSupporters();
startClearTo10Origns(adminService!);
startClearVeryOldOrigns(adminService!);
pollPatreon(server, settings);
}
if (args.admin) {