@@ -43,6 +43,8 @@ import { findById } from '../domain/user';
43
43
44
44
const DOC_URI = 'https://docs.opencti.io' ;
45
45
const PUBLISHER_ENGINE_KEY = conf . get ( 'publisher_manager:lock_key' ) ;
46
+ const PUBLISHER_ENABLE_BUFFERING = conf . get ( 'publisher_manager:enable_buffering' ) ;
47
+ const PUBLISHER_BUFFERING_SECONDS = conf . get ( 'publisher_manager:buffering_seconds' ) ;
46
48
const STREAM_SCHEDULE_TIME = 10000 ;
47
49
48
50
export const internalProcessNotification = async (
@@ -52,11 +54,13 @@ export const internalProcessNotification = async (
52
54
user : NotificationUser ,
53
55
notifier : BasicStoreEntityNotifier | NotifierTestInput ,
54
56
data : NotificationData [ ] ,
55
- notification : BasicStoreEntityTrigger ,
57
+ triggers : BasicStoreEntityTrigger [ ] ,
56
58
// eslint-disable-next-line consistent-return
57
59
) : Promise < { error : string } | void > => {
58
60
try {
59
- const { name : notification_name , id : trigger_id , trigger_type } = notification ;
61
+ const notification_name = triggers . map ( ( t ) => t ?. name ) . join ( ';' ) ;
62
+ const trigger_type = triggers . length > 1 ? 'buffer' : triggers [ 0 ] . trigger_type ;
63
+ const trigger_id = triggers . map ( ( t ) => t ?. id ) . filter ( ( t ) => t ) ;
60
64
const { notifier_connector_id, notifier_configuration : configuration } = notifier ;
61
65
const generatedContent : Record < string , Array < NotificationContentEvent > > = { } ;
62
66
for ( let index = 0 ; index < data . length ; index += 1 ) {
@@ -78,7 +82,7 @@ export const internalProcessNotification = async (
78
82
// region data generation
79
83
const background_color = ( settings . platform_theme_dark_background ?? '#0a1929' ) . substring ( 1 ) ;
80
84
const platformOpts = { doc_uri : DOC_URI , platform_uri : getBaseUrl ( ) , background_color } ;
81
- const templateData = { content, notification_content : content , notification, settings, user, data, ...platformOpts } ;
85
+ const templateData = { content, notification_content : content , notification : triggers [ 0 ] , settings, user, data, ...platformOpts } ;
82
86
// endregion
83
87
if ( notifier_connector_id === NOTIFIER_CONNECTOR_UI ) {
84
88
const createNotification = {
@@ -171,7 +175,7 @@ const processNotificationEvent = async (
171
175
const notifier = userNotifiers [ notifierIndex ] ;
172
176
173
177
// There is no await in purpose, the goal is to send notification and continue without waiting result.
174
- internalProcessNotification ( context , settings , notificationMap , user , notifierMap . get ( notifier ) ?? { } as BasicStoreEntityNotifier , data , notification ) . catch ( ( reason ) => logApp . error ( '[OPENCTI-MODULE] Publisher manager unknown error.' , { cause : reason } ) ) ;
178
+ internalProcessNotification ( context , settings , notificationMap , user , notifierMap . get ( notifier ) ?? { } as BasicStoreEntityNotifier , data , [ notification ] ) . catch ( ( reason ) => logApp . error ( '[OPENCTI-MODULE] Publisher manager unknown error.' , { cause : reason } ) ) ;
175
179
}
176
180
} ;
177
181
@@ -216,6 +220,95 @@ const processDigestNotificationEvent = async (context: AuthContext, notification
216
220
await processNotificationEvent ( context , notificationMap , event . notification_id , user , dataWithFullMessage ) ;
217
221
} ;
218
222
223
+ const liveNotificationBufferPerEntity : Record < string , { timestamp : number , events : SseEvent < KnowledgeNotificationEvent > [ ] } > = { } ;
224
+
225
+ const processBufferedEvents = async (
226
+ context : AuthContext ,
227
+ triggerMap : Map < string , BasicStoreEntityTrigger > ,
228
+ events : KnowledgeNotificationEvent [ ]
229
+ ) => {
230
+ const usersFromCache = await getEntitiesMapFromCache < AuthUser > ( context , SYSTEM_USER , ENTITY_TYPE_USER ) ;
231
+ const notifDataPerUser : Record < string , { user : NotificationUser , data : NotificationData } [ ] > = { } ;
232
+ // We process all events to transform them into notification data per user
233
+ for ( let i = 0 ; i < events . length ; i += 1 ) {
234
+ const event = events [ i ] ;
235
+ const { targets, data : instance , origin } = event ;
236
+ // For each event, transform it into NotificationData for all targets
237
+ for ( let index = 0 ; index < targets . length ; index += 1 ) {
238
+ const { user, type, message } = targets [ index ] ;
239
+ const notificationMessage = createFullNotificationMessage ( message , usersFromCache , event . streamMessage , origin ) ;
240
+ const currentData = { notification_id : event . notification_id , instance, type, message : notificationMessage } ;
241
+ const currentNotifDataForUser = notifDataPerUser [ user . user_id ] ;
242
+ if ( currentNotifDataForUser ) {
243
+ currentNotifDataForUser . push ( { user, data : currentData } ) ;
244
+ } else {
245
+ notifDataPerUser [ user . user_id ] = [ { user, data : currentData } ] ;
246
+ }
247
+ }
248
+ }
249
+ const settings = await getEntityFromCache < BasicStoreSettings > ( context , SYSTEM_USER , ENTITY_TYPE_SETTINGS ) ;
250
+ const allNotifiers = await getEntitiesListFromCache < BasicStoreEntityNotifier > ( context , SYSTEM_USER , ENTITY_TYPE_NOTIFIER ) ;
251
+ const allNotifiersMap = new Map ( allNotifiers . map ( ( n ) => [ n . internal_id , n ] ) ) ;
252
+
253
+ const notifUsers = Object . keys ( notifDataPerUser ) ;
254
+ // Handle notification data for each user
255
+ for ( let i = 0 ; i < notifUsers . length ; i += 1 ) {
256
+ const currentUserId = notifUsers [ i ] ;
257
+ const userNotificationData = notifDataPerUser [ currentUserId ] ;
258
+ const userNotifiers = [ ...new Set ( userNotificationData . map ( ( d ) => d . user . notifiers ) . flat ( ) ) ] ;
259
+
260
+ // For each notifier of the user, filter the relevant notification data, and send it
261
+ for ( let notifierIndex = 0 ; notifierIndex < userNotifiers . length ; notifierIndex += 1 ) {
262
+ const notifier = userNotifiers [ notifierIndex ] ;
263
+
264
+ // Only include the notificationData that has the current notifier included in its trigger config
265
+ const impactedData = userNotificationData . filter ( ( d ) => d . user . notifiers . includes ( notifier ) ) ;
266
+ if ( impactedData . length > 0 ) {
267
+ const currentUser = impactedData [ 0 ] . user ;
268
+ const dataToSend = impactedData . map ( ( d ) => d . data ) ;
269
+ const triggersInDataToSend = [ ...new Set ( dataToSend . map ( ( d ) => triggerMap . get ( d . notification_id ) ) . filter ( ( t ) => t ) ) ] ;
270
+ // If triggers can't be found, no need to send the data
271
+ if ( triggersInDataToSend . length >= 1 ) {
272
+ // There is no await in purpose, the goal is to send notification and continue without waiting result.
273
+ internalProcessNotification (
274
+ context ,
275
+ settings ,
276
+ triggerMap ,
277
+ currentUser ,
278
+ allNotifiersMap . get ( notifier ) ?? { } as BasicStoreEntityNotifier ,
279
+ dataToSend ,
280
+ triggersInDataToSend as BasicStoreEntityTrigger [ ]
281
+ ) . catch ( ( reason ) => logApp . error ( '[OPENCTI-MODULE] Publisher manager unknown error.' , { cause : reason } ) ) ;
282
+ }
283
+ }
284
+ }
285
+ }
286
+ } ;
287
+
288
+ const handleEntityNotificationBuffer = async ( forceSend = false ) => {
289
+ const dateNow = Date . now ( ) ;
290
+ const context = executionContext ( 'publisher_manager' ) ;
291
+ const bufferKeys = Object . keys ( liveNotificationBufferPerEntity ) ;
292
+ // Iterate on all buffers to check if they need to be sent
293
+ for ( let i = 0 ; i < bufferKeys . length ; i += 1 ) {
294
+ const key = bufferKeys [ i ] ;
295
+ const value = liveNotificationBufferPerEntity [ key ] ;
296
+ if ( value ) {
297
+ const isBufferingTimeElapsed = ( dateNow - value . timestamp ) > PUBLISHER_BUFFERING_SECONDS * 1000 ;
298
+ // If buffer is older than configured buffering time length OR we want to forceSend, it needs to be sent
299
+ if ( forceSend || isBufferingTimeElapsed ) {
300
+ const bufferEvents = value . events . map ( ( e ) => e . data ) ;
301
+ // We remove current buffer from buffers map before processing buffer events, otherwise some new events coming in might be lost
302
+ // This way, if new events are coming in from the stream, they will initiate a new buffer that will be handled later
303
+ delete liveNotificationBufferPerEntity [ key ] ;
304
+ const allExistingTriggers = await getNotifications ( context ) ;
305
+ const allExistingTriggersMap = new Map ( allExistingTriggers . map ( ( n ) => [ n . trigger . internal_id , n . trigger ] ) ) ;
306
+ await processBufferedEvents ( context , allExistingTriggersMap , bufferEvents ) ;
307
+ }
308
+ }
309
+ }
310
+ } ;
311
+
219
312
const publisherStreamHandler = async ( streamEvents : Array < SseEvent < StreamNotifEvent > > ) => {
220
313
try {
221
314
if ( streamEvents . length === 0 ) {
@@ -229,7 +322,19 @@ const publisherStreamHandler = async (streamEvents: Array<SseEvent<StreamNotifEv
229
322
const { data : { notification_id, type } } = streamEvent ;
230
323
if ( type === 'live' || type === 'action' ) {
231
324
const liveEvent = streamEvent as SseEvent < KnowledgeNotificationEvent > ;
232
- await processLiveNotificationEvent ( context , notificationMap , liveEvent . data ) ;
325
+ // If buffering is enabled, we store the event in local buffer instead of handling it directly
326
+ if ( PUBLISHER_ENABLE_BUFFERING ) {
327
+ const liveEventEntityId = liveEvent . data . data . id ;
328
+ const currentEntityBuffer = liveNotificationBufferPerEntity [ liveEventEntityId ] ;
329
+ // If there are buffered events already, simply add current event to array of buffered events
330
+ if ( currentEntityBuffer ) {
331
+ currentEntityBuffer . events . push ( liveEvent ) ;
332
+ } else { // If there are currently no buffered events for this entity, initialize them using current time as timestamp
333
+ liveNotificationBufferPerEntity [ liveEventEntityId ] = { timestamp : Date . now ( ) , events : [ liveEvent ] } ;
334
+ }
335
+ } else { // If no buffering is enabled, we handle the notification directly
336
+ await processLiveNotificationEvent ( context , notificationMap , liveEvent . data ) ;
337
+ }
233
338
}
234
339
if ( type === 'digest' ) {
235
340
const digestEvent = streamEvent as SseEvent < DigestEvent > ;
@@ -269,6 +374,9 @@ const initPublisherManager = () => {
269
374
await streamProcessor . start ( 'live' ) ;
270
375
while ( ! shutdown && streamProcessor . running ( ) ) {
271
376
lock . signal . throwIfAborted ( ) ;
377
+ if ( PUBLISHER_ENABLE_BUFFERING ) {
378
+ await handleEntityNotificationBuffer ( ) ;
379
+ }
272
380
await wait ( WAIT_TIME_ACTION ) ;
273
381
}
274
382
logApp . info ( '[OPENCTI-MODULE] End of publisher manager processing' ) ;
@@ -280,6 +388,9 @@ const initPublisherManager = () => {
280
388
}
281
389
} finally {
282
390
if ( streamProcessor ) await streamProcessor . shutdown ( ) ;
391
+ if ( PUBLISHER_ENABLE_BUFFERING ) {
392
+ await handleEntityNotificationBuffer ( true ) ;
393
+ }
283
394
if ( lock ) await lock . unlock ( ) ;
284
395
}
285
396
} ;
0 commit comments