Skip to content

Commit a02a33e

Browse files
steveseguinactions-user
authored andcommitted
feat(velora): capture volts, channel points, and subs
- Add DOM parsers for Volts (donation), Channel Points, and Subscription events in sources/velora.js. - Introduce event deduplication via `processedEvents` Set. - Enhance WebSocket handler for Channel Points to include user messages and metadata. - Update event reference documentation with Velora payload details. [auto-enhanced]
1 parent fd7e993 commit a02a33e

3 files changed

Lines changed: 198 additions & 0 deletions

File tree

docs/event-reference.html

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1145,6 +1145,51 @@ <h3>SharePlay.tv</h3>
11451145
</table>
11461146
</article>
11471147

1148+
<article class="platform-card" id="velora">
1149+
<h3>Velora</h3>
1150+
<p class="source-note">Implementation: <code>sources/velora.js</code> and <code>sources/websocket/velora.js</code></p>
1151+
<ul class="requirements">
1152+
<li>Standard mode reads the visible chat DOM; WebSocket mode uses the Velora Events API with OAuth.</li>
1153+
<li>Volts and channel point style cards are emitted as event payloads when exposed by the DOM or Events API.</li>
1154+
</ul>
1155+
<table class="event-table">
1156+
<thead>
1157+
<tr>
1158+
<th>Event</th>
1159+
<th>When it Fires</th>
1160+
<th>Payload Notes</th>
1161+
</tr>
1162+
</thead>
1163+
<tbody>
1164+
<tr>
1165+
<td><code>message</code></td>
1166+
<td>New Velora chat rows appear or Events API chat messages arrive.</td>
1167+
<td>Standard chat payload with badges, author color, links, and emotes preserved when not in text-only mode.</td>
1168+
</tr>
1169+
<tr>
1170+
<td class="donation-event"><code>volts</code></td>
1171+
<td>Velora Volts cards or <code>channel.volts</code> Events API payloads arrive.</td>
1172+
<td><code>hasDonation</code> carries the displayed Volts amount; DOM captures include <code>meta.source = "dom"</code>.</td>
1173+
</tr>
1174+
<tr>
1175+
<td><code>channel_points</code></td>
1176+
<td>Velora channel point/redemption cards or <code>channel.channel_points_redemption</code> Events API payloads arrive.</td>
1177+
<td><code>chatmessage</code> carries the redemption message or reward title; <code>meta.rewardTitle</code> identifies the reward when available.</td>
1178+
</tr>
1179+
<tr>
1180+
<td class="membership-event"><code>subscription</code></td>
1181+
<td>Visible Velora activity row says a user became a channel member/subscriber.</td>
1182+
<td><code>membership</code> carries the visible membership label.</td>
1183+
</tr>
1184+
<tr>
1185+
<td class="status-event"><code>viewer_update</code></td>
1186+
<td>Visible viewer count changes while viewer-count capture or hype mode is enabled.</td>
1187+
<td><code>meta</code> integer viewer count.</td>
1188+
</tr>
1189+
</tbody>
1190+
</table>
1191+
</article>
1192+
11481193
<article class="platform-card" id="rumble-dom">
11491194
<h3>Rumble - Standard DOM Capture</h3>
11501195
<p class="source-note">Implementation: <code>sources/rumble.js</code></p>

sources/velora.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
var channelName = "";
8282

8383
var processedMessages = new Set();
84+
var processedEvents = new Set();
8485

8586
function getChatMessageRoot(ele){
8687
if (!ele || !ele.isConnected || !ele.querySelector){ return null; }
@@ -234,6 +235,130 @@
234235
pushMessage(data);
235236
}
236237

