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