Parse sold events and send them to the system
// ==UserScript==
// @name WhatNot Username Parser
// @namespace http://tampermonkey.net/
// @version 1.28
// @description Parse sold events and send them to the system
// @author You
// @match https://www.whatnot.com/dashboard/live/*
// @match https://www.whatnot.com/live/*
// @match https://www.whatnot.com/user/*/obs/break-widget
// @match http://localhost:3000/break/*
// @match https://whatnot-frontend.vercel.app/break/*
// @match https://whatnot-frontend-psi.vercel.app/break/*
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @run-at document-start
// ==/UserScript==
GM_addStyle(`
.mob-mobile-chat {
height: 75vh;
}
.mob-chat {
// position: absolute;
max-height: 90% !important;
top: 10% !important;
}
.mob-chat > * > * > div {
// background-color: rgba(0, 0, 0, 0.75);
border-radius: 10px;
padding: 2px;
}
.mob-price {
position: absolute;
z-index: 999;
background-color: black;
padding: 1%;
}
.mob-price * {
font-size: 45px !important;
}
.mob-price-child {
top: -20px;
position:relative;
}
.bottom-container {
background-color: rgba(0, 0, 0, 0);
height: 100% !important;
}
.mob-chat:first-child {
height: 78% !important;
background: '';
}
.mob-last-buyer-container > div:first-of-type {
position: absolute !important;
right: 1% !important;
top: 1% !important;
width: 50% !important;
}
.mob-last-buyer-container > div:first-of-type > :first-child {
background-color: black;
}
.mob-chat-parent {
justify-content: end !important;
}
.mob-online {
font-size: 25px;
position: absolute;
right: 2%;
top: 2%;
display: flex;
align-items: center;
gap: 10px;
}
.mob-online-icon {
width: 45px !important;
}
`);
(function() {
'use strict';
const currentURL = document.URL.toString()
console.log(currentURL)
// --- WebSocket interception ---
// Must inject a <script> tag to patch WebSocket in the real page context,
// because Tampermonkey sandboxes the userscript window from the page window.
const wsFrames = []
let wsCapturing = false
let globalLastBreakData = null
if (currentURL.indexOf('obs/break-widget') !== -1) {
window.addEventListener('__wnBreakDetails', function(e) {
globalLastBreakData = e.detail
console.log('[Break Parser] early capture stored', globalLastBreakData?.data?.getBreak?.id)
})
}
;(function injectWsPatch() {
const script = document.createElement('script')
script.textContent = `(function() {
if (window.__wnParserWsPatched) return;
window.__wnParserWsPatched = true;
var wsConnCount = 0;
var NativeWS = window.WebSocket;
function bufToBase64(buf) {
var s = '', bytes = new Uint8Array(buf);
for (var i = 0; i < bytes.byteLength; i++) s += String.fromCharCode(bytes[i]);
return btoa(s);
}
function PatchedWebSocket() {
var ws = new (Function.prototype.bind.apply(NativeWS, [null].concat(Array.from(arguments))))();
var connId = ++wsConnCount;
var connUrl = arguments[0];
console.log('[WS Capture] new connection #' + connId + ':', connUrl);
ws.addEventListener('message', function(e) {
var raw = e.data, frame;
if (typeof raw === 'string') {
frame = { connId: connId, connUrl: connUrl, ts: Date.now(), encoding: 'text', data: raw };
window.dispatchEvent(new CustomEvent('__wnWsFrame', { detail: frame }));
} else if (raw instanceof ArrayBuffer) {
frame = { connId: connId, connUrl: connUrl, ts: Date.now(), encoding: 'base64', data: bufToBase64(raw) };
window.dispatchEvent(new CustomEvent('__wnWsFrame', { detail: frame }));
} else if (raw instanceof Blob) {
raw.arrayBuffer().then(function(buf) {
frame = { connId: connId, connUrl: connUrl, ts: Date.now(), encoding: 'base64', data: bufToBase64(buf) };
window.dispatchEvent(new CustomEvent('__wnWsFrame', { detail: frame }));
});
}
});
return ws;
}
PatchedWebSocket.prototype = NativeWS.prototype;
PatchedWebSocket.CONNECTING = NativeWS.CONNECTING;
PatchedWebSocket.OPEN = NativeWS.OPEN;
PatchedWebSocket.CLOSING = NativeWS.CLOSING;
PatchedWebSocket.CLOSED = NativeWS.CLOSED;
window.WebSocket = PatchedWebSocket;
console.log('[WS Capture] WebSocket patched in page context');
if (!window.__wnParserFetchPatched) {
window.__wnParserFetchPatched = true;
var _fetch = window.fetch;
window.fetch = function() {
var args = arguments;
var url = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].url) || '';
var p = _fetch.apply(this, args);
if (url.indexOf('GetBreakDetailsOBS') !== -1) {
p.then(function(res) {
var clone = res.clone();
clone.json().then(function(data) {
window.dispatchEvent(new CustomEvent('__wnBreakDetails', { detail: data }));
});
});
}
return p;
};
}
if (!window.__wnParserXhrPatched) {
window.__wnParserXhrPatched = true;
var _xhrOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
if (typeof url === 'string' && url.indexOf('GetBreakDetailsOBS') !== -1) {
this.addEventListener('load', function() {
try {
var data = JSON.parse(this.responseText);
window.dispatchEvent(new CustomEvent('__wnBreakDetails', { detail: data }));
} catch(e) {}
});
}
return _xhrOpen.apply(this, arguments);
};
}
})();`
document.documentElement.appendChild(script)
script.remove()
})()
window.addEventListener('__wnWsFrame', e => {
if (!wsCapturing) return
wsFrames.push(e.detail)
})
function getElementByXpath(path) {
return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}
function createToolsNode() {
var parentNode = document.createElement('div');
parentNode.style.position = 'fixed';
parentNode.style.top = '50%';
parentNode.style.right = '0';
parentNode.style.transform = 'translateY(-50%)';
parentNode.style.backgroundColor = 'green';
parentNode.style.padding = '10px';
parentNode.style.fontSize = '2em'; // 2 times bigger font size
parentNode.style.zIndex = '9000'; // Set a high z-index
document.body.appendChild(parentNode);
return parentNode
}
// Add a dropdown list for selecting tools
function createToolSelector(parentNode) {
var toolSelector = document.createElement('select');
toolSelector.style.marginBottom = '10px'; // Add some margin for spacing
parentNode.appendChild(toolSelector);
var button = document.createElement('button')
button.textContent = 'X'
parentNode.appendChild(button);
button.addEventListener('click', function () {
parentNode.remove()
})
var toolContainer = document.createElement('div')
parentNode.appendChild(toolContainer);
// Define tool options
var toolOptions = [
{ name: 'Username Parser', tool: createUsernameParserTool },
{ name: 'WS Parser', tool: createWSParserTool },
{ name: 'WS Capture', tool: createWSCaptureTool },
{ name: 'Break Parser', tool: createBreakParserTool },
{ name: 'Chat Only', tool: createChatOnlyTool },
{ name: 'Notes', tool: createNotesTool},
{ name: 'Giveaway Alarm', tool: createGiveawayAlarmTool}
];
// Populate dropdown options
toolOptions.forEach(function(option) {
var optionElement = document.createElement('option');
optionElement.value = option.name;
optionElement.textContent = option.name;
toolSelector.appendChild(optionElement);
});
// Function to hide all tool interfaces
function hideAllToolInterfaces() {
// Remove all child nodes from the parent node
while (toolContainer.firstChild) {
toolContainer.removeChild(toolContainer.firstChild);
}
}
toolSelector.addEventListener('change', function() {
var selectedTool = toolOptions.find(option => option.name === toolSelector.value);
hideAllToolInterfaces()
selectedTool.tool(toolContainer);
});
const defaultTool = currentURL.indexOf('obs/break-widget') !== -1
? toolOptions.find(t => t.name === 'Break Parser')
: toolOptions[0]
toolSelector.value = defaultTool.name
defaultTool.tool(toolContainer);
}
function initUI() {
let toolsNode = createToolsNode()
createToolSelector(toolsNode)
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initUI)
} else {
initUI()
}
function createUsernameParserTool(parentNode) {
function start() {
if (currentURL.indexOf('live') !== -1) {
var element = null;
var usernameList = []
const Teams = [
"Arizona Cardinals",
"Atlanta Falcons",
"Baltimore Ravens",
"Buffalo Bills",
"Carolina Panthers",
"Chicago Bears",
"Cincinnati Bengals",
"Cleveland Browns",
"Dallas Cowboys",
"Denver Broncos",
"Detroit Lions",
"Green Bay Packers",
"Houston Texans",
"Indianapolis Colts",
"Jacksonville Jaguars",
"Kansas City Chiefs",
"Las Vegas Raiders",
"Los Angeles Chargers",
"Los Angeles Rams",
"Miami Dolphins",
"Minnesota Vikings",
"New England Patriots",
"New Orleans Saints",
"New York Giants",
"New York Jets",
"Philadelphia Eagles",
"Pittsburgh Steelers",
"San Francisco 49ers",
"Seattle Seahawks",
"Tampa Bay Buccaneers",
"Tennessee Titans",
"Washington Commanders"
]
function isATeamGiveaway(value) {
return Teams.filter(i => value.indexOf(i) !== -1).length > 0
}
let prev = null
let prevMouseHandler = null
let id = null
let teamIds = new Map();
let giveawayIds = new Map();
function checkForSoldButton() {
let buttons = Array.from(document.querySelectorAll('h5'))
let optionsButtons = buttons.filter(i => i.textContent == 'Sold')
let soldCategory = optionsButtons.length > 0 ? optionsButtons[0] : null
if (soldCategory === null) {
console.log("didn't find sold category")
return;
}
if (prev === soldCategory) return;
console.log('found sold category', soldCategory)
console.log(soldCategory)
soldCategory.style.backgroundColor = 'green';
console.log("Username parser is init")
function mouseHandler() {
clearInterval(id)
let latestScheduleTime = 0
const processedSoldNames = new Set()
const perItemObservers = new Map()
function setupRandomizingObserver(divItem, divListingItem, log) {
if (perItemObservers.has(divItem)) {
log('per-item observer already set up for this node')
return
}
const randomizingBadge = document.createElement('div')
randomizingBadge.textContent = 'Randomizing...'
randomizingBadge.style.backgroundColor = 'orange'
randomizingBadge.style.color = 'white'
randomizingBadge.style.padding = '5px'
randomizingBadge.style.borderRadius = '5px'
divListingItem.appendChild(randomizingBadge)
function cleanup() {
itemObserver.disconnect()
clearTimeout(safetyTimeout)
perItemObservers.delete(divItem)
}
const safetyTimeout = setTimeout(() => {
log('per-item observer safety timeout reached, disconnecting')
cleanup()
}, 60000)
const itemObserver = new MutationObserver(() => {
try {
if (divItem.childNodes.length <= 0) return
const dli = divItem.childNodes[0]
if (dli.childNodes.length <= 0) return
const ddf = dli.childNodes[0]
if (ddf.childNodes.length <= 0) return
const df = ddf.childNodes[1]
if (!df || df.childNodes.length <= 6) return
const content = df.textContent
if (/randomizing/i.test(content) || content.indexOf('Pending') !== -1) return
const contentMatch = content.match(/^(.+?)Qty:\s*\d+Buyer:\s*(.+?)Sold for \$(\d+)/)
if (!contentMatch) return
const soldName = contentMatch[1].trim()
const username = contentMatch[2].trim()
const price = parseInt(contentMatch[3])
cleanup()
randomizingBadge.remove()
if (processedSoldNames.has(soldName)) {
log('per-item observer: duplicate soldName', soldName)
const dupElement = document.createElement('div')
dupElement.textContent = 'Duplicate detected'
dupElement.style.backgroundColor = 'orange'
dupElement.style.color = 'white'
dupElement.style.padding = '5px'
dupElement.style.borderRadius = '5px'
divListingItem.appendChild(dupElement)
return
}
processedSoldNames.add(soldName)
let entity
let wasSent = false
if (soldName.toLowerCase().indexOf('giveaway') !== -1) {
entity = { customer: username, price: 0, name: soldName }
const id = soldName.split('#')[1] || soldName
if (giveawayIds.has(id)) wasSent = true
giveawayIds.set(id, true)
log('per-item observer: parsed giveaway id', id)
} else {
entity = { customer: username, price: price, name: soldName }
const id = soldName.split('#')[1] || soldName
if (teamIds.has(id)) wasSent = true
teamIds.set(id, true)
log('per-item observer: parsed team id', id)
}
const badge = document.createElement('div')
badge.style.color = 'white'
badge.style.padding = '5px'
badge.style.borderRadius = '5px'
if (wasSent) {
badge.textContent = 'Already added'
badge.style.backgroundColor = 'red'
divListingItem.appendChild(badge)
log('per-item observer: skip, already added', entity.name)
} else {
badge.textContent = 'Sent'
badge.style.backgroundColor = 'green'
divListingItem.appendChild(badge)
log('per-item observer: setting entity to', entity)
GM_setValue('newEvent', entity)
}
} catch(e) {
log('per-item observer error:', e)
}
})
itemObserver.observe(divItem, { subtree: true, childList: true, characterData: true })
perItemObservers.set(divItem, cleanup)
log('per-item observer set up for', divItem)
}
const observer = new MutationObserver(mutationsList => {
const sid = Math.random().toString(36).slice(2, 7)
const log = (...args) => console.log(`[${sid}]`, ...args)
log.sid = sid
log('mut list', mutationsList)
// Loop through each mutation in the mutationsList
for (let mutation of mutationsList) {
// Check if nodes were added
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
log('muts', mutation)
// Process the added nodes
mutation.addedNodes.forEach(addedNode => {
const sid = log.sid + '.' + Math.random().toString(36).slice(2, 7)
try {
const log = (...args) => console.log(`[${sid}]`, ...args)
log.sid = sid
let divItem = addedNode
let attributes = divItem.attributes
let index = attributes.getNamedItem('data-index')
let isParsed = addedNode.getAttribute('data-parsed');
log('new added node', addedNode, index)
if (isParsed) {
log('parsed already')
return
}
addedNode.setAttribute('data-parsed', 'true');
log('parsing', addedNode)
const myTime = Date.now()
latestScheduleTime = myTime
setTimeout(() => {
const sid = log.sid + '.' + Math.random().toString(36).slice(2, 7)
try {
const log = (...args) => console.log(`[${sid}]`, ...args)
log.sid = sid
if (myTime < latestScheduleTime) {
log('stale timeout blocked, scheduled at', myTime, 'latest is', latestScheduleTime)
return
}
const sentElement = document.createElement('div');
if (divItem.childNodes.length <= 0) {
log('wrong node 1', divItem)
return
}
let divListingItem = divItem.childNodes[0]
if (divListingItem.childNodes.length <= 0) {
log('wrong node 2', divListingItem)
return
}
let divDisplayFlex = divListingItem.childNodes[0]
if (divDisplayFlex.childNodes.length <= 0) {
log('wrong node 3', divDisplayFlex)
return
}
let divFlex = divDisplayFlex.childNodes[1]
if (divFlex.childNodes.length <= 0) {
log('wrong node 4', divFlex)
return
}
let entity = {customer: '?', price: 0, name: ''}
let wasSent = false
if (divFlex.childNodes.length > 6) {
let content = divFlex.textContent
log("content is", content)
if (/randomizing/i.test(content)) {
log('content is randomizing, setting up per-item observer')
setupRandomizingObserver(divItem, divListingItem, log)
return
}
if (content.indexOf("Pending") !== -1) {
log("content contains Pending, skipping", content)
return
}
let contentMatch = content.match(/^(.+?)Qty:\s*\d+Buyer:\s*(.+?)Sold for \$(\d+)/)
if (!contentMatch) {
log("failed to parse content", content)
return
}
let soldName = contentMatch[1].trim()
let username = contentMatch[2].trim()
let price = parseInt(contentMatch[3])
if (processedSoldNames.has(soldName)) {
log('already processed soldName, skipping', soldName)
const dupElement = document.createElement('div');
dupElement.textContent = 'Duplicate detected';
dupElement.style.backgroundColor = 'blue';
dupElement.style.color = 'white';
dupElement.style.padding = '5px';
dupElement.style.borderRadius = '5px';
divListingItem.appendChild(dupElement);
return
}
processedSoldNames.add(soldName)
log("found name", soldName, ", is givy:", soldName.toLowerCase().indexOf("giveaway") != -1)
if (soldName.toLowerCase().indexOf("giveaway") !== -1) {
entity = {customer: username, price: 0, name: soldName}
let id = soldName.split('#')[1] || soldName
if (giveawayIds.has(id)) {
wasSent = true
}
giveawayIds.set(id, true)
log("parsed giveaway id is", id)
} else {
entity = {customer: username, price: price, name: soldName}
let id = soldName.split('#')[1] || soldName
if (teamIds.has(id)) {
wasSent = true
}
teamIds.set(id, true)
log("parsed team id is", id)
}
} else {
log("skip, invalid node", divFlex)
return;
}
if (wasSent) {
// Set the text content
sentElement.textContent = 'Already added';
// Set the styles
sentElement.style.backgroundColor = 'red';
sentElement.style.color = 'white';
sentElement.style.padding = '5px';
sentElement.style.borderRadius = '5px';
// Append the new element to the existing element
divListingItem.appendChild(sentElement);
log("skip, already added", entity.name)
return
} else {
// Set the text content
sentElement.textContent = 'Sent';
// Set the styles
sentElement.style.backgroundColor = 'green';
sentElement.style.color = 'white';
sentElement.style.padding = '5px';
sentElement.style.borderRadius = '5px';
// Append the new element to the existing element
divListingItem.appendChild(sentElement);
log('setting entity to', entity)
log('old value was', GM_getValue('newEvent'))
GM_setValue('newEvent', entity)
}
} catch (e) {
log('element is preparing:', e)
}
}, 2000)
} catch(e) {
log('an error occured:', e)
}
});
}
if (mutation.type === 'childList' && mutation.removedNodes.length > 0) {
mutation.removedNodes.forEach(removedNode => {
if (perItemObservers.has(removedNode)) {
perItemObservers.get(removedNode)()
log('disconnected per-item observer for removed node', removedNode)
}
})
}
}
});
setTimeout(() => {
let eventLog = document.querySelector('[data-testid=virtuoso-item-list]');
console.log(eventLog)
if (!eventLog) {
console.log('event log is not found')
return
}
observer.observe(eventLog, {
childList: true,
});
console.log("New event observer is started")
}, 5000)
soldCategory.style.backgroundColor = 'red';
console.log("New event observer is init")
}
if (prev != null && prevMouseHandler != null) {
prev.removeEventListener('click', prevMouseHandler)
prev.style.backgroundColor = '';
}
prev = soldCategory
prevMouseHandler = mouseHandler
soldCategory.addEventListener('click', mouseHandler)
}
checkForSoldButton();
id = setInterval(checkForSoldButton, 100);
console.log("Username sender is started")
} else {
function setReactInput(node, value) {
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
'value').set;
nativeInputValueSetter.call(node, value);
const event = new Event('input', { bubbles: true });
node.dispatchEvent(event);
}
const newEventEvent = 'new_event_event'
let price = 1
setInterval(() => {
let event = GM_getValue('newEvent', null)
if (!event) {
console.log('new event not found yet')
return
}
console.log('got event', event)
GM_setValue("newEvent", null)
window.dispatchEvent(new CustomEvent(newEventEvent, { detail: {event: event} }));
}, 1500)
const breakOverviewEvent = 'break_overview_event'
setInterval(() => {
let breakOverview = GM_getValue('BreakOverview', null)
if (!breakOverview) {
console.log('new event not found yet')
return
}
console.log('got break overview', breakOverview)
GM_setValue("BreakOverview", null)
window.dispatchEvent(new CustomEvent(breakOverviewEvent, {detail: breakOverview}));
}, 1500)
console.log("Username receiver is started")
parentNode.removeChild(parentDiv)
}
}
// Create a new div for the quantity tool
var parentDiv = document.createElement('div');
// parentDiv.style.border = '1px solid black'; // Add border
parentDiv.style.padding = '10px'; // Add padding for spacing
// Create a button
const dButton = document.createElement('button');
dButton.textContent = 'Turn On';
dButton.addEventListener('mouseenter', () => { !dButton.disabled && (dButton.style.color = 'white') })
dButton.addEventListener('mouseleave', () => { !dButton.disabled && (dButton.style.color = '') })
parentDiv.appendChild(dButton);
dButton.addEventListener('click', async () => {
dButton.disabled = true
dButton.textContent = 'Is active';
dButton.style.backgroundColor = 'red'
dButton.style.color = 'white'
start()
})
parentNode.appendChild(parentDiv)
}
function createWSParserTool(parentNode) {
const processedListings = new Set()
let isActive = false
let eventCount = 0
let logEl = null // set once UI is built
function uiLog(msg) {
console.log('[WS Parser]', msg)
if (!logEl) return
const line = document.createElement('div')
line.textContent = new Date().toLocaleTimeString() + ' | ' + msg
line.style.borderBottom = '1px solid #333'
line.style.padding = '2px 0'
logEl.appendChild(line)
logEl.scrollTop = logEl.scrollHeight
}
const sendQueue = []
let queueInterval = null
function enqueue(entity) {
sendQueue.push(entity)
uiLog('queued "' + entity.name + '" [queue: ' + sendQueue.length + ']')
if (!queueInterval) {
queueInterval = setInterval(flushQueue, 300)
}
}
function flushQueue() {
if (sendQueue.length === 0) {
clearInterval(queueInterval)
queueInterval = null
return
}
if (GM_getValue('newEvent', null) !== null) {
uiLog('queue: slot busy, retrying [' + sendQueue.length + ' pending]')
return
}
const entity = sendQueue.shift()
GM_setValue('newEvent', entity)
eventCount++
uiLog('queue: sent "' + entity.name + '" [' + sendQueue.length + ' remaining]')
}
const callbacks = new Map()
function on(type, cb) {
if (!callbacks.has(type)) callbacks.set(type, new Set())
callbacks.get(type).add(cb)
}
function off(type, cb) {
callbacks.get(type)?.delete(cb)
}
function onFrame(e) {
if (!isActive) return
let d
try { d = JSON.parse(e.detail.data) } catch(err) { return }
if (!Array.isArray(d) || d.length < 5) return
const type = d[3], payload = d[4]
callbacks.get(type)?.forEach(cb => cb(payload))
}
window.addEventListener('__wnWsFrame', onFrame)
on('auction_ended', payload => {
const productId = payload?.product?.id
if (!productId || processedListings.has(productId)) return
const listingId = payload?.product?.listingId
processedListings.add(productId)
const isBreakSpot = !!payload.product.isBreakSpot
const name = payload?.product?.name
if (isBreakSpot) {
uiLog('Break auction ' + name + 'just ended with id ' + listingId)
} else {
uiLog('Auction ' + name + 'just ended with id ' + listingId)
}
function onAuctionPayment(pmPayload) {
if (pmPayload?.product?.id !== productId) return
off('payment_succeeded', onAuctionPayment)
const price = pmPayload.product.soldPrice.amount / 100
if (!isBreakSpot) {
const entity = {
customer: payload.product.purchaserUser.username,
price,
name: payload.product.name
}
uiLog('Sold auction "' + entity.name + '" to ' + entity.customer + ' for $' + entity.price)
enqueue(entity)
return
}
const paymentListingId = pmPayload.product.listingId
uiLog('Payment confirmed for id ' + listingId + ', waiting for randomizer result')
function onRandomizerResult(rrPayload) {
if (String(rrPayload.listing_id) !== String(paymentListingId)) return
off('randomizer_result_event', onRandomizerResult)
const entity = {
customer: rrPayload.buyer_username,
price,
name: rrPayload.result
}
uiLog('Sold break auction "' + entity.name + '" to ' + entity.customer + ' for $' + entity.price)
enqueue(entity)
}
on('randomizer_result_event', onRandomizerResult)
}
on('payment_succeeded', onAuctionPayment)
})
on('giveaway_won', payload => {
const listingId = payload?.product?.listingId
if (!listingId || processedListings.has(listingId)) return
processedListings.add(listingId)
const entity = {
customer: payload.product.purchaserUser.username,
price: 0,
name: payload.product.name
}
uiLog('Given away a giveaway "' + entity.name + '" to ' + entity.customer)
enqueue(entity)
})
// UI
const parentDiv = document.createElement('div')
parentDiv.style.padding = '10px'
parentDiv.style.display = 'flex'
parentDiv.style.flexDirection = 'column'
parentDiv.style.gap = '6px'
const statusLabel = document.createElement('div')
statusLabel.textContent = 'Status: inactive'
statusLabel.style.fontSize = '0.75em'
parentDiv.appendChild(statusLabel)
const eventCountLabel = document.createElement('div')
eventCountLabel.textContent = 'Events sent: 0'
eventCountLabel.style.fontSize = '0.75em'
parentDiv.appendChild(eventCountLabel)
const startBtn = document.createElement('button')
startBtn.textContent = 'Start'
startBtn.style.backgroundColor = '#2196F3'
startBtn.style.color = 'white'
startBtn.style.border = 'none'
startBtn.style.padding = '4px 8px'
startBtn.style.borderRadius = '4px'
startBtn.style.cursor = 'pointer'
parentDiv.appendChild(startBtn)
startBtn.addEventListener('click', () => {
isActive = !isActive
startBtn.textContent = isActive ? 'Stop' : 'Start'
startBtn.style.backgroundColor = isActive ? '#f44336' : '#2196F3'
statusLabel.textContent = 'Status: ' + (isActive ? 'active' : 'inactive')
})
setInterval(() => {
eventCountLabel.textContent = 'Events sent: ' + eventCount
}, 500)
logEl = document.createElement('div')
logEl.style.marginTop = '6px'
logEl.style.maxHeight = '200px'
logEl.style.overflowY = 'auto'
logEl.style.fontSize = '0.65em'
logEl.style.fontFamily = 'monospace'
logEl.style.backgroundColor = '#111'
logEl.style.color = '#ddd'
logEl.style.padding = '4px'
logEl.style.borderRadius = '4px'
logEl.style.minHeight = '40px'
parentDiv.appendChild(logEl)
parentNode.appendChild(parentDiv)
}
function createWSCaptureTool(parentNode) {
const parentDiv = document.createElement('div')
parentDiv.style.padding = '10px'
parentDiv.style.display = 'flex'
parentDiv.style.flexDirection = 'column'
parentDiv.style.gap = '6px'
const statusLabel = document.createElement('div')
statusLabel.textContent = 'Status: idle'
statusLabel.style.fontSize = '0.75em'
parentDiv.appendChild(statusLabel)
const frameCountLabel = document.createElement('div')
frameCountLabel.textContent = 'Frames: 0'
frameCountLabel.style.fontSize = '0.75em'
parentDiv.appendChild(frameCountLabel)
const connLabel = document.createElement('div')
connLabel.textContent = 'Connections: 0'
connLabel.style.fontSize = '0.75em'
parentDiv.appendChild(connLabel)
function makeButton(text, color) {
const btn = document.createElement('button')
btn.textContent = text
btn.style.backgroundColor = color
btn.style.color = 'white'
btn.style.border = 'none'
btn.style.padding = '4px 8px'
btn.style.borderRadius = '4px'
btn.style.cursor = 'pointer'
return btn
}
const startBtn = makeButton('Start Capture', '#2196F3')
const clearBtn = makeButton('Clear', '#888')
const exportBtn = makeButton('Export JSON', '#4CAF50')
parentDiv.appendChild(startBtn)
parentDiv.appendChild(clearBtn)
parentDiv.appendChild(exportBtn)
startBtn.addEventListener('click', () => {
wsCapturing = !wsCapturing
startBtn.textContent = wsCapturing ? 'Stop Capture' : 'Start Capture'
startBtn.style.backgroundColor = wsCapturing ? '#f44336' : '#2196F3'
statusLabel.textContent = 'Status: ' + (wsCapturing ? 'capturing...' : 'paused')
})
clearBtn.addEventListener('click', () => {
wsFrames.length = 0
})
exportBtn.addEventListener('click', () => {
const blob = new Blob([JSON.stringify(wsFrames, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'ws-frames-' + Date.now() + '.json'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
})
setInterval(() => {
frameCountLabel.textContent = 'Frames: ' + wsFrames.length
const knownConns = new Set(wsFrames.map(f => f.connId))
connLabel.textContent = 'Connections seen: ' + knownConns.size
}, 500)
parentNode.appendChild(parentDiv)
}
function createBreakParserTool(parentNode) {
const parentDiv = document.createElement('div')
let updateCount = 0
let lastUpdateTs = null
let lastData = null
const statusEl = document.createElement('div')
statusEl.textContent = 'Listening for GetBreakDetailsOBS...'
statusEl.style.cssText = 'color:#aaa;font-size:12px;margin-bottom:6px;'
parentDiv.appendChild(statusEl)
const counterEl = document.createElement('div')
counterEl.textContent = 'Updates sent: 0'
counterEl.style.cssText = 'color:#fff;font-size:12px;margin-bottom:6px;'
parentDiv.appendChild(counterEl)
const syncBtn = document.createElement('button')
syncBtn.textContent = 'Sync'
syncBtn.disabled = true
syncBtn.style.cssText = 'font-size:24px;padding:2px 8px;cursor:pointer;background-color:#fff;color:#000;border:1px solid #ccc;border-radius:4px;'
syncBtn.addEventListener('click', () => {
if (!lastData) return
GM_setValue('BreakOverview', lastData)
console.log('[Break Parser] manual sync sent', JSON.stringify(lastData))
})
parentDiv.appendChild(syncBtn)
function formatAgo(ms) {
const s = Math.floor(ms / 1000)
if (s < 60) return s + 's ago'
const m = Math.floor(s / 60)
if (m < 60) return m + 'm ' + (s % 60) + 's ago'
return Math.floor(m / 60) + 'h ' + (m % 60) + 'm ago'
}
setInterval(() => {
if (lastUpdateTs !== null) {
statusEl.textContent = 'Last update: ' + formatAgo(Date.now() - lastUpdateTs)
}
}, 1000)
function onBreakDetails(e) {
console.log('[Break Parser] GetBreakDetailsOBS response:', e.detail)
lastData = e.detail
syncBtn.disabled = false
GM_setValue('BreakOverview', e.detail)
console.log("Sent", JSON.stringify(e.detail))
updateCount++
lastUpdateTs = Date.now()
statusEl.textContent = 'Last update: 0s ago'
counterEl.textContent = 'Updates sent: ' + updateCount
}
window.addEventListener('__wnBreakDetails', onBreakDetails)
if (globalLastBreakData) {
onBreakDetails({ detail: globalLastBreakData })
}
parentNode.appendChild(parentDiv)
}
function createChatOnlyTool(parentNode) {
function removeNonRelatedNodes(rootElement, targetElements) {
const queue = [rootElement]; // Queue to traverse the DOM tree
let targetHit = false;
while (queue.length > 0) {
const currentElement = queue.shift(); // Dequeue current element
if (!targetHit) {
currentElement.style.height = '100%';
currentElement.style.width = '100%';
}
if (targetElements.includes(currentElement)) {
targetHit = true;
}
// Check if the current element is not related to any target elements
if (!targetElements.some(targetElement => currentElement && targetElement && (currentElement === targetElement || currentElement.contains(targetElement) || targetElement.contains(currentElement)))) {
// Remove the current element
currentElement.style.display = "none"
//currentElement.parentNode.removeChild(currentElement);
} else {
// Add the children of the current element to the queue for further traversal
Array.from(currentElement.children).forEach(child => queue.push(child));
}
}
}
// Create a new div for the quantity tool
var parentDiv = document.createElement('div');
parentDiv.style.border = '1px solid black'; // Add border
parentDiv.style.padding = '10px'; // Add padding for spacing
// Create a button
const dButton = document.createElement('button');
dButton.textContent = 'Clean page';
parentDiv.appendChild(dButton);
dButton.addEventListener('click', async () => {
const rootElement = document.body;
const chatContainer = document.querySelector('#bottom-section-stream-container > div > div > div:nth-child(1)')
chatContainer.classList.add('mob-chat')
let chatContainerParent = chatContainer.parentNode
chatContainerParent.classList.add('mob-chat-parent')
const chatWindow = document.querySelector('#bottom-section-stream-container > div > div > div.mob-chat > div:nth-child(1) > div:nth-child(3)')
function autoScrollToBottom(element) {
// Create a mutation observer to watch for changes
const observer = new MutationObserver(() => {
// Scroll to the bottom
element.scrollTop = element.scrollHeight;
});
// Start observing the element for DOM changes
observer.observe(element, {
childList: true, // Watch for child additions/removals
subtree: true, // Watch all descendants, not just direct children
characterData: true // Watch for text content changes
});
// Initial scroll to bottom
element.scrollTop = element.scrollHeight;
return observer; // Return observer so it can be disconnected later if needed
}
const scrollViewport = chatWindow.querySelector('[data-overlayscrollbars-viewport]')
if (scrollViewport) {
const currentValue = scrollViewport.getAttribute('data-overlayscrollbars-viewport');
if (currentValue) {
const updatedValue = currentValue.replace('scrollbarHidden', '').trim();
scrollViewport.setAttribute('data-overlayscrollbars-viewport', updatedValue);
}
autoScrollToBottom(scrollViewport);
}
const videoElement = document.querySelector('video')
const targetElements = [
chatWindow,
videoElement,
];
console.log(targetElements);
const priceDiv = document.querySelector('#bottom-section-stream-container > div > div.mob-chat-parent > div:nth-child(2) > div:nth-child(2)')
if (priceDiv) {
targetElements.push(priceDiv)
Array.from(priceDiv.parentNode.children).forEach(sibling => {
if (sibling !== priceDiv) sibling.style.display = 'none'
})
}
const online = document.querySelector('#top-section-stream-container > div:nth-child(1) > div:nth-child(2) > div > div > div:nth-child(1)')
if (online) targetElements.push(online)
removeNonRelatedNodes(rootElement, targetElements); // Call the function to remove non-related nodes
document.querySelector('#bottom-section-stream-container').style.height = '100%';
var styleElement = document.createElement('style');
styleElement.type = 'text/css';
styleElement.innerHTML = '#bottom-section-stream-container > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) * { background-color: rgba(0, 0, 0, 0.1) !important; }';
document.head.appendChild(styleElement);
function updateNestedStyles(element, property, value) {
// Apply the style to the current element
element.style[property] = value
// Recursively apply the style to all child elements
Array.from(element.children).forEach(child => {
updateNestedStyles(child, property, value);
});
}
if (online) {
online.classList.add('mob-online')
const onlineNumber = online.querySelector('div > div:nth-child(4)')
if (onlineNumber) onlineNumber.style.fontSize = '45px'
const onlineIcon = online.querySelector('div > div:nth-child(1)')
if (onlineIcon) {
const iconFirstDiv = onlineIcon.querySelector('div')
if (iconFirstDiv) iconFirstDiv.classList.add('mob-online-icon')
}
}
if (priceDiv) {
priceDiv.classList.add('mob-price')
priceDiv.style.width = ''
priceDiv.style.position = 'static'
priceDiv.style.right = ''
priceDiv.style.left = ''
priceDiv.style.top = ''
priceDiv.childNodes.forEach(i => {
i.classList.add('mob-price-child')
})
if (online) online.appendChild(priceDiv)
}
const lastBuyerDiv = document.querySelector('#bottom-section-stream-container > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(3) > div:nth-child(1)')
if (lastBuyerDiv) {
lastBuyerDiv.style.position = 'fixed'
lastBuyerDiv.style.right = '2%'
lastBuyerDiv.style.zIndex = '999'
let firstDiv = lastBuyerDiv.childNodes[0]
firstDiv.style.flexDirection = 'row-reverse'
let firstDivChild = firstDiv.childNodes[0]
let logo = firstDivChild.childNodes[0]
logo.style.display = 'none'
let buyerName = firstDivChild.childNodes[1]
buyerName.style.color = 'white'
let lastBuyerStatus = firstDivChild.childNodes[2]
lastBuyerStatus.style.display = 'none'
updateNestedStyles(lastBuyerDiv, 'font-size', '45px')
document.body.appendChild(lastBuyerDiv)
function updateLastBuyerPosition() {
const onlineRect = online ? online.getBoundingClientRect() : null
if (onlineRect) {
lastBuyerDiv.style.top = (onlineRect.bottom + 5) + 'px'
}
}
updateLastBuyerPosition()
const lastBuyerPositionObserver = new MutationObserver(updateLastBuyerPosition)
if (online) {
lastBuyerPositionObserver.observe(online, { childList: true, subtree: true, characterData: true })
}
const lastBuyerStyleObserver = new MutationObserver((mutationsList) => {
let firstDiv = mutationsList[0].addedNodes[0]
firstDiv.style.flexDirection = 'row-reverse'
let firstDivChild = firstDiv.childNodes[0]
let logo = firstDivChild.childNodes[0]
logo.style.display = 'none'
let buyerName = firstDivChild.childNodes[1]
buyerName.style.color = 'white'
let lastBuyerStatus = firstDivChild.childNodes[2]
lastBuyerStatus.style.display = 'none'
updateNestedStyles(lastBuyerDiv, 'font-size', '45px')
})
lastBuyerStyleObserver.observe(lastBuyerDiv, { childList: true, subtree: true })
}
parentNode.removeChild(parentDiv);
});
parentNode.appendChild(parentDiv);
}
function createNotesTool(parentNode) {
// Create a new div for the quantity tool
const parentDiv = document.createElement('div');
parentDiv.style.border = '1px solid black'; // Add border
parentDiv.style.padding = '10px'; // Add padding for spacing
// Create a textarea
const textarea = document.createElement('textarea');
textarea.placeholder = 'Notes';
textarea.style.width = '150px'; // Adjust width as necessary
textarea.rows = 4; // Set number of visible rows
parentDiv.appendChild(textarea);
// Create a button
const setNotesButton = document.createElement('button');
setNotesButton.textContent = 'Set notes';
parentDiv.appendChild(setNotesButton);
setNotesButton.addEventListener('click', () => {
// Replace line breaks with "\n" character
const text = textarea.value
// Assuming you have a specific textarea to update
const textAreaToUpdate = document.querySelector('textarea[placeholder*="notes"]');
// Set the value of the specific textarea
const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLTextAreaElement.prototype,
'value'
).set;
nativeTextAreaValueSetter.call(textAreaToUpdate, text);
// Dispatch input event to notify any listeners
const event = new Event('input', { bubbles: true });
textAreaToUpdate.dispatchEvent(event);
});
parentNode.appendChild(parentDiv);
}
let isGiveawayAlarmToolRunning = false;
function createGiveawayAlarmTool(parentNode) {
if (isGiveawayAlarmToolRunning) {
console.log('Giveaway alarm tool is already running.');
return;
}
isGiveawayAlarmToolRunning = true;
let foundEntries = false;
let isCheckingEntries = false;
let rememberedEntriesDiv = null;
function checkEntries() {
if (isCheckingEntries) {
console.log('checkEntries is already running.');
return;
}
isCheckingEntries = true;
// Check if rememberedEntriesDiv is still in the document
if (rememberedEntriesDiv && !document.body.contains(rememberedEntriesDiv)) {
rememberedEntriesDiv = null;
}
const entriesDiv = rememberedEntriesDiv || Array.from(document.querySelectorAll('div')).find(div => /^\d+Entries$/.test(div.textContent));
// console.log('Checking for entries div:', entriesDiv);
if (entriesDiv) {
if (!foundEntries) {
console.log('Entries div found for the first time.', entriesDiv);
rememberedEntriesDiv = entriesDiv;
}
foundEntries = true;
} else if (foundEntries) {
console.log('Entries div was found before but is now missing.');
showAlarm();
foundEntries = false;
rememberedEntriesDiv = null;
} else {
// console.log('Entries div not found.');
}
isCheckingEntries = false;
}
function showAlarm() {
console.log('Showing alarm.');
const alarmDiv = document.createElement('div');
alarmDiv.textContent = 'Start the giveaway';
alarmDiv.style.position = 'fixed';
alarmDiv.style.top = '10%';
alarmDiv.style.left = '25%'; // Center the div horizontally
alarmDiv.style.width = '50%';
alarmDiv.style.backgroundColor = 'red';
alarmDiv.style.color = 'white';
alarmDiv.style.textAlign = 'center';
alarmDiv.style.padding = '10px';
alarmDiv.style.zIndex = '10000';
alarmDiv.style.fontSize = '2em'; // Increase text size
alarmDiv.style.border = '5px solid white'; // Restore white border
// Create a close button
const closeButton = document.createElement('button');
closeButton.textContent = 'Close';
closeButton.style.position = 'absolute';
closeButton.style.top = '50%';
closeButton.style.right = '10px'; // Slight distance from the right border
closeButton.style.transform = 'translateY(-50%)'; // Center vertically
closeButton.style.fontSize = '1em'; // Set text size to 1em
closeButton.style.padding = '5px'; // Add padding for spacing around text
closeButton.style.backgroundColor = 'grey'; // Add grey background
closeButton.style.border = '2px solid darkgrey'; // Add darkgrey border
closeButton.addEventListener('click', () => {
document.body.removeChild(alarmDiv);
});
alarmDiv.appendChild(closeButton);
document.body.appendChild(alarmDiv);
// Auto-close the alarm after 30 seconds
setTimeout(() => {
if (document.body.contains(alarmDiv)) {
document.body.removeChild(alarmDiv);
}
}, 30000);
}
console.log('Starting giveaway alarm tool.');
setInterval(checkEntries, 1000);
}
})();