Skip to content

Commit 220810c

Browse files
authored
Merge pull request #619 from steveseguin/beta
Beta
2 parents 9801045 + 1bb635d commit 220810c

57 files changed

Lines changed: 11897 additions & 3851 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ Much more than just an overlay - Social Stream Ninja is a complete chat ecosyste
108108
- zoom.us (web version)
109109
- owncast demo page (`watch.owncast.online`, or for a pop-out chat version, open `https://watch.owncast.online/embed/chat/readwrite/` )
110110
- crowdcast.io
111+
- lfg.tv
111112
- livestream.com
112113
- mixcloud.com (pop out chat)
113114
- ms teams (teams.live.com and teams.microsoft.com)

actions/EventFlowEditor.js

Lines changed: 113 additions & 45 deletions
Large diffs are not rendered by default.

actions/EventFlowSystem.js

Lines changed: 200 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
class EventFlowSystem {
1+
class EventFlowSystem {
22
constructor(options = {}) {
33
this.flows = [];
44
this.db = null;
@@ -29,7 +29,10 @@ class EventFlowSystem {
2929
this.stateTimers = new Map(); // Timeout management for auto-resets
3030
this.messageQueues = new Map(); // Queue storage for queue nodes
3131
this.semaphoreStates = new Map(); // Track concurrent operations for semaphore nodes
32-
this.throttleStates = new Map(); // Track rate limiting for throttle nodes
32+
this.throttleStates = new Map(); // Track rate limiting for throttle nodes
33+
34+
// Reflection control (per Event Flow) for "allow-first" windows
35+
this.reflectionSeen = new Map();
3336

3437
// Internal scheduler for time-based triggers
3538
this._tickHandle = null;
@@ -963,22 +966,22 @@ class EventFlowSystem {
963966
return this.flows;
964967
}
965968

966-
async getFlowById(flowId) {
967-
return this.flows.find(flow => flow.id === flowId) || null;
968-
}
969-
970-
async processMessage(message) {
971-
972-
if (!message) {
973-
////console.log("[RELAY DEBUG - ProcessMessage] Message is null/undefined at start.");
974-
return message;
975-
}
976-
977-
let processed = { ...message };
978-
let blocked = false;
979-
980-
const activeFlows = this.flows.filter(f => f.active);
981-
////console.log(`[RELAY DEBUG - ProcessMessage] Processing ${activeFlows.length} active flows`);
969+
async getFlowById(flowId) {
970+
return this.flows.find(flow => flow.id === flowId) || null;
971+
}
972+
973+
async processMessage(message) {
974+
975+
if (!message) {
976+
////console.log("[RELAY DEBUG - ProcessMessage] Message is null/undefined at start.");
977+
return message;
978+
}
979+
980+
let processed = { ...message };
981+
let blocked = false;
982+
983+
const activeFlows = this.flows.filter(f => f.active);
984+
////console.log(`[RELAY DEBUG - ProcessMessage] Processing ${activeFlows.length} active flows`);
982985
////console.log(`[RELAY DEBUG - ProcessMessage] Active flow names:`, activeFlows.map(f => f.name));
983986

984987
for (const flow of this.flows) {
@@ -1005,9 +1008,9 @@ class EventFlowSystem {
10051008
}
10061009
}
10071010

1008-
//console.log(`[ProcessMessage] Final result: ${blocked ? 'BLOCKED (returning null)' : 'NOT BLOCKED (returning message)'}`);
1009-
return blocked ? null : processed;
1010-
}
1011+
//console.log(`[ProcessMessage] Final result: ${blocked ? 'BLOCKED (returning null)' : 'NOT BLOCKED (returning message)'}`);
1012+
return blocked ? null : processed;
1013+
}
10111014

10121015
async evaluateFlow(flow, message) {
10131016
//console.log(`[EvaluateFlow "${flow.name}"] Starting evaluation with message:`, JSON.stringify(message));
@@ -1888,11 +1891,91 @@ class EventFlowSystem {
18881891
//console.log(`[ExecuteAction] Node: ${actionNode.id}, Type: ${actionType}, Config: ${JSON.stringify(config)}`);
18891892
let result = { modified: false, message, blocked: false };
18901893

1891-
switch (actionType) {
1892-
case 'blockMessage':
1893-
result.blocked = true;
1894-
//console.log(`[ExecuteAction - blockMessage] Set result.blocked to true.`);
1895-
break;
1894+
switch (actionType) {
1895+
case 'blockMessage':
1896+
result.blocked = true;
1897+
//console.log(`[ExecuteAction - blockMessage] Set result.blocked to true.`);
1898+
break;
1899+
1900+
case 'reflectionFilter': {
1901+
// Apply only to reflected messages
1902+
if (!message || !message.reflection) {
1903+
break;
1904+
}
1905+
1906+
const policy = config.policy || 'block-all'; // 'block-all' | 'allow-first' | 'allow-all'
1907+
const sourceMode = config.sourceMode || 'none'; // 'none' | 'allow' | 'block'
1908+
const rawTypes = (config.sourceTypes || '').toString();
1909+
const typeList = rawTypes
1910+
.split(',')
1911+
.map(t => t.trim().toLowerCase())
1912+
.filter(Boolean);
1913+
const msgType = (message.type || '').toLowerCase();
1914+
1915+
// Source-type allow/block pre-filter
1916+
if (sourceMode === 'block' && typeList.length && typeList.includes(msgType)) {
1917+
result.blocked = true;
1918+
break;
1919+
}
1920+
if (sourceMode === 'allow' && typeList.length && !typeList.includes(msgType)) {
1921+
result.blocked = true;
1922+
break;
1923+
}
1924+
1925+
if (policy === 'allow-all') {
1926+
// Never block reflections via this node
1927+
break;
1928+
}
1929+
1930+
if (policy === 'block-all') {
1931+
result.blocked = true;
1932+
break;
1933+
}
1934+
1935+
// allow-first
1936+
const windowMs = parseInt(config.windowMs || 10000, 10) || 10000;
1937+
let basis = '';
1938+
try {
1939+
// Use sanitized text basis for matching
1940+
if (message.chatmessage) {
1941+
if (typeof this.sanitizeSendMessage === 'function') {
1942+
basis = this.sanitizeSendMessage(message.chatmessage, true) || '';
1943+
} else if (typeof this.sanitizeRelay === 'function') {
1944+
basis = this.sanitizeRelay(message.chatmessage, true) || '';
1945+
} else {
1946+
basis = (message.chatmessage || '').toString();
1947+
}
1948+
}
1949+
} catch (e) {
1950+
basis = (message.chatmessage || '').toString();
1951+
}
1952+
basis = basis.trim().toLowerCase();
1953+
if (!basis) {
1954+
// No content to compare; treat as first (allow)
1955+
break;
1956+
}
1957+
1958+
const now = Date.now();
1959+
// Cleanup old entries
1960+
try {
1961+
for (const [k, ts] of this.reflectionSeen) {
1962+
if (now - ts > Math.max(10000, windowMs)) {
1963+
this.reflectionSeen.delete(k);
1964+
}
1965+
}
1966+
} catch (e) {}
1967+
1968+
const lastSeen = this.reflectionSeen.get(basis);
1969+
if (!lastSeen || (now - lastSeen > windowMs)) {
1970+
// Allow first occurrence in window, record it
1971+
this.reflectionSeen.set(basis, now);
1972+
// Not blocked
1973+
} else {
1974+
// Within window and seen already → block
1975+
result.blocked = true;
1976+
}
1977+
break;
1978+
}
18961979

18971980
case 'modifyMessage':
18981981
if (typeof config.newMessage !== 'string') {
@@ -2143,16 +2226,20 @@ class EventFlowSystem {
21432226
let reverse = true; // Always exclude source
21442227
let relayMode = true; // Always in relay mode
21452228

2146-
if (config.destination && config.destination !== 'all') {
2147-
// Relay to specific platform/destination
2148-
relayMessage.destination = config.destination;
2149-
//console.log('[RELAY DEBUG - Action] Mode: Relay to specific platform:', config.destination);
2150-
} else {
2151-
// Relay to all platforms (excluding source)
2152-
// CRITICAL: Must pass tid for source exclusion to work with reverse=true
2153-
if (message.tid) {
2154-
relayMessage.tid = message.tid;
2155-
}
2229+
if (config.destination && config.destination !== 'all') {
2230+
// Relay to specific platform/destination
2231+
relayMessage.destination = config.destination;
2232+
// Pass source tid so reverse=true can exclude the source tab
2233+
if (message.tid) {
2234+
relayMessage.tid = message.tid;
2235+
}
2236+
//console.log('[RELAY DEBUG - Action] Mode: Relay to specific platform:', config.destination);
2237+
} else {
2238+
// Relay to all platforms (excluding source)
2239+
// CRITICAL: Must pass tid for source exclusion to work with reverse=true
2240+
if (message.tid) {
2241+
relayMessage.tid = message.tid;
2242+
}
21562243
//console.log('[RELAY DEBUG - Action] Mode: Relay to all platforms (excluding source)');
21572244
}
21582245

@@ -2172,61 +2259,83 @@ class EventFlowSystem {
21722259
break;
21732260
}
21742261

2175-
case 'webhook':
2176-
try {
2177-
const url = config.url;
2178-
if (!url) {
2179-
console.warn(`[ExecuteAction - webhook] URL is not configured for node ${actionNode.id}`);
2180-
result.message = { ...message, webhookError: "URL not configured" };
2181-
result.modified = true;
2182-
// result.blocked = true; // Optionally block
2183-
break;
2184-
}
2185-
2186-
const method = config.method || 'POST';
2187-
const headers = { 'Content-Type': 'application/json', ...(config.headers || {}) }; // Allow custom headers from config
2188-
const body = config.includeMessage ? JSON.stringify(message) : (config.body || '{}');
2189-
const webhookTimeout = config.timeout || 8000; // Make timeout configurable per node, default 8s
2190-
2191-
// Prepare fetch options
2192-
const fetchOpts = {
2193-
method,
2194-
headers
2195-
};
2196-
if (method !== 'GET' && method !== 'HEAD') {
2197-
fetchOpts.body = body;
2198-
}
2199-
2200-
//console.log(`[ExecuteAction - webhook] Calling ${method} ${url} with timeout ${webhookTimeout}ms`);
2201-
const response = await this.fetchWithTimeout(url, fetchOpts, webhookTimeout); // Use the injected function
2202-
2203-
if (!response.ok) {
2204-
const errorText = await response.text();
2205-
console.error(`[ExecuteAction - webhook] Webhook for ${url} failed with status ${response.status}: ${errorText}`);
2206-
result.message = { ...message, webhookError: `Webhook failed: ${response.status} - ${errorText.substring(0, 200)}` };
2207-
result.modified = true;
2208-
result.blocked = config.blockOnFailure !== undefined ? !!config.blockOnFailure : true; // Block on failure by default, make it configurable
2209-
} else {
2210-
//console.log(`[ExecuteAction - webhook] Webhook to ${url} successful.`);
2211-
try {
2212-
const responseData = await response.json(); // Attempt to parse as JSON
2213-
result.message = { ...message, webhookResponse: responseData, webhookStatus: response.status };
2214-
} catch (e) { // If not JSON, take as text
2215-
const responseText = await response.text(); // This won't work if response.json() already consumed the body. Need to handle this better.
2216-
// A better way is to clone the response if you need to read body multiple times or in different formats.
2217-
// For simplicity now: let's assume we primarily want JSON or care about success status.
2218-
result.message = { ...message, webhookResponseText: responseText.substring(0, 500), webhookStatus: response.status };
2219-
}
2220-
result.modified = true;
2221-
}
2222-
2223-
} catch (error) { // This catch is for errors from fetchWithTimeout (e.g., timeout, network error)
2224-
console.error(`[ExecuteAction - webhook] Error executing webhook for node ${actionNode.id}:`, error.message);
2225-
result.message = { ...message, webhookError: `Webhook execution error: ${error.message}` };
2226-
result.modified = true;
2227-
result.blocked = config.blockOnFailure !== undefined ? !!config.blockOnFailure : true; // Block on failure by default
2228-
}
2229-
break;
2262+
case 'webhook':
2263+
try {
2264+
const url = config.url;
2265+
if (!url) {
2266+
console.warn(`[ExecuteAction - webhook] URL is not configured for node ${actionNode.id}`);
2267+
result.message = { ...message, webhookError: "URL not configured" };
2268+
result.modified = true;
2269+
break;
2270+
}
2271+
2272+
const method = config.method || 'POST';
2273+
const headers = { 'Content-Type': 'application/json', ...(config.headers || {}) };
2274+
const body = config.includeMessage ? JSON.stringify(message) : (config.body || '{}');
2275+
const webhookTimeout = config.timeout || 8000;
2276+
2277+
// Prepare fetch options
2278+
const fetchOpts = { method, headers };
2279+
if (method !== 'GET' && method !== 'HEAD') {
2280+
fetchOpts.body = body;
2281+
}
2282+
2283+
if (config.syncMode) {
2284+
// Synchronous mode: await and optionally block on failure
2285+
try {
2286+
const response = await this.fetchWithTimeout(url, fetchOpts, webhookTimeout);
2287+
let responseText = '';
2288+
try { responseText = await response.text(); } catch (e) { responseText = ''; }
2289+
2290+
if (!response.ok) {
2291+
console.error(`[ExecuteAction - webhook] Webhook for ${url} failed with status ${response.status}: ${responseText}`);
2292+
result.message = { ...message, webhookError: `Webhook failed: ${response.status} - ${responseText.substring(0, 200)}` };
2293+
result.modified = true;
2294+
if (config.blockOnFailure) {
2295+
result.blocked = true;
2296+
}
2297+
} else {
2298+
// Try parse JSON from text; if fails, keep text
2299+
let parsed = null;
2300+
try { parsed = responseText ? JSON.parse(responseText) : null; } catch (e) { parsed = null; }
2301+
if (parsed !== null) {
2302+
result.message = { ...message, webhookResponse: parsed, webhookStatus: response.status };
2303+
} else {
2304+
result.message = { ...message, webhookResponseText: responseText.substring(0, 500), webhookStatus: response.status };
2305+
}
2306+
result.modified = true;
2307+
}
2308+
} catch (err) {
2309+
console.error(`[ExecuteAction - webhook] Error executing webhook for node ${actionNode.id}:`, err.message);
2310+
result.message = { ...message, webhookError: `Webhook execution error: ${err.message}` };
2311+
result.modified = true;
2312+
if (config.blockOnFailure) {
2313+
result.blocked = true;
2314+
}
2315+
}
2316+
} else {
2317+
// Asynchronous mode: fire-and-forget; do not block message processing
2318+
this.fetchWithTimeout(url, fetchOpts, webhookTimeout)
2319+
.then(async (response) => {
2320+
let responseText = '';
2321+
try { responseText = await response.text(); } catch (e) { responseText = ''; }
2322+
if (!response.ok) {
2323+
console.error(`[ExecuteAction - webhook] Webhook for ${url} failed with status ${response.status}: ${responseText}`);
2324+
return;
2325+
}
2326+
// Optionally parse for logging
2327+
try { JSON.parse(responseText); } catch (e) {}
2328+
})
2329+
.catch((error) => {
2330+
console.error(`[ExecuteAction - webhook] Error executing webhook for node ${actionNode.id}:`, error.message);
2331+
});
2332+
// Do not modify or block in async mode
2333+
}
2334+
2335+
} catch (error) {
2336+
console.error(`[ExecuteAction - webhook] Unexpected error preparing webhook for node ${actionNode.id}:`, error.message);
2337+
}
2338+
break;
22302339

22312340
case 'addPoints':
22322341
if (this.pointsSystem && config.amount > 0) {

0 commit comments

Comments
 (0)