mirror of
https://github.com/Terncode/pixel.horse.git
synced 2026-09-24 13:55:04 +02:00
Remove Patreon functionality (#129)
* Remove Patreon links, supper status checking * Add warnings to admin panel and README
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
@@ -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(() =>
|
||||
|
||||
@@ -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')}`,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
-2
@@ -11,7 +11,7 @@ html(lang="en")
|
||||
base(href=base)
|
||||
title=title
|
||||
style!=inlineStyle
|
||||
|
||||
|
||||
link(rel="stylesheet" href=style)
|
||||
|
||||
//- open graph
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user