// ==UserScript==
// @name UNIT3D BON Giveaway
// @description Enables the functionality to become poor
// @version 1.1.5
// @updateURL https://openuserjs.org/meta/greenbeanyum/UNIT3D_BON_Giveaway.meta.js
// @downloadURL https://openuserjs.org/install/greenbeanyum/UNIT3D_BON_Giveaway.user.js
// @license GPL-3.0-or-later
// @match https://oldtoons.world/
// @match https://aither.cc/
// @match https://reelflix.xyz/
// @match https://fearnopeer.com/
// @grant none
// ==/UserScript==
// ==OpenUserJS==
// @author jacksaw
// ==/OpenUserJS==
// UNIT3D support dantayy
//Split entries table on scroll? Maybe just use tabulation for form and entries. Tabulation would allow for more detailed form
//2nd and 3rd?
//Add option to extend the time with gifts
//Add option to remove a user from the giveaway (not ban, just delete entry).
//remove panel-body padding for div that contains the form so the input layout will be condensed
// Optional setting (checkbox) "Lower rank priority" > when two users tie, the lower rank wins.
// Detect ongoing giveaways
// Possibly send all duplicate entries via chatpm instead of in the general chat to avoid spam. This could also allow to send messages to notify that the entry was successful.
// Add more ! commands (!abort, !removeBon, !expandEntryRange, !addTime, !pauseTimer, !resumeTimer)
// Handle decimal places for initial BON value
// BUGS
//Switching tabs reloads ALL messages in the cb, duplicating all BON gifts and spamming the chat. This can be achieved with the timestamps now included in the chatbox messages
//giveaway amount incorrect when BON donated after reset
// When doing a giveaway for 123 minutes, it will trigger a reminder right at the start (6 reminders, triggered at 2 hours, 2 minutes, 58 seconds)
//Modify timer so that timestamp is used in order to keep accuracy
// Validation of the entry range only sets one field red
const GENERAL_SETTINGS = {
default_mins_per_reminder: 5,
mins_per_reminder_limit: 3
}
// These settings can be used to test different portions of the script. By default, all should be set to false.
const DEBUG_SETTINGS = {
log_chat_messages: false,
disable_chat_output: false
}
// DOM Selectors here for easier compatibility
let messageSelector;
let authorSelector;
let botSelector;
let fancySelector;
let chatboxID;
let chatboxSelector;
const currentUrl = window.location.href
let newUnited = false
var processedGiftMessages = []
let autoSponsor = null
let giveawayStartTime = null
let aither = currentUrl.includes("aither")
let fear = currentUrl.includes("fear")
// Sites that don't broadcast gift messages in the main chatbox
if (aither || fear) {
autoSponsor = false
} else {
autoSponsor = true
}
let chatbox = null
let observer, giveawayData
let numberEntries = new Map()
let fancyNames = new Map()
const regNum = /^-?\d+$/ //
const regGift = /([^ \n]+)\shas\sgifted\s([0-9.]+)\sBON\sto\s([^ \n]+)/
const regAith = /">([^ \n]+)<\/a.*\(taxed\s([0-9.]+)\)\sBON\sto.*">([^ \n]+)<\/a/
// messages through the api are different <div><div><a href="https://fearnopeer.com/users/gifter">gifter</a> has gifted 13 BON to <a href="https://fearnopeer.com/users/recipient">recipient</a></div></div>
const regApi = /">([^ \n]+)<\/a.\shas\sgifted\s([0-9.]+)\sBON\sto.*">([^ \n]+)<\/a/
const whitespace = document.createTextNode(" ")
const sponsorMessages = {
"aither": "Note: [color=#999999][b]Bon gifted to the host during the duration of the Giveaway will be added to the Pot! Adjusted with Aither tax.[/b][/color]",
"fear": "Note: [color=#999999][b]Bon gifted to the host during the duration of the Giveaway will be added to the Pot![/b][/color]",
"default": "Note: [color=#999999][b]Any BON gifted to the host during the duration of the Giveaway is automatically added to the Pot![/b][/color]"
,
}
// Setup giveaway menu
let coinsIcon = document.createElement("i")
coinsIcon.setAttribute("class", "fas fa-coins")
let goldCoins = document.createElement("i")
goldCoins.setAttribute("class", "fas fa-coins")
goldCoins.style.color = "#ffc00a"
goldCoins.style.padding = "5px"
let giveawayBTN = document.createElement("a")
giveawayBTN.setAttribute("class", "form__button form__button--text")
giveawayBTN.textContent = "Giveaway"
giveawayBTN.prepend(coinsIcon.cloneNode(false))
giveawayBTN.onclick = toggleMenu
let frameHTML = `
<section id="giveawayFrame" class="panelV2" style="width: 450px; height: 90%; position: fixed; z-index: 9999; inset: 50px 150px auto auto; overflow: auto; border-style: solid; border-width: 1px; border-color: black" hidden="true">
<header class="panel__heading">
<div class="button-holder no-space">
<div class="button-left">
<h4 class="panel__heading">
<i class="fas fa-coins" style="padding: 5px;"></i>
Giveaway Menu
</h4>
</div>
<div class="button-right">
<button id="resetButton" class="form__button form__button--text">
Reset
</button>
<button id="closeButton" class="form__button form__button--text">
Close
</button>
</div>
</div>
</header>
<div class="panel__body">
<h1 id="coinHeader" class="panel__heading--centered">
</h1>
<form class="form" id="giveawayForm" style="display: flex; flex-flow: column; align-items: center;">
<p class="form__group" style="max-width: 35%;">
<input class="form__text" required="" id="giveawayAmount" pattern="[0-9]*" value="" inputmode="numeric" type="text">
<label class="form__label form__label--floating" for="giveawayAmount">
Giveaway Amount
</label>
</p>
<div class="panel__body" style="display: flex; justify-content: center; gap: 20px">
<p class="form__group" style="width: 20%;">
<input class="form__text" required="" id="startNum" pattern="[0-9]*" value="1" inputmode="numeric" type="text" maxlength="6">
<label class="form__label form__label--floating" for="startNum">
Start #
</label>
</p>
<p class="form__group" style="width: 20%;">
<input class="form__text" required="" id="endNum" pattern="[0-9]*" value="50" inputmode="numeric" type="text" maxlength="6">
<label class="form__label form__label--floating" for="endNum">
End #
</label>
</p>
</div>
<div class="panel__body" style="display: flex; justify-content: center; gap: 20px">
<p class="form__group" style="width: 35%;">
<input class="form__text" required="" id="timerNum" pattern="[0-9]*" value="15" inputmode="numeric" type="text">
<label class="form__label form__label--floating" for="timerNum">
Time (minutes)
</label>
</p>
<p class="form__group" style="width: 35%;">
<input class="form__text" required="" id="reminderNum" pattern="[0-9]*" value="2" inputmode="numeric" type="text">
<label class="form__label form__label--floating" for="reminderNum">
# of Reminders
</label>
</p>
</div>
<p class="form__group" style="text-align: center;">
<button id="startButton" class="form__button form__button--filled">
Start
</button>
</p>
</form>
<h2 id="countdownHeader" class="panel__heading--centered" hidden="">
</h2>
<div id="entriesWrapper" class="data-table-wrapper" hidden="">
<table id="entriesTable" class="data-table">
<thead>
<tr>
<th>
User
</th>
<th>
Entry #
</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</section>
`
// instantiate UI variables
let giveawayFrame, resetButton, closeButton, coinHeader, coinInput, startInput, endInput, timerInput, reminderInput, startButton, countdownHeader, entriesWrapper, giveawayForm
injectMenu()
function reminderAutoScaling() {
let reminders = Math.floor(parseInt(timerInput.value) / GENERAL_SETTINGS.default_mins_per_reminder) - 1
if (reminders < 0) {
reminderInput.value = 0
} else {
reminderInput.value = reminders
}
reminderInput.setCustomValidity("")
}
// This could be improved
function entryRangeValidation() {
if (parseInt(startInput.value) > parseInt(endInput.value)) {
startInput.setCustomValidity("Start # should be lower than End #")
endInput.setCustomValidity("Start # should be lower than End #")
} else {
startInput.setCustomValidity("")
endInput.setCustomValidity("")
}
}
function remindersValidation() {
if (timerInput.value / reminderInput.value < GENERAL_SETTINGS.mins_per_reminder_limit) {
reminderInput.setCustomValidity(`There cannot be more than 1 reminder every: ${parseTime(GENERAL_SETTINGS.mins_per_reminder_limit * 60000)}.`)
reminderInput.reportValidity()
} else {
reminderInput.setCustomValidity("")
}
}
function checkUnit3d() {
const newUnit3d = document.querySelector("#chatbox_header div")
const oldUnit3d = document.querySelector(".panel__heading#frameHeader .button-right")
if (newUnit3d == null && oldUnit3d == null) {
setTimeout(function () { checkUnit3d() }, 100)
} else if (newUnit3d) {
newUnited = true
messageSelector = ".chatbox-message__content"
authorSelector = ".user-tag"
botSelector = ".chatbox-message__content"
fancySelector = ".user-tag"
chatboxSelector = "#chatbox_header div"
chatboxID = "#chatbox__messages-create"
} else if (oldUnit3d) {
messageSelector = ".sent .text-bright div"
authorSelector = ".list-group-item-heading span a"
botSelector = ".sent div.system.bot"
fancySelector = ".badge-user.text-bold"
chatboxSelector = ".panel__heading#frameHeader .button-right"
chatboxID = "#chat-message"
} else {
console.log("Something went wrong")
}
}
function injectMenu() {
checkUnit3d()
var chatbox_header = document.querySelector(`${chatboxSelector}`);
document.body.insertAdjacentHTML("beforeend", frameHTML)
// New panel name
chatbox_header.prepend(giveawayBTN)
giveawayBTN.parentNode.insertBefore(whitespace, giveawayBTN.nextSibling)
giveawayFrame = document.getElementById("giveawayFrame")
resetButton = document.getElementById("resetButton")
resetButton.onclick = resetGiveaway
closeButton = document.getElementById("closeButton")
closeButton.onclick = toggleMenu
coinHeader = document.getElementById("coinHeader")
coinHeader.textContent = document.getElementsByClassName("ratio-bar__points")[0].firstElementChild.textContent.trim()
coinHeader.prepend(goldCoins.cloneNode(false))
coinInput = document.getElementById("giveawayAmount")
startInput = document.getElementById("startNum")
endInput = document.getElementById("endNum")
timerInput = document.getElementById("timerNum")
reminderInput = document.getElementById("reminderNum")
startButton = document.getElementById("startButton")
startButton.onclick = startGiveaway
countdownHeader = document.getElementById("countdownHeader")
entriesWrapper = document.getElementById("entriesWrapper")
giveawayForm = document.getElementById("giveawayForm")
document.body.appendChild(giveawayFrame)
// Attach event listener to scale the number of reminders automatically
timerInput.addEventListener("input", function () { reminderAutoScaling() })
// Add validation of the reminders to ensure that the frequency is not too high
reminderInput.addEventListener("input", function () { remindersValidation() })
// Add entry range validation to ensure endInput > startInput
startInput.addEventListener("input", function () { entryRangeValidation() })
endInput.addEventListener("input", function () { entryRangeValidation() })
}
function toggleMenu() {
giveawayFrame.hidden = !giveawayFrame.hidden
}
function startGiveaway() {
if (!giveawayForm[0].checkValidity() || !giveawayForm[1].checkValidity() || !giveawayForm[2].checkValidity() || !giveawayForm[3].checkValidity() || !giveawayForm[4].checkValidity() || !giveawayForm[5].checkValidity()) {
return;
}
// Chatbox isnt caught at the beginning when the script loads, so I moved it here for now
if (chatbox == null) {
chatbox = document.querySelector(chatboxID)
}
giveawayStartTime = null;
giveawayStartTime = new Date()
startButton.disabled = true
coinInput.disabled = true
startInput.disabled = true
endInput.disabled = true
timerInput.disabled = true
reminderInput.disabled = true
let sponsorMessage
if (aither) sponsorMessage = sponsorMessages.aither
else if (fear) sponsorMessage = sponsorMessages.fear
else sponsorMessage = sponsorMessages.default
startButton.parentElement.hidden = true
entriesWrapper.hidden = false
var totalTimeMs = timerInput.value * 60000
var reminderNum = parseInt(reminderInput.value)
// Using this to pass by reference
giveawayData = {
host: document.getElementsByClassName("top-nav__username")[0].children[0].textContent.trim(),
amount: parseInt(coinInput.value),
startNum: parseInt(startInput.value),
endNum: parseInt(endInput.value),
totalEntries: parseInt(endInput.value) - parseInt(startInput.value) + 1,
winningNumber: null,
totalSeconds: totalTimeMs / 1000,
timeLeft: totalTimeMs / 1000,
reminderNum: reminderNum,
reminderFreqSec: (totalTimeMs / 1000 / (reminderNum + 1)).toFixed(0),
sponsors: [],
// hack probably crappy, but it should work for now.
winnerSent: false,
}
var currentBon = parseInt(document.getElementsByClassName("ratio-bar__points")[0].textContent.trim().replace(/\s/g, ''), 10)
if (currentBon < giveawayData.amount) {
window.alert(`GIVEAWAY ERROR: The amount entered (${giveawayData.amount}), is above your current BON (${currentBon}). You may need to refresh the page to update your BON amount.`)
resetGiveaway(giveawayData)
}
else {
giveawayData.winningNumber = getRandomInt(giveawayData.startNum, giveawayData.endNum)
// Setup an alert when trying to exit the tab during a giveaway
window.onbeforeunload = function () {
return "Giveaway in progress"
}
var introMessage = "I am hosting a giveaway for [b][color=#ffc00a]" +
`${giveawayData.amount} BON[/color][/b]. Entries will be open for [b][color=green]` +
`${parseTime(totalTimeMs)}[/color][/b]. You may enter by submitting a whole number [b]between [color=red]` +
`${giveawayData.startNum} and ${giveawayData.endNum}[/color] inclusive[/b]. ${sponsorMessage} [img]https://cdn.7tv.app/emote/635764cc11b745c4f0230c96/1x.webp[/img]`
sendMessage(introMessage)
if (observer) {
startObserver()
}
else {
addObserver(giveawayData)
}
giveawayData.countdownTimerID = countdownTimer(countdownHeader, giveawayData)
if (!autoSponsor) {
sponsorsInterval = setInterval(async () => { await getSponsors() }, 15000)
}
//MAYBE UNNEEDED RETURN?
return false;
}
}
function addObserver(giveawayData) {
observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
for (var i = 0; i < mutation.addedNodes.length; i++) {
var tablist = document.querySelectorAll('[role="tablist"]');
var general = tablist[0].childNodes[0]
//console.log(general)
//if(general.className != "panel__tab panel__tab--active") {
// console.log("General chat is not active")
//}
// !!! Here it is really important not to pass the giveawayData object to the parseMessage function. If done so, then it will for some reason always retain the pointer of the first
// reference it was given, therefore when more than one giveaway is done in a row, the following giveaways messages will be parsed against the old giveawayData information.
parseMessage(mutation.addedNodes[i])
}
})
})
startObserver()
}
function startObserver() {
let messageList
if (newUnited) {
messageList = document.getElementsByClassName("chatroom__messages")[0]
} else {
messageList = document.querySelectorAll(".messages .list-group")[0]
}
observer.observe(messageList, {
childList: true
})
}
function parseMessage(messageNode) {
let messageContent
let isBot
if (newUnited) {
isBot = messageNode.querySelector(botSelector) == null
} else {
isBot = messageNode.querySelector(botSelector) !== null
}
if (isBot) {
if (newUnited) {
messageContent = messageNode.querySelector(botSelector).querySelector("div").textContent
} else {
messageContent = messageNode.querySelector(botSelector).querySelector("div div").textContent
}
} else {
messageContent = messageNode.querySelector(messageSelector).textContent
var author = messageNode.querySelector(authorSelector).textContent.trim()
var fancyName = messageNode.querySelector(fancySelector).outerHTML
}
let isValid = regNum.test(messageContent)
if (isValid) {
handleEntryMessage(parseInt(messageContent, 10), author, fancyName, giveawayData)
} else if (messageContent[0] == "!") {
handleGiveawayCommands(author, messageContent, fancyName, giveawayData)
} else if (isBot && autoSponsor) {
handleGiftMessage(messageContent, giveawayData)
}
}
function handleGiveawayCommands(author, messageContent, fancyName, giveawayData) {
let arguments = messageContent.substring(1).trim().split(" ")
let command = arguments[0].toLowerCase()
let userNumber = numberEntries.get(author)
let message
const validCommands = ['time', 'random', 'number', 'lucky', 'addbon', 'commands']
if (!validCommands.includes(command)) {
return
}
switch (command) {
case 'time':
message = `Time left in the giveaway: [b][color=green]${parseTime(giveawayData.timeLeft * 1000)}[/color][/b].`
sendMessage(message)
break
case 'random':
if (userNumber) {
message = `Sorry [color=#d85e27]${author}[/color], but [color=#32cd53]you[/color] already entered with number [color=red][b]${userNumber}[/b][/color]!`
} else {
let randomNum = 0
let currentNumbers = Array.from(numberEntries.values())
do {
randomNum = Math.floor(Math.random() * (giveawayData.endNum - giveawayData.startNum + 1)) + giveawayData.startNum
} while (currentNumbers.includes(randomNum))
addNewEntry(author, fancyName, randomNum)
message = `[color=#d85e27]${author}[/color] entered with number [color=green][b]${randomNum}[/b][/color]`
}
sendMessage(message)
break
case 'number':
if (userNumber) {
message = `[color=#d85e27]${author}[/color] your number is [color=red][b]${userNumber}[/b][/color]`
} else {
message = `[color=#d85e27]${author}[/color] you are not currently in the giveaway.`
}
sendMessage(message)
break
case 'lucky':
message = `The current giveaway lucky number is: [b][color=green]${getLuckyNumber(giveawayData)}[/color][/b].`
sendMessage(message)
break
case 'addbon':
if (author == giveawayData.host) {
let amount = parseFloat(arguments[1])
if (!isNaN(amount) && amount > 0) {
giveawayData.amount += amount
message = `The host is adding [color=red][b]${amount}[/b][/color] BON to the pot! The total is now: [b][color=#ffc00a]${cleanPotString(giveawayData.amount)} BON[/color][/b]`
}
} else {
message = "Only the host can use the !addbon command"
}
sendMessage(message)
break
case 'commands':
message = "Valid commands are !random !number !lucky !time & for hosts !addbon"
sendMessage(message)
break
default:
break
}
}
function handleEntryMessage(number, author, fancyName, giveawayData) {
var repeatMessage
// Check if number is legal
if (number < giveawayData.startNum || number > giveawayData.endNum) {
const outOfBoundsMessage = `Sorry [color=#d85e27]${author}[/color], but the number [color=red][b]${number}[/b][/color] is outside of the given range! Please try another number that is [b]between ${giveawayData.startNum} and ${giveawayData.endNum} inclusive[/b]!`
sendMessage(outOfBoundsMessage)
return;
}
for (let [msgAuthor, msgValue] of numberEntries.entries()) {
if (msgAuthor == author) {
repeatMessage = `Sorry [color=#d85e27]${author}[/color], but [color=#32cd53]you[/color] already entered with number [color=red][b]${msgValue}[/b][/color]!`
sendMessage(repeatMessage)
return;
}
else if (msgValue == number) {
repeatMessage = `Sorry [color=#d85e27]${author}[/color], but [color=#32cd53]${msgAuthor}[/color] already entered with number [color=red][b]${number}[/b][/color]! Please try another number!`
sendMessage(repeatMessage)
return;
}
}
if (!numberEntries.has(author)) {
addNewEntry(author, fancyName, number)
}
}
function addNewEntry(author, fancyName, number) {
numberEntries.set(author, number)
fancyNames.set(author, fancyName)
updateEntries()
}
function handleGiftMessage(messageContent, giveawayData) {
let gift;
if (fear) {
gift = regApi.exec(messageContent)
} else if (aither) {
gift = regAith.exec(messageContent)
} else {
gift = regGift.exec(messageContent)
}
var addAmount = parseFloat(gift[2])
var gifter = gift[1]
var recpt = gift[3]
if (recpt == giveawayData.host) {
giveawayData.amount += addAmount
var giftMessage = `[color=green][b]${gifter}[/b][/color] is sponsoring [color=red][b]${addAmount}[/b][/color] additional BON! The total pot is now: [b][color=#ffc00a]${cleanPotString(giveawayData.amount)} BON[/color][/b]`
sendMessage(giftMessage)
// If not yet included, add gifter to the list of giveaway sponsors
if (!giveawayData.sponsors.includes(gifter)) {
giveawayData.sponsors.push(gifter)
}
}
}
const getSponsors = async () => {
let chatroom = aither ? '4' : '2';
const api = `${currentUrl}api/chat/messages/${chatroom}`;
const startTimeTimestamp = giveawayStartTime.getTime();
try {
const response = await fetch(api, {
method: 'GET',
headers: {
'Cookie': document.cookie // Include cookies from the main page
}
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
const systemMessages = data.data;
const filteredMessages = systemMessages.filter(message => {
const messageTime = new Date(message.created_at).getTime();
const giftMessage = message.message.includes("gifted");
return giftMessage && messageTime > startTimeTimestamp;
});
for (const msg of filteredMessages) {
// prevent duplicate sponsorship messages
if (!processedGiftMessages.includes(msg.id)) {
const messageContent = msg.message;
processedGiftMessages.push(msg.id)
handleGiftMessage(messageContent, giveawayData);
}
}
} catch (error) {
console.error('There was a problem with the fetch operation:', error);
}
};
function updateEntries() {
let tableStart = "<thead><tr><th>User</th><th>Entry #</th></tr></thead><tbody>"
let tableEntries = ""
let tableEnd = "</tbody>"
numberEntries.forEach((entry, author) => {
let fancyName = fancyNames.get(author)
tableEntries += `<tr><td>${fancyName}</td><td>${entry}</td></tr>`; //need ; to fix syntax highligthing
})
document.getElementById("entriesTable").innerHTML = tableStart + tableEntries + tableEnd
}
async function endGiveaway(giveawayData) {
observer.disconnect()
clearInterval(sponsorsInterval)
// If the site doesn't have a gift messages broadcast, we need to get the sponsors from the api
if (!autoSponsor) {
await getSponsors()
}
if (numberEntries.size == 0) {
var emptyMessage = `Unfortunately, no one has entered the giveaway so no one wins!`
sendMessage(emptyMessage)
} else {
if (giveawayData.sponsors.length > 0) {
var sponsorsMessage = `Thank you to all the additional sponsors! `
sponsorsMessage += `[color=green][b]${giveawayData.sponsors[0]}[/b][/color]`
var i = 1
while (i < giveawayData.sponsors.length) {
sponsorsMessage += `, [color=green][b]${giveawayData.sponsors[i]}[/b][/color]`
i++
}
sendMessage(sponsorsMessage)
}
var bestGuess = Number.MAX_VALUE
var tie = false
var gapToWinningNumber, currentBestEntryGap
var entryAuthor, tieAuthor, tieGuess
numberEntries.forEach((entry, author) => {
currentBestEntryGap = Math.abs(giveawayData.winningNumber - bestGuess)
gapToWinningNumber = Math.abs(giveawayData.winningNumber - entry)
if (currentBestEntryGap > gapToWinningNumber) {
tie = false
bestGuess = entry
entryAuthor = author
}
else if (gapToWinningNumber == currentBestEntryGap) {
tie = true
tieAuthor = author
tieGuess = entry
}
})
if (bestGuess == giveawayData.winningNumber) {
var winMessage = `With a guess of [color=green][b]${bestGuess}[/b][/color] hitting the winning number exactly, [color=red][b]${entryAuthor}[/b][/color] has won [color=#ffc00a][b]${cleanPotString(giveawayData.amount)} BON[/b][/color]!`
sendMessage(winMessage)
}
else if (!tie) {
var winMessage = `With a guess of [color=green][b]${bestGuess}[/b][/color] only [color=green][b]${Math.abs(giveawayData.winningNumber - bestGuess)}[/b][/color] away from the winning number [color=green][b]${giveawayData.winningNumber}[/b][/color], [color=red][b]${entryAuthor}[/b][/color] has won [color=#ffc00a][b]${cleanPotString(giveawayData.amount)} BON[/b][/color]!`
sendMessage(winMessage)
}
else if (tie) {
var tieMessage = `With a tie between [color=#d85e27][b]${entryAuthor}[/b][/color] ([b]${bestGuess}[/b]) and [color=#d85e27][b]${tieAuthor}[/b][/color] ([b]${tieGuess}[/b]), both being only [color=green][b]${Math.abs(giveawayData.winningNumber - bestGuess)}[/b][/color] away from the winning number [color=green][b]${giveawayData.winningNumber}[/b][/color], [color=red][b]${entryAuthor}[/b][/color] has won [color=#ffc00a][b]${cleanPotString(giveawayData.amount)} BON[/b][/color] as their entry was submitted first!`
sendMessage(tieMessage)
}
else {
console.log("Something went wrong while ending the giveaway")
}
if (!giveawayData.winnerSent) {
var giftMessage = `/gift ${entryAuthor} ${giveawayData.amount} Congratulations! You won the giveaway!`
sendMessage(giftMessage)
// hopefully stops repeat gift bug
giveawayData.winnerSent = true;
}
}
// Clear onbeforeunload alert
window.onbeforeunload = null
clearInterval(giveawayData.countdownTimerID)
observer.disconnect()
delete giveawayData
}
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// Unified all time related events in the countdown (endGiveaway and reminders) to avoid any kind of drifting between them.
function countdownTimer(display, giveawayData) {
display.hidden = false
var minutes, seconds
var timerID = setInterval(function () {
giveawayData.timeLeft--
minutes = parseInt(giveawayData.timeLeft / 60, 10)
seconds = parseInt(giveawayData.timeLeft % 60, 10)
minutes = minutes < 0 ? "0" + minutes : minutes
seconds = seconds < 10 ? "0" + seconds : seconds
display.textContent = minutes + ":" + seconds
if (giveawayData.timeLeft <= 0) {
endGiveaway(giveawayData)
return
}
else if (giveawayData.totalEntries == numberEntries.size) {
// Color scheme of this message could be improved
var earlyFinishMessage = "All [b][color=#ffc00a]" +
`${giveawayData.totalEntries}[/color][/b] slot(s) filled up! Therefore, the giveaway is ending with [b][color=green]` +
`${parseTime(giveawayData.timeLeft * 1000)}[/color][/b] remaining!`
sendMessage(earlyFinishMessage)
endGiveaway(giveawayData)
return
}
else if ((giveawayData.timeLeft) % giveawayData.reminderFreqSec == 0) {
let reminderMessage;
let reminderSponsor;
if (aither) reminderSponsor = sponsorMessages.aither;
else if (fear) reminderSponsor = sponsorMessages.fear;
else reminderSponsor = sponsorMessages.default;
reminderMessage = `There is an ongoing giveaway for [b][color=#ffc00a]` +
`${giveawayData.amount} BON[/color][/b]. Time left: [b][color=green]` +
`${parseTime(giveawayData.timeLeft * 1000)}[/color][/b]. You may enter by submitting a whole number [b]between [color=red]` +
`${giveawayData.startNum} and ${giveawayData.endNum}[/color] inclusive[/b]. ${reminderSponsor}`
sendMessage(reminderMessage)
}
}, 1000)
return timerID
}
function sendMessage(messageStr) {
if (!DEBUG_SETTINGS.disable_chat_output) {
chatbox.value = messageStr
chatbox.dispatchEvent(new KeyboardEvent("keydown", {
keyCode: 13
}))
}
if (DEBUG_SETTINGS.log_chat_messages) {
console.log(messageStr)
}
}
function getLuckyNumber(giveawayData) {
var rangeStart = giveawayData.startNum
var rangeEnd = giveawayData.endNum
var numbers = Array.from(numberEntries.values()).sort((a, b) => {
if (a < b) {
return -1
} else {
return 1
}
})
numbers.push(rangeEnd + 1)
var bestGap = 0
var lucky = 0
var pastNum = rangeStart - 1
var currentNum, gap
for (var i = 0; i < numbers.length; i++) {
currentNum = numbers[i]
gap = currentNum - pastNum
if (gap > bestGap) {
lucky = Math.floor(gap / 2) + pastNum
bestGap = gap
}
pastNum = currentNum
}
return lucky
}
function cleanPotString(giveawayPotAmount) {
if (giveawayPotAmount % 1 == 0) {
return giveawayPotAmount
} else {
return giveawayPotAmount.toFixed(2)
}
}
function parseTime(timeInMs) {
var hours = Math.floor((timeInMs / 3600000) % 60)
var minutes = Math.floor((timeInMs / 60000) % 60)
var seconds = Math.floor((timeInMs / 1000) % 60)
var timeString = ``
if (hours > 0) {
timeString += `${hours} hour`
if (hours > 1) {
timeString += `s`
}
}
if (minutes > 0) {
if (timeString != ``) {
timeString += `, `
}
timeString += `${minutes} minute`
if (minutes > 1) {
timeString += `s`
}
}
if (seconds > 0) {
if (timeString != ``) {
timeString += `, `
}
timeString += `${seconds} second`
if (seconds > 1) {
timeString += `s`
}
}
return timeString
}
function resetGiveaway() {
clearInterval(giveawayData.countdownTimerID)
// Clear onbeforeunload alert
window.onbeforeunload = null
numberEntries = new Map()
fancyNames = new Map()
entriesWrapper.hidden = true
countdownHeader.hidden = true
startButton.parentElement.hidden = false
startButton.disabled = false
coinInput.disabled = false
startInput.disabled = false
endInput.disabled = false
timerInput.disabled = false
reminderInput.disabled = false
observer.disconnect()
delete observer
giveawayForm.reset()
updateEntries()
}
Donate for the site OpenUserJS
Are you sure you want to go to an external site to donate a monetary value?
WARNING: Some countries laws may supersede the payment processors policy such as the GDPR and PayPal. While it is highly appreciated to donate, please check with your countries privacy and identity laws regarding privacy of information first. Use at your utmost discretion.