mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-27 07:58:55 -07:00
[econ] add button for supporter boosts
This commit is contained in:
@@ -614,6 +614,10 @@ role that role's RecCenterTokens in a gift box, every time it fires. The map is
|
|||||||
account links its Discord, so a new supporter gets one box now rather than after the next
|
account links its Discord, so a new supporter gets one box now rather than after the next
|
||||||
run. `www` decides "first" from the link table; this module keeps no ledger either way, and
|
run. `www` decides "first" from the link table; this module keeps no ledger either way, and
|
||||||
the cron pays that account again on its next run like everyone else.
|
the cron pays that account again on its next run like everyone else.
|
||||||
|
- **A whole run can be paid by hand.** `www`'s staff panel ("Bonus reward!", developer-only,
|
||||||
|
`POST /api/staff/discord-roles/supporter-gift`) calls `grantDiscordRoleGifts` with its own
|
||||||
|
bindings — the same map, audience and box as the cron — and answers with the run's summary.
|
||||||
|
No ledger here either: it pays everyone again on top of the schedule, which is the point.
|
||||||
|
|
||||||
One D1 read for the links, then a handful of writes and two hub calls per box, sequentially.
|
One D1 read for the links, then a handful of writes and two hub calls per box, sequentially.
|
||||||
A cron invocation has a fixed subrequest budget (50 free, 1000 paid); a community whose
|
A cron invocation has a fixed subrequest budget (50 free, 1000 paid); a community whose
|
||||||
|
|||||||
@@ -205,9 +205,13 @@ export interface RoleGiftSummary {
|
|||||||
* `startingTokens` is the signup grant to seed before the credit — the credit upserts the
|
* `startingTokens` is the signup grant to seed before the credit — the credit upserts the
|
||||||
* balance row, so a never-touched balance would otherwise start from this gift instead of
|
* balance row, so a never-touched balance would otherwise start from this gift instead of
|
||||||
* the grant plus this gift.
|
* the grant plus this gift.
|
||||||
|
*
|
||||||
|
* Takes the {@link RoleGiftEnv} pick, not econ's whole `Env`: `www`'s staff panel runs this
|
||||||
|
* same function on a button (`POST /api/staff/discord-roles/supporter-gift`) with its own
|
||||||
|
* bindings, and the three it reads are declared on both workers.
|
||||||
*/
|
*/
|
||||||
export async function grantDiscordRoleGifts(
|
export async function grantDiscordRoleGifts(
|
||||||
env: Env,
|
env: RoleGiftEnv,
|
||||||
startingTokens: number
|
startingTokens: number
|
||||||
): Promise<RoleGiftSummary> {
|
): Promise<RoleGiftSummary> {
|
||||||
const summary: RoleGiftSummary = {
|
const summary: RoleGiftSummary = {
|
||||||
|
|||||||
@@ -278,6 +278,15 @@ leaving a silent log line on a worker with observability off. Use it after chang
|
|||||||
bot token or inviting the bot, or when the role drop needs this morning's roles. The run
|
bot token or inviting the bot, or when the role drop needs this morning's roles. The run
|
||||||
is written to `audit_log` as `sync_discord_roles` against the developer who pressed it.
|
is written to `audit_log` as `sync_discord_roles` against the developer who pressed it.
|
||||||
|
|
||||||
|
A developer can also pay `econ`'s scheduled supporter gift again, now, with
|
||||||
|
`POST /api/staff/discord-roles/supporter-gift` (developer role required, empty body) — the
|
||||||
|
"Bonus reward!" card on the account page's Token drop tab. It runs econ's
|
||||||
|
`grantDiscordRoleGifts` with www's own bindings: every account whose Discord link holds a
|
||||||
|
role mapped in `DISCORD_ROLE_TOKENS` is boxed that role's tokens, exactly as the cron does,
|
||||||
|
and the response is the run's summary — `{ skipped, roles, links, granted, tokens, failed }`.
|
||||||
|
The gift keeps no ledger, so each press pays everyone again on top of the schedule (the panel
|
||||||
|
asks first). The run is written to `audit_log` as `run_discord_role_gift`.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
### Run in dev mode
|
### Run in dev mode
|
||||||
|
|||||||
@@ -1606,7 +1606,8 @@ function halloweenActive(): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The sprite, 23x25 native pixels, drawn at 3x with hard edges. */
|
/** The sprite, 23x25 native pixels, drawn at 3x with hard edges. */
|
||||||
const PUMPKIN_SRC = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAAZCAYAAADaILXQAAAFD0lEQVR42pWVX2xcxRXGf2fu3Lu7jh0bO3FsnMQ2idP8gZoIGkootZsIGrUlVEVCTVW1qVREUPtSiTckYgQPqEIg1FZU6gtP8EJVAaZpIqE4UtrQOCSIkj+YVgRsp3E2mzisnb27OzOnD+uuE2yJZJ7u6J755jvf+eYc4SbX7qHBbNyjpe5VS5k4Nw3VZj4evcrFiWn+9fZxuTbW3Cw43WcxBMBCNYtYRYwiujDU3ijmwK++rWv7G7BSpLdrGf8Zv8TY+54kCuQ/v4IsckZuBPg7Twzq2ntq1IwXjvzlCz4cPv6VZxcN2LttTfXh9phKLPxtMmVnW7BqAiPnU2aipTzUFnjzonMiFhFn9747vihO9P+PoW29Dz7Yt3LD/sdl3dRl/9NmLRu1GJPzpqfFk2SV6ZIhGwlrVoBP1MQO0xQrQ/1N76/qXL7u4KeXxxbV3Jjwuveu1USGH98XOH44EIopu+7PECJBXGBmVpFKhr71JfqAD0cTVMGY5G1MukAJC/DM9l597IEmVsRTAJz4u2XrG+cxQGnPHuyOP6L797AxOYh1KdGmnYAhHPsrrjrLlq05dNTCtm71Jd82dGTiUh3cKDV7ac2ZAUP60moEB6ffQvfvQeucQt3Bd+3eAZlb+OCV1+n/Zpl3hpsAP8/86QdW6VM/tEjlCmWf8sizGcaiHB9thYQZgg+sfngYMPxuR46VpkS/exMRRQWE5bWL1fD9LsuhcV/Yu707febdz3I29gapBMg0467maG5pI3JlitaSAZaEIk1tHbXqNyxBowTT4IjK0zW72Qoyl7GmJdTPu8SC4dg/Krw6GrhcbeUDrYIY7v+th4uTnHxuaT3NR157j0CVvoN3cvo303PyBPRLjz3MSWiDwMvvrQACEpRg5v6oQVs3sfHFgEpg7NzxufRhbPIE0ZNw+NG1xDbHvnxA/+mR+kVz4Af9p/G31vjq5NkGyppjszpOYFFVQBERfmkNfvVmHIqo4e5cBBiy4RIVyhiRmhnUAG6+cY2M4FzSMNyQc9XWlgoiwi0mkAhkgO4AagUTZVkeWZZloSnyeHzNTdfIMeWgHMmBGbtkX12o5w+ceWj5ra7Y1j4LQK8qwZfRNGVbDgKWINCei+kVT1+Yt6XB4VGEwNEZoZKaXS8cOPWjBV1RCKAWpMLPlrYg1at4CWzf/DkXTt7K16qeFhNxVCI6br/Avrwl+Dw/6Grl5zMrWNbXQFquwpGJhf1cTJYnt16ou8BVpxm8q7bvvCPPW50T/L6rQMcd+TmPQ5rt4RezLdz7aCNrtwi5ZlMYeGKgdF0v2NR/W8FFcatUS/ykI8POzhasVf787/N8rzNi31TKRyu7KSyJicXw30/ykFiCEZbd1k6aFvn6fRmSOEsZ5ZPDbl4Wn2QxCmQa0VCiWrlMKjE+00g5MlQyEaXY4GyEw+EaDZFP8FJG1WGTLGhMpeJAPS5yNeYb7t543ZA6fexUPaP139ioEULcnqGtu5mKKOu3eFDDn84eihiiXt7BXw/o4HdXYnIlxicK12uuaHotMMCZ0VMSVClPpRTOzlL4rMjYUaGnq5nHewb8Y68O1ImpKucu5BmfrNXkpgb0lUK+oz3X1IRa1MiCoSwnTay2dEmI+fio1jRXleFIwYupLgZqggwDJMXkKhtwEEhLji+uKEhdFUZGRtztu3YeMBI1Znzxxgb0tWtw92BWGyitu8dzZlQxCof+cHhRnP8B5aopRAfi2ZcAAAAASUVORK5CYII='
|
const PUMPKIN_SRC =
|
||||||
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAAZCAYAAADaILXQAAAFD0lEQVR42pWVX2xcxRXGf2fu3Lu7jh0bO3FsnMQ2idP8gZoIGkootZsIGrUlVEVCTVW1qVREUPtSiTckYgQPqEIg1FZU6gtP8EJVAaZpIqE4UtrQOCSIkj+YVgRsp3E2mzisnb27OzOnD+uuE2yJZJ7u6J755jvf+eYc4SbX7qHBbNyjpe5VS5k4Nw3VZj4evcrFiWn+9fZxuTbW3Cw43WcxBMBCNYtYRYwiujDU3ijmwK++rWv7G7BSpLdrGf8Zv8TY+54kCuQ/v4IsckZuBPg7Twzq2ntq1IwXjvzlCz4cPv6VZxcN2LttTfXh9phKLPxtMmVnW7BqAiPnU2aipTzUFnjzonMiFhFn9747vihO9P+PoW29Dz7Yt3LD/sdl3dRl/9NmLRu1GJPzpqfFk2SV6ZIhGwlrVoBP1MQO0xQrQ/1N76/qXL7u4KeXxxbV3Jjwuveu1USGH98XOH44EIopu+7PECJBXGBmVpFKhr71JfqAD0cTVMGY5G1MukAJC/DM9l597IEmVsRTAJz4u2XrG+cxQGnPHuyOP6L797AxOYh1KdGmnYAhHPsrrjrLlq05dNTCtm71Jd82dGTiUh3cKDV7ac2ZAUP60moEB6ffQvfvQeucQt3Bd+3eAZlb+OCV1+n/Zpl3hpsAP8/86QdW6VM/tEjlCmWf8sizGcaiHB9thYQZgg+sfngYMPxuR46VpkS/exMRRQWE5bWL1fD9LsuhcV/Yu707febdz3I29gapBMg0467maG5pI3JlitaSAZaEIk1tHbXqNyxBowTT4IjK0zW72Qoyl7GmJdTPu8SC4dg/Krw6GrhcbeUDrYIY7v+th4uTnHxuaT3NR157j0CVvoN3cvo303PyBPRLjz3MSWiDwMvvrQACEpRg5v6oQVs3sfHFgEpg7NzxufRhbPIE0ZNw+NG1xDbHvnxA/+mR+kVz4Af9p/G31vjq5NkGyppjszpOYFFVQBERfmkNfvVmHIqo4e5cBBiy4RIVyhiRmhnUAG6+cY2M4FzSMNyQc9XWlgoiwi0mkAhkgO4AagUTZVkeWZZloSnyeHzNTdfIMeWgHMmBGbtkX12o5w+ceWj5ra7Y1j4LQK8qwZfRNGVbDgKWINCei+kVT1+Yt6XB4VGEwNEZoZKaXS8cOPWjBV1RCKAWpMLPlrYg1at4CWzf/DkXTt7K16qeFhNxVCI6br/Avrwl+Dw/6Grl5zMrWNbXQFquwpGJhf1cTJYnt16ou8BVpxm8q7bvvCPPW50T/L6rQMcd+TmPQ5rt4RezLdz7aCNrtwi5ZlMYeGKgdF0v2NR/W8FFcatUS/ykI8POzhasVf787/N8rzNi31TKRyu7KSyJicXw30/ykFiCEZbd1k6aFvn6fRmSOEsZ5ZPDbl4Wn2QxCmQa0VCiWrlMKjE+00g5MlQyEaXY4GyEw+EaDZFP8FJG1WGTLGhMpeJAPS5yNeYb7t543ZA6fexUPaP139ioEULcnqGtu5mKKOu3eFDDn84eihiiXt7BXw/o4HdXYnIlxicK12uuaHotMMCZ0VMSVClPpRTOzlL4rMjYUaGnq5nHewb8Y68O1ImpKucu5BmfrNXkpgb0lUK+oz3X1IRa1MiCoSwnTay2dEmI+fio1jRXleFIwYupLgZqggwDJMXkKhtwEEhLji+uKEhdFUZGRtztu3YeMBI1Znzxxgb0tWtw92BWGyitu8dzZlQxCof+cHhRnP8B5aopRAfi2ZcAAAAASUVORK5CYII='
|
||||||
const PUMPKIN_SCALE = 3
|
const PUMPKIN_SCALE = 3
|
||||||
const PUMPKIN_W = 23 * PUMPKIN_SCALE
|
const PUMPKIN_W = 23 * PUMPKIN_SCALE
|
||||||
const PUMPKIN_H = 25 * PUMPKIN_SCALE
|
const PUMPKIN_H = 25 * PUMPKIN_SCALE
|
||||||
@@ -1842,8 +1843,12 @@ function HalloweenOverlay() {
|
|||||||
<style>{HALLOWEEN_CSS}</style>
|
<style>{HALLOWEEN_CSS}</style>
|
||||||
{!off && (
|
{!off && (
|
||||||
<div className="rf-halloween" aria-hidden="true">
|
<div className="rf-halloween" aria-hidden="true">
|
||||||
<span className="rf-halloween-web" style={{ left: 0 }}>🕸️</span>
|
<span className="rf-halloween-web" style={{ left: 0 }}>
|
||||||
<span className="rf-halloween-web" style={{ right: 0, transform: 'scaleX(-1)' }}>🕸️</span>
|
🕸️
|
||||||
|
</span>
|
||||||
|
<span className="rf-halloween-web" style={{ right: 0, transform: 'scaleX(-1)' }}>
|
||||||
|
🕸️
|
||||||
|
</span>
|
||||||
<LilPumpkin />
|
<LilPumpkin />
|
||||||
{HALLOWEEN_SPRITES.map((emoji, i) => (
|
{HALLOWEEN_SPRITES.map((emoji, i) => (
|
||||||
<span
|
<span
|
||||||
@@ -3728,6 +3733,7 @@ function Dashboard({
|
|||||||
<>
|
<>
|
||||||
<TokenDropForm />
|
<TokenDropForm />
|
||||||
<RoleTokenDropForm />
|
<RoleTokenDropForm />
|
||||||
|
<SupporterBonusCard />
|
||||||
<DiscordRoleSyncCard />
|
<DiscordRoleSyncCard />
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
@@ -4203,6 +4209,88 @@ function RoleTokenDropForm() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** What one run of the supporter gift did — econ's `RoleGiftSummary`, as the route answers. */
|
||||||
|
interface RoleGiftSummary {
|
||||||
|
skipped: boolean
|
||||||
|
roles: number
|
||||||
|
links: number
|
||||||
|
granted: number
|
||||||
|
tokens: number
|
||||||
|
failed: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Developer-only: pay econ's scheduled supporter gift again, right now — a bonus round. Every
|
||||||
|
* player whose linked Discord holds a role in the server's role→tokens map is boxed that
|
||||||
|
* role's tokens, exactly as the weekly cron does it; the amounts and the box message are the
|
||||||
|
* server's, not typed here, which is what tells this apart from the role drop above.
|
||||||
|
*
|
||||||
|
* The gift keeps no ledger, so pressing this pays everyone again on top of whatever the
|
||||||
|
* schedule paid — that is the point, and why the button asks first. A `skipped` run means
|
||||||
|
* no role is mapped on the server, shown as the error it is; a run that went through is
|
||||||
|
* summed up in a sentence.
|
||||||
|
*/
|
||||||
|
function SupporterBonusCard() {
|
||||||
|
const [confirming, setConfirming] = useState(false)
|
||||||
|
const { pending, error, done, run } = useAction()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="card">
|
||||||
|
<h2>Bonus reward</h2>
|
||||||
|
<p className="muted">
|
||||||
|
Run the scheduled supporter reward again now. Everyone whose linked Discord holds a rewarded
|
||||||
|
role gets their role's tokens in a gift box, online or not, using the amounts and
|
||||||
|
message the server is configured with. It pays on top of the scheduled run, every time it is
|
||||||
|
pressed.
|
||||||
|
</p>
|
||||||
|
{error && <p className="error">{error}</p>}
|
||||||
|
{done && <p className="ok">{done}</p>}
|
||||||
|
{confirming ? (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={pending}
|
||||||
|
onClick={() =>
|
||||||
|
void run(async () => {
|
||||||
|
const s = await call<RoleGiftSummary>('/api/staff/discord-roles/supporter-gift', {
|
||||||
|
authed: true,
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
setConfirming(false)
|
||||||
|
if (s.skipped) {
|
||||||
|
throw new Error(
|
||||||
|
'The supporter reward is off: no Discord role is mapped to tokens on the server.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const parts = [
|
||||||
|
`Sent ${s.granted} gift box${s.granted === 1 ? '' : 'es'} for ${s.tokens.toLocaleString()} tokens`,
|
||||||
|
`${s.roles} rewarded role${s.roles === 1 ? '' : 's'} over ${s.links} linked Discord${s.links === 1 ? '' : 's'}`,
|
||||||
|
]
|
||||||
|
if (s.failed > 0) parts.push(`${s.failed} failed — see the worker log`)
|
||||||
|
return `${parts.join(', ')}.`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{pending ? 'Sending…' : 'Confirm: pay every supporter again'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="linkish"
|
||||||
|
disabled={pending}
|
||||||
|
onClick={() => setConfirming(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<button type="button" onClick={() => setConfirming(true)}>
|
||||||
|
Bonus reward!
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/** What one run of the Discord role sweep did — www's `SweepSummary`, as the sync answers. */
|
/** What one run of the Discord role sweep did — www's `SweepSummary`, as the sync answers. */
|
||||||
interface SweepSummary {
|
interface SweepSummary {
|
||||||
skipped: boolean
|
skipped: boolean
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import {
|
|||||||
toCatalogSkin,
|
toCatalogSkin,
|
||||||
} from '../../econ/src/catalog-db'
|
} from '../../econ/src/catalog-db'
|
||||||
import { countConsumable, grantConsumable } from '../../econ/src/consumables-db'
|
import { countConsumable, grantConsumable } from '../../econ/src/consumables-db'
|
||||||
|
import { grantDiscordRoleGifts } from '../../econ/src/discord-role-gift'
|
||||||
import { getEquipment, grantEquipment } from '../../econ/src/equipment-db'
|
import { getEquipment, grantEquipment } from '../../econ/src/equipment-db'
|
||||||
import { grantCustomAvatarItem, ownedCustomAvatarItemIds } from '../../econ/src/inventory-custom-db'
|
import { grantCustomAvatarItem, ownedCustomAvatarItemIds } from '../../econ/src/inventory-custom-db'
|
||||||
// The notification ids and the kick frame's recovered shape, owned by `notify`. Both are
|
// The notification ids and the kick frame's recovered shape, owned by `notify`. Both are
|
||||||
@@ -1132,6 +1133,29 @@ export async function syncDiscordRolesHandler(c: Context<App>) {
|
|||||||
return c.json(summary)
|
return c.json(summary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run econ's supporter gift NOW — the weekly cron (econ's discord-role-gift.ts) on a button,
|
||||||
|
* a bonus round for a developer who wants every supporter paid again today. Same function,
|
||||||
|
* same map (`DISCORD_ROLE_TOKENS`), same box: every account whose Discord link holds a mapped
|
||||||
|
* role is boxed its best role's tokens, online or not. The role drop above is the same
|
||||||
|
* audience with an operator's amount and message; this one pays what the map says and
|
||||||
|
* says so on the box.
|
||||||
|
*
|
||||||
|
* It pays everyone it finds, every time — the gift keeps no ledger, and that module is
|
||||||
|
* explicit that a hand run is an extra gift on purpose. So the panel asks before sending,
|
||||||
|
* and the run is on the audit log with how many boxes and tokens went out.
|
||||||
|
*
|
||||||
|
* Always a 200 with the run's summary, as the sweep's sync is: a `skipped` run (no role
|
||||||
|
* mapped) is the gift reporting on its configuration, and the panel shows the reason.
|
||||||
|
*/
|
||||||
|
export async function runDiscordRoleGiftHandler(c: Context<App>) {
|
||||||
|
const startingTokens = intVar(c.env.STARTING_TOKENS, DEFAULT_STARTING_TOKENS)
|
||||||
|
const summary = await grantDiscordRoleGifts(c.env, startingTokens)
|
||||||
|
await recordPlayerAudit(c, 'run_discord_role_gift', { ...summary })
|
||||||
|
logger.info('staff ran the discord supporter gift', { moderatorId: staffId(c), ...summary })
|
||||||
|
return c.json(summary)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Give a player XP, in a gift box — the way a game reward pays its XP. The XP is banked
|
* Give a player XP, in a gift box — the way a game reward pays its XP. The XP is banked
|
||||||
* here (the box, like every box, grants nothing when opened) and spent on any levels it now
|
* here (the box, like every box, grants nothing when opened) and spent on any levels it now
|
||||||
|
|||||||
@@ -877,7 +877,8 @@ it('refuses every staff endpoint without a token, and without a staff role', asy
|
|||||||
'/api/staff/rooms/1/gift-tokens',
|
'/api/staff/rooms/1/gift-tokens',
|
||||||
'/api/staff/online/gift-tokens',
|
'/api/staff/online/gift-tokens',
|
||||||
'/api/staff/discord-roles/1/gift-tokens',
|
'/api/staff/discord-roles/1/gift-tokens',
|
||||||
'/api/staff/discord-roles/sync'
|
'/api/staff/discord-roles/sync',
|
||||||
|
'/api/staff/discord-roles/supporter-gift'
|
||||||
)
|
)
|
||||||
for (const path of writes) {
|
for (const path of writes) {
|
||||||
expect((await SELF.fetch(`https://example.com${path}`, { method: 'POST' })).status).toBe(401)
|
expect((await SELF.fetch(`https://example.com${path}`, { method: 'POST' })).status).toBe(401)
|
||||||
@@ -888,10 +889,14 @@ it('refuses every staff endpoint without a token, and without a staff role', asy
|
|||||||
expect(res.status).toBe(403)
|
expect(res.status).toBe(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The gifts, the Plus grant and the role sweep are narrower: a moderator is staff, but
|
// The gifts, the Plus grant, the role sweep and the supporter gift are narrower: a
|
||||||
// not a developer.
|
// moderator is staff, but not a developer.
|
||||||
for (const path of writes.filter(
|
for (const path of writes.filter(
|
||||||
(p) => p.includes('/gift-') || p.includes('/grant-plus') || p.endsWith('/sync')
|
(p) =>
|
||||||
|
p.includes('/gift-') ||
|
||||||
|
p.includes('/grant-plus') ||
|
||||||
|
p.endsWith('/sync') ||
|
||||||
|
p.endsWith('/supporter-gift')
|
||||||
)) {
|
)) {
|
||||||
expect((await staffPost(path, 8101, { amount: 1 })).status).toBe(403)
|
expect((await staffPost(path, 8101, { amount: 1 })).status).toBe(403)
|
||||||
}
|
}
|
||||||
@@ -2661,6 +2666,41 @@ it('runs the sweep on demand for a developer and answers with the summary', asyn
|
|||||||
expect(JSON.parse(row?.data ?? '{}')).toMatchObject({ skipped: true })
|
expect(JSON.parse(row?.data ?? '{}')).toMatchObject({ skipped: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// The supporter gift on a button: econ's cron run (`grantDiscordRoleGifts`, covered in econ's
|
||||||
|
// own tests) with www's bindings, answered with its summary and written to the audit log.
|
||||||
|
// The test bindings map no role, so the run is a `skipped` one and pays nothing — which is
|
||||||
|
// also what the panel must be able to tell the developer.
|
||||||
|
it('runs the supporter gift on demand for a developer and answers with the summary', async () => {
|
||||||
|
await onlyDiscordLinks([[9100, '900000000000000100', [ROLE_A]]])
|
||||||
|
const before = await getBalance(
|
||||||
|
env.DB,
|
||||||
|
9100,
|
||||||
|
CurrencyType.RecCenterTokens,
|
||||||
|
DEFAULT_STARTING_TOKENS
|
||||||
|
)
|
||||||
|
|
||||||
|
const res = await devPost('/api/staff/discord-roles/supporter-gift', 8110, {})
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(await res.json()).toEqual({
|
||||||
|
skipped: true,
|
||||||
|
roles: 0,
|
||||||
|
links: 0,
|
||||||
|
granted: 0,
|
||||||
|
tokens: 0,
|
||||||
|
failed: 0,
|
||||||
|
})
|
||||||
|
expect(
|
||||||
|
await getBalance(env.DB, 9100, CurrencyType.RecCenterTokens, DEFAULT_STARTING_TOKENS)
|
||||||
|
).toBe(before)
|
||||||
|
|
||||||
|
const row = await env.DB.prepare(
|
||||||
|
`SELECT player_id, data FROM audit_log WHERE action = 'run_discord_role_gift'
|
||||||
|
ORDER BY audit_log_id DESC LIMIT 1`
|
||||||
|
).first<{ player_id: number; data: string }>()
|
||||||
|
expect(row?.player_id).toBe(8110)
|
||||||
|
expect(JSON.parse(row?.data ?? '{}')).toMatchObject({ skipped: true, granted: 0 })
|
||||||
|
})
|
||||||
|
|
||||||
it('re-reads every discord link and writes the roles back', async () => {
|
it('re-reads every discord link and writes the roles back', async () => {
|
||||||
await onlyDiscordLinks([
|
await onlyDiscordLinks([
|
||||||
[9111, '900000000000000111', [ROLE_A]],
|
[9111, '900000000000000111', [ROLE_A]],
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ import {
|
|||||||
playerHistoryHandler,
|
playerHistoryHandler,
|
||||||
requireDeveloper,
|
requireDeveloper,
|
||||||
requireStaff,
|
requireStaff,
|
||||||
|
runDiscordRoleGiftHandler,
|
||||||
searchReportsHandler,
|
searchReportsHandler,
|
||||||
syncDiscordRolesHandler,
|
syncDiscordRolesHandler,
|
||||||
topReportedHandler,
|
topReportedHandler,
|
||||||
@@ -576,6 +577,8 @@ const app = new Hono<App>()
|
|||||||
// The daily role sweep, run now and answered with its summary (halt reason included) —
|
// The daily role sweep, run now and answered with its summary (halt reason included) —
|
||||||
// the cron reports only to a log line, and this is the way to see why it wrote nothing.
|
// the cron reports only to a log line, and this is the way to see why it wrote nothing.
|
||||||
.post('/api/staff/discord-roles/sync', requireDeveloper, syncDiscordRolesHandler)
|
.post('/api/staff/discord-roles/sync', requireDeveloper, syncDiscordRolesHandler)
|
||||||
|
// Econ's weekly supporter gift, paid now: a bonus round for everyone holding a mapped role.
|
||||||
|
.post('/api/staff/discord-roles/supporter-gift', requireDeveloper, runDiscordRoleGiftHandler)
|
||||||
// Rec Room Plus is worth tokens and a discount, so granting it is developer-only too.
|
// Rec Room Plus is worth tokens and a discount, so granting it is developer-only too.
|
||||||
.post('/api/staff/players/:id/grant-plus', requireDeveloper, grantPlusHandler)
|
.post('/api/staff/players/:id/grant-plus', requireDeveloper, grantPlusHandler)
|
||||||
.post('/api/staff/players/:id/username-changes', addUsernameChangeHandler)
|
.post('/api/staff/players/:id/username-changes', addUsernameChangeHandler)
|
||||||
|
|||||||
Reference in New Issue
Block a user