diff --git a/.github/workflows/publish-bonanza-gist.yml b/.github/workflows/publish-bonanza-gist.yml index a63ead9..d76b10e 100644 --- a/.github/workflows/publish-bonanza-gist.yml +++ b/.github/workflows/publish-bonanza-gist.yml @@ -8,6 +8,7 @@ on: - userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js - userscripts/giveaway/src/** - scripts/build-bonanza.mjs + - .github/workflows/publish-bonanza-gist.yml workflow_dispatch: permissions: @@ -27,12 +28,57 @@ jobs: git diff --exit-code -- userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js node --check userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js - - name: Publish userscript to Gist + - name: Generate userscript update metadata + env: + SOURCE_FILE: userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js + META_FILE: /tmp/DarkPeers_BONanza_Giveaway.meta.js + run: | + set -euo pipefail + + python3 - <<'PY' + import os + import re + from pathlib import Path + + source = Path(os.environ["SOURCE_FILE"]).read_text(encoding="utf-8") + metadata_keys = ( + "@name", + "@namespace", + "@version", + "@updateURL", + "@downloadURL", + ) + + selected = [] + for key in metadata_keys: + pattern = re.compile(rf"^//\s+{re.escape(key)}\s+.+$", re.MULTILINE) + matches = pattern.findall(source) + if len(matches) != 1: + raise SystemExit( + f"Expected exactly one {key} metadata line, found {len(matches)}" + ) + selected.append(matches[0]) + + meta = "// ==UserScript==\n" + "\n".join(selected) + "\n// ==/UserScript==\n" + meta_path = Path(os.environ["META_FILE"]) + meta_path.write_text(meta, encoding="utf-8") + + if "DarkPeers_BONanza_Giveaway.meta.js" not in selected[3]: + raise SystemExit("@updateURL does not point to the .meta.js update manifest") + if "DarkPeers_BONanza_Giveaway.user.js" not in selected[4]: + raise SystemExit("@downloadURL does not point to the full .user.js payload") + + print(meta, end="") + PY + + - name: Publish userscript and metadata to Gist env: GIST_TOKEN: ${{ secrets.BONANZA_GIST_TOKEN }} GIST_ID: ${{ vars.BONANZA_GIST_ID }} SOURCE_FILE: userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js - GIST_FILE: DarkPeers_BONanza_Giveaway.user.js + META_FILE: /tmp/DarkPeers_BONanza_Giveaway.meta.js + GIST_USER_FILE: DarkPeers_BONanza_Giveaway.user.js + GIST_META_FILE: DarkPeers_BONanza_Giveaway.meta.js run: | set -euo pipefail @@ -44,14 +90,15 @@ jobs: import os from pathlib import Path - source = Path(os.environ["SOURCE_FILE"]) - content = source.read_text(encoding="utf-8") + files = { + os.environ["GIST_USER_FILE"]: Path(os.environ["SOURCE_FILE"]).read_text(encoding="utf-8"), + os.environ["GIST_META_FILE"]: Path(os.environ["META_FILE"]).read_text(encoding="utf-8"), + } payload = { "files": { - os.environ["GIST_FILE"]: { - "content": content, - } + name: {"content": content} + for name, content in files.items() } } @@ -75,19 +122,27 @@ jobs: import os from pathlib import Path - source = Path(os.environ["SOURCE_FILE"]).read_text(encoding="utf-8") + expected = { + os.environ["GIST_USER_FILE"]: Path(os.environ["SOURCE_FILE"]).read_text(encoding="utf-8"), + os.environ["GIST_META_FILE"]: Path(os.environ["META_FILE"]).read_text(encoding="utf-8"), + } response = json.loads(Path("/tmp/gist-response.json").read_text(encoding="utf-8")) - gist_file = os.environ["GIST_FILE"] - published = response.get("files", {}).get(gist_file) - if not published: - raise SystemExit(f"Gist update response does not contain {gist_file!r}") + for gist_file, wanted in expected.items(): + published = response.get("files", {}).get(gist_file) + + if not published: + raise SystemExit(f"Gist update response does not contain {gist_file!r}") - if published.get("truncated"): - raise SystemExit("Gist API returned truncated content; refusing an unverified publish") + if published.get("truncated"): + raise SystemExit( + f"Gist API returned truncated content for {gist_file}; refusing an unverified publish" + ) - if published.get("content") != source: - raise SystemExit("Published Gist content does not match the repository userscript") + if published.get("content") != wanted: + raise SystemExit( + f"Published Gist content for {gist_file} does not match the generated content" + ) - print(f"Published and verified {gist_file} in gist {os.environ['GIST_ID']}.") + print(f"Published and verified {gist_file} in gist {os.environ['GIST_ID']}.") PY diff --git a/.github/workflows/validate-bonanza.yml b/.github/workflows/validate-bonanza.yml index ae49643..f56d52f 100644 --- a/.github/workflows/validate-bonanza.yml +++ b/.github/workflows/validate-bonanza.yml @@ -7,6 +7,7 @@ on: - "scripts/build-bonanza.mjs" - "tests/bonanza-*.test.mjs" - ".github/workflows/validate-bonanza.yml" + - ".github/workflows/publish-bonanza-gist.yml" push: branches: - main @@ -15,6 +16,7 @@ on: - "scripts/build-bonanza.mjs" - "tests/bonanza-*.test.mjs" - ".github/workflows/validate-bonanza.yml" + - ".github/workflows/publish-bonanza-gist.yml" workflow_dispatch: permissions: diff --git a/tests/bonanza-review-hardening.test.mjs b/tests/bonanza-review-hardening.test.mjs index e61e8de..b864bca 100644 --- a/tests/bonanza-review-hardening.test.mjs +++ b/tests/bonanza-review-hardening.test.mjs @@ -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\.9$/m); + assert.match(source, /^\/\/ @version\s+1\.5\.10$/m); assert.doesNotMatch(source, /pollChatFallback/); assert.doesNotMatch(source, /onlyguardians/i); assert.match(source, /async function getLatestMainChatReplayBoundary\(\)/); @@ -28,6 +28,39 @@ test("review hardening invariants stay present", () => { ); }); +test("public update metadata is split from the install payload", () => { + const header = source.match(/\/\/ ==UserScript==[\s\S]*?\/\/ ==\/UserScript==/)?.[0] || ""; + + assert.match(header, /^\/\/ @namespace\s+https:\/\/darkpeers\.org\/users\/maghuro$/m); + assert.match(header, /^\/\/ @homepageURL\s+https:\/\/darkpeers\.org\/users\/maghuro$/m); + assert.match( + header, + /^\/\/ @updateURL\s+https:\/\/gist\.githubusercontent\.com\/maghuro\/da2dbfec94951990cbc54e75a9aee318\/raw\/DarkPeers_BONanza_Giveaway\.meta\.js$/m + ); + assert.match( + header, + /^\/\/ @downloadURL\s+https:\/\/gist\.githubusercontent\.com\/maghuro\/da2dbfec94951990cbc54e75a9aee318\/raw\/DarkPeers_BONanza_Giveaway\.user\.js$/m + ); + assert.doesNotMatch(header, /github\.com\/maghuro\/unit3d-userscripts/); + assert.match(source, /v1\.5\.10 intentionally changes @namespace/); +}); + +test("Gist workflow generates and verifies a minimal .meta.js manifest", () => { + const workflow = readFileSync( + new URL("../.github/workflows/publish-bonanza-gist.yml", import.meta.url), + "utf8" + ); + + assert.match(workflow, /GIST_META_FILE: DarkPeers_BONanza_Giveaway\.meta\.js/); + assert.match(workflow, /META_FILE: \/tmp\/DarkPeers_BONanza_Giveaway\.meta\.js/); + assert.match( + workflow, + /metadata_keys = \([\s\S]*?"@name"[\s\S]*?"@namespace"[\s\S]*?"@version"[\s\S]*?"@updateURL"[\s\S]*?"@downloadURL"[\s\S]*?\)/ + ); + assert.match(workflow, /Gist update response does not contain/); + assert.match(workflow, /Published Gist content for \{gist_file\} does not match the generated content/); +}); + test("winner-count commands are host-only", () => { const winners = source.match(/winners\(ctx\) \{[\s\S]*?\n\s*\},\n\n\s*maxwinners\(ctx\)/)?.[0] || ""; const maxWinners = source.match(/maxwinners\(ctx\) \{[\s\S]*?\n\s*\},\n\n\s*scale\(ctx\)/)?.[0] || ""; @@ -180,6 +213,33 @@ test("rehearsal toggle handles forced overrides honestly", () => { }); +test("rehearsal statements keep simulated transfers distinct from live confirmations", () => { + assert.match(source, /rehearsalMode: REHEARSAL_MODE/); + assert.match(source, /"dry-run": "simulated \(no BON sent\)"/); + assert.match(source, /isRehearsalStatementRecord\(targetStatement\)/); + assert.match(source, /REHEARSAL \/ SIMULATION \(no BON-moving requests sent\)/); + assert.match(source, /const rehearsalPool = poolResult\.dryRun === true/); + assert.match(source, /simulated only \(no BON Pool contribution sent\)/); + assert.match(source, /REHEARSAL:[\s\S]*?No BON was sent\./); + assert.match(source, /noEntryPoolResult\.dryRun/); + assert.match(source, /simulated only \(zero entrants; no BON Pool contribution sent\)/); + assert.match(source, /const finalPoolStatus = donationActive[\s\S]*?poolResult\.dryRun[\s\S]*?simulated only \(no BON Pool contribution sent\)/); + assert.match(source, /currentStatement\.donationStatus = finalPoolStatus/); + assert.match(source, /currentStatement\.verification = poolResult\.dryRun[\s\S]*?rehearsal simulation complete; no BON-moving requests sent/); +}); + +test("legacy rehearsal statements inherit rehearsal context and lose false live confirmations", () => { + assert.match(source, /function normalizeStatementRecord\(record\)/); + assert.match(source, /const hasExplicitMode = typeof normalized\.rehearsalMode === "boolean"/); + assert.match(source, /if \(!hasExplicitMode\) \{[\s\S]*?normalized\.rehearsalMode = REHEARSAL_MODE/); + assert.match(source, /legacy rehearsal; no BON Pool contribution sent/); + assert.match(source, /normalized\.verification === "nothing to verify"/); + assert.match(source, /normalized\.verification === "all gifts confirmed in DarkPeers"/); + assert.match(source, /return Array\.isArray\(arr\) \? arr\.map\(normalizeStatementRecord\) : \[\]/); + assert.match(source, /function isRehearsalStatementRecord\(record\)/); + assert.match(source, /typeof record\.rehearsalMode === "boolean"[\s\S]*?\? record\.rehearsalMode[\s\S]*?: REHEARSAL_MODE/); +}); + test("rehearsal persistent state is isolated from live giveaway state", () => { assert.match(source, /const REHEARSAL_STORAGE_SUFFIX = REHEARSAL_MODE \? "::rehearsal" : ""/); assert.match(source, /BONANZA_GIVEAWAY_STATS_v2::\$\{location\.hostname\}\$\{REHEARSAL_STORAGE_SUFFIX\}/); diff --git a/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js b/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js index 30cae00..61c7fb1 100644 --- a/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js +++ b/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js @@ -1,11 +1,11 @@ // ==UserScript== // @name DarkPeers BONanza Giveaway | Maghuro Fork -// @namespace https://github.com/maghuro/unit3d-userscripts +// @namespace https://darkpeers.org/users/maghuro // @description BON giveaways on DarkPeers with an optional direct contribution to the BON Pool -// @version 1.5.9 +// @version 1.5.10 // @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 +// @homepageURL https://darkpeers.org/users/maghuro +// @updateURL https://gist.githubusercontent.com/maghuro/da2dbfec94951990cbc54e75a9aee318/raw/DarkPeers_BONanza_Giveaway.meta.js // @downloadURL https://gist.githubusercontent.com/maghuro/da2dbfec94951990cbc54e75a9aee318/raw/DarkPeers_BONanza_Giveaway.user.js // @icon https://darkpeers.org/img/logo.png // @grant GM_getValue @@ -15,6 +15,9 @@ // @run-at document-idle // ==/UserScript== +// NOTE: v1.5.10 intentionally changes @namespace. This release is treated as a +// fresh userscript identity rather than an in-place identity migration. + // DarkPeers-only fork of "Blutopia BON Giveaway" v6.2.2 by Nums (GPL-3.0-or-later). // Changes in this fork: // - All non-DarkPeers tracker support, the upload.cx extra commands and the @@ -220,6 +223,14 @@ // 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. +// - v1.5.10 separates update metadata from the install payload: @updateURL now uses +// a minimal .meta.js published beside the full .user.js, while @downloadURL keeps +// fetching the full userscript. @homepageURL and @namespace now point to Maghuro's +// DarkPeers profile; v1.5.10 is intentionally treated as a fresh userscript identity. +// Rehearsal statements and settlement messages now label simulated transfers +// explicitly, preserve that status through finalization, and infer/sanitize +// rehearsal-only v1.5.9 statements so dry-run success cannot be mistaken for +// proof of a real BON movement. //// 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. @@ -8562,11 +8573,14 @@ body.host-panel-dragging * { noEntryPoolResult = await contributeBonPool(noEntryTotal); if (noEntryPoolResult.confirmed) { + const zeroEntryPoolMessage = noEntryPoolResult.dryRun + ? `[b][color=#FFC00A]REHEARSAL:[/color][/b] ${fmtBONCurrency(noEntryTotal)} BON full-pot contribution simulated. No BON was sent to the ${BONANZA.FUND_NAME}.` + : `${bridgeMarker(BRIDGE_MARKERS.POOL_PAID, "๐Ÿ’™", "pool")} ` + + `[b][color=${BONANZA.GIVEAWAY_COLOR}]${BONANZA.FUND_NAME} contribution confirmed:[/color][/b] ` + + `[b][color=${BONANZA.GIVEAWAY_COLOR}]${fmtBONCurrency(noEntryTotal)} BON[/color][/b] paid directly into the pool.\n` + + `No entrants. 100% of the pot was contributed. โœจ`; if (!(await sendSettlementMessage( - `${bridgeMarker(BRIDGE_MARKERS.POOL_PAID, "๐Ÿ’™", "pool")} ` + - `[b][color=${BONANZA.GIVEAWAY_COLOR}]${BONANZA.FUND_NAME} contribution confirmed:[/color][/b] ` + - `[b][color=${BONANZA.GIVEAWAY_COLOR}]${fmtBONCurrency(noEntryTotal)} BON[/color][/b] paid directly into the pool.\n` + - `No entrants. 100% of the pot was contributed. โœจ`, + zeroEntryPoolMessage, "zero-entry BON Pool confirmation", "zero-entry-pool-confirmation" ))) return; @@ -8607,7 +8621,7 @@ body.host-panel-dragging * { logEvent( "Giveaway ended", - `Entrants=0 | Winners=0 | Host-funded=${fmtBONCurrency(noEntryHostFunded)} BON | Sponsored=${fmtBONCurrency(finalSponsoredTotal)} BON | Total=${fmtBONCurrency(noEntryTotal)} BON | BON Pool=${fmtBONCurrency(noEntryTotal)} BON (100%, ${noEntryPoolResult.confirmed ? "confirmed" : "NOT CONFIRMED"})` + `Entrants=0 | Winners=0 | Host-funded=${fmtBONCurrency(noEntryHostFunded)} BON | Sponsored=${fmtBONCurrency(finalSponsoredTotal)} BON | Total=${fmtBONCurrency(noEntryTotal)} BON | BON Pool=${fmtBONCurrency(noEntryTotal)} BON (100%, ${noEntryPoolResult.dryRun ? "simulated" : (noEntryPoolResult.confirmed ? "confirmed" : "NOT CONFIRMED")})` ); const noEntryDonationInfo = { @@ -8636,16 +8650,20 @@ body.host-panel-dragging * { net: [], donations: [], split: noEntrySplit, - poolStatus: noEntryPoolResult.confirmed - ? "confirmed directly in BON Pool (zero entrants, 100% of pot)" - : "NOT CONFIRMED, zero-entry full pot requires manual /bon-pool check", + poolStatus: noEntryPoolResult.dryRun + ? "simulated only (zero entrants; no BON Pool contribution sent)" + : noEntryPoolResult.confirmed + ? "confirmed directly in BON Pool (zero entrants, 100% of pot)" + : "NOT CONFIRMED, zero-entry full pot requires manual /bon-pool check", entrants: 0, refunds: [] }); if (currentStatement) { - currentStatement.verification = noEntryPoolResult.confirmed - ? "nothing to verify" - : "BON Pool contribution requires manual verification"; + currentStatement.verification = noEntryPoolResult.dryRun + ? "rehearsal simulation complete; no BON-moving requests sent" + : noEntryPoolResult.confirmed + ? "nothing to verify" + : "BON Pool contribution requires manual verification"; persistCurrentStatement(); } } catch (e) { /* statements are best-effort */ } @@ -9235,7 +9253,9 @@ body.host-panel-dragging * { } if (currentStatement && !expectedGifts.length) { - currentStatement.verification = "nothing to verify"; + currentStatement.verification = REHEARSAL_MODE + ? "rehearsal simulation complete; no winner gift required" + : "nothing to verify"; persistCurrentStatement(); } @@ -9247,15 +9267,24 @@ body.host-panel-dragging * { poolResult = await contributeBonPool(split.total); donationInfo.confirmed = !!poolResult.confirmed; if (poolResult.confirmed) { + const rehearsalPool = poolResult.dryRun === true; markFundGiftStatus("confirmed"); if (currentStatement) { - currentStatement.donationStatus = "confirmed directly in BON Pool"; + currentStatement.donationStatus = rehearsalPool + ? "simulated only (no BON Pool contribution sent)" + : "confirmed directly in BON Pool"; + if (rehearsalPool) { + currentStatement.verification = + "rehearsal simulation complete; no BON-moving requests sent"; + } currentStatement.endedAt = Date.now(); persistCurrentStatement(); } - const paidMessage = riggedMode - ? `${bridgeMarker(BRIDGE_MARKERS.TAXES_PAID, "๐Ÿงพ")} [b][color=#FF4F9A]TAXES PAID:[/color][/b] [b][color=#FFC00A]${fmtBONCurrency(split.total)} BON[/color][/b] successfully paid directly into the [b]${BONANZA.FUND_NAME}[/b]. The taxman is satisfied. ๐Ÿ˜ˆ` - : `${bridgeMarker(BRIDGE_MARKERS.POOL_PAID, "๐Ÿ’™")} [b][color=${BONANZA.GIVEAWAY_COLOR}]${BONANZA.FUND_NAME} contribution confirmed:[/color][/b] [b][color=${BONANZA.GIVEAWAY_COLOR}]${fmtBONCurrency(split.total)} BON[/color][/b] paid directly into the pool.\nThank you for supporting the event! โœจ`; + const paidMessage = rehearsalPool + ? `[b][color=#FFC00A]REHEARSAL:[/color][/b] ${fmtBONCurrency(split.total)} BON ${riggedMode ? "Rigged Taxes payment" : BONANZA.FUND_NAME + " contribution"} simulated. No BON was sent.` + : riggedMode + ? `${bridgeMarker(BRIDGE_MARKERS.TAXES_PAID, "๐Ÿงพ")} [b][color=#FF4F9A]TAXES PAID:[/color][/b] [b][color=#FFC00A]${fmtBONCurrency(split.total)} BON[/color][/b] successfully paid directly into the [b]${BONANZA.FUND_NAME}[/b]. The taxman is satisfied. ๐Ÿ˜ˆ` + : `${bridgeMarker(BRIDGE_MARKERS.POOL_PAID, "๐Ÿ’™")} [b][color=${BONANZA.GIVEAWAY_COLOR}]${BONANZA.FUND_NAME} contribution confirmed:[/color][/b] [b][color=${BONANZA.GIVEAWAY_COLOR}]${fmtBONCurrency(split.total)} BON[/color][/b] paid directly into the pool.\nThank you for supporting the event! โœจ`; if (!(await sendSettlementMessage( paidMessage, "BON Pool confirmation", @@ -9289,20 +9318,36 @@ body.host-panel-dragging * { } } catch (e) { /* ignore stats errors */ } try { + const finalPoolStatus = donationActive + ? ( + poolResult.dryRun + ? "simulated only (no BON Pool contribution sent)" + : ( + poolResult.confirmed + ? "confirmed directly in BON Pool" + : "NOT CONFIRMED, check /bon-pool manually" + ) + ) + : "none"; + if (!currentStatement) { currentStatement = createStatementRecord({ winners, gross: allocated, net, donations: split.donations, split, - poolStatus: donationActive - ? (poolResult.confirmed ? "confirmed directly in BON Pool" : "NOT CONFIRMED, check /bon-pool manually") - : "none", + poolStatus: finalPoolStatus, entrants: entrantsTotal }); } if (currentStatement) { - currentStatement.donationStatus = donationActive - ? (poolResult.confirmed ? "confirmed directly in BON Pool" : "NOT CONFIRMED, check /bon-pool manually") - : "none"; - if (!expectedGifts.length) currentStatement.verification = "nothing to verify"; + currentStatement.donationStatus = finalPoolStatus; + if (!expectedGifts.length) { + currentStatement.verification = poolResult.dryRun + ? "rehearsal simulation complete; no BON-moving requests sent" + : ( + REHEARSAL_MODE + ? "rehearsal simulation complete; no winner gift required" + : "nothing to verify" + ); + } currentStatement.endedAt = Date.now(); persistCurrentStatement(); } @@ -10373,11 +10418,52 @@ body.host-panel-dragging * { let currentStatement = null; // record for the giveaway that just ended + function normalizeStatementRecord(record) { + if (!record || typeof record !== "object") return record; + + const normalized = { ...record }; + const hasExplicitMode = typeof normalized.rehearsalMode === "boolean"; + + // v1.5.9 statements predate rehearsalMode but already lived in a + // rehearsal-specific storage namespace. Therefore the current storage + // context is authoritative when the field is absent. + if (!hasExplicitMode) { + normalized.rehearsalMode = REHEARSAL_MODE; + } + + if (normalized.rehearsalMode && !hasExplicitMode) { + if ( + normalized.donationTotal > 0 && + /^confirmed directly in BON Pool/.test(String(normalized.donationStatus || "")) + ) { + normalized.donationStatus = + "simulated only (legacy rehearsal; no BON Pool contribution sent)"; + } + + if ( + normalized.verification === "nothing to verify" || + normalized.verification === "all gifts confirmed in DarkPeers" + ) { + normalized.verification = + "rehearsal simulation complete; no BON-moving requests sent"; + } + } + + return normalized; + } + + function isRehearsalStatementRecord(record) { + if (!record || typeof record !== "object") return false; + return typeof record.rehearsalMode === "boolean" + ? record.rehearsalMode + : REHEARSAL_MODE; + } + function readStatements() { try { const raw = localStorage.getItem(LS_STATEMENTS); const arr = raw ? JSON.parse(raw) : []; - return Array.isArray(arr) ? arr : []; + return Array.isArray(arr) ? arr.map(normalizeStatementRecord) : []; } catch { return []; } } @@ -10474,7 +10560,9 @@ body.host-panel-dragging * { gross: p.gross[i], donation: p.donations[i], net: p.net[i], - status: (normalizeUserKey(w.author) === hostKey) ? "self (host, no gift sent)" : "sent, awaiting confirmation" + status: (normalizeUserKey(w.author) === hostKey) + ? (REHEARSAL_MODE ? "simulated self (host, no gift sent)" : "self (host, no gift sent)") + : "sent, awaiting confirmation" })); const pct = p.split ? p.split.percent : 0; @@ -10490,6 +10578,7 @@ body.host-panel-dragging * { return { id: getActiveGiveawayId() || Date.now(), scriptVersion: SCRIPT_VERSION, + rehearsalMode: REHEARSAL_MODE, site: location.hostname, host: data.host, startedAt: giveawayStartTime ? giveawayStartTime.getTime() : null, @@ -10524,6 +10613,7 @@ body.host-panel-dragging * { "confirmed-history": "confirmed in Gift History", "confirmed-system": "confirmed in System room", "observed-system": "seen in System room; awaiting Gift History", + "dry-run": "simulated (no BON sent)", failed: "NOT CONFIRMED, check manually", self: "self (host, no gift sent)" })[status] || status; @@ -10544,9 +10634,17 @@ body.host-panel-dragging * { function finalizeStatementVerification(ok, missingCount, statementId = null) { const targetStatement = getStatementRecordById(statementId); if (!targetStatement) return; - targetStatement.verification = ok - ? "all gifts confirmed in DarkPeers" - : `${missingCount} gift(s) could not be confirmed`; + targetStatement.verification = isRehearsalStatementRecord(targetStatement) + ? ( + ok + ? "rehearsal simulation complete; no BON-moving requests sent" + : `rehearsal simulation incomplete; ${missingCount} simulated gift(s) unresolved` + ) + : ( + ok + ? "all gifts confirmed in DarkPeers" + : `${missingCount} gift(s) could not be confirmed` + ); persistStatementRecord(targetStatement); } @@ -10564,6 +10662,7 @@ body.host-panel-dragging * { L.push(`Giveaway ID : ${rec.id}`); L.push(`Site : ${rec.site}`); L.push(`Host : ${rec.host}`); + L.push(`Mode : ${isRehearsalStatementRecord(rec) ? "REHEARSAL / SIMULATION (no BON-moving requests sent)" : "LIVE"}`); L.push(`Started : ${rec.startedAt ? statementTimestamp(rec.startedAt) : "n/a"}`); L.push(`Ended : ${statementTimestamp(rec.endedAt)}`); L.push(`Number range : ${rec.range[0]} - ${rec.range[1]}`); diff --git a/userscripts/giveaway/src/00-preamble.js b/userscripts/giveaway/src/00-preamble.js index 5b6d3ba..1984355 100644 --- a/userscripts/giveaway/src/00-preamble.js +++ b/userscripts/giveaway/src/00-preamble.js @@ -1,11 +1,11 @@ // ==UserScript== // @name DarkPeers BONanza Giveaway | Maghuro Fork -// @namespace https://github.com/maghuro/unit3d-userscripts +// @namespace https://darkpeers.org/users/maghuro // @description BON giveaways on DarkPeers with an optional direct contribution to the BON Pool -// @version 1.5.9 +// @version 1.5.10 // @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 +// @homepageURL https://darkpeers.org/users/maghuro +// @updateURL https://gist.githubusercontent.com/maghuro/da2dbfec94951990cbc54e75a9aee318/raw/DarkPeers_BONanza_Giveaway.meta.js // @downloadURL https://gist.githubusercontent.com/maghuro/da2dbfec94951990cbc54e75a9aee318/raw/DarkPeers_BONanza_Giveaway.user.js // @icon https://darkpeers.org/img/logo.png // @grant GM_getValue @@ -15,6 +15,9 @@ // @run-at document-idle // ==/UserScript== +// NOTE: v1.5.10 intentionally changes @namespace. This release is treated as a +// fresh userscript identity rather than an in-place identity migration. + // DarkPeers-only fork of "Blutopia BON Giveaway" v6.2.2 by Nums (GPL-3.0-or-later). // Changes in this fork: // - All non-DarkPeers tracker support, the upload.cx extra commands and the @@ -220,6 +223,14 @@ // 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. +// - v1.5.10 separates update metadata from the install payload: @updateURL now uses +// a minimal .meta.js published beside the full .user.js, while @downloadURL keeps +// fetching the full userscript. @homepageURL and @namespace now point to Maghuro's +// DarkPeers profile; v1.5.10 is intentionally treated as a fresh userscript identity. +// Rehearsal statements and settlement messages now label simulated transfers +// explicitly, preserve that status through finalization, and infer/sanitize +// rehearsal-only v1.5.9 statements so dry-run success cannot be mistaken for +// proof of a real BON movement. //// 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. diff --git a/userscripts/giveaway/src/11-winner-selection-payouts.js b/userscripts/giveaway/src/11-winner-selection-payouts.js index 71f1883..d752470 100644 --- a/userscripts/giveaway/src/11-winner-selection-payouts.js +++ b/userscripts/giveaway/src/11-winner-selection-payouts.js @@ -888,11 +888,14 @@ noEntryPoolResult = await contributeBonPool(noEntryTotal); if (noEntryPoolResult.confirmed) { + const zeroEntryPoolMessage = noEntryPoolResult.dryRun + ? `[b][color=#FFC00A]REHEARSAL:[/color][/b] ${fmtBONCurrency(noEntryTotal)} BON full-pot contribution simulated. No BON was sent to the ${BONANZA.FUND_NAME}.` + : `${bridgeMarker(BRIDGE_MARKERS.POOL_PAID, "๐Ÿ’™", "pool")} ` + + `[b][color=${BONANZA.GIVEAWAY_COLOR}]${BONANZA.FUND_NAME} contribution confirmed:[/color][/b] ` + + `[b][color=${BONANZA.GIVEAWAY_COLOR}]${fmtBONCurrency(noEntryTotal)} BON[/color][/b] paid directly into the pool.\n` + + `No entrants. 100% of the pot was contributed. โœจ`; if (!(await sendSettlementMessage( - `${bridgeMarker(BRIDGE_MARKERS.POOL_PAID, "๐Ÿ’™", "pool")} ` + - `[b][color=${BONANZA.GIVEAWAY_COLOR}]${BONANZA.FUND_NAME} contribution confirmed:[/color][/b] ` + - `[b][color=${BONANZA.GIVEAWAY_COLOR}]${fmtBONCurrency(noEntryTotal)} BON[/color][/b] paid directly into the pool.\n` + - `No entrants. 100% of the pot was contributed. โœจ`, + zeroEntryPoolMessage, "zero-entry BON Pool confirmation", "zero-entry-pool-confirmation" ))) return; @@ -933,7 +936,7 @@ logEvent( "Giveaway ended", - `Entrants=0 | Winners=0 | Host-funded=${fmtBONCurrency(noEntryHostFunded)} BON | Sponsored=${fmtBONCurrency(finalSponsoredTotal)} BON | Total=${fmtBONCurrency(noEntryTotal)} BON | BON Pool=${fmtBONCurrency(noEntryTotal)} BON (100%, ${noEntryPoolResult.confirmed ? "confirmed" : "NOT CONFIRMED"})` + `Entrants=0 | Winners=0 | Host-funded=${fmtBONCurrency(noEntryHostFunded)} BON | Sponsored=${fmtBONCurrency(finalSponsoredTotal)} BON | Total=${fmtBONCurrency(noEntryTotal)} BON | BON Pool=${fmtBONCurrency(noEntryTotal)} BON (100%, ${noEntryPoolResult.dryRun ? "simulated" : (noEntryPoolResult.confirmed ? "confirmed" : "NOT CONFIRMED")})` ); const noEntryDonationInfo = { @@ -962,16 +965,20 @@ net: [], donations: [], split: noEntrySplit, - poolStatus: noEntryPoolResult.confirmed - ? "confirmed directly in BON Pool (zero entrants, 100% of pot)" - : "NOT CONFIRMED, zero-entry full pot requires manual /bon-pool check", + poolStatus: noEntryPoolResult.dryRun + ? "simulated only (zero entrants; no BON Pool contribution sent)" + : noEntryPoolResult.confirmed + ? "confirmed directly in BON Pool (zero entrants, 100% of pot)" + : "NOT CONFIRMED, zero-entry full pot requires manual /bon-pool check", entrants: 0, refunds: [] }); if (currentStatement) { - currentStatement.verification = noEntryPoolResult.confirmed - ? "nothing to verify" - : "BON Pool contribution requires manual verification"; + currentStatement.verification = noEntryPoolResult.dryRun + ? "rehearsal simulation complete; no BON-moving requests sent" + : noEntryPoolResult.confirmed + ? "nothing to verify" + : "BON Pool contribution requires manual verification"; persistCurrentStatement(); } } catch (e) { /* statements are best-effort */ } @@ -1561,7 +1568,9 @@ } if (currentStatement && !expectedGifts.length) { - currentStatement.verification = "nothing to verify"; + currentStatement.verification = REHEARSAL_MODE + ? "rehearsal simulation complete; no winner gift required" + : "nothing to verify"; persistCurrentStatement(); } @@ -1573,15 +1582,24 @@ poolResult = await contributeBonPool(split.total); donationInfo.confirmed = !!poolResult.confirmed; if (poolResult.confirmed) { + const rehearsalPool = poolResult.dryRun === true; markFundGiftStatus("confirmed"); if (currentStatement) { - currentStatement.donationStatus = "confirmed directly in BON Pool"; + currentStatement.donationStatus = rehearsalPool + ? "simulated only (no BON Pool contribution sent)" + : "confirmed directly in BON Pool"; + if (rehearsalPool) { + currentStatement.verification = + "rehearsal simulation complete; no BON-moving requests sent"; + } currentStatement.endedAt = Date.now(); persistCurrentStatement(); } - const paidMessage = riggedMode - ? `${bridgeMarker(BRIDGE_MARKERS.TAXES_PAID, "๐Ÿงพ")} [b][color=#FF4F9A]TAXES PAID:[/color][/b] [b][color=#FFC00A]${fmtBONCurrency(split.total)} BON[/color][/b] successfully paid directly into the [b]${BONANZA.FUND_NAME}[/b]. The taxman is satisfied. ๐Ÿ˜ˆ` - : `${bridgeMarker(BRIDGE_MARKERS.POOL_PAID, "๐Ÿ’™")} [b][color=${BONANZA.GIVEAWAY_COLOR}]${BONANZA.FUND_NAME} contribution confirmed:[/color][/b] [b][color=${BONANZA.GIVEAWAY_COLOR}]${fmtBONCurrency(split.total)} BON[/color][/b] paid directly into the pool.\nThank you for supporting the event! โœจ`; + const paidMessage = rehearsalPool + ? `[b][color=#FFC00A]REHEARSAL:[/color][/b] ${fmtBONCurrency(split.total)} BON ${riggedMode ? "Rigged Taxes payment" : BONANZA.FUND_NAME + " contribution"} simulated. No BON was sent.` + : riggedMode + ? `${bridgeMarker(BRIDGE_MARKERS.TAXES_PAID, "๐Ÿงพ")} [b][color=#FF4F9A]TAXES PAID:[/color][/b] [b][color=#FFC00A]${fmtBONCurrency(split.total)} BON[/color][/b] successfully paid directly into the [b]${BONANZA.FUND_NAME}[/b]. The taxman is satisfied. ๐Ÿ˜ˆ` + : `${bridgeMarker(BRIDGE_MARKERS.POOL_PAID, "๐Ÿ’™")} [b][color=${BONANZA.GIVEAWAY_COLOR}]${BONANZA.FUND_NAME} contribution confirmed:[/color][/b] [b][color=${BONANZA.GIVEAWAY_COLOR}]${fmtBONCurrency(split.total)} BON[/color][/b] paid directly into the pool.\nThank you for supporting the event! โœจ`; if (!(await sendSettlementMessage( paidMessage, "BON Pool confirmation", @@ -1615,20 +1633,36 @@ } } catch (e) { /* ignore stats errors */ } try { + const finalPoolStatus = donationActive + ? ( + poolResult.dryRun + ? "simulated only (no BON Pool contribution sent)" + : ( + poolResult.confirmed + ? "confirmed directly in BON Pool" + : "NOT CONFIRMED, check /bon-pool manually" + ) + ) + : "none"; + if (!currentStatement) { currentStatement = createStatementRecord({ winners, gross: allocated, net, donations: split.donations, split, - poolStatus: donationActive - ? (poolResult.confirmed ? "confirmed directly in BON Pool" : "NOT CONFIRMED, check /bon-pool manually") - : "none", + poolStatus: finalPoolStatus, entrants: entrantsTotal }); } if (currentStatement) { - currentStatement.donationStatus = donationActive - ? (poolResult.confirmed ? "confirmed directly in BON Pool" : "NOT CONFIRMED, check /bon-pool manually") - : "none"; - if (!expectedGifts.length) currentStatement.verification = "nothing to verify"; + currentStatement.donationStatus = finalPoolStatus; + if (!expectedGifts.length) { + currentStatement.verification = poolResult.dryRun + ? "rehearsal simulation complete; no BON-moving requests sent" + : ( + REHEARSAL_MODE + ? "rehearsal simulation complete; no winner gift required" + : "nothing to verify" + ); + } currentStatement.endedAt = Date.now(); persistCurrentStatement(); } diff --git a/userscripts/giveaway/src/12-utilities.js b/userscripts/giveaway/src/12-utilities.js index fd5755b..ae79a7f 100644 --- a/userscripts/giveaway/src/12-utilities.js +++ b/userscripts/giveaway/src/12-utilities.js @@ -227,11 +227,52 @@ let currentStatement = null; // record for the giveaway that just ended + function normalizeStatementRecord(record) { + if (!record || typeof record !== "object") return record; + + const normalized = { ...record }; + const hasExplicitMode = typeof normalized.rehearsalMode === "boolean"; + + // v1.5.9 statements predate rehearsalMode but already lived in a + // rehearsal-specific storage namespace. Therefore the current storage + // context is authoritative when the field is absent. + if (!hasExplicitMode) { + normalized.rehearsalMode = REHEARSAL_MODE; + } + + if (normalized.rehearsalMode && !hasExplicitMode) { + if ( + normalized.donationTotal > 0 && + /^confirmed directly in BON Pool/.test(String(normalized.donationStatus || "")) + ) { + normalized.donationStatus = + "simulated only (legacy rehearsal; no BON Pool contribution sent)"; + } + + if ( + normalized.verification === "nothing to verify" || + normalized.verification === "all gifts confirmed in DarkPeers" + ) { + normalized.verification = + "rehearsal simulation complete; no BON-moving requests sent"; + } + } + + return normalized; + } + + function isRehearsalStatementRecord(record) { + if (!record || typeof record !== "object") return false; + return typeof record.rehearsalMode === "boolean" + ? record.rehearsalMode + : REHEARSAL_MODE; + } + function readStatements() { try { const raw = localStorage.getItem(LS_STATEMENTS); const arr = raw ? JSON.parse(raw) : []; - return Array.isArray(arr) ? arr : []; + return Array.isArray(arr) ? arr.map(normalizeStatementRecord) : []; } catch { return []; } } @@ -328,7 +369,9 @@ gross: p.gross[i], donation: p.donations[i], net: p.net[i], - status: (normalizeUserKey(w.author) === hostKey) ? "self (host, no gift sent)" : "sent, awaiting confirmation" + status: (normalizeUserKey(w.author) === hostKey) + ? (REHEARSAL_MODE ? "simulated self (host, no gift sent)" : "self (host, no gift sent)") + : "sent, awaiting confirmation" })); const pct = p.split ? p.split.percent : 0; @@ -344,6 +387,7 @@ return { id: getActiveGiveawayId() || Date.now(), scriptVersion: SCRIPT_VERSION, + rehearsalMode: REHEARSAL_MODE, site: location.hostname, host: data.host, startedAt: giveawayStartTime ? giveawayStartTime.getTime() : null, @@ -378,6 +422,7 @@ "confirmed-history": "confirmed in Gift History", "confirmed-system": "confirmed in System room", "observed-system": "seen in System room; awaiting Gift History", + "dry-run": "simulated (no BON sent)", failed: "NOT CONFIRMED, check manually", self: "self (host, no gift sent)" })[status] || status; @@ -398,9 +443,17 @@ function finalizeStatementVerification(ok, missingCount, statementId = null) { const targetStatement = getStatementRecordById(statementId); if (!targetStatement) return; - targetStatement.verification = ok - ? "all gifts confirmed in DarkPeers" - : `${missingCount} gift(s) could not be confirmed`; + targetStatement.verification = isRehearsalStatementRecord(targetStatement) + ? ( + ok + ? "rehearsal simulation complete; no BON-moving requests sent" + : `rehearsal simulation incomplete; ${missingCount} simulated gift(s) unresolved` + ) + : ( + ok + ? "all gifts confirmed in DarkPeers" + : `${missingCount} gift(s) could not be confirmed` + ); persistStatementRecord(targetStatement); } @@ -418,6 +471,7 @@ L.push(`Giveaway ID : ${rec.id}`); L.push(`Site : ${rec.site}`); L.push(`Host : ${rec.host}`); + L.push(`Mode : ${isRehearsalStatementRecord(rec) ? "REHEARSAL / SIMULATION (no BON-moving requests sent)" : "LIVE"}`); L.push(`Started : ${rec.startedAt ? statementTimestamp(rec.startedAt) : "n/a"}`); L.push(`Ended : ${statementTimestamp(rec.endedAt)}`); L.push(`Number range : ${rec.range[0]} - ${rec.range[1]}`);