From 61de268bd885967d2ae48016c1d8161ff42b0899 Mon Sep 17 00:00:00 2001 From: Maghuro <1546139+maghuro@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:08:41 +0100 Subject: [PATCH 1/9] Bump BONanza to 1.5.12 --- userscripts/giveaway/src/00-preamble.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/userscripts/giveaway/src/00-preamble.js b/userscripts/giveaway/src/00-preamble.js index 310ae887..7a887730 100644 --- a/userscripts/giveaway/src/00-preamble.js +++ b/userscripts/giveaway/src/00-preamble.js @@ -2,7 +2,7 @@ // @name DarkPeers BONanza Giveaway | Maghuro Fork // @namespace https://darkpeers.org/users/maghuro // @description BON giveaways on DarkPeers with an optional direct contribution to the BON Pool -// @version 1.5.11 +// @version 1.5.12 // @author 🤖 T.R.A.V.I.S., Maghuro & M.A.E.S.T.R.O. // @homepageURL https://darkpeers.org/users/maghuro // @updateURL https://gist.githubusercontent.com/maghuro/da2dbfec94951990cbc54e75a9aee318/raw/DarkPeers_BONanza_Giveaway.meta.js @@ -231,6 +231,11 @@ // 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. +// - v1.5.12 adds best-effort post-settlement cleanup for unread BON gift +// notifications received during the giveaway window. It reuses UNIT3D's own +// per-notification CSRF-protected PATCH forms, never mass-marks unrelated +// notifications, and runs fire-and-forget so cleanup failure cannot block or +// roll back giveaway settlement. //// 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. From 700ca5019a934d74fe0dd2176a9f45f5a336f9ff Mon Sep 17 00:00:00 2001 From: Maghuro <1546139+maghuro@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:08:44 +0100 Subject: [PATCH 2/9] Add best-effort BON notification cleanup --- .../giveaway/src/09-sponsorship-polling.js | 155 +++++++++++++++++- 1 file changed, 154 insertions(+), 1 deletion(-) diff --git a/userscripts/giveaway/src/09-sponsorship-polling.js b/userscripts/giveaway/src/09-sponsorship-polling.js index 89ee8648..be2b8f8b 100644 --- a/userscripts/giveaway/src/09-sponsorship-polling.js +++ b/userscripts/giveaway/src/09-sponsorship-polling.js @@ -148,6 +148,27 @@ const rawNote = String(bodyMatch[3] || "").replace(/\s+/g, " ").trim(); const message = /^no note$/i.test(rawNote) ? "" : rawNote; + // UNIT3D renders one CSRF-protected PATCH form per notification. + // Preserve the form's own action/hidden fields so cleanup can mirror + // the site's supported "mark read" operation instead of guessing an API. + const markReadForm = Array.from(row.querySelectorAll('form[method="POST"], form[method="post"]')) + .find(form => + String(form.querySelector('input[name="_method"]')?.value || "").toUpperCase() === "PATCH" + ) || null; + const markReadButton = markReadForm?.querySelector("button"); + const markReadAction = String(markReadForm?.getAttribute("action") || "").trim(); + const markReadFields = markReadForm + ? Array.from(markReadForm.querySelectorAll("input[name]")) + .map(input => [ + String(input.getAttribute("name") || "").trim(), + String(input.value ?? "") + ]) + .filter(([name]) => !!name) + : []; + const unread = + cells[0].classList.contains("notification--unread") || + (!!markReadButton && !markReadButton.disabled); + return { sender, recipient: host, @@ -155,7 +176,10 @@ message, rawTimestamp, createdAtTs, - createdAtAltTs + createdAtAltTs, + unread, + markReadAction, + markReadFields }; }) .filter(item => @@ -166,6 +190,135 @@ ); } + function giftNotificationOverlapsWindow(notification, startTs, endTs) { + const start = Number(startTs); + const end = Number(endTs); + if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return false; + + const resolutionMs = unit3dTimestampResolutionMs(notification?.rawTimestamp); + const candidates = [ + Number(notification?.createdAtTs), + Number(notification?.createdAtAltTs) + ].filter(Number.isFinite); + + return candidates.some(ts => + (ts + resolutionMs) >= start && + ts <= (end + resolutionMs) + ); + } + + async function markGiveawayBonNotificationsRead({ hostName, startTs, endTs } = {}) { + // Cleanup is deliberately outside settlement correctness. It never runs in + // rehearsal mode and every failure is absorbed by this helper/caller. + if (REHEARSAL_MODE) { + return { matched: 0, marked: 0, failed: 0, skipped: "rehearsal" }; + } + + const host = String(hostName || "").trim(); + const start = Number(startTs); + const end = Number(endTs); + const senderSlug = getAuthenticatedUserSlug(); + if ( + !host || + !senderSlug || + !Number.isFinite(start) || + !Number.isFinite(end) || + end < start + ) { + return { matched: 0, marked: 0, failed: 0, skipped: "invalid-context" }; + } + + const notificationsPath = `/users/${encodeURIComponent(decodeURIComponent(senderSlug))}/notifications`; + const targetsByAction = new Map(); + const maxPages = 6; + + for (let page = 1; page <= maxPages; page++) { + try { + const notificationsUrl = new URL(notificationsPath, location.origin); + if (page > 1) notificationsUrl.searchParams.set("page", String(page)); + notificationsUrl.searchParams.set("_dpgw_cleanup", String(Date.now())); + + const res = await fetchWithTimeout( + notificationsUrl, + { + credentials: "same-origin", + cache: "no-store" + }, + 5000 + ); + if (!res?.ok) continue; + + const rows = parseGiftNotificationsPage(await res.text(), host); + for (const notification of rows) { + if (!notification.unread) continue; + if (!notification.markReadAction || !notification.markReadFields?.length) continue; + if (!giftNotificationOverlapsWindow(notification, start, end)) continue; + + let actionUrl; + try { + actionUrl = new URL(notification.markReadAction, location.origin); + } catch { + continue; + } + + // Never POST a parsed form away from DarkPeers or outside this + // authenticated user's notification routes. + if (actionUrl.origin !== location.origin) continue; + if (!actionUrl.pathname.startsWith(notificationsPath + "/")) continue; + + targetsByAction.set(actionUrl.href, { + actionUrl, + fields: notification.markReadFields + }); + } + } catch (e) { + console.warn(`[BON Giveaway] Notification cleanup page ${page} skipped:`, e); + } + } + + const targets = Array.from(targetsByAction.values()); + let marked = 0; + let failed = 0; + const concurrency = 4; + + for (let offset = 0; offset < targets.length; offset += concurrency) { + const batch = targets.slice(offset, offset + concurrency); + const results = await Promise.allSettled(batch.map(async target => { + const body = new URLSearchParams(); + for (const [name, value] of target.fields) body.append(name, value); + + const res = await fetchWithTimeout( + target.actionUrl, + { + method: "POST", + credentials: "same-origin", + cache: "no-store", + redirect: "follow", + headers: { + "Accept": "text/html", + "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" + }, + body: body.toString() + }, + 5000 + ); + if (!res?.ok) { + throw new Error(`HTTP ${res?.status || "unknown"}`); + } + })); + + for (const result of results) { + if (result.status === "fulfilled") marked += 1; + else failed += 1; + } + } + + console.info( + `[BON Giveaway] BON notification cleanup: matched=${targets.length}, marked=${marked}, failed=${failed}` + ); + return { matched: targets.length, marked, failed }; + } + function giftHistoryBaseKey(item) { const sender = normalizeUserKey(item?.sender); const recipient = normalizeUserKey(item?.recipient); From 4c233bb7926b44b359358c0c049f6a0cf48a377f Mon Sep 17 00:00:00 2001 From: Maghuro <1546139+maghuro@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:08:47 +0100 Subject: [PATCH 3/9] Run notification cleanup after settlement --- .../giveaway/src/11-winner-selection-payouts.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/userscripts/giveaway/src/11-winner-selection-payouts.js b/userscripts/giveaway/src/11-winner-selection-payouts.js index d7524702..b93208bb 100644 --- a/userscripts/giveaway/src/11-winner-selection-payouts.js +++ b/userscripts/giveaway/src/11-winner-selection-payouts.js @@ -1708,7 +1708,21 @@ giveawayData.settlement.completedAt = Date.now(); snapshotGiveaway({ force: true }); } - stopGiveaway(); + + // Capture the notification window before stopGiveaway() clears runtime + // state. Cleanup starts only after settlement is terminal and is explicitly + // fire-and-forget: notification failures can never block or roll back BON + // transfers, verification, statements, stats, or snapshot retirement. + const notificationCleanupContext = { + hostName: giveawayData?.host || "", + startTs: giveawayStartTime instanceof Date ? giveawayStartTime.getTime() : null, + endTs: settlementCutoffTs + }; + const stopped = stopGiveaway(); + if (stopped) { + void markGiveawayBonNotificationsRead(notificationCleanupContext) + .catch(e => console.warn("[BON Giveaway] BON notification cleanup failed:", e)); + } } function clearWinnersStatusUI() { From 073a879ee004628e885d56e5509abd4ba2d030e1 Mon Sep 17 00:00:00 2001 From: Maghuro <1546139+maghuro@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:08:50 +0100 Subject: [PATCH 4/9] Test non-blocking notification cleanup --- tests/bonanza-review-hardening.test.mjs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/bonanza-review-hardening.test.mjs b/tests/bonanza-review-hardening.test.mjs index 0879c8d6..6b038235 100644 --- a/tests/bonanza-review-hardening.test.mjs +++ b/tests/bonanza-review-hardening.test.mjs @@ -28,6 +28,23 @@ test("review hardening invariants stay present", () => { ); }); +test("BON gift notification cleanup is scoped and non-blocking", () => { + assert.match(source, /async function markGiveawayBonNotificationsRead\(\{ hostName, startTs, endTs \} = \{\}\)/); + assert.match(source, /giftNotificationOverlapsWindow\(notification, start, end\)/); + assert.match(source, /input\[name="_method"\][\s\S]*?PATCH/); + assert.match(source, /actionUrl\.pathname\.startsWith\(notificationsPath \+ "\/"\)/); + assert.doesNotMatch(source, /notifications\/mass-update/); + + const completion = source.indexOf('giveawayData.settlement.phase = "complete"'); + const stop = source.indexOf("const stopped = stopGiveaway()", completion); + const cleanup = source.indexOf("void markGiveawayBonNotificationsRead(notificationCleanupContext)", stop); + assert.ok(completion >= 0 && stop > completion && cleanup > stop); + assert.doesNotMatch( + source.slice(stop, cleanup + 100), + /await\s+markGiveawayBonNotificationsRead/ + ); +}); + test("public update metadata is split from the install payload", () => { const header = source.match(/\/\/ ==UserScript==[\s\S]*?\/\/ ==\/UserScript==/)?.[0] || ""; From 37fea3e1b51bdd96fade3ea5790008942bd6b2ce Mon Sep 17 00:00:00 2001 From: Maghuro <1546139+maghuro@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:09:14 +0100 Subject: [PATCH 5/9] Rebuild BONanza userscript --- .../DarkPeers_BONanza_Giveaway.user.js | 178 +++++++++++++++++- 1 file changed, 175 insertions(+), 3 deletions(-) diff --git a/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js b/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js index 2d74421a..47775091 100644 --- a/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js +++ b/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js @@ -2,7 +2,7 @@ // @name DarkPeers BONanza Giveaway | Maghuro Fork // @namespace https://darkpeers.org/users/maghuro // @description BON giveaways on DarkPeers with an optional direct contribution to the BON Pool -// @version 1.5.11 +// @version 1.5.12 // @author 🤖 T.R.A.V.I.S., Maghuro & M.A.E.S.T.R.O. // @homepageURL https://darkpeers.org/users/maghuro // @updateURL https://gist.githubusercontent.com/maghuro/da2dbfec94951990cbc54e75a9aee318/raw/DarkPeers_BONanza_Giveaway.meta.js @@ -231,6 +231,11 @@ // 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. +// - v1.5.12 adds best-effort post-settlement cleanup for unread BON gift +// notifications received during the giveaway window. It reuses UNIT3D's own +// per-notification CSRF-protected PATCH forms, never mass-marks unrelated +// notifications, and runs fire-and-forget so cleanup failure cannot block or +// roll back giveaway settlement. //// 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. @@ -5008,6 +5013,27 @@ body.host-panel-dragging * { const rawNote = String(bodyMatch[3] || "").replace(/\s+/g, " ").trim(); const message = /^no note$/i.test(rawNote) ? "" : rawNote; + // UNIT3D renders one CSRF-protected PATCH form per notification. + // Preserve the form's own action/hidden fields so cleanup can mirror + // the site's supported "mark read" operation instead of guessing an API. + const markReadForm = Array.from(row.querySelectorAll('form[method="POST"], form[method="post"]')) + .find(form => + String(form.querySelector('input[name="_method"]')?.value || "").toUpperCase() === "PATCH" + ) || null; + const markReadButton = markReadForm?.querySelector("button"); + const markReadAction = String(markReadForm?.getAttribute("action") || "").trim(); + const markReadFields = markReadForm + ? Array.from(markReadForm.querySelectorAll("input[name]")) + .map(input => [ + String(input.getAttribute("name") || "").trim(), + String(input.value ?? "") + ]) + .filter(([name]) => !!name) + : []; + const unread = + cells[0].classList.contains("notification--unread") || + (!!markReadButton && !markReadButton.disabled); + return { sender, recipient: host, @@ -5015,7 +5041,10 @@ body.host-panel-dragging * { message, rawTimestamp, createdAtTs, - createdAtAltTs + createdAtAltTs, + unread, + markReadAction, + markReadFields }; }) .filter(item => @@ -5026,6 +5055,135 @@ body.host-panel-dragging * { ); } + function giftNotificationOverlapsWindow(notification, startTs, endTs) { + const start = Number(startTs); + const end = Number(endTs); + if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return false; + + const resolutionMs = unit3dTimestampResolutionMs(notification?.rawTimestamp); + const candidates = [ + Number(notification?.createdAtTs), + Number(notification?.createdAtAltTs) + ].filter(Number.isFinite); + + return candidates.some(ts => + (ts + resolutionMs) >= start && + ts <= (end + resolutionMs) + ); + } + + async function markGiveawayBonNotificationsRead({ hostName, startTs, endTs } = {}) { + // Cleanup is deliberately outside settlement correctness. It never runs in + // rehearsal mode and every failure is absorbed by this helper/caller. + if (REHEARSAL_MODE) { + return { matched: 0, marked: 0, failed: 0, skipped: "rehearsal" }; + } + + const host = String(hostName || "").trim(); + const start = Number(startTs); + const end = Number(endTs); + const senderSlug = getAuthenticatedUserSlug(); + if ( + !host || + !senderSlug || + !Number.isFinite(start) || + !Number.isFinite(end) || + end < start + ) { + return { matched: 0, marked: 0, failed: 0, skipped: "invalid-context" }; + } + + const notificationsPath = `/users/${encodeURIComponent(decodeURIComponent(senderSlug))}/notifications`; + const targetsByAction = new Map(); + const maxPages = 6; + + for (let page = 1; page <= maxPages; page++) { + try { + const notificationsUrl = new URL(notificationsPath, location.origin); + if (page > 1) notificationsUrl.searchParams.set("page", String(page)); + notificationsUrl.searchParams.set("_dpgw_cleanup", String(Date.now())); + + const res = await fetchWithTimeout( + notificationsUrl, + { + credentials: "same-origin", + cache: "no-store" + }, + 5000 + ); + if (!res?.ok) continue; + + const rows = parseGiftNotificationsPage(await res.text(), host); + for (const notification of rows) { + if (!notification.unread) continue; + if (!notification.markReadAction || !notification.markReadFields?.length) continue; + if (!giftNotificationOverlapsWindow(notification, start, end)) continue; + + let actionUrl; + try { + actionUrl = new URL(notification.markReadAction, location.origin); + } catch { + continue; + } + + // Never POST a parsed form away from DarkPeers or outside this + // authenticated user's notification routes. + if (actionUrl.origin !== location.origin) continue; + if (!actionUrl.pathname.startsWith(notificationsPath + "/")) continue; + + targetsByAction.set(actionUrl.href, { + actionUrl, + fields: notification.markReadFields + }); + } + } catch (e) { + console.warn(`[BON Giveaway] Notification cleanup page ${page} skipped:`, e); + } + } + + const targets = Array.from(targetsByAction.values()); + let marked = 0; + let failed = 0; + const concurrency = 4; + + for (let offset = 0; offset < targets.length; offset += concurrency) { + const batch = targets.slice(offset, offset + concurrency); + const results = await Promise.allSettled(batch.map(async target => { + const body = new URLSearchParams(); + for (const [name, value] of target.fields) body.append(name, value); + + const res = await fetchWithTimeout( + target.actionUrl, + { + method: "POST", + credentials: "same-origin", + cache: "no-store", + redirect: "follow", + headers: { + "Accept": "text/html", + "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" + }, + body: body.toString() + }, + 5000 + ); + if (!res?.ok) { + throw new Error(`HTTP ${res?.status || "unknown"}`); + } + })); + + for (const result of results) { + if (result.status === "fulfilled") marked += 1; + else failed += 1; + } + } + + console.info( + `[BON Giveaway] BON notification cleanup: matched=${targets.length}, marked=${marked}, failed=${failed}` + ); + return { matched: targets.length, marked, failed }; + } + function giftHistoryBaseKey(item) { const sender = normalizeUserKey(item?.sender); const recipient = normalizeUserKey(item?.recipient); @@ -9446,7 +9604,21 @@ body.host-panel-dragging * { giveawayData.settlement.completedAt = Date.now(); snapshotGiveaway({ force: true }); } - stopGiveaway(); + + // Capture the notification window before stopGiveaway() clears runtime + // state. Cleanup starts only after settlement is terminal and is explicitly + // fire-and-forget: notification failures can never block or roll back BON + // transfers, verification, statements, stats, or snapshot retirement. + const notificationCleanupContext = { + hostName: giveawayData?.host || "", + startTs: giveawayStartTime instanceof Date ? giveawayStartTime.getTime() : null, + endTs: settlementCutoffTs + }; + const stopped = stopGiveaway(); + if (stopped) { + void markGiveawayBonNotificationsRead(notificationCleanupContext) + .catch(e => console.warn("[BON Giveaway] BON notification cleanup failed:", e)); + } } function clearWinnersStatusUI() { From 96e6dec231471eaca004d6c34afae1ea5aafe718 Mon Sep 17 00:00:00 2001 From: Maghuro <1546139+maghuro@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:11:21 +0100 Subject: [PATCH 6/9] Fix BONanza version assertion --- tests/bonanza-review-hardening.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bonanza-review-hardening.test.mjs b/tests/bonanza-review-hardening.test.mjs index 6b038235..31418562 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\.11$/m); + assert.match(source, /^\/\/ @version\s+1\.5\.12$/m); assert.doesNotMatch(source, /pollChatFallback/); assert.doesNotMatch(source, /onlyguardians/i); assert.match(source, /async function getLatestMainChatReplayBoundary\(\)/); From 5525c67f6d435ab3347985d9733eaba9c21649bb Mon Sep 17 00:00:00 2001 From: Maghuro <1546139+maghuro@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:14:36 +0100 Subject: [PATCH 7/9] Tighten BON notification timestamp matching --- .../giveaway/src/09-sponsorship-polling.js | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/userscripts/giveaway/src/09-sponsorship-polling.js b/userscripts/giveaway/src/09-sponsorship-polling.js index be2b8f8b..9e4384fc 100644 --- a/userscripts/giveaway/src/09-sponsorship-polling.js +++ b/userscripts/giveaway/src/09-sponsorship-polling.js @@ -195,16 +195,20 @@ const end = Number(endTs); if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return false; + // DarkPeers notification datetimes are rendered without an explicit zone, + // but the existing clock-calibration path treats them as UTC-scale event + // time. Prefer that interpretation and only fall back to browser-local time + // if the UTC parse is unavailable. + const utcTs = Number(notification?.createdAtAltTs); + const localTs = Number(notification?.createdAtTs); + const ts = Number.isFinite(utcTs) ? utcTs : localTs; + if (!Number.isFinite(ts)) return false; + + // Timestamp precision describes the notification's own represented interval. + // Never extend the giveaway's upper cutoff, otherwise a late gift can be + // swept into the just-finished giveaway. const resolutionMs = unit3dTimestampResolutionMs(notification?.rawTimestamp); - const candidates = [ - Number(notification?.createdAtTs), - Number(notification?.createdAtAltTs) - ].filter(Number.isFinite); - - return candidates.some(ts => - (ts + resolutionMs) >= start && - ts <= (end + resolutionMs) - ); + return (ts + resolutionMs) >= start && ts <= end; } async function markGiveawayBonNotificationsRead({ hostName, startTs, endTs } = {}) { From 9b81965df278dae947fe102ce40c096319bcc75c Mon Sep 17 00:00:00 2001 From: Maghuro <1546139+maghuro@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:14:50 +0100 Subject: [PATCH 8/9] Test notification timestamp boundaries --- tests/bonanza-review-hardening.test.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/bonanza-review-hardening.test.mjs b/tests/bonanza-review-hardening.test.mjs index 31418562..59a88425 100644 --- a/tests/bonanza-review-hardening.test.mjs +++ b/tests/bonanza-review-hardening.test.mjs @@ -31,6 +31,9 @@ test("review hardening invariants stay present", () => { test("BON gift notification cleanup is scoped and non-blocking", () => { assert.match(source, /async function markGiveawayBonNotificationsRead\(\{ hostName, startTs, endTs \} = \{\}\)/); assert.match(source, /giftNotificationOverlapsWindow\(notification, start, end\)/); + assert.match(source, /const ts = Number\.isFinite\(utcTs\) \? utcTs : localTs/); + assert.match(source, /return \(ts \+ resolutionMs\) >= start && ts <= end/); + assert.doesNotMatch(source, /ts <= \(end \+ resolutionMs\)/); assert.match(source, /input\[name="_method"\][\s\S]*?PATCH/); assert.match(source, /actionUrl\.pathname\.startsWith\(notificationsPath \+ "\/"\)/); assert.doesNotMatch(source, /notifications\/mass-update/); From 230cbe2f295d72908fafdf90e34f021059a6c020 Mon Sep 17 00:00:00 2001 From: Maghuro <1546139+maghuro@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:15:01 +0100 Subject: [PATCH 9/9] Rebuild BONanza after review fixes --- .../DarkPeers_BONanza_Giveaway.user.js | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js b/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js index 47775091..5d378b75 100644 --- a/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js +++ b/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js @@ -5060,16 +5060,20 @@ body.host-panel-dragging * { const end = Number(endTs); if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return false; + // DarkPeers notification datetimes are rendered without an explicit zone, + // but the existing clock-calibration path treats them as UTC-scale event + // time. Prefer that interpretation and only fall back to browser-local time + // if the UTC parse is unavailable. + const utcTs = Number(notification?.createdAtAltTs); + const localTs = Number(notification?.createdAtTs); + const ts = Number.isFinite(utcTs) ? utcTs : localTs; + if (!Number.isFinite(ts)) return false; + + // Timestamp precision describes the notification's own represented interval. + // Never extend the giveaway's upper cutoff, otherwise a late gift can be + // swept into the just-finished giveaway. const resolutionMs = unit3dTimestampResolutionMs(notification?.rawTimestamp); - const candidates = [ - Number(notification?.createdAtTs), - Number(notification?.createdAtAltTs) - ].filter(Number.isFinite); - - return candidates.some(ts => - (ts + resolutionMs) >= start && - ts <= (end + resolutionMs) - ); + return (ts + resolutionMs) >= start && ts <= end; } async function markGiveawayBonNotificationsRead({ hostName, startTs, endTs } = {}) {