From cb5e757fd36398f2cae1de19b2946e0070a0af5f Mon Sep 17 00:00:00 2001 From: Eliot Partridge Date: Tue, 17 Sep 2019 17:55:21 -0500 Subject: [PATCH] Remove Patreon functionality (#129) * Remove Patreon links, supper status checking * Add warnings to admin panel and README --- README.md | 5 + src/ts/client/data.ts | 1 - src/ts/common/adminInterfaces.ts | 4 - .../admin/admin-other/admin-other.pug | 28 +- .../admin/admin-other/admin-other.ts | 16 - src/ts/components/app/about/about.pug | 17 - src/ts/components/app/about/about.ts | 3 +- src/ts/components/app/app.pug | 2 - src/ts/components/app/app.ts | 3 +- src/ts/components/app/character/character.pug | 1 - src/ts/components/app/home/home.pug | 2 - src/ts/components/services/adminModel.ts | 12 - .../shared/play-notice/play-notice.ts | 3 +- src/ts/components/shared/shared.module.ts | 2 - .../shared/support-button/support-button.pug | 10 - .../shared/support-button/support-button.scss | 45 -- .../shared/support-button/support-button.ts | 17 - src/ts/server/adminServerActions.ts | 30 - src/ts/server/patreon.ts | 237 ------- src/ts/server/polling.ts | 47 +- src/ts/server/routes/index.ts | 1 - src/ts/server/server.ts | 6 +- src/ts/tests/server/patreon.spec.ts | 650 ------------------ views/index.pug | 3 +- 24 files changed, 18 insertions(+), 1127 deletions(-) delete mode 100644 src/ts/components/shared/support-button/support-button.pug delete mode 100644 src/ts/components/shared/support-button/support-button.scss delete mode 100644 src/ts/components/shared/support-button/support-button.ts delete mode 100644 src/ts/server/patreon.ts delete mode 100644 src/ts/tests/server/patreon.spec.ts diff --git a/README.md b/README.md index 10614e9..bfc9a9f 100644 --- a/README.md +++ b/README.md @@ -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 **ponytownhelp@gmail.com**. + ## Prerequisites * [Node.js](https://nodejs.org/download/release/v9.11.2/) (version 9) diff --git a/src/ts/client/data.ts b/src/ts/client/data.ts index 294c3b3..ee8bc71 100644 --- a/src/ts/client/data.ts +++ b/src/ts/client/data.ts @@ -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'); diff --git a/src/ts/common/adminInterfaces.ts b/src/ts/common/adminInterfaces.ts index ce12b7e..ed8f2e5 100644 --- a/src/ts/common/adminInterfaces.ts +++ b/src/ts/common/adminInterfaces.ts @@ -845,10 +845,6 @@ export interface IAdminServerActions { getChat(search: string, date: string, caseInsensitive: boolean): Promise; getChatForAccounts(accountIds: string[], date: string): Promise; getRequestStats(): Promise; - updatePatreon(): Promise; - resetSupporter(accountId: string): Promise; - getLastPatreonData(): Promise; - updatePastSupporters(): Promise; // live get(endPoint: 'events', id: string): Promise; getAll(endPoint: 'events', timestamp?: string): Promise; diff --git a/src/ts/components/admin/admin-other/admin-other.pug b/src/ts/components/admin/admin-other/admin-other.pug index c51c212..32af955 100644 --- a/src/ts/components/admin/admin-other/admin-other.pug +++ b/src/ts/components/admin/admin-other/admin-other.pug @@ -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 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 license applied to the art and music assets supplied with the game. Please submit a request for approval to ponytownhelp@gmail.com. .row .col-12 diff --git a/src/ts/components/admin/admin-other/admin-other.ts b/src/ts/components/admin/admin-other/admin-other.ts index 6ce952c..2373879 100644 --- a/src/ts/components/admin/admin-other/admin-other.ts +++ b/src/ts/components/admin/admin-other/admin-other.ts @@ -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(); - } } diff --git a/src/ts/components/app/about/about.pug b/src/ts/components/app/about/about.pug index 086e399..47de9ba 100644 --- a/src/ts/components/app/about/about.pug +++ b/src/ts/components/app/about/about.pug @@ -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. diff --git a/src/ts/components/app/about/about.ts b/src/ts/components/app/about/about.ts index f59c994..a923583 100644 --- a/src/ts/components/app/about/about.ts +++ b/src/ts/components/app/about/about.ts @@ -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; } diff --git a/src/ts/components/app/app.pug b/src/ts/components/app/app.pug index b1c4776..3458bdd 100644 --- a/src/ts/components/app/app.pug +++ b/src/ts/components/app/app.pug @@ -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 diff --git a/src/ts/components/app/app.ts b/src/ts/components/app/app.ts index 1b7297e..0c9a571 100644 --- a/src/ts/components/app/app.ts +++ b/src/ts/components/app/app.ts @@ -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; diff --git a/src/ts/components/app/character/character.pug b/src/ts/components/app/character/character.pug index 3e6d2cd..d961d97 100644 --- a/src/ts/components/app/character/character.pug +++ b/src/ts/components/app/character/character.pug @@ -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( diff --git a/src/ts/components/app/home/home.pug b/src/ts/components/app/home/home.pug index e757cf0..5151268 100644 --- a/src/ts/components/app/home/home.pug +++ b/src/ts/components/app/home/home.pug @@ -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") diff --git a/src/ts/components/services/adminModel.ts b/src/ts/components/services/adminModel.ts index 98d58aa..30cb6e3 100644 --- a/src/ts/components/services/adminModel.ts +++ b/src/ts/components/services/adminModel.ts @@ -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) diff --git a/src/ts/components/shared/play-notice/play-notice.ts b/src/ts/components/shared/play-notice/play-notice.ts index f858d52..bc28c88 100644 --- a/src/ts/components/shared/play-notice/play-notice.ts +++ b/src/ts/components/shared/play-notice/play-notice.ts @@ -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; } diff --git a/src/ts/components/shared/shared.module.ts b/src/ts/components/shared/shared.module.ts index ed810eb..f550305 100644 --- a/src/ts/components/shared/shared.module.ts +++ b/src/ts/components/shared/shared.module.ts @@ -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, diff --git a/src/ts/components/shared/support-button/support-button.pug b/src/ts/components/shared/support-button/support-button.pug deleted file mode 100644 index ab372d0..0000000 --- a/src/ts/components/shared/support-button/support-button.pug +++ /dev/null @@ -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") diff --git a/src/ts/components/shared/support-button/support-button.scss b/src/ts/components/shared/support-button/support-button.scss deleted file mode 100644 index 03b79ab..0000000 --- a/src/ts/components/shared/support-button/support-button.scss +++ /dev/null @@ -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; - } -} diff --git a/src/ts/components/shared/support-button/support-button.ts b/src/ts/components/shared/support-button/support-button.ts deleted file mode 100644 index 4e8700f..0000000 --- a/src/ts/components/shared/support-button/support-button.ts +++ /dev/null @@ -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; - } -} diff --git a/src/ts/server/adminServerActions.ts b/src/ts/server/adminServerActions.ts index a2c9c74..967965f 100644 --- a/src/ts/server/adminServerActions.ts +++ b/src/ts/server/adminServerActions.ts @@ -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) { diff --git a/src/ts/server/patreon.ts b/src/ts/server/patreon.ts deleted file mode 100644 index cdbec4a..0000000 --- a/src/ts/server/patreon.ts +++ /dev/null @@ -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 = { - [rewardLevel1]: PatreonFlags.Supporter1, - [rewardLevel2]: PatreonFlags.Supporter2, - [rewardLevel3]: PatreonFlags.Supporter3, -}; - -export type RemoveOldSupporters = ReturnType; -export type UpdateSupporters = ReturnType; -export type AddTotalPledged = ReturnType; - -let lastPatreonData: PatreonData | undefined = undefined; - -export function getLastPatreonData() { - return lastPatreonData; -} - -/* istanbul ignore next */ -export function createPatreonClient(accessToken: string): (path: string) => Promise { - 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, log: LogMessage): Promise { - const campaignData = await client('/current_user/campaigns'); - const rewards = campaignData.rawJson.included - .filter(x => x.type === 'reward') - .map(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(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(); - const accountsWithPatreonMap = new Map(); - - 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 }); - }; diff --git a/src/ts/server/polling.ts b/src/ts/server/polling.ts index cd18208..f8d5362 100644 --- a/src/ts/server/polling.ts +++ b/src/ts/server/polling.ts @@ -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 | 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(() => diff --git a/src/ts/server/routes/index.ts b/src/ts/server/routes/index.ts index a81dfaf..9ab5744 100644 --- a/src/ts/server/routes/index.ts +++ b/src/ts/server/routes/index.ts @@ -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')}`, diff --git a/src/ts/server/server.ts b/src/ts/server/server.ts index a303a3b..41a8c71 100644 --- a/src/ts/server/server.ts +++ b/src/ts/server/server.ts @@ -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) { diff --git a/src/ts/tests/server/patreon.spec.ts b/src/ts/tests/server/patreon.spec.ts deleted file mode 100644 index 49bb182..0000000 --- a/src/ts/tests/server/patreon.spec.ts +++ /dev/null @@ -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 }); - }); - }); -}); diff --git a/views/index.pug b/views/index.pug index 724b289..b80f893 100644 --- a/views/index.pug +++ b/views/index.pug @@ -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