// ==UserScript== // @name UNIT3D BON Giveaway // @namespace https://github.com/flowerey/unit3d-scripts // @version 6.5.3 // @description Enables the functionality to become poor // @author blueberry, Nums // @match https://*/chatbox* // @match https://*/chat/* // @downloadURL https://raw.githubusercontent.com/flowerey/unit3d-scripts/main/giveaway.user.js // @updateURL https://raw.githubusercontent.com/flowerey/unit3d-scripts/main/giveaway.user.js.meta.js // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @run-at document-idle // ==/UserScript== // If the website is not listed as a match already, please verify with tracker admins before using this script on their site. // Additional credits // @TheEther - Integration with Aither + some additional features // @Nums - added new commands, command spam detection, admin controls, multi-winners, refactored BON API polling + trying to keep the public version updated // @ahoimate - got BON gifting API polling working + added new commands // @ruckus612 - fixed BON gift bug // @ZukoXZuko - added formatting to the giveaway menu (function() { 'use strict'; // ─────────────────────────────────────────────────────────── // SECTION 1: Global Constants and Configuration // ─────────────────────────────────────────────────────────── const COMMAND_WINDOW_MS = 10000; // look back 10 seconds const MAX_COMMANDS_PER_WINDOW = 3; // allow 3 commands in that window const BASE_PENALTY_SECONDS = 30; // base lockout for exceeding (in seconds) // Spam filter tightening (keeps responses snappy but reduces chat spam): // - MIN_ACTION_GAP_MS blocks ultra-fast repeat triggers (usually bots/double-sends) // - REPEAT_COMMAND_COOLDOWNS_MS prevents the same command from being spammed for identical output // - strikes increase lockout length for repeat offenders (decays over time) const MIN_ACTION_GAP_MS = 900; // ignore triggers faster than this per user const ENTRY_FEEDBACK_COOLDOWN_MS = 8000; // throttle duplicate/out-of-range feedback per user const STRIKE_WINDOW_MS = 10 * 60 * 1000; // 10 minutes const MAX_STRIKE_MULTIPLIER = 8; // caps exponential backoff const REPEAT_COMMAND_COOLDOWNS_MS = Object.freeze({ entry: 2000, time: 3000, entries: 5000, free: 7000, lucky: 7000, luckye: 7000, random: 7000, range: 5000, sponsors: 8000, stats: 8000, top: 8000, most: 8000, largest: 8000, scale: 5000 });const RIG_DENY_COOLDOWN_MS = 10000; // 10s per-user cooldown for funny !rig/!unrig denial messages const MAX_WINNERS = 50; // central location to update max allowable number of winners const MAX_REMINDERS = 6; //maximum number of reminders allowed // Persistent stats (saved in localStorage on this site) const STATS_KEY = `BON_GIVEAWAY_STATS::${location.hostname}`; const STATS_VERSION = 1; const STATS_DEFAULT_TOP_N = 3; const STATS_MAX_TOP_N = 10; // Default text to populate the custom giveaway message field const DEFAULT_CUSTOM_MESSAGE = ""; const GIFT_HINT_COLOR = "#F3D34A"; const SCALING_ACCENT_COLOR = "#7C4DFF"; const ENTRY_IGNORE_WINDOW_MS = 2000; // Sponsor announcement controls (host chat spam reduction) // - mode: "immediate" (old behavior), "digest" (recommended), or "off" (silent; still counts sponsors) // - digest_ms: max frequency for sponsor announcements in chat // - immediate_single_min: big single gifts are announced right away (even in digest mode) // - flush_min_total: announce early if combined pending sponsorship reaches this BON // - show_top_n / show_min_per_user: keep the line short; omit tiny sponsors from the name list (still counted in totals) const SPONSOR_ANNOUNCE = { mode: "digest", digest_ms: 60_000, immediate_single_min: 500, flush_min_total: 250, max_pending_events: 50, show_top_n: Infinity, show_min_per_user: 0 }; const GENERAL_SETTINGS = { disable_random: false, disable_lucky: false, disable_free: false, suppress_entry_replies: false, silent_mode: false }; const DEBUG_SETTINGS = { log_chat_messages: false, disable_chat_output: false, verify_extractor: false, verify_sendmessage: false, verify_cacheChatContext: false, suppressApiMessages: false, // new flag to suppress API message sending enable_self_checks: false }; const PERF = false; // Debug-only perf counters (must stay false in normal use) const PERF_LOG_EVERY = 50; const perfCounters = PERF ? Object.create(null) : null; function perfMeasure(section, startMs) { if (!PERF) return; const elapsed = performance.now() - startMs; const rec = perfCounters[section] || (perfCounters[section] = { count: 0, total: 0, max: 0 }); rec.count += 1; rec.total += elapsed; if (elapsed > rec.max) rec.max = elapsed; if ((rec.count % PERF_LOG_EVERY) === 0) { console.debug( `[BON Giveaway PERF] ${section}: count=${rec.count}, avg=${(rec.total / rec.count).toFixed(2)}ms, max=${rec.max.toFixed(2)}ms` ); } } const SELF_CHECK_FLAG = "BON_GIVEAWAY_SELF_CHECKS"; const SELF_CHECK_QUERY_RE = /(?:^|[?&])bg_self_checks=1(?:&|$)/i; const SELF_CHECKS_ENABLED = !!( DEBUG_SETTINGS.enable_self_checks || localStorage.getItem(SELF_CHECK_FLAG) === "true" || SELF_CHECK_QUERY_RE.test(String(window.location.search || "")) ); function selfCheck(condition, message, details) { if (!SELF_CHECKS_ENABLED || condition) return; const err = new Error(`[BON Giveaway self-check] ${message}`); if (details && typeof details === "object") { try { console.error(err.message, details); } catch { console.error(err.message); } } else { console.error(err.message); } throw err; } const SCRIPT_ID = 'bon-giveaway-update'; const CHECK_EVERY_HOURS = 48; const CHATROOM_IDS = { 'upload.cx': '11', 'oldtoons.world': '4', 'aither.cc': '4', 'reelflix.cc': '1', 'homiehelpdesk.net': '3', 'darkpeers.org': '2', 'yu-scene.net': '5', 'polishtorrent.top': '16', 'luminarr.me': '4', 'midnightscene.cc': '2', 'znth.cx' : '2', }; // Central host/site adapter: isolate per-site quirks in one place function createSiteAdapter(hostname, chatroomMap) { const host = String(hostname || '').trim().toLowerCase(); const isUploadCx = host === 'upload.cx'; const chatroomId = (chatroomMap && chatroomMap[host]) ? String(chatroomMap[host]) : '2'; function getMessageContentElement(messageNode) { if (!messageNode || messageNode.nodeType !== 1) return null; return messageNode.querySelector('.chatbox-message__content'); } function getGiftEndpointPath(slug) { const safeSlug = String(slug || '').trim(); if (!safeSlug) return null; return `/users/${safeSlug}/gifts`; } return Object.freeze({ host, chatroomId, isUploadCx, getMessageContentElement, getGiftEndpointPath }); } const LS_SUPPRESS = "giveaway-suppressEntryReplies"; const LS_SILENT = "giveaway-silentMode"; const LS_SHOW_GIVEAWAY_LOG = "giveaway-showLog"; const LS_HOST_PANEL_OPEN = "giveaway-hostPanelOpen"; const LS_HOST_PANEL_POS = "bonGiveaway_hostPanelPos"; const LS_MINIMIZED = "giveaway-minimized"; const LS_PRESETS = "giveaway-presets"; const LS_ACTIVE_GIVEAWAY = `giveaway-activeState::${location.hostname}`; const LS_TAB_LOCK = `giveaway-tabLock::${location.hostname}`; // Per-giveaway ledger of completed gift attempts. Survives reload + visible to other tabs, // so even if endGiveaway runs in two tabs the second one won't re-pay. const LS_PAID_GIFTS = `giveaway-paidGifts::${location.hostname}`; // Cap retained giveaway-id entries in the ledger so it can't grow unbounded over time. const PAID_GIFTS_MAX_GIVEAWAYS = 50; const TAB_ID = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const TAB_LOCK_HEARTBEAT_MS = 5000; // update lock every 5s const TAB_LOCK_STALE_MS = 15000; // lock is stale if no heartbeat for 15s let tabLockHeartbeatTimer = null; function readStoredBooleanSetting(key, fallback = false, persistFallback = false) { const raw = localStorage.getItem(key); if (raw === "true") return true; if (raw === "false") return false; if (persistFallback) { localStorage.setItem(key, String(!!fallback)); } return !!fallback; } // Initialize silent mode as early as possible (no page refresh needed). GENERAL_SETTINGS.silent_mode = readStoredBooleanSetting(LS_SILENT, false, true); GENERAL_SETTINGS.show_giveaway_log = readStoredBooleanSetting(LS_SHOW_GIVEAWAY_LOG, false, true); const currentHost = window.location.hostname; const Site = createSiteAdapter(currentHost, CHATROOM_IDS); const chatroomId = Site.chatroomId; const chatboxId = "chatbox__messages-create"; const COMMAND_PANEL_SECTIONS = Object.freeze({ giveaway: "General Commands", stats: "Stats Commands", entry: "Entry Commands", help: "Help", rigging: "Rigging Commands", pot: "BON Commands", fun: "Fun / Extras" }); const HOST_PANEL_NAUGHTY_SECTION_TITLE = "Naughty List"; const HOST_PANEL_END_COMMAND_DENYLIST = Object.freeze(["end", "endgiveaway", "giveawayend", "stop", "stopgiveaway"]); const HOST_PANEL_INTERNAL_COMMAND_DENYLIST = Object.freeze(["commands"]); const HOST_PANEL_SECTION_ORDER = Object.freeze(["giveaway", "stats", "pot", "entry", "rigging", "help", "fun"]); const HOST_PANEL_COMMAND_SECTION_BY_KEY = Object.freeze({ reminder: "giveaway", entries: "giveaway", time: "giveaway", addtime: "giveaway", removetime: "giveaway", winners: "giveaway", maxwinners: "giveaway", scale: "giveaway", stats: "stats", top: "stats", most: "stats", largest: "stats", unlucky: "stats", sponsors: "stats", help: "help", gift: "help", bon: "pot", addbon: "pot", rig: "rigging", unrig: "rigging", naughty: "naughty" }); const HOST_PANEL_COMMAND_ORDER_BY_SECTION = Object.freeze({ giveaway: Object.freeze(["reminder", "entries", "time", "addtime", "removetime", "winners", "maxwinners", "scale"]), stats: Object.freeze(["stats", "top", "most", "largest", "unlucky", "sponsors"]), help: Object.freeze(["help", "gift"]), pot: Object.freeze(["bon", "addbon"]), rigging: Object.freeze(["rig", "unrig"]) }); // only run the cooldown/spam‑detection logic on available commands const baseCommands = ["time", "entries", "help", "commands", "bon", "range", "gift","random", "number", "free", "lucky", "luckye", "rig", "unrig", "stats", "top", "most", "sponsors", "unlucky", "largest", "scale", "host"]; const hostCommands = ["addtime", "removetime", "reminder", "addbon", "end", "winners", "maxwinners", "naughty", "history"]; const uploadCxExtras = ["ruckus", "ick", "corigins", "lejosh", "suckur", "bloom", "dawg", "greglechin"]; const validCommands = new Set([ ...baseCommands, ...hostCommands, ...(Site.isUploadCx ? uploadCxExtras : []) ]); const HOST_PANEL_COMMAND_METADATA = Object.freeze({ time: { label: "Time", section: "giveaway", description: "Show remaining giveaway time.", usage: "!time", requiresGiveaway: true }, entries: { label: "Entries", section: "info", description: "List current entries.", usage: "!entries", requiresGiveaway: true }, help: { label: "Help", section: "info", description: "Show available commands in chat.", usage: "!help", requiresGiveaway: false }, commands: { label: "Commands", section: "info", description: "Alias for !help.", usage: "!commands", requiresGiveaway: false }, stats: { label: "Stats", section: "info", description: "Show saved stats for a user.", usage: "!stats [username]", requiresGiveaway: false, args: [{ name: "username", label: "User", type: "username", required: false, placeholder: "optional username" }] }, top: { label: "Top", section: "info", description: "Top winners leaderboard.", usage: "!top [N]", requiresGiveaway: false, args: [{ name: "count", label: "N", type: "int", required: false, min: 1, max: STATS_MAX_TOP_N, placeholder: String(STATS_DEFAULT_TOP_N) }] }, most: { label: "Most", section: "info", description: "Most BON won leaderboard.", usage: "!most [N]", requiresGiveaway: false, args: [{ name: "count", label: "N", type: "int", required: false, min: 1, max: STATS_MAX_TOP_N, placeholder: String(STATS_DEFAULT_TOP_N) }] }, sponsors: { label: "Sponsors", section: "pot", description: "Show top sponsors.", usage: "!sponsors [N]", requiresGiveaway: false, args: [{ name: "count", label: "N", type: "int", required: false, min: 1, max: STATS_MAX_TOP_N, placeholder: String(STATS_DEFAULT_TOP_N) }] }, unlucky: { label: "Unlucky", section: "info", description: "Show most losses leaderboard.", usage: "!unlucky [N]", requiresGiveaway: false, args: [{ name: "count", label: "N", type: "int", required: false, min: 1, max: STATS_MAX_TOP_N, placeholder: String(STATS_DEFAULT_TOP_N) }] }, largest: { label: "Largest", section: "info", description: "Show largest giveaways.", usage: "!largest [N]", requiresGiveaway: false, args: [{ name: "count", label: "N", type: "int", required: false, min: 1, max: STATS_MAX_TOP_N, placeholder: String(STATS_DEFAULT_TOP_N) }] }, gift: { label: "Gift", section: "pot", description: "Show giveaway gift status.", usage: "!gift", requiresGiveaway: true }, bon: { label: "BON", section: "pot", description: "Show current pot amount.", usage: "!bon", requiresGiveaway: true }, range: { label: "Range", section: "entry", description: "Show valid entry range.", usage: "!range", requiresGiveaway: true }, lucky: { label: "Lucky", section: "entry", description: "Show lucky number.", usage: "!lucky", requiresGiveaway: true }, luckye: { label: "Lucky Enter", section: "entry", description: "Enter using lucky number.", usage: "!luckye", requiresGiveaway: true }, rig: { label: "Rig", section: "entry", description: "Fun rig toggle command.", usage: "!rig", requiresGiveaway: true }, unrig: { label: "Unrig", section: "entry", description: "Fun rig toggle command.", usage: "!unrig", requiresGiveaway: true }, random: { label: "Random", section: "entry", description: "Enter with a random number.", usage: "!random", requiresGiveaway: true }, number: { label: "Number", section: "entry", description: "Show your current entry.", usage: "!number", requiresGiveaway: true }, free: { label: "Free", section: "entry", description: "Show available entry numbers.", usage: "!free", requiresGiveaway: true }, addbon: { label: "Add BON", section: "pot", description: "Add BON to the pot.", usage: "!addbon ", requiresGiveaway: true, hostOnly: true, args: [{ name: "amount", label: "BON", type: "int", required: true, min: 1, placeholder: "amount" }] }, reminder: { label: "Reminder", section: "giveaway", description: "Send reminder now.", usage: "!reminder", requiresGiveaway: true, hostOnly: true }, winners: { label: "Winners", section: "giveaway", description: "Set winner count.", usage: `!winners 1-${MAX_WINNERS}`, requiresGiveaway: true, hostOnly: true, args: [{ name: "count", label: "Count", type: "int", required: true, min: 1, max: MAX_WINNERS, placeholder: "winners" }] }, maxwinners: { label: "Max Winners", section: "giveaway", description: "Set max scaled winners.", usage: `!maxwinners 1-${MAX_WINNERS}`, requiresGiveaway: true, hostOnly: true, args: [{ name: "count", label: "Max", type: "int", required: true, min: 1, max: MAX_WINNERS, placeholder: "max" }] }, scale: { label: "Scale", section: "info", description: "Show scaling progress.", usage: "!scale", requiresGiveaway: true }, addtime: { label: "Add Time", section: "giveaway", description: "Add giveaway minutes.", usage: "!addtime ", requiresGiveaway: true, hostOnly: true, args: [{ name: "minutes", label: "Min", type: "int", required: true, min: 1, placeholder: "minutes" }] }, removetime: { label: "Remove Time", section: "giveaway", description: "Remove giveaway minutes.", usage: "!removetime ", requiresGiveaway: true, hostOnly: true, args: [{ name: "minutes", label: "Min", type: "int", required: true, min: 1, placeholder: "minutes" }] }, naughty: { label: "Naughty", section: "giveaway", description: "Manage naughty list.", usage: "!naughty (add|remove|list) [username]", requiresGiveaway: true, hostOnly: true, args: [ { name: "action", label: "Action", type: "select", required: true, placeholder: "action", options: [{ label: "Add", value: "add" }, { label: "Remove", value: "remove" }, { label: "List", value: "list" }], validate: (v) => /^(add|remove|list)$/i.test(String(v || "").trim()), hint: "Use add, remove, or list." }, { name: "username", label: "User", type: "username", required: false, placeholder: "username", requiredWhen: (all) => /^(add|remove)$/i.test(String(all.action || "").trim()), hint: "Username is required for add/remove." } ] }, end: { label: "End", section: "giveaway", description: "End the active giveaway.", usage: "!end [host]", requiresGiveaway: true, hostOnly: true, args: [{ name: "host", label: "Host", type: "username", required: false, placeholder: "optional host" }] }, host: { label: "Quick Host", section: "giveaway", description: "Start giveaway: !host -