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
+5
View File
@@ -2,6 +2,11 @@
A fork of Pony Town, a game of ponies building a town.
## Patreon Warning
Code relating to Patreon syncing has been removed from the public pixel.horse codebase.
You **MUST** contact the Pony Town Team before opening a Patreon based off the Pony Town Custom Server source code. Not doing so is a violation of the [CC-BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/) license on to the art and music assets supplied with the game. Please submit a request for approval to **[email protected]**.
## Prerequisites
* [Node.js](https://nodejs.org/download/release/v9.11.2/) (version 9)
-1
View File
@@ -24,7 +24,6 @@ export const local = attr('data-local') === 'true';
export const token = attr('data-token');
export const version = attr('data-version');
export const isPublic = attr('data-public') === 'true';
export const supporterLink = attr('data-supporter-link');
export const discordLink = attr('data-discord-link');
export const twitterLink = attr('data-twitter-link');
export const contactEmail = attr('data-email');
-4
View File
@@ -845,10 +845,6 @@ export interface IAdminServerActions {
getChat(search: string, date: string, caseInsensitive: boolean): Promise<string>;
getChatForAccounts(accountIds: string[], date: string): Promise<string>;
getRequestStats(): Promise<LoginStats>;
updatePatreon(): Promise<void>;
resetSupporter(accountId: string): Promise<void>;
getLastPatreonData(): Promise<PatreonData | undefined>;
updatePastSupporters(): Promise<void>;
// live
get(endPoint: 'events', id: string): Promise<any>;
getAll(endPoint: 'events', timestamp?: string): Promise<LiveResponse>;
@@ -13,30 +13,10 @@
button.btn.btn-default.ml-1(type="button" (click)="resetFields()") Reset
.col-6
.form
.form-group
h3 Patreon
.form-group.d-flex
button.btn.btn-default((click)="updatePatreon()")
| Update Data
.input-group.ml-1(style="width: 400px")
input.form-control(#patreonToken placeholder="patreon token")
.input-group-append
button.btn.btn-warning((click)="updatePatreonToken(patreonToken.value); patreonToken.value = ''")
| Update Token
.form-group
| To get new token, got to #[a(href="https://www.patreon.com/portal/registration/register-clients") My Clients],
| refresh token and update patreon token field with "Creator's Access Token" from the site.
.form-group
button.btn.btn-default((click)="getLastPatreonData()")
| Get last patreon data
.form
.form-group
h3 Other
.form-group
button.btn.btn-default((click)="updatePastSupporters()")
| Update past supporters
h2.text-danger PATREON WARNING:
p.lead Code relating to Patreon syncing has been removed from the public pixel.horse codebase.
p.lead
| You <b>MUST</b> contact the Pony Town Team before opening a Patreon based off the Pony Town Custom Server source code. Not doing so is a violation of the <a href="https://creativecommons.org/licenses/by-nc/4.0/">CC-BY-NC 4.0</a> license applied to the art and music assets supplied with the game. Please submit a request for approval to <a href="mailto:[email protected]">[email protected]</a>.
.row
.col-12
@@ -3,7 +3,6 @@ import { compact } from 'lodash';
import { AdminModel } from '../../services/adminModel';
import { Account, GeneralSettings } from '../../../common/adminInterfaces';
import { Subscription } from '../../../common/interfaces';
import { showTextInNewTab } from '../../../client/htmlUtils';
interface Field {
key: keyof GeneralSettings;
@@ -72,19 +71,4 @@ export class AdminOther implements OnInit, OnDestroy {
resetSuspiciousPonies() {
this.suspiciousPonies = this.model.state.loginServers[0].suspiciousPonies;
}
updatePatreon() {
this.model.server.updatePatreon();
}
updatePatreonToken(patreonToken: string) {
this.model.updateSettings({ patreonToken });
}
getLastPatreonData() {
this.model.getLastPatreonData()
.then(data => {
showTextInNewTab(JSON.stringify(data, null, 2));
});
}
updatePastSupporters() {
this.model.updatePastSupporters();
}
}
-17
View File
@@ -34,23 +34,6 @@ h1(focusTitle) About
hr
div(*ngIf="patreonLink")
h2 Support {{title}}
p.
If you'd like to help keep {{title}} running, and support future development of the project,
please consider #[a([href]="patreonLink" target="_blank" rel="noopener noreferrer") supporting us on Patreon].
support-button.d-block.mx-auto.my-4(style="max-width: 400px;")
p.
Patreon supporter rewards include the following:
ul
li(*ngFor="let r of rewards")
| {{r}}
p.
Note that in order to receive any patreon rewards you need to connect your Patreon to your {{title}} account.
h2 Technology
p.
+1 -2
View File
@@ -4,7 +4,7 @@ import { getUrl } from '../../../client/rev';
import { CREDITS, CONTRIBUTORS, Credit } from '../../../client/credits';
import { CHANGELOG } from '../../../generated/changelog';
import { SUPPORTER_REWARDS_LIST } from '../../../common/constants';
import { supporterLink, discordLink, contactEmail } from '../../../client/data';
import { discordLink, contactEmail } from '../../../client/data';
function toCredit(credit: Credit) {
return {
@@ -26,7 +26,6 @@ export class About {
readonly contributors = CONTRIBUTORS;
readonly changelog = CHANGELOG;
readonly rewards = SUPPORTER_REWARDS_LIST;
readonly patreonLink = supporterLink;
readonly discordLink = discordLink;
readonly contactEmail = contactEmail;
}
-2
View File
@@ -42,8 +42,6 @@
.text-nowrap
a.text-muted.mr-2(*ngIf="twitterLink" [href]="twitterLink" target="_blank" title="Twitter")
fa-icon([icon]="twitterIcon" [fixedWidth]="true")
a.text-muted.mr-2(*ngIf="patreonLink" [href]="patreonLink" target="_blank" title="Patreon")
fa-icon([icon]="patreonIcon" [fixedWidth]="true")
a.text-muted.mr-2(*ngIf="contactEmail" href="mailto:{{contactEmail}}" target="_blank" title="Email")
fa-icon([icon]="emailIcon" [fixedWidth]="true")
div
+1 -2
View File
@@ -6,7 +6,7 @@ import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
import { Subscription } from 'rxjs';
import { GameService } from '../services/gameService';
import { Model, Friend } from '../services/model';
import { version, host, contactEmail, supporterLink, twitterLink, discordLink, copyrightName } from '../../client/data';
import { version, host, contactEmail, twitterLink, discordLink, copyrightName } from '../../client/data';
import { PonyTownGame } from '../../client/game';
import { faTwitter, faPatreon, faEnvelope, faCog, faHome, faGamepad, faInfoCircle, faHorseHead, faDiscord } from '../../client/icons';
import { InstallService } from '../services/installService';
@@ -54,7 +54,6 @@ export class App implements OnInit, OnDestroy {
readonly aboutIcon = faInfoCircle;
readonly charactersIcon = faHorseHead;
readonly contactEmail = contactEmail;
readonly patreonLink = supporterLink;
readonly discordLink = discordLink;
readonly twitterLink = twitterLink;
readonly copyright = copyrightName;
@@ -13,7 +13,6 @@ page-loader(*ngIf="!playing")
h1.sr-only(focusTitle) Characters
h2.sr-only Character selection
.mx-auto(style="max-width: 400px;")
support-button.mb-3
discord-button.mb-3
install-button.mb-3
character-select(
-2
View File
@@ -18,8 +18,6 @@ div(*ngIf="!playing")
sign-in-box((signIn)="signIn($event)")
div(*ngIf="!!account")
.form-group
support-button
.form-group
discord-button
.form-group(*ngIf="canInstall")
-12
View File
@@ -502,18 +502,6 @@ export class AdminModel {
.then(() => this.updateState())
.catch(this.handleError);
}
resetSupporter(accountId: string) {
return this.server.resetSupporter(accountId)
.catch(this.handleError);
}
getLastPatreonData() {
return this.server.getLastPatreonData()
.catch(this.handleError);
}
updatePastSupporters() {
return this.server.updatePastSupporters()
.catch(this.handleError);
}
// other
getTimings(server: string) {
return this.server.getTimings(server)
@@ -1,5 +1,5 @@
import { Component } from '@angular/core';
import { supporterLink, discordLink } from '../../../client/data';
import { discordLink } from '../../../client/data';
import { GENERAL_RULES } from '../../../common/constants';
@Component({
@@ -7,7 +7,6 @@ import { GENERAL_RULES } from '../../../common/constants';
templateUrl: 'play-notice.pug',
})
export class PlayNotice {
readonly patreonLink = supporterLink;
readonly discordLink = discordLink;
readonly rules = GENERAL_RULES;
}
@@ -23,7 +23,6 @@ import { EmoteBox } from './emote-box/emote-box';
import { SliderBar } from './slider-bar/slider-bar';
import { SpriteBox } from './sprite-box/sprite-box';
import { SpriteSelection } from './sprite-selection/sprite-selection';
import { SupportButton } from './support-button/support-button';
import { SupporterPony } from './supporter-pony/supporter-pony';
import { DiscordButton } from './discord-button/discord-button';
import { DiscordPony } from './discord-pony/discord-pony';
@@ -91,7 +90,6 @@ const declarations = [
SliderBar,
SpriteBox,
SpriteSelection,
SupportButton,
SupporterPony,
DiscordButton,
DiscordPony,
@@ -1,10 +0,0 @@
a.btn.btn-lg.btn-outline-patreon.btn-block.mb-2(
*ngIf="patreonLink" (mouseenter)="pony.excite()" (mouseout)="pony.reset()" [href]="patreonLink" target="_blank"
rel="noopener noreferrer")
| {{supporter ? 'Thank you for supporting us!' : 'Support us on Patreon!'}}
svg.patreon-logo(xmlns="http://www.w3.org/2000/svg" viewBox="0 0 150 150")
circle(fill="#f9685f" cx="75" cy="75" r="75")
path(fill="#052d49" d="M31.729 27.659h17.578v95.888H31.729z")
circle(fill="#fff" cy="63.565" cx="95.783" r="35.955")
.supporter-pony
supporter-pony(#pony [scale]="2")
@@ -1,45 +0,0 @@
@import '../../../../styles/partials/variables';
:host {
display: block;
}
.btn {
position: relative;
@media (max-width: 320px) {
white-space: normal;
}
}
.patreon-logo {
position: absolute;
left: 8px;
top: 5px;
width: 30px;
height: 30px;
@media (max-width: 320px) {
display: none;
}
}
.supporter-pony {
position: absolute;
right: 0;
bottom: 0;
height: 70px;
width: 100px;
overflow: hidden;
border-radius: $border-radius-lg - 2px;
> supporter-pony {
position: absolute;
bottom: -95px;
left: -20px;
}
@media (max-width: 380px) {
display: none;
}
}
@@ -1,17 +0,0 @@
import { Component } from '@angular/core';
import { Model } from '../../services/model';
import { supporterLink } from '../../../client/data';
@Component({
selector: 'support-button',
templateUrl: 'support-button.pug',
styleUrls: ['support-button.scss'],
})
export class SupportButton {
readonly patreonLink = supporterLink;
constructor(private model: Model) {
}
get supporter() {
return this.model.supporter;
}
}
-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) {
-650
View File
@@ -1,650 +0,0 @@
import '../lib';
import { noop } from 'lodash';
import { Types } from 'mongoose';
import { expect } from 'chai';
import { stub, assert, SinonStub, SinonFakeTimers, useFakeTimers, match } from 'sinon';
import {
createRemoveOldSupporters, createUpdateSupporters, fetchPatreonData, createUpdatePatreonInfo,
RemoveOldSupporters, UpdateSupporters, createAddTotalPledged, AddTotalPledged
} from '../../server/patreon';
import { genId, auth, account } from '../mocks';
import { PatreonFlags, PatreonData } from '../../common/adminInterfaces';
import { fromNow, fromDate } from '../../common/utils';
import { DAY, rewardLevel1, rewardLevel2 } from '../../common/constants';
const dbId = () => Types.ObjectId(genId());
const queryBase = 'page%5Bcount%5D=100&sort=created';
const queryAdd = '&include=patron.null,reward.null&fields%5Bpledge%5D=total_historical_amount_cents,declined_since';
const query = `${queryBase}${queryAdd}`;
describe('patreon', () => {
describe('fetchPatreonData()', () => {
let client: SinonStub;
beforeEach(() => {
client = stub();
client.withArgs('/current_user/campaigns')
.resolves({ rawJson: { data: [{ id: 'foo' }], included: [] } });
client.withArgs(`/campaigns/foo/pledges?${query}`)
.resolves({ rawJson: { data: [], links: {} } });
});
it('returns rewards', async () => {
client.withArgs('/current_user/campaigns')
.resolves({
rawJson: {
data: [{ id: 'foo', attributes: {} }],
included: [
{ type: 'reward', id: '123', attributes: { title: 'title', description: 'desc' } },
{ type: 'other', id: '111', attributes: { title: 'ttt', description: 'ddd' } },
],
},
});
const result = await fetchPatreonData(client as any, noop);
expect(result.rewards).eql([
{ id: '123', title: 'title', description: 'desc' },
]);
});
it('fixes incomplete data', async () => {
client.withArgs('/current_user/campaigns')
.resolves({
rawJson: {
data: [{ id: 'foo' }],
included: [
{ type: 'reward', id: '123', attributes: {} },
],
},
});
const result = await fetchPatreonData(client as any, noop);
expect(result.rewards).eql([
{ id: '123', title: '', description: '' },
]);
});
it('returns pledges', async () => {
client.withArgs(`/campaigns/foo/pledges?${query}`)
.resolves({
rawJson: {
data: [
{
attributes: { total_historical_amount_cents: 100 },
relationships: { patron: { data: { id: 'patid1' } }, reward: { data: { id: 'rewid1' } } },
},
{
attributes: { total_historical_amount_cents: 200 },
relationships: { patron: { data: { id: 'patid2' } }, reward: { data: { id: 'rewid2' } } },
},
],
links: {},
},
});
const result = await fetchPatreonData(client as any, noop);
expect(result.pledges).eql([
{ user: 'patid1', reward: 'rewid1', total: 100, declinedSince: undefined },
{ user: 'patid2', reward: 'rewid2', total: 200, declinedSince: undefined },
]);
});
it('skips pledges with missing data', async () => {
client.withArgs(`/campaigns/foo/pledges?${query}`)
.resolves({
rawJson: {
data: [
{
attributes: {},
relationships: { patron: { data: { id: 'patid1' } }, reward: { data: { id: 'rewid1' } } },
},
{
attributes: {},
relationships: { patron: {}, reward: { data: { id: 'rewid2' } } },
},
{
attributes: {},
relationships: { patron: { data: { id: 'patid3' } }, reward: {} },
},
],
links: {},
},
});
const result = await fetchPatreonData(client as any, noop);
expect(result.pledges).eql([
{ user: 'patid1', reward: 'rewid1', total: 0, declinedSince: undefined },
]);
});
it('fetches multiple pages of pledges (with correct parameters for next page)', async () => {
client.withArgs(`/campaigns/foo/pledges?${query}`)
.resolves({
rawJson: {
data: [
{
attributes: {},
relationships: { patron: { data: { id: 'patid1' } }, reward: { data: { id: 'rewid1' } } },
},
],
links: {
next: 'https://www.patreon.com/api/oauth2/api/link-to-next-page?page=5',
},
},
});
client.withArgs('/link-to-next-page?page=5' + queryAdd)
.resolves({
rawJson: {
data: [
{
attributes: {},
relationships: { patron: { data: { id: 'patid2' } }, reward: { data: { id: 'rewid2' } } },
},
],
links: {},
},
});
const result = await fetchPatreonData(client as any, noop);
assert.calledWith(client, '/link-to-next-page?page=5' + queryAdd);
expect(result.pledges).eql([
{ user: 'patid1', reward: 'rewid1', total: 0, declinedSince: undefined },
{ user: 'patid2', reward: 'rewid2', total: 0, declinedSince: undefined },
]);
});
it('throws on too many pages', async () => {
client.withArgs(`/campaigns/foo/pledges?${query}`)
.resolves({
rawJson: {
data: [
{
attributes: {},
relationships: { patron: { data: { id: 'patid1' } }, reward: { data: { id: 'rewid1' } } },
},
],
links: {
next: `/campaigns/foo/pledges?${queryBase}`,
},
},
});
await expect(fetchPatreonData(client as any, noop)).rejectedWith('Exceeded 100 pages of patreon data');
});
});
describe('updatePatreonInfo()', () => {
let queryAuths: SinonStub;
let queryAccounts: SinonStub;
let removeOldSupporters: SinonStub;
let updateSupporters: SinonStub;
let addTotalPledged: SinonStub;
let updatePatreonInfo: (data: PatreonData, now: Date) => any;
beforeEach(() => {
queryAuths = stub();
queryAccounts = stub();
removeOldSupporters = stub();
updateSupporters = stub();
addTotalPledged = stub();
updatePatreonInfo = createUpdatePatreonInfo(
queryAuths, queryAccounts, removeOldSupporters, updateSupporters, addTotalPledged);
});
it('queries auths', async () => {
queryAuths.resolves([]);
queryAccounts.resolves([]);
await updatePatreonInfo({
pledges: [
{ reward: '123', user: 'foo', total: 0 },
{ reward: '123', user: 'bar', total: 0 },
],
rewards: [],
}, new Date());
assert.calledWith(queryAuths, match({
provider: 'patreon',
openId: { $in: ['foo', 'bar'] },
account: { $exists: true },
banned: { $ne: true },
disabled: { $ne: true },
}), '_id account openId pledged');
});
it('queries accounts', async () => {
queryAuths.resolves([]);
queryAccounts.resolves([]);
await updatePatreonInfo({ pledges: [], rewards: [] }, new Date());
assert.calledWith(
queryAccounts, match({ patreon: { $exists: true, $ne: 0 } }), '_id patreon supporterDeclinedSince');
});
it('removes old supporters', async () => {
const auths = [{}] as any;
const accounts = [{}] as any;
queryAuths.resolves(auths);
queryAccounts.resolves(auths);
await updatePatreonInfo({ pledges: [], rewards: [] }, new Date());
assert.calledWith(removeOldSupporters, auths, accounts);
});
it('adds new supporters', async () => {
const auths = [{}] as any;
const accounts = [{}] as any;
const pledges = [{}] as any;
const now = new Date();
queryAuths.resolves(auths);
queryAccounts.resolves(auths);
await updatePatreonInfo({ pledges, rewards: [] }, now);
assert.calledWith(updateSupporters, auths, accounts, pledges, now);
});
it('adds total pledged', async () => {
const auths = [{}] as any;
const pledges = [{}] as any;
queryAuths.resolves(auths);
queryAccounts.resolves(auths);
await updatePatreonInfo({ pledges, rewards: [] }, new Date());
assert.calledWith(addTotalPledged, auths, pledges);
});
});
describe('removeOldSupporters()', () => {
let updateAccounts: SinonStub;
let log: SinonStub;
let removeOldSupporters: RemoveOldSupporters;
let clock: SinonFakeTimers;
const update = {
$unset: { patreon: 1, supporterDeclinedSince: 1 },
$push: {
supporterLog: {
$each: [{ date: new Date(1234), message: 'removed supporter' }],
$slice: -10,
},
},
};
beforeEach(() => {
clock = useFakeTimers();
clock.setSystemTime(1234);
updateAccounts = stub();
log = stub();
removeOldSupporters = createRemoveOldSupporters(updateAccounts, log);
});
afterEach(() => {
clock.restore();
});
it('does nothing if list of auth and accounts are empty', async () => {
await removeOldSupporters([], []);
assert.calledWithMatch(updateAccounts, { _id: { $in: [] } }, update);
});
it('unsets patreon for all accounts without corresponding auths', async () => {
const accountId = dbId();
await removeOldSupporters([], [account({ _id: accountId })]);
assert.calledWithMatch(updateAccounts, { _id: { $in: [accountId] } }, update);
assert.calledWith(log, accountId.toHexString(), `removed supporter`);
});
it('unsets patreon for all accounts without corresponding auths (2)', async () => {
const account1Id = dbId();
const account2Id = dbId();
await removeOldSupporters(
[auth({ account: Types.ObjectId(account2Id.toHexString()) })],
[account({ _id: account1Id }), account({ _id: account2Id })]);
assert.calledWithMatch(updateAccounts, { _id: { $in: [account1Id] } }, update);
assert.calledWith(log, account1Id.toHexString(), `removed supporter`);
});
it('works with unassigned auths', async () => {
const accountId = dbId();
await removeOldSupporters(
[auth({ account: undefined })],
[account({ _id: accountId })]);
assert.calledWithMatch(updateAccounts, { _id: { $in: [accountId] } }, update);
});
});
describe('updateSupporters()', () => {
let updateAccount: SinonStub;
let log: SinonStub;
let updateSupporters: UpdateSupporters;
let clock: SinonFakeTimers;
function push(message: string) {
return {
supporterLog: {
$each: [{ date: new Date(1234), message }],
$slice: -10,
},
};
}
beforeEach(() => {
clock = useFakeTimers();
clock.setSystemTime(1234);
updateAccount = stub();
log = stub();
updateSupporters = createUpdateSupporters(updateAccount, log);
});
afterEach(() => {
clock.restore();
});
it('does nothing if list of auth and accounts are empty', async () => {
await updateSupporters([], [], [], new Date());
assert.notCalled(updateAccount);
});
it('adds patreon info to account', async () => {
const accountId = dbId();
await updateSupporters(
[auth({ account: accountId, openId: '123' })],
[],
[{ reward: rewardLevel1, user: '123', total: 0 }],
new Date());
assert.calledWith(updateAccount, accountId.toString(), {
patreon: PatreonFlags.Supporter1,
supporterDeclinedSince: undefined,
$push: push('added supporter (1)'),
});
assert.calledWith(log, accountId.toString(), 'added supporter (1)');
});
it('handles duplicate auths', async () => {
const accountId = dbId();
await updateSupporters(
[auth({ account: accountId, openId: '123' }), auth({ account: accountId, openId: '321' })],
[],
[{ reward: rewardLevel2, user: '321', total: 0 }, { reward: rewardLevel1, user: '123', total: 0 }],
new Date());
assert.calledWith(updateAccount, accountId.toString(), {
patreon: PatreonFlags.Supporter2,
supporterDeclinedSince: undefined,
$push: push('added supporter (2)'),
});
assert.calledWith(log, accountId.toString(), 'added supporter (2)');
});
it('handles unassigned auths', async () => {
await updateSupporters(
[auth({ account: undefined, openId: '123' })],
[],
[{ reward: rewardLevel2, user: '123', total: 0 }],
new Date());
assert.notCalled(updateAccount);
});
it('sets supporter to none if pledge is missing', async () => {
const accountId = dbId();
await updateSupporters(
[auth({ account: accountId, openId: '123' })],
[account({ _id: accountId, patreon: PatreonFlags.Supporter1 })],
[],
new Date());
assert.calledWith(updateAccount, accountId.toString(), {
patreon: PatreonFlags.None,
supporterDeclinedSince: undefined,
$push: push('removed supporter'),
});
assert.calledWith(log, accountId.toString(), 'removed supporter');
});
it('updates existing info', async () => {
const accountId = dbId();
await updateSupporters(
[auth({ account: accountId, openId: '123' })],
[account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })],
[{ reward: rewardLevel2, user: '123', total: 0 }],
new Date());
assert.calledWith(updateAccount, accountId.toString(), {
patreon: PatreonFlags.Supporter2,
supporterDeclinedSince: undefined,
$push: push('added supporter (2)'),
});
assert.calledWith(log, accountId.toString(), 'added supporter (2)');
});
it('does not update if supporter level did not change (2 patreon accounts)', async () => {
const accountId = dbId();
await updateSupporters(
[auth({ account: accountId, openId: '123' }), auth({ account: accountId, openId: '321' })],
[account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter2 })],
[{ reward: rewardLevel2, user: '123', total: 0 }],
new Date());
assert.notCalled(updateAccount);
assert.notCalled(log);
});
it('does not remove support if decline is set but day of month is < 7', async () => {
const accountId = dbId();
const now = new Date('2018-04-02T09:00:00.000Z');
const date = fromDate(now, -1 * DAY);
await updateSupporters(
[auth({ account: accountId, openId: '123' })],
[account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })],
[{ reward: rewardLevel1, user: '123', total: 0, declinedSince: date.toISOString() }],
now);
assert.calledWith(updateAccount, accountId.toString(), {
supporterDeclinedSince: date,
});
assert.notCalled(log);
});
it('removes support if decline is set and day of month is > 14', async () => {
const accountId = dbId();
const now = new Date('2018-04-16T09:00:00.000Z');
const date = fromDate(now, -1 * DAY);
await updateSupporters(
[auth({ account: accountId, openId: '123' })],
[account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })],
[{ reward: rewardLevel1, user: '123', total: 0, declinedSince: date.toISOString() }],
now);
assert.calledWith(updateAccount, accountId.toString(), {
patreon: PatreonFlags.None,
supporterDeclinedSince: date,
$push: push('removed supporter (declined)'),
});
assert.calledWith(log, accountId.toString(), 'removed supporter (declined)');
});
it('removes support if decline is set and day of month is < 14 and decline is > 14 days old', async () => {
const accountId = dbId();
const now = new Date('2018-04-02T09:00:00.000Z');
const date = fromDate(now, -16 * DAY);
await updateSupporters(
[auth({ account: accountId, openId: '123' })],
[account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })],
[{ reward: rewardLevel1, user: '123', total: 0, declinedSince: date.toISOString() }],
now);
assert.calledWith(updateAccount, accountId.toString(), {
patreon: PatreonFlags.None,
supporterDeclinedSince: date,
$push: push('removed supporter (declined)'),
});
assert.calledWith(log, accountId.toString(), 'removed supporter (declined)');
});
it('does not add log if declined but supporter is already removed', async () => {
const accountId = dbId();
const now = new Date('2018-04-16T09:00:00.000Z');
const date = fromDate(now, -1 * DAY);
await updateSupporters(
[auth({ account: accountId, openId: '123' })],
[],
[{ reward: rewardLevel1, user: '123', total: 0, declinedSince: date.toISOString() }],
now);
assert.calledWith(updateAccount, accountId.toString(), {
patreon: PatreonFlags.None,
supporterDeclinedSince: date,
});
assert.notCalled(log);
});
it('updates declined date', async () => {
const accountId = dbId();
const date = fromNow(-100);
await updateSupporters(
[auth({ account: accountId, openId: '123' })],
[account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })],
[{ reward: rewardLevel2, user: '123', total: 0, declinedSince: date.toISOString() }],
new Date());
assert.calledWith(updateAccount, accountId.toString(), {
patreon: PatreonFlags.Supporter2,
supporterDeclinedSince: date,
$push: push('added supporter (2)'),
});
assert.calledWith(log, accountId.toString(), 'added supporter (2)');
});
it('updates declined date even if patreon is not changed', async () => {
const accountId = dbId();
const date = fromNow(-100);
await updateSupporters(
[auth({ account: accountId, openId: '123' })],
[account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter2 })],
[{ reward: rewardLevel2, user: '123', total: 0, declinedSince: date.toISOString() }],
new Date());
assert.calledWith(updateAccount, accountId.toString(), {
supporterDeclinedSince: date,
});
assert.notCalled(log);
});
it('sets supporter to none if reward has invalid ID', async () => {
const accountId = dbId();
await updateSupporters(
[auth({ account: accountId, openId: '123' })],
[account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })],
[{ reward: 'invalid', user: '123', total: 0 }],
new Date());
assert.calledWith(updateAccount, accountId.toString(), {
patreon: PatreonFlags.None,
supporterDeclinedSince: undefined,
$push: push('removed supporter'),
});
assert.calledWith(log, accountId.toString(), 'removed supporter');
});
it('does nothing if patreon info is already set', async () => {
const accountId = dbId();
await updateSupporters(
[auth({ account: accountId, openId: '123' })],
[account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })],
[{ reward: rewardLevel1, user: '123', total: 0 }],
new Date());
assert.notCalled(updateAccount);
});
it('does nothing if declined date is the same', async () => {
const accountId = dbId();
await updateSupporters(
[auth({ account: accountId, openId: '123' })],
[account({
_id: Types.ObjectId(accountId.toHexString()),
patreon: PatreonFlags.Supporter1,
supporterDeclinedSince: new Date(1234),
})],
[{ reward: rewardLevel1, user: '123', total: 0, declinedSince: (new Date(1234).toISOString()) }],
new Date());
assert.notCalled(updateAccount);
});
});
describe('addTotalPledged()', () => {
let updateAuth: SinonStub;
let addTotalPledged: AddTotalPledged;
beforeEach(() => {
updateAuth = stub();
addTotalPledged = createAddTotalPledged(updateAuth);
});
it('does nothing if lists of auth and pledges are empty', async () => {
await addTotalPledged([], []);
assert.notCalled(updateAuth);
});
it('does nothing if cannot find auths for pledges', async () => {
await addTotalPledged([], [{ reward: 'some', user: '123', total: 0 }]);
assert.notCalled(updateAuth);
});
it('does nothing if cannot find pledges for auth', async () => {
await addTotalPledged([auth({ openId: '123' })], []);
assert.notCalled(updateAuth);
});
it('does nothing if total is already correct', async () => {
await addTotalPledged([auth({ openId: '123', pledged: 10 })], [{ reward: 'some', user: '123', total: 10 }]);
assert.notCalled(updateAuth);
});
it('updates total if different', async () => {
const authId = dbId();
await addTotalPledged([auth({ _id: authId, openId: '123' })], [{ reward: 'some', user: '123', total: 10 }]);
assert.calledWith(updateAuth, authId, { pledged: 10 });
});
});
});
-1
View File
@@ -49,7 +49,6 @@ html(lang="en")
body(
data-key="kmncxb908z"
data-version=version
data-supporter-link=supporterLink
data-discord-link=discordLink
data-twitter-link=twitterLink
data-email=email