Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# BONanza incident runbook
# BONanza operations runbook

This runbook is for the host of an active DarkPeers BONanza giveaway.
This is the operational reference for hosting, rehearsing and recovering a DarkPeers BONanza giveaway.

## If the pot or sponsor accounting looks wrong

Expand Down Expand Up @@ -38,7 +38,7 @@ The host and recognized staff are exempt from the escalating command spam lockou

## Rehearsal mode

Rehearsal mode runs the userscript logic without sending userscript chat output and without moving BON.
Rehearsal mode runs the userscript logic without moving BON. Output that would normally be sent by the userscript is redirected privately to the host via `/msg` instead of being posted in public chat.

Enable it from the host panel:

Expand All @@ -58,7 +58,8 @@ location.reload();

While rehearsal mode is active:

- script-generated chat messages are suppressed;
- script-generated output is redirected privately to the host via `/msg`, prefixed as rehearsal output;
- output that would normally be a private reply to another user is still shown only to the host, with the intended recipient identified;
- winner gifts are simulated only;
- sponsor refunds are simulated only;
- BON Pool contributions are simulated only;
Expand All @@ -77,7 +78,7 @@ Suggested pre-release rehearsal:
6. Exercise time controls.
7. End the giveaway.
8. Inspect the host log and statement.
9. Confirm that no script output appeared in chat and no BON moved.
9. Confirm that no script output appeared in public chat, the host received the rehearsal output by private message, and no BON moved.

Disable rehearsal mode from **Settings > Safety & Testing** by turning off **Rehearsal / Debug mode**. The page reloads automatically.

Expand Down
16 changes: 13 additions & 3 deletions tests/bonanza-review-hardening.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const sourcePath = new URL(
const source = readFileSync(sourcePath, "utf8");

test("review hardening invariants stay present", () => {
assert.match(source, /^\/\/ @version\s+1\.5\.8$/m);
assert.match(source, /^\/\/ @version\s+1\.5\.9$/m);
assert.doesNotMatch(source, /pollChatFallback/);
assert.doesNotMatch(source, /onlyguardians/i);
assert.match(source, /async function getLatestMainChatReplayBoundary\(\)/);
Expand Down Expand Up @@ -137,13 +137,23 @@ test("rehearsal guards precede every money/chat POST path", () => {
assert.ok(giftStart >= 0 && giftDryRun > giftStart && giftPost > giftDryRun);

const sendStart = source.indexOf("async function sendMessage");
const sendDryRun = source.indexOf("if (REHEARSAL_MODE)", sendStart);
const sendRehearsalRoute = source.indexOf("if (REHEARSAL_MODE && !allowRehearsalPrivateOutput)", sendStart);
const sendApiCall = source.indexOf("trySendViaApi(messageStr)", sendStart);
assert.ok(sendStart >= 0 && sendDryRun > sendStart && sendApiCall > sendDryRun);
assert.ok(sendStart >= 0 && sendRehearsalRoute > sendStart && sendApiCall > sendRehearsalRoute);

assert.equal((source.match(/trySendViaApi\(/g) || []).length, 2);
});

test("rehearsal routes userscript output privately to the host", () => {
assert.match(source, /function formatRehearsalPrivateOutput\(messageStr\)/);
assert.match(source, /async function sendPrivateMessage\(username, messageStr, options = \{\}\)/);
assert.match(source, /const allowRehearsalPrivateOutput =[\s\S]*?options\?\.rehearsalPrivateOutput === true/);
assert.match(source, /const rehearsalHost = String\(giveawayData\?\.host \|\| getLoggedInUsername\(\) \|\| ""\)\.trim\(\)/);
assert.match(source, /return sendPrivateMessage\(rehearsalHost, rehearsalBody, \{[\s\S]*?rehearsalPrivateOutput: true,[\s\S]*?requireExclusiveGiveawayOwnership[\s\S]*?\}\)/);
assert.match(source, /return sendMessage\(`\/msg \$\{to\} \$\{body\}`, options\)/);
assert.doesNotMatch(source, /logEvent\("Rehearsal chat suppressed"/);
});

test("staff detection uses exact role-title allow-list matching", () => {
assert.match(source, /DARKPEERS_STAFF_ROLE_NAMES\.has\(title\)/);
assert.doesNotMatch(source, /roleTokens\.some/);
Expand Down
63 changes: 50 additions & 13 deletions userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// @name DarkPeers BONanza Giveaway | Maghuro Fork
// @namespace https://github.com/maghuro/unit3d-userscripts
// @description BON giveaways on DarkPeers with an optional direct contribution to the BON Pool
// @version 1.5.8
// @version 1.5.9
// @author 🤖 T.R.A.V.I.S., Maghuro & M.A.E.S.T.R.O.
// @homepageURL https://gist.github.com/maghuro/da2dbfec94951990cbc54e75a9aee318
// @updateURL https://gist.githubusercontent.com/maghuro/da2dbfec94951990cbc54e75a9aee318/raw/DarkPeers_BONanza_Giveaway.user.js
Expand Down Expand Up @@ -218,6 +218,8 @@
// - v1.5.8 makes the Rehearsal / Debug UI toggle honest under forced overrides:
// URL-forced rehearsal can be switched off from the UI, while a source-level
// DEBUG_SETTINGS.dry_run override is shown as forced instead of pretending to disable.
// - v1.5.9 routes rehearsal chat output privately to the host instead of suppressing
// it, while keeping winner/refund gifts and BON Pool contributions fully simulated.
//// DarkPeers BONanza fork created and maintained by T.R.A.V.I.S. for the DarkPeers staff.
// Further development and maintenance by Maghuro & M.A.E.S.T.R.O.

Expand Down Expand Up @@ -1369,12 +1371,12 @@
<div class="settings-group__header">
<p class="settings-group__title">Safety & Testing</p>
</div>
<label class="settings-row" title="Rehearsal/debug mode suppresses userscript chat output and all BON-moving actions. Changing it reloads the page." for="rehearsalModeToggle">
<label class="settings-row" title="Rehearsal/debug mode sends userscript output privately to the host and simulates all BON-moving actions. Changing it reloads the page." for="rehearsalModeToggle">
<span class="settings-row__label">Rehearsal / Debug mode</span>
<input
type="checkbox"
id="rehearsalModeToggle"
title="No userscript chat output or BON transfers. Changing this setting reloads the page."
title="Userscript output is sent privately to the host; no BON is transferred. Changing this setting reloads the page."
class="settings-row__toggle"
>
</label>
Expand Down Expand Up @@ -1776,10 +1778,10 @@ body.host-panel-dragging * {
if (REHEARSAL_MODE) {
setHostSafetyBanner(
"rehearsal",
"REHEARSAL MODE: userscript chat output, winner/refund gifts and BON Pool contributions are disabled.",
"REHEARSAL MODE: userscript output is sent privately to the host; winner/refund gifts and BON Pool contributions are simulated only.",
"danger"
);
console.warn("[BON Giveaway] REHEARSAL MODE active: no script chat output or BON-moving request will be sent.");
console.warn("[BON Giveaway] REHEARSAL MODE active: script output is redirected to a private host message; no BON-moving request will be sent.");
}

// ── Mutual exclusion with the original "Blutopia BON Giveaway" script ──
Expand Down Expand Up @@ -11565,11 +11567,24 @@ body.host-panel-dragging * {
return str;
}
}
async function sendPrivateMessage(username, messageStr) {
function formatRehearsalPrivateOutput(messageStr) {
const raw = String(messageStr ?? "").replace(/[\r\n]+/g, " ").trim();
const privateMatch = raw.match(/^\/msg\s+(\S+)\s*(.*)$/i);

if (privateMatch) {
const intendedRecipient = sanitizeNick(privateMatch[1]);
const body = String(privateMatch[2] || "").trim();
return `[b][color=#ff3333][REHEARSAL][/color][/b] [i](would PM ${intendedRecipient})[/i]${body ? ` ${body}` : ""}`;
}

return `[b][color=#ff3333][REHEARSAL][/color][/b]${raw ? ` ${raw}` : ""}`;
}

async function sendPrivateMessage(username, messageStr, options = {}) {
const to = String(username || "").trim();
if (!to) {
// No target → fall back to normal chat output
return sendMessage(messageStr);
// No target -> fall back to the normal output path.
return sendMessage(messageStr, options);
}

let body = String(messageStr ?? "");
Expand All @@ -11583,7 +11598,7 @@ body.host-panel-dragging * {
// Avoid newlines which can confuse slash-command parsers
body = body.replace(/[\r\n]+/g, " ").trim();

return sendMessage(`/msg ${to} ${body}`);
return sendMessage(`/msg ${to} ${body}`, options);
}

async function sendCommandResponse(username, messageStr) {
Expand Down Expand Up @@ -11673,13 +11688,35 @@ body.host-panel-dragging * {
: prepareOutgoingMessage(messageStr);
const requireExclusiveGiveawayOwnership =
options?.requireExclusiveGiveawayOwnership === true;
const allowRehearsalPrivateOutput =
options?.rehearsalPrivateOutput === true;

if (REHEARSAL_MODE && !allowRehearsalPrivateOutput) {
const rehearsalHost = String(giveawayData?.host || getLoggedInUsername() || "").trim();
const rehearsalBody = formatRehearsalPrivateOutput(messageStr);

logEvent(
"Rehearsal chat redirected",
rehearsalHost
? `Private host output -> ${sanitizeNick(rehearsalHost)}: ${rehearsalBody}`
: `No host identity available; output suppressed: ${rehearsalBody}`
);

if (!rehearsalHost) {
if (DEBUG_SETTINGS.log_chat_messages || DEBUG_SETTINGS.verify_sendmessage) {
console.warn("[BON Giveaway rehearsal] host identity unavailable; output suppressed:", messageStr);
}
return true;
}

if (REHEARSAL_MODE) {
logEvent("Rehearsal chat suppressed", messageStr);
if (DEBUG_SETTINGS.log_chat_messages || DEBUG_SETTINGS.verify_sendmessage) {
console.debug("[BON Giveaway rehearsal] chat suppressed:", messageStr);
console.debug("[BON Giveaway rehearsal] redirecting output privately to host:", rehearsalHost, messageStr);
}
return true;

return sendPrivateMessage(rehearsalHost, rehearsalBody, {
rehearsalPrivateOutput: true,
requireExclusiveGiveawayOwnership
});
}
if (DEBUG_SETTINGS.disable_chat_output) return true;

Expand Down
4 changes: 3 additions & 1 deletion userscripts/giveaway/src/00-preamble.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// @name DarkPeers BONanza Giveaway | Maghuro Fork
// @namespace https://github.com/maghuro/unit3d-userscripts
// @description BON giveaways on DarkPeers with an optional direct contribution to the BON Pool
// @version 1.5.8
// @version 1.5.9
// @author 🤖 T.R.A.V.I.S., Maghuro & M.A.E.S.T.R.O.
// @homepageURL https://gist.github.com/maghuro/da2dbfec94951990cbc54e75a9aee318
// @updateURL https://gist.githubusercontent.com/maghuro/da2dbfec94951990cbc54e75a9aee318/raw/DarkPeers_BONanza_Giveaway.user.js
Expand Down Expand Up @@ -218,6 +218,8 @@
// - v1.5.8 makes the Rehearsal / Debug UI toggle honest under forced overrides:
// URL-forced rehearsal can be switched off from the UI, while a source-level
// DEBUG_SETTINGS.dry_run override is shown as forced instead of pretending to disable.
// - v1.5.9 routes rehearsal chat output privately to the host instead of suppressing
// it, while keeping winner/refund gifts and BON Pool contributions fully simulated.
//// DarkPeers BONanza fork created and maintained by T.R.A.V.I.S. for the DarkPeers staff.
// Further development and maintenance by Maghuro & M.A.E.S.T.R.O.

Expand Down
4 changes: 2 additions & 2 deletions userscripts/giveaway/src/04-ui-template.js
Original file line number Diff line number Diff line change
Expand Up @@ -338,12 +338,12 @@
<div class="settings-group__header">
<p class="settings-group__title">Safety & Testing</p>
</div>
<label class="settings-row" title="Rehearsal/debug mode suppresses userscript chat output and all BON-moving actions. Changing it reloads the page." for="rehearsalModeToggle">
<label class="settings-row" title="Rehearsal/debug mode sends userscript output privately to the host and simulates all BON-moving actions. Changing it reloads the page." for="rehearsalModeToggle">
<span class="settings-row__label">Rehearsal / Debug mode</span>
<input
type="checkbox"
id="rehearsalModeToggle"
title="No userscript chat output or BON transfers. Changing this setting reloads the page."
title="Userscript output is sent privately to the host; no BON is transferred. Changing this setting reloads the page."
class="settings-row__toggle"
>
</label>
Expand Down
4 changes: 2 additions & 2 deletions userscripts/giveaway/src/05-initialization.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@
if (REHEARSAL_MODE) {
setHostSafetyBanner(
"rehearsal",
"REHEARSAL MODE: userscript chat output, winner/refund gifts and BON Pool contributions are disabled.",
"REHEARSAL MODE: userscript output is sent privately to the host; winner/refund gifts and BON Pool contributions are simulated only.",
"danger"
);
console.warn("[BON Giveaway] REHEARSAL MODE active: no script chat output or BON-moving request will be sent.");
console.warn("[BON Giveaway] REHEARSAL MODE active: script output is redirected to a private host message; no BON-moving request will be sent.");
}

// ── Mutual exclusion with the original "Blutopia BON Giveaway" script ──
Expand Down
51 changes: 43 additions & 8 deletions userscripts/giveaway/src/12-utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -1421,11 +1421,24 @@
return str;
}
}
async function sendPrivateMessage(username, messageStr) {
function formatRehearsalPrivateOutput(messageStr) {
const raw = String(messageStr ?? "").replace(/[\r\n]+/g, " ").trim();
const privateMatch = raw.match(/^\/msg\s+(\S+)\s*(.*)$/i);

if (privateMatch) {
const intendedRecipient = sanitizeNick(privateMatch[1]);
const body = String(privateMatch[2] || "").trim();
return `[b][color=#ff3333][REHEARSAL][/color][/b] [i](would PM ${intendedRecipient})[/i]${body ? ` ${body}` : ""}`;
}

return `[b][color=#ff3333][REHEARSAL][/color][/b]${raw ? ` ${raw}` : ""}`;
}

async function sendPrivateMessage(username, messageStr, options = {}) {
const to = String(username || "").trim();
if (!to) {
// No target → fall back to normal chat output
return sendMessage(messageStr);
// No target -> fall back to the normal output path.
return sendMessage(messageStr, options);
}

let body = String(messageStr ?? "");
Expand All @@ -1439,7 +1452,7 @@
// Avoid newlines which can confuse slash-command parsers
body = body.replace(/[\r\n]+/g, " ").trim();

return sendMessage(`/msg ${to} ${body}`);
return sendMessage(`/msg ${to} ${body}`, options);
}

async function sendCommandResponse(username, messageStr) {
Expand Down Expand Up @@ -1529,13 +1542,35 @@
: prepareOutgoingMessage(messageStr);
const requireExclusiveGiveawayOwnership =
options?.requireExclusiveGiveawayOwnership === true;
const allowRehearsalPrivateOutput =
options?.rehearsalPrivateOutput === true;

if (REHEARSAL_MODE && !allowRehearsalPrivateOutput) {
const rehearsalHost = String(giveawayData?.host || getLoggedInUsername() || "").trim();
const rehearsalBody = formatRehearsalPrivateOutput(messageStr);

logEvent(
"Rehearsal chat redirected",
rehearsalHost
? `Private host output -> ${sanitizeNick(rehearsalHost)}: ${rehearsalBody}`
: `No host identity available; output suppressed: ${rehearsalBody}`
);

if (!rehearsalHost) {
if (DEBUG_SETTINGS.log_chat_messages || DEBUG_SETTINGS.verify_sendmessage) {
console.warn("[BON Giveaway rehearsal] host identity unavailable; output suppressed:", messageStr);
}
return true;
}

if (REHEARSAL_MODE) {
logEvent("Rehearsal chat suppressed", messageStr);
if (DEBUG_SETTINGS.log_chat_messages || DEBUG_SETTINGS.verify_sendmessage) {
console.debug("[BON Giveaway rehearsal] chat suppressed:", messageStr);
console.debug("[BON Giveaway rehearsal] redirecting output privately to host:", rehearsalHost, messageStr);
}
return true;

return sendPrivateMessage(rehearsalHost, rehearsalBody, {
rehearsalPrivateOutput: true,
requireExclusiveGiveawayOwnership
});
Comment thread
maghuro marked this conversation as resolved.
}
if (DEBUG_SETTINGS.disable_chat_output) return true;

Expand Down
Loading