diff --git a/README.md b/README.md
index f8c5f95f1..1a4673cff 100644
--- a/README.md
+++ b/README.md
@@ -646,6 +646,7 @@ chatmessage | string | Chat message
chatimg | string | URL or DataBlob (under ~55KB) of the user's avatar image
type | lower-case string | the pre-qualified name of the source, eg: `twitch`, also used as the source png image
sourceImg | string | an alternative URL to the source image; relative or absolute
+sourceName | string | the channel's name or the username of the host for the channel
textonly | boolean | Whether the chat message is only plain text; or does it contain HTML, etc.
hasDonation | string | The donation amount with its units. eg: "3 roses" or "$50 USD".
chatbadges | array | An array of URLs/Objects. If an object, it may define itself as an img/svg and other attributes
diff --git a/actions/EventFlowEditor.js b/actions/EventFlowEditor.js
index b2f9f6915..1fdd0128b 100644
--- a/actions/EventFlowEditor.js
+++ b/actions/EventFlowEditor.js
@@ -24,6 +24,7 @@ class EventFlowEditor {
{ id: 'messageEquals', name: 'Message Equals' },
{ id: 'messageRegex', name: 'Message Regex' },
{ id: 'fromSource', name: 'From Source' },
+ { id: 'fromChannelName', name: 'From Channel Name' },
{ id: 'fromUser', name: 'From User' },
{ id: 'userRole', name: 'User Role' },
{ id: 'hasDonation', name: 'Has Donation' },
@@ -502,6 +503,12 @@ class EventFlowEditor {
// alert('Flow saved successfully!');
await this.loadFlowList(); // Refresh list
+ // Notify background instance to reload flows
+ if (window.parent && window.parent.eventFlowSystem && window.parent.eventFlowSystem !== this.eventFlowSystem) {
+ console.log('[EventFlowEditor] Notifying parent window to reload flows after save');
+ window.parent.eventFlowSystem.reloadFlows();
+ }
+
// Re-select the current flow in the list
document.querySelectorAll('.flow-item').forEach(item => {
item.classList.toggle('selected-flow', item.dataset.id === this.currentFlow.id);
@@ -545,6 +552,12 @@ class EventFlowEditor {
}
this.loadFlowList();
// alert('Flow deleted successfully.');
+
+ // Notify background instance to reload flows
+ if (window.parent && window.parent.eventFlowSystem && window.parent.eventFlowSystem !== this.eventFlowSystem) {
+ console.log('[EventFlowEditor] Notifying parent window to reload flows');
+ window.parent.eventFlowSystem.reloadFlows();
+ }
} catch (error) {
console.error('Error deleting flow:', error);
alert('Failed to delete flow. Check console for details.');
@@ -675,6 +688,7 @@ class EventFlowEditor {
case 'messageEquals': return `Text: "${(node.config.text || '').substring(0,15)}${(node.config.text || '').length > 15 ? '...' : ''}"`;
case 'messageRegex': return `Pattern: "${(node.config.pattern || '').substring(0,15)}${(node.config.pattern || '').length > 15 ? '...' : ''}"`;
case 'fromSource': return `Source: ${node.config.source || 'Any'}`;
+ case 'fromChannelName': return `Channel: ${node.config.channelName || 'Any'}`;
case 'fromUser': return `User: ${node.config.username || 'Any'}`;
case 'userRole': return `Role: ${node.config.role || 'Any'}`;
case 'hasDonation': return 'Has donation';
@@ -946,6 +960,7 @@ class EventFlowEditor {
case 'messageEquals': node.config = { text: 'hello' }; break;
case 'messageRegex': node.config = { pattern: 'pattern', flags: 'i' }; break;
case 'fromSource': node.config = { source: '*' }; break;
+ case 'fromChannelName': node.config = { channelName: '' }; break;
case 'fromUser': node.config = { username: 'user' }; break;
case 'userRole': node.config = { role: 'mod' }; break;
case 'hasDonation': node.config = {}; break;
@@ -1171,6 +1186,10 @@ class EventFlowEditor {
${['twitch', 'youtube', 'facebook', 'kick', 'tiktok', 'instagram', 'discord', 'slack', 'other'].map(s => ``).join('')}
`;
break;
+ case 'fromChannelName':
+ html += `
+
Match messages from a specific channel name or host username
`;
+ break;
case 'fromUser':
html += ``;
break;
diff --git a/actions/EventFlowSystem.js b/actions/EventFlowSystem.js
index 089e82c32..b40845136 100644
--- a/actions/EventFlowSystem.js
+++ b/actions/EventFlowSystem.js
@@ -8,7 +8,9 @@ class EventFlowSystem {
this.sendMessageToTabs = options.sendMessageToTabs || null;
this.sendToDestinations = options.sendToDestinations || null;
this.fetchWithTimeout = options.fetchWithTimeout || window.fetch; // Fallback to window.fetch if not provided
-
+ this.sanitizeRelay = options.sanitizeRelay || null;
+ this.checkExactDuplicateAlreadyRelayed = options.checkExactDuplicateAlreadyRelayed || null;
+
console.log('[EventFlowSystem Constructor] Initialized with:');
console.log(' - sendMessageToTabs:', this.sendMessageToTabs ? 'Function provided' : 'NULL - Relay will not work!');
console.log(' - sendToDestinations:', this.sendToDestinations ? 'Function provided' : 'NULL');
@@ -322,6 +324,13 @@ class EventFlowSystem {
return this.flows;
}
+ async reloadFlows() {
+ // Force reload flows from database
+ console.log('[EventFlowSystem] Reloading flows from database');
+ await this.loadFlows();
+ return this.flows;
+ }
+
async getFlowById(flowId) {
return this.flows.find(flow => flow.id === flowId) || null;
}
@@ -472,38 +481,71 @@ class EventFlowSystem {
return overallResult;
}
+ stripHtml(html) {
+ // Simple HTML stripping function that preserves emoji alt text
+ if (!html || typeof html !== 'string') return html;
+
+ // Create a temporary element to use browser's HTML parsing
+ const tmp = document.createElement('div');
+ tmp.innerHTML = html;
+
+ // Replace img tags with their alt text (especially for emojis)
+ tmp.querySelectorAll('img[alt]').forEach(img => {
+ const alt = img.getAttribute('alt');
+ img.replaceWith(document.createTextNode(alt));
+ });
+
+ // Get text content and clean up extra whitespace
+ const text = tmp.textContent || tmp.innerText || '';
+ return text.replace(/\s\s+/g, ' ').trim();
+ }
+
async evaluateTrigger(triggerNode, message) {
const { triggerType, config } = triggerNode;
// console.log(`[EvaluateTrigger] Node: ${triggerNode.id}, Type: ${triggerType}, Config: ${JSON.stringify(config)}, Message: ${message.chatmessage}`);
let match = false;
+
+ // Get the message text for comparison
+ let messageText = message.chatmessage;
+ if (message && messageText && typeof messageText === 'string') {
+ // If textonly flag is set, the message is already plain text
+ if (!message.textonly) {
+ // Check if we've already cleaned this message (cache the result)
+ if (!message.textContent) {
+ message.textContent = this.stripHtml(messageText);
+ }
+ messageText = message.textContent;
+ }
+ }
+
switch (triggerType) {
case 'messageContains':
// Ensure properties exist before trying to access them
- match = message && message.chatmessage && typeof message.chatmessage === 'string' &&
+ match = message && messageText && typeof messageText === 'string' &&
config && typeof config.text === 'string' &&
- message.chatmessage.includes(config.text);
- //console.log(`[EvaluateTrigger - messageContains] Config Text: "${config.text}", Message Chatmessage: "${message.chatmessage}", Match: ${match}`);
+ messageText.includes(config.text);
+ //console.log(`[EvaluateTrigger - messageContains] Config Text: "${config.text}", Message Text: "${messageText}", Match: ${match}`);
return match;
case 'messageStartsWith':
- match = message && message.chatmessage && typeof message.chatmessage === 'string' &&
+ match = message && messageText && typeof messageText === 'string' &&
config && typeof config.text === 'string' &&
- message.chatmessage.startsWith(config.text);
- //console.log(`[EvaluateTrigger - messageStartsWith] Config Text: "${config.text}", Message Chatmessage: "${message.chatmessage}", Match: ${match}`);
+ messageText.startsWith(config.text);
+ //console.log(`[EvaluateTrigger - messageStartsWith] Config Text: "${config.text}", Message Text: "${messageText}", Match: ${match}`);
return match;
case 'messageEquals':
- match = message && typeof message.chatmessage === 'string' &&
+ match = message && typeof messageText === 'string' &&
config && typeof config.text === 'string' &&
- message.chatmessage === config.text;
- //console.log(`[EvaluateTrigger - messageEquals] Config Text: "${config.text}", Message Chatmessage: "${message.chatmessage}", Match: ${match}`);
+ messageText === config.text;
+ //console.log(`[EvaluateTrigger - messageEquals] Config Text: "${config.text}", Message Text: "${messageText}", Match: ${match}`);
return match;
case 'messageRegex':
try {
const regex = new RegExp(config.pattern, config.flags || '');
- match = regex.test(message.chatmessage);
- //console.log(`[EvaluateTrigger - messageRegex] Pattern: "${config.pattern}", Flags: "${config.flags}", Message: "${message.chatmessage}", Match: ${match}`);
+ match = regex.test(messageText);
+ //console.log(`[EvaluateTrigger - messageRegex] Pattern: "${config.pattern}", Flags: "${config.flags}", Message: "${messageText}", Match: ${match}`);
return match;
} catch (e) {
console.error('[EvaluateTrigger - messageRegex] Invalid regex:', e);
@@ -519,6 +561,16 @@ class EventFlowSystem {
console.log(`[RELAY DEBUG - fromSource Trigger] Config Source: "${config.source}", Message Type: "${message.type}", Match: ${match}`);
return match;
+ case 'fromChannelName':
+ if (!config.channelName || config.channelName.trim() === '') {
+ match = true; // Match any channel if no name specified
+ } else {
+ const channelName = (message.sourceName || '').toLowerCase();
+ match = channelName === config.channelName.toLowerCase();
+ }
+ console.log(`[RELAY DEBUG - fromChannelName Trigger] Config Channel: "${config.channelName}", Message Channel: "${message.sourceName}", Match: ${match}`);
+ return match;
+
case 'fromUser':
const identifier = (message.userid || message.chatname || '').toLowerCase();
match = config && typeof config.username === 'string' && identifier === config.username.toLowerCase();
@@ -641,7 +693,7 @@ class EventFlowSystem {
console.log('[RELAY DEBUG - Action] sendMessageToTabs available?', !!this.sendMessageToTabs);
console.log('[RELAY DEBUG - Action] sendMessageToTabs type:', typeof this.sendMessageToTabs);
- if (this.sendMessageToTabs) {
+ if (this.sendMessageToTabs && message && !message.reflection) {
const relayMessage = {
response: config.template
.replace('{source}', message.type || '')
@@ -653,10 +705,6 @@ class EventFlowSystem {
relayMessage.tid = message.tid;
}
- if (config.destination && config.destination.trim()) {
- relayMessage.destination = config.destination.trim();
- }
-
console.log('[RELAY DEBUG - Action] Relay message prepared:', relayMessage);
console.log('[RELAY DEBUG - Action] Config:', config);
console.log('[RELAY DEBUG - Action] Calling sendMessageToTabs with params:');
@@ -666,9 +714,15 @@ class EventFlowSystem {
console.log(' - relayMode:', true);
console.log(' - antispam:', false);
console.log(' - timeout:', config.timeout || 5100);
-
- // Use relayMode=true to mark this as a relayed message and prevent circular relaying
- const result = this.sendMessageToTabs(relayMessage, config.toAll === true, null, true, false, config.timeout || 5100);
+
+ let result = false;
+ relayMessage.response = this.sanitizeRelay(relayMessage.response, false).trim();
+ if (relayMessage.response) {
+ if (!this.checkExactDuplicateAlreadyRelayed(relayMessage.response, false, relayMessage.tid, false)){
+ result = this.sendMessageToTabs(relayMessage, true, message, true, false, 1000);
+ }
+ }
+
console.log('[RELAY DEBUG - Action] sendMessageToTabs returned:', result);
} else {
console.error('[RELAY DEBUG - Action] CRITICAL: sendMessageToTabs is not available!');
diff --git a/actions/index.html b/actions/index.html
index 6f444e33d..f1afd9d01 100644
--- a/actions/index.html
+++ b/actions/index.html
@@ -10,7 +10,8 @@
Event Flow Editor
-
The Flow Editor lets you create custom and complex triggers and action workflows. Have fun with your stream!
+
The Flow Editor lets you create custom and complex triggers and action workflows. Have fun with your stream!
+ โก For media playback and OBS actions, add the Actions Overlay to your stream
@@ -22,9 +23,12 @@
-
-
-
Test Flow
+
+
+
+
Test Flow
+
+
+
+
+
+
+
+ Warning: The current flow has unsaved changes. Test results may not reflect the latest changes.
+
+
Test results will appear here after running a test.
+
diff --git a/api.md b/api.md
index 815f16368..023f3c09c 100644
--- a/api.md
+++ b/api.md
@@ -58,6 +58,10 @@ There is an easy to use sandbox to play with some of the common API commands and
- [Best Practices and Improvements](#best-practices-and-improvements-3)
- [Integration with Other Components](#integration-with-other-components-1)
- [Battle royale, Polls, etc](#battle-royale-polls-etc)
+ - [Poll Control via API](#poll-control-via-api)
+ - [Basic Poll Controls](#basic-poll-controls)
+ - [Advanced Poll Controls](#advanced-poll-controls)
+ - [Example Usage](#example-usage)
- [Battle Page (battle.html)](#battle-page-battlehtml)
- [Communication Method](#communication-method)
- [Game Features](#game-features)
@@ -182,6 +186,14 @@ When a message is sent, it goes to the specified output channel. Those who have
14. **Draw Mode**
- `{"action": "drawmode", "value": true}`
+15. **Poll Operations**
+ - Reset: `{"action": "resetpoll"}`
+ - Close: `{"action": "closepoll"}`
+ - Load Preset: `{"action": "loadpoll", "value": {"pollId": "poll-123456"}}`
+ - Set Settings: `{"action": "setpollsettings", "value": {"pollQuestion": "What's your favorite color?", "pollType": "multiple", "multipleChoiceOptions": "Red\nBlue\nGreen"}}`
+ - Get Presets: `{"action": "getpollpresets"}`
+ - Create New: `{"action": "createpoll", "value": {"settings": {"pollQuestion": "New Poll", "pollType": "freeform"}}}`
+
### Channel-Specific Messaging
You can send messages to specific channels using the `content` action with a channel number:
@@ -387,6 +399,7 @@ chatmessage | string | Chat message
chatimg | string | URL or DataBlob (under ~55KB) of the user's avatar image
type | lower-case string | the pre-qualified name of the source, eg: `twitch`, also used as the source png image
sourceImg | string | an alternative URL to the source image; relative or absolute
+sourceName | string | the channel's name or the username of the host for the channel
textonly | boolean | Whether the chat message is only plain text; or does it contain HTML, etc.
hasDonation | string | The donation amount with its units. eg: "3 roses" or "$50 USD".
chatbadges | array | An array of URLs/Objects. If an object, it may define itself as an img/svg and other attributes
@@ -762,7 +775,7 @@ This integration allows streamers or moderators to manage waitlists or giveaways
These pages may lack API support directly, however in some cases they can be controlled via the extension's API.
-For example the waitlist has some functions that can be controlled via the extensiion:
+For example the waitlist has some functions that can be controlled via the extension:
```
removefromwaitlist
@@ -773,6 +786,44 @@ downloadwaitlist
selectwinner
```
+## Poll Control via API
+
+The poll system can now be controlled through the API with the following actions:
+
+### Basic Poll Controls
+- **Reset Poll**: `{"action": "resetpoll"}` - Resets the current poll, clearing all votes
+- **Close Poll**: `{"action": "closepoll"}` - Closes the current poll, preventing new votes
+
+### Advanced Poll Controls
+- **Load Poll Preset**: `{"action": "loadpoll", "value": {"pollId": "poll-123456"}}` - Loads a previously saved poll preset by its ID
+- **Get Poll Presets**: `{"action": "getpollpresets"}` - Returns a list of all saved poll presets with their IDs and names
+- **Set Poll Settings**: `{"action": "setpollsettings", "value": {...}}` - Updates the current poll settings
+ - Available settings: `pollType`, `pollQuestion`, `multipleChoiceOptions`, `pollStyle`, `pollTimer`, `pollTimerState`, `pollTally`, `pollSpam`
+- **Create New Poll**: `{"action": "createpoll", "value": {"settings": {...}}}` - Creates a new poll with specified settings
+
+### Example Usage
+```javascript
+// Create a multiple choice poll
+ws.send(JSON.stringify({
+ action: "createpoll",
+ value: {
+ settings: {
+ pollType: "multiple",
+ pollQuestion: "What's your favorite streaming platform?",
+ multipleChoiceOptions: "Twitch\nYouTube\nFacebook\nOther",
+ pollTimer: "120",
+ pollTimerState: true
+ }
+ }
+}));
+
+// Load a saved poll preset
+ws.send(JSON.stringify({
+ action: "loadpoll",
+ value: { pollId: "poll-1234567890" }
+}));
+```
+
Just to touch on the Battle Royal game though,
## Battle Page (battle.html)
diff --git a/background.html b/background.html
index 0ec5d730d..3a33a2785 100644
--- a/background.html
+++ b/background.html
@@ -719,11 +719,11 @@
Event Flow Editor
-
The Flow Editor lets you create custom and complex triggers and action workflows. Have fun with your stream!
+
The Flow Editor lets you create custom and complex triggers and action workflows. Have fun with your stream!
+ โก For media playback and OBS actions, add the Actions Overlay to your stream
-
-
-
+
+
diff --git a/background.js b/background.js
index 9f7522835..96f43dd00 100644
--- a/background.js
+++ b/background.js
@@ -236,9 +236,16 @@ if (typeof chrome.runtime == "undefined") {
var sender = {};
sender.tab = {};
sender.tab.id = null;
- onMessageCallback(args[0], sender, function (response) {
+ const request = args[0];
+ const callbackId = request ? request.callbackId : null;
+
+ onMessageCallback(request, sender, function (response) {
// (request, sender, sendResponse)
//log("sending response to pop up:",response);
+ // Preserve callbackId in response if it exists
+ if (callbackId && response) {
+ response.callbackId = callbackId;
+ }
ipcRenderer.send("fromBackgroundPopupResponse", response);
});
});
@@ -2881,6 +2888,9 @@ chrome.runtime.onMessage.addListener(async function (request, sender, sendRespon
if (request.setting == "textonlymode") {
pushSettingChange();
}
+ if (request.setting == "vdoninjadiscord") {
+ pushSettingChange();
+ }
if (request.setting == "ignorealternatives") {
pushSettingChange();
}
@@ -3363,6 +3373,30 @@ chrome.runtime.onMessage.addListener(async function (request, sender, sendRespon
triggerFakeRandomMessage();
+ } else if (request.action === "startReplay") {
+ // Handle replay messages from timestamp
+ console.log('Received startReplay request:', request);
+
+ // Check if extension is on
+ if (!isExtensionOn) {
+ sendResponse({ error: 'Social Stream is not enabled. Please turn it on first.' });
+ return;
+ }
+
+ handleReplayMessages(request, sendResponse);
+ return true; // async response
+ } else if (request.action === "pauseReplay") {
+ pauseReplay(request.sessionId);
+ sendResponse({success: true, state: isExtensionOn});
+ } else if (request.action === "resumeReplay") {
+ resumeReplay(request.sessionId);
+ sendResponse({success: true, state: isExtensionOn});
+ } else if (request.action === "stopReplay") {
+ stopReplay(request.sessionId);
+ sendResponse({success: true, state: isExtensionOn});
+ } else if (request.action === "updateReplaySpeed") {
+ updateReplaySpeed(request.sessionId, request.speed);
+ sendResponse({success: true, state: isExtensionOn});
} else if (request.cmd && request.cmd === "sidUpdated") {
if (request.streamID) {
streamID = request.streamID;
@@ -3656,7 +3690,8 @@ async function sendToDestinations(message) {
}
if (settings.filtereventstoggle && settings.filterevents && settings.filterevents.textsetting && message.chatmessage && message.event) {
- if (settings.filterevents.textsetting.split(",").some(v => (v.trim() && message.chatmessage.includes(v)))) {
+ const messageText = message.textContent || message.chatmessage;
+ if (settings.filterevents.textsetting.split(",").some(v => (v.trim() && messageText.includes(v)))) {
return false;
}
}
@@ -3757,7 +3792,7 @@ async function sendToDestinations(message) {
return true;
}
-async function replayMessagesFromTimestamp(startTimestamp) {
+async function replayMessagesFromTimestamp(startTimestamp, endTimestamp = null, speed = 1, sessionId = null) {
const db = await messageStoreDB.ensureDB();
return new Promise((resolve, reject) => {
@@ -3766,7 +3801,13 @@ async function replayMessagesFromTimestamp(startTimestamp) {
const index = store.index("timestamp");
const messages = [];
- const range = IDBKeyRange.lowerBound(startTimestamp);
+ let range;
+ if (endTimestamp) {
+ range = IDBKeyRange.bound(startTimestamp, endTimestamp);
+ } else {
+ range = IDBKeyRange.lowerBound(startTimestamp);
+ }
+
const cursorRequest = index.openCursor(range);
cursorRequest.onsuccess = (event) => {
@@ -3776,24 +3817,98 @@ async function replayMessagesFromTimestamp(startTimestamp) {
cursor.continue();
} else {
if (messages.length === 0) {
- resolve(0);
+ resolve({ messageCount: 0, messages: [] });
return;
}
messages.sort((a, b) => a.timestamp - b.timestamp);
- const baseTime = messages[0].timestamp;
+
+ // Calculate when replay should start relative to now
+ const replayStartTime = Date.now();
+ const originalStartTime = startTimestamp;
+
+ // Store session info for control
+ if (sessionId) {
+ replaySessions[sessionId] = {
+ messages: messages,
+ currentIndex: 0,
+ isPaused: false,
+ speed: speed,
+ timeouts: [],
+ startTime: replayStartTime,
+ originalStartTime: originalStartTime
+ };
+ }
- messages.forEach(message => {
-
- const relativeDelay = message.timestamp - baseTime;
- delete message.mid; // only found in messages restored from db.
-
- setTimeout(() => {
- sendDataP2P(message);
- }, relativeDelay);
+ // Log first and last message times for debugging
+ if (messages.length > 0) {
+ console.log('Replay timeline:', {
+ requestedStart: new Date(originalStartTime).toLocaleString(),
+ firstMessage: new Date(messages[0].timestamp).toLocaleString(),
+ lastMessage: new Date(messages[messages.length - 1].timestamp).toLocaleString(),
+ totalDuration: ((messages[messages.length - 1].timestamp - originalStartTime) / 1000 / 60).toFixed(1) + ' minutes',
+ messageCount: messages.length
+ });
+ }
+
+ messages.forEach((message, index) => {
+ // Calculate delay from the requested start time, not from first message
+ const messageOffsetFromStart = message.timestamp - originalStartTime;
+ const scaledDelay = messageOffsetFromStart / speed;
+
+ // Skip messages that would have negative delay (shouldn't happen with proper range query)
+ if (scaledDelay < 0) {
+ console.warn('Skipping message with negative delay:', message);
+ return;
+ }
+
+ // Log timing for first few messages
+ if (index < 3) {
+ console.log(`Message ${index + 1} will play after ${(scaledDelay / 1000).toFixed(1)}s - "${message.chatmessage?.substring(0, 50)}..."`);
+ }
+
+ delete message.mid; // only found in messages restored from db.
+
+ const timeoutId = setTimeout(() => {
+ if (sessionId && replaySessions[sessionId]) {
+ if (!replaySessions[sessionId].isPaused) {
+ sendDataP2P(message);
+ replaySessions[sessionId].currentIndex = index + 1;
+
+ // Send progress update
+ const progress = ((index + 1) / messages.length) * 100;
+ // Send to all extension pages
+ chrome.runtime.sendMessage({
+ action: 'replayProgress',
+ sessionId: sessionId,
+ progress: progress,
+ currentMessage: index + 1,
+ totalMessages: messages.length,
+ currentTimestamp: message.timestamp,
+ messageDetails: {
+ chatname: message.chatname,
+ chatmessage: message.chatmessage
+ }
+ }).catch(() => {
+ // Ignore errors if no listeners
+ });
+
+ // Clean up if this was the last message
+ if (index === messages.length - 1) {
+ delete replaySessions[sessionId];
+ }
+ }
+ } else {
+ sendDataP2P(message);
+ }
+ }, scaledDelay);
+
+ if (sessionId && replaySessions[sessionId]) {
+ replaySessions[sessionId].timeouts.push(timeoutId);
+ }
});
- resolve(messages.length);
+ resolve({ messageCount: messages.length, messages: messages });
}
};
@@ -3801,6 +3916,66 @@ async function replayMessagesFromTimestamp(startTimestamp) {
});
}
+// Replay session management
+const replaySessions = {};
+
+async function handleReplayMessages(request, sendResponse) {
+ try {
+ console.log('Starting replay with params:', {
+ start: new Date(request.startTimestamp),
+ end: request.endTimestamp ? new Date(request.endTimestamp) : 'none',
+ speed: request.speed
+ });
+
+ // Make sure we have the database
+ if (!messageStoreDB) {
+ sendResponse({ error: 'Database not initialized' });
+ return;
+ }
+
+ const result = await replayMessagesFromTimestamp(
+ request.startTimestamp,
+ request.endTimestamp || null,
+ request.speed || 1,
+ request.sessionId
+ );
+
+ console.log('Replay started successfully:', result);
+ sendResponse(result);
+ } catch (error) {
+ console.error('Error in handleReplayMessages:', error);
+ sendResponse({ error: error.message || 'Unknown error occurred' });
+ }
+}
+
+function pauseReplay(sessionId) {
+ if (replaySessions[sessionId]) {
+ replaySessions[sessionId].isPaused = true;
+ }
+}
+
+function resumeReplay(sessionId) {
+ if (replaySessions[sessionId]) {
+ replaySessions[sessionId].isPaused = false;
+ }
+}
+
+function stopReplay(sessionId) {
+ if (replaySessions[sessionId]) {
+ // Clear all pending timeouts
+ replaySessions[sessionId].timeouts.forEach(timeoutId => clearTimeout(timeoutId));
+ delete replaySessions[sessionId];
+ }
+}
+
+function updateReplaySpeed(sessionId, newSpeed) {
+ if (replaySessions[sessionId]) {
+ // This would require re-calculating timeouts, which is complex
+ // For now, just update the speed for future reference
+ replaySessions[sessionId].speed = newSpeed;
+ }
+}
+
function unescapeHtml(safe) {
return safe
@@ -4074,7 +4249,8 @@ function sendToS10(data, fakechat=false, relayed=false) {
return;
}
- if (data.chatmessage.includes(miscTranslations.said)){
+ const checkMessage = data.textContent || data.chatmessage;
+ if (checkMessage.includes(miscTranslations.said)){
return null;
}
@@ -4089,6 +4265,9 @@ function sendToS10(data, fakechat=false, relayed=false) {
return;
}
+ // Store the cleaned text content for reuse elsewhere
+ data.textContent = cleaned;
+
if (relayed && !verifyOriginalNewIncomingMessage(cleaned, true)){
if (data.bot) {
return null;
@@ -4681,8 +4860,9 @@ function sendToStreamerBot(data, fakechat = false, relayed = false) {
}
// Skip messages that contain certain translations or empty messages
+ const checkMessage = data.textContent || data.chatmessage;
if (!data.chatmessage ||
- (data.chatmessage.includes && data.chatmessage.includes(miscTranslations.said))) {
+ (checkMessage.includes && checkMessage.includes(miscTranslations.said))) {
return null;
}
@@ -4698,6 +4878,9 @@ function sendToStreamerBot(data, fakechat = false, relayed = false) {
if (!cleaned) {
return;
}
+
+ // Store the cleaned text content for reuse elsewhere
+ data.textContent = cleaned;
// Duplicate message handling logic (assuming functions are defined elsewhere)
if (relayed && typeof verifyOriginalNewIncomingMessage === 'function' && !verifyOriginalNewIncomingMessage(cleaned, true)) {
@@ -5277,6 +5460,36 @@ function setupSocket() {
} else if (data.action && data.action === "closepoll") {
sendTargetP2P({cmd:"closepoll"},"poll");
resp = true;
+ } else if (data.action && data.action === "loadpoll") {
+ // Load a saved poll preset by ID
+ if (data.value && data.value.pollId) {
+ loadPollPreset(data.value.pollId);
+ resp = true;
+ }
+ } else if (data.action && data.action === "setpollsettings") {
+ // Directly set poll settings via API
+ if (data.value && typeof data.value === 'object') {
+ updatePollSettings(data.value);
+ resp = true;
+ }
+ } else if (data.action && data.action === "getpollpresets") {
+ // Return list of saved poll presets
+ getPollPresets(function(presets) {
+ if (data.get && e.data.UUID) {
+ var ret = {};
+ ret.callback = {};
+ ret.callback.get = data.get;
+ ret.callback.result = presets;
+ socketserver.send(JSON.stringify(ret));
+ }
+ });
+ resp = true;
+ } else if (data.action && data.action === "createpoll") {
+ // Create a new poll with specific settings
+ if (data.value && data.value.settings) {
+ createNewPoll(data.value.settings);
+ resp = true;
+ }
} else if (data.action && data.action === "stopentries") {
toggleEntries(false);
resp = true;
@@ -6294,6 +6507,95 @@ function initializePoll() {
} catch (e) {}
}
+function loadPollPreset(pollId) {
+ chrome.storage.local.get(['savedPolls'], function(result) {
+ if (result.savedPolls) {
+ try {
+ const savedPolls = JSON.parse(result.savedPolls);
+ const poll = savedPolls.find(p => p.id === pollId);
+ if (poll && poll.settings) {
+ // Update settings with the loaded poll
+ Object.keys(poll.settings).forEach(key => {
+ if (settings.hasOwnProperty(key)) {
+ settings[key] = poll.settings[key];
+ }
+ });
+ // Send updated settings to poll overlay
+ sendTargetP2P({settings:settings}, "poll");
+ // Save updated settings
+ chrome.storage.local.set({settings: settings});
+ }
+ } catch (e) {
+ log("Error loading poll preset: " + e.message);
+ }
+ }
+ });
+}
+
+function updatePollSettings(newSettings) {
+ try {
+ // Update poll-related settings
+ const pollKeys = ['pollType', 'pollQuestion', 'multipleChoiceOptions',
+ 'pollStyle', 'pollTimer', 'pollTimerState', 'pollTally', 'pollSpam'];
+
+ pollKeys.forEach(key => {
+ if (newSettings.hasOwnProperty(key)) {
+ settings[key] = newSettings[key];
+ }
+ });
+
+ // Send updated settings to poll overlay
+ sendTargetP2P({settings:settings}, "poll");
+ // Save settings
+ chrome.storage.local.set({settings: settings});
+ } catch (e) {
+ log("Error updating poll settings: " + e.message);
+ }
+}
+
+function getPollPresets(callback) {
+ chrome.storage.local.get(['savedPolls'], function(result) {
+ try {
+ if (result.savedPolls) {
+ const savedPolls = JSON.parse(result.savedPolls);
+ // Return simplified list with id and name
+ const presets = savedPolls.map(poll => ({
+ id: poll.id,
+ name: poll.name
+ }));
+ callback(presets);
+ } else {
+ callback([]);
+ }
+ } catch (e) {
+ log("Error getting poll presets: " + e.message);
+ callback([]);
+ }
+ });
+}
+
+function createNewPoll(pollSettings) {
+ try {
+ // Reset to default poll settings
+ const defaultSettings = {
+ pollType: 'freeform',
+ pollQuestion: '',
+ multipleChoiceOptions: '',
+ pollStyle: 'default',
+ pollTimer: '60',
+ pollTimerState: false,
+ pollTally: true,
+ pollSpam: false
+ };
+
+ // Merge with provided settings
+ const finalSettings = {...defaultSettings, ...pollSettings};
+ updatePollSettings(finalSettings);
+ } catch (e) {
+ log("Error creating new poll: " + e.message);
+ }
+}
+
function initializeWaitlist() {
try {
if (!settings.waitlistmode) { // stop and clear
@@ -7843,7 +8145,7 @@ class HostMessageFilter {
// Determine message content based on available fields
let messageContent = '';
if (message.textonly) {
- messageContent = message.textonly;
+ messageContent = message.chatmessage;
} else if (message.chatmessage !== undefined) {
messageContent = this.sanitizeMessage(message.chatmessage);
} else if (message.hasDonation || (message.membership && message.event)) {
@@ -8359,7 +8661,8 @@ async function applyBotActions(data, tab = false) {
}
- if (settings.relayall && data.chatmessage && !data.event && tab && data.chatmessage.includes(miscTranslations.said)){
+ const messageToCheck = data.textContent || data.chatmessage;
+ if (settings.relayall && data.chatmessage && !data.event && tab && messageToCheck.includes(miscTranslations.said)){
//console.log("1");
return null;
@@ -8447,9 +8750,10 @@ async function applyBotActions(data, tab = false) {
if (!event?.setting || !command || !response) continue;
const isFullMatch = settings.botReplyMessageFull;
+ const messageText = data.textContent || data.chatmessage;
const messageMatches = isFullMatch ?
- data.chatmessage === command :
- data.chatmessage.includes(command);
+ messageText === command :
+ messageText.includes(command);
if (!messageMatches) continue;
@@ -8519,14 +8823,16 @@ async function applyBotActions(data, tab = false) {
if (settings.highlightevent && settings.highlightevent.textsetting.trim() && data.chatmessage && data.event) {
const eventTexts = settings.highlightevent.textsetting.split(',').map(text => text.trim());
- if (eventTexts.some(text => data.chatmessage.includes(text))) {
+ const messageText = data.textContent || data.chatmessage;
+ if (eventTexts.some(text => messageText.includes(text))) {
data.highlightColor = "#fff387";
}
}
if (settings.highlightword && settings.highlightword.textsetting.trim() && data.chatmessage) {
const wordTexts = settings.highlightword.textsetting.split(',').map(text => text.trim());
- if (wordTexts.some(text => data.chatmessage.includes(text))) {
+ const messageText = data.textContent || data.chatmessage;
+ if (wordTexts.some(text => messageText.includes(text))) {
data.highlightColor = "#fff387";
}
}
@@ -8535,7 +8841,8 @@ async function applyBotActions(data, tab = false) {
//if (Date.now() - messageTimeout > 100) {
// respond to "1" with a "1" automatically; at most 1 time per 100ms.
- if (data.chatmessage.includes(". Thank you") && data.chatmessage.includes(" donated ")) {
+ const messageText = data.textContent || data.chatmessage;
+ if (messageText.includes(". Thank you") && messageText.includes(" donated ")) {
return null;
} // probably a reply
@@ -8582,7 +8889,8 @@ async function applyBotActions(data, tab = false) {
}
}
} else if (settings.giphyKey && settings.giphyKey.textsetting && settings.giphy2 && data.chatmessage && data.chatmessage.indexOf("#") != -1 && !data.contentimg) {
- var xx = data.chatmessage.split(" ");
+ const messageText = data.textContent || data.chatmessage;
+ var xx = messageText.split(" ");
for (var i = 0; i < xx.length; i++) {
var word = xx[i];
if (!word.startsWith("#")) {
@@ -8596,9 +8904,9 @@ async function applyBotActions(data, tab = false) {
}
if (settings.hidegiphytrigger) {
- if (data.chatmessage.includes("#" + word + " " + order)) {
+ if (messageText.includes("#" + word + " " + order)) {
data.chatmessage = data.chatmessage.replace("#" + word + " " + order, "");
- } else if (data.chatmessage.includes("#" + word + " ")) {
+ } else if (messageText.includes("#" + word + " ")) {
data.chatmessage = data.chatmessage.replace("#" + word + " ", "");
} else {
data.chatmessage = data.chatmessage.replace("#" + word, "");
@@ -9502,6 +9810,8 @@ function isEqualMessage(message1, message2) {
window.sendMessageToTabs = sendMessageToTabs;
window.sendToDestinations = sendToDestinations;
window.fetchWithTimeout = fetchWithTimeout;
+window.sanitizeRelay = sanitizeRelay;
+window.checkExactDuplicateAlreadyRelayed = checkExactDuplicateAlreadyRelayed;
console.log('[EventFlow Init] Checking sendMessageToTabs function:', typeof window.sendMessageToTabs, window.sendMessageToTabs ? window.sendMessageToTabs.toString().substring(0, 100) : 'null');
@@ -9509,7 +9819,9 @@ let tmp = new EventFlowSystem({
sendMessageToTabs: window.sendMessageToTabs || null,
sendToDestinations: window.sendToDestinations || null,
pointsSystem: window.pointsSystem || null,
- fetchWithTimeout: window.fetchWithTimeout // Assuming fetchWithTimeout is on window from background.js
+ fetchWithTimeout: window.fetchWithTimeout || null, // Assuming fetchWithTimeout is on window from background.js
+ sanitizeRelay: window.sanitizeRelay || null,
+ checkExactDuplicateAlreadyRelayed: window.checkExactDuplicateAlreadyRelayed || null,
});
tmp.initPromise.then(() => {
diff --git a/baretempate.html b/baretempate.html
index 4c26fc6f9..9a1624200 100644
--- a/baretempate.html
+++ b/baretempate.html
@@ -55,7 +55,7 @@
// we get the data via this embedded iFRAME.
const iframe = document.createElement("iframe");
const filename = "dock"; //new URL(window.location.href).pathname.split('/').pop().split('.')[0];
- iframe.src = "https://vdo.socialstream.ninja/?ln&salt=vdo.ninja¬mobile¬mobile&password=false&solo&view=" + urlParams.get('session') + "&novideo&noaudio&label="+filename+"&cleanoutput&room=" + urlParams.get('session');
+ iframe.src = "https://vdo.socialstream.ninja/?ln&salt=vdo.ninja¬mobile&password=false&solo&view=" + urlParams.get('session') + "&novideo&noaudio&label="+filename+"&cleanoutput&room=" + urlParams.get('session');
iframe.style.width = "0px";
iframe.style.height = "0px";
iframe.style.position = "fixed";
diff --git a/battle.html b/battle.html
index dba1d9e94..32882cdac 100644
--- a/battle.html
+++ b/battle.html
@@ -7,11 +7,14 @@
+
--
Players: 0
Type !join to play
+
+
+
+
+
Commands & API - Social Stream Ninja
@@ -1046,7 +1057,7 @@
if (!data.chatname && !data.chatmessage && !data.hasDonation){return;}
- if (giftSubsOnly && (!data.event || !data.chatmessage.includes("gifted"))){
+ if (giftSubsOnly && (!data.event || !(data.textContent || data.chatmessage).includes("gifted"))){
return;
}
@@ -600,16 +600,13 @@
Stream Events & Donations Dashboard
}
iframe.style.cssText = "width: 0px; height: 0px; position: fixed; left: -100px; top: -100px;";
- /* Add this here */
iframe.connectedPeers = {}; // Initialize connectedPeers for this iframe instance
- /* End Add this here */
document.body.appendChild(iframe);
window.addEventListener("message", function (e) {
if (e.source != iframe.contentWindow) return;
-
- /* Add this here */
+
// Logic to populate iframe.connectedPeers, similar to dock.html
if ("action" in e.data && e.data.UUID && "value" in e.data) {
const peerUUID = e.data.UUID;
@@ -634,7 +631,6 @@
This page is the output for actions triggered by the Flow Editor page.
+
+
+ Note: To access the Event Flow Editor, visit actions/index.html instead.
+
@@ -5764,26 +5787,15 @@
Ranking Criteria
Rank By
Display Options
-
-
-
- ๐ Show rank numbers
-
-
-
-
-
- ๐ Show score values
-
-
@@ -5858,95 +5857,237 @@
Theme Options
+
Data Options
+
+
+
+
+
+
+
+ ๐พ Persist data between sessions (7 days max)
+
+