238+
function makeBaseData(){
239+
return {
240+
chatbadges: [],
241+
backgroundColor: "",
242+
textColor: "",
243+
nameColor: "",
244+
chatimg: "",
245+
hasDonation: "",
246+
membership: "",
247+
contentimg: "",
248+
textonly: settings.textonlymode || false,
249+
type: "velora"
250+
};
251+
}
252+
253+
function rememberEvent(key, row){
254+
key = String(key || "").replace(/\s+/g, " ").trim();
255+
if (!key || processedEvents.has(key)){
256+
if (row){ row.skip = true; }
257+
return false;
258+
}
259+
processedEvents.add(key);
260+
if (processedEvents.size > 200){
261+
processedEvents.delete(processedEvents.values().next().value);
262+
}
263+
if (row){ row.skip = true; }
264+
return true;
265+
}
266+
267+
function nearestVeloraEventRow(ele){
268+
if (!ele || !ele.closest){ return null; }
269+
return ele.closest(".mx-1.my-1\\.5") || ele.closest("[class*='mx-1'][class*='my-1.5']") || ele.closest(".group") || ele;
270+
}
271+
272+
function parseVoltsEvent(row){
273+
var amountNode = null;
274+
var nameNode = null;
275+
try {
276+
var spans = Array.from(row.querySelectorAll("span"));
277+
amountNode = spans.find(function(span){
278+
return /\b\d[\d,.]*\s*V(?:olts?)?\b/i.test((span.textContent || "").trim());
279+
}) || null;
280+
if (!amountNode){ return null; }
281+
nameNode = spans.find(function(span){
282+
var text = (span.textContent || "").trim();
283+
return text && span !== amountNode && !/\b\d[\d,.]*\s*V(?:olts?)?\b/i.test(text);
284+
}) || null;
285+
} catch(e){
286+
return null;
287+
}
288+
var amountText = amountNode ? (amountNode.textContent || "").trim() : "";
289+
var name = nameNode ? (nameNode.textContent || "").trim() : "";
290+
if (!name || !amountText){ return null; }
291+
var data = makeBaseData();
292+
data.chatname = escapeHtml(name);
293+
data.chatmessage = "";
294+
data.hasDonation = escapeHtml(amountText.replace(/\bV\b/i, "Volts"));
295+
data.event = "volts";
296+
data.meta = {
297+
amountText: amountText,
298+
source: "dom"
299+
};
300+
return data;
301+
}
302+
303+
function parseChannelPointsEvent(row){
304+
var labelNode = null;
305+
var valueNode = null;
306+
try {
307+
var divs = Array.from(row.querySelectorAll("div"));
308+
labelNode = divs.find(function(div){
309+
return /\bsays\b/i.test((div.textContent || "").trim()) && (div.textContent || "").trim().length < 80;
310+
}) || null;
311+
if (!labelNode){ return null; }
312+
valueNode = labelNode.parentElement ? Array.from(labelNode.parentElement.querySelectorAll("span")).find(function(span){
313+
return (span.textContent || "").trim();
314+
}) : null;
315+
} catch(e){
316+
return null;
317+
}
318+
var label = labelNode ? (labelNode.textContent || "").trim() : "";
319+
var match = label.match(/^(.+?)\s+says$/i);
320+
var name = match && match[1] ? match[1].trim() : "";
321+
var message = valueNode ? getAllContentNodes(valueNode).trim() : "";
322+
if (!name || !message){ return null; }
323+
var data = makeBaseData();
324+
data.chatname = escapeHtml(name);
325+
data.chatmessage = message;
326+
data.event = "channel_points";
327+
data.meta = {
328+
rewardTitle: "Says",
329+
source: "dom"
330+
};
331+
return data;
332+
}
333+
334+
function parseSimpleActivityEvent(row){
335+
var text = (row && row.textContent || "").replace(/\s+/g, " ").trim();
336+
var match;
337+
var data;
338+
if (!text){ return null; }
339+
match = text.match(/^(.+?)\s+just became a\s+(.+?)!?$/i);
340+
if (!match){ return null; }
341+
data = makeBaseData();
342+
data.chatname = escapeHtml(match[1].trim());
343+
data.chatmessage = escapeHtml("became a " + match[2].trim());
344+
data.membership = escapeHtml(match[2].trim());
345+
data.event = "subscription";
346+
data.meta = { source: "dom" };
347+
return data;
348+
}
349+
350+
function processEventNode(ele){
351+
var row = nearestVeloraEventRow(ele);
352+
var data;
353+
if (!row || row.skip || getChatMessageRoot(row)){ return; }
354+
data = parseVoltsEvent(row) || parseChannelPointsEvent(row) || parseSimpleActivityEvent(row);
355+
if (!data){ return; }
356+
if (!rememberEvent("event|\u00b6|" + (data.event || "") + "|\u00b6|" + (data.chatname || "") + "|\u00b6|" + (data.chatmessage || "") + "|\u00b6|" + (data.hasDonation || "") + "|\u00b6|" + (row.textContent || ""), row)){
357+
return;
358+
}
359+
pushMessage(data);
360+
}
361+
237362
function pushMessage(data){
238363
try{
239364
chrome.runtime.sendMessage(chrome.runtime.id, { "message": data }, function(e){});
@@ -372,6 +497,12 @@
372497
processMessage(chatNode);
373498
});
374499
}
500+
processEventNode(addedNode);
501+
if (addedNode.querySelectorAll){
502+
addedNode.querySelectorAll(".mx-1.my-1\\.5, [class*='mx-1'][class*='my-1.5'], .flex.items-baseline").forEach(function(eventNode){
503+
processEventNode(eventNode);
504+
});
505+
}
375506
},300);
376507

377508
} catch(e){

sources/websocket/velora.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1353,7 +1353,29 @@ function handleChannelPoints(data) {
13531353
if (!data) return;
13541354
const name = data.displayName || data.username || 'Someone';
13551355
const reward = data.rewardTitle || 'channel point reward';
1356+
const message = data.userInput || data.message || data.input || '';
13561357
addAlert(`${escapeHtml(name)} redeemed: ${escapeHtml(reward)}`, 'points');
1358+
1359+
pushMessage({
1360+
chatname: escapeHtml(name),
1361+
chatbadges: [],
1362+
backgroundColor: '',
1363+
textColor: '',
1364+
nameColor: '',
1365+
chatmessage: message ? escapeHtml(message) : escapeHtml(reward),
1366+
chatimg: '',
1367+
hasDonation: '',
1368+
membership: '',
1369+
contentimg: '',
1370+
textonly: false,
1371+
type: 'velora',
1372+
event: 'channel_points',
1373+
meta: {
1374+
rewardTitle: reward,
1375+
rewardCost: data.rewardCost || data.cost || '',
1376+
redemptionId: data.redemptionId || data.id || ''
1377+
}
1378+
});
13571379
}
13581380

13591381
// ─── Chat sending ─────────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)