Index: openacs-4/packages/notifications/notifications.info =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/notifications/notifications.info,v diff -u -r1.62 -r1.63 --- openacs-4/packages/notifications/notifications.info 3 Sep 2024 15:37:39 -0000 1.62 +++ openacs-4/packages/notifications/notifications.info 19 Dec 2024 16:59:14 -0000 1.63 @@ -8,7 +8,7 @@ t notifications - + OpenACS Email notifications management 2024-09-02 @@ -17,7 +17,7 @@ 3 #notifications.Notifications# - + Index: openacs-4/packages/notifications/tcl/apm-callback-procs.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/notifications/tcl/apm-callback-procs.tcl,v diff -u -r1.8 -r1.9 --- openacs-4/packages/notifications/tcl/apm-callback-procs.tcl 11 Sep 2024 06:15:52 -0000 1.8 +++ openacs-4/packages/notifications/tcl/apm-callback-procs.tcl 19 Dec 2024 16:59:14 -0000 1.9 @@ -23,6 +23,12 @@ # Register the service contract implementation with the notifications service register_email_delivery_method -impl_id $impl_id + # Register sse delivery method service contract implementation + set impl_id [create_sse_delivery_method_impl] + + # Register the service contract implementation with the notifications service + register_sse_delivery_method -impl_id $impl_id + # Create the notification type service contract create_notification_type_contract } @@ -42,6 +48,12 @@ # Unregister email delivery method service contract implementation delete_email_delivery_method_impl + # Delete the service contract implementation from the notifications service + unregister_sse_delivery_method + + # Unregister sse delivery method service contract implementation + delete_sse_delivery_method_impl + # Delete the delivery method service contract delete_delivery_method_contract @@ -106,6 +118,17 @@ } } + 5.10.1 6.0.0d1 { + db_transaction { + + # Register sse delivery method service contract implementation + set impl_id [create_sse_delivery_method_impl] + + # Register the service contract implementation with the notifications service + register_sse_delivery_method -impl_id $impl_id + + } + } } } @@ -183,6 +206,45 @@ -pretty_name "Email" } +ad_proc -private notification::apm::create_sse_delivery_method_impl {} { + Register the service contract implementation and return the impl_id + + @return impl_id of the created implementation +} { + return [acs_sc::impl::new_from_spec -spec { + contract_name "NotificationDeliveryMethod" + name "notification_sse" + owner "notifications" + aliases { + Send notification::sse::send + ScanReplies notification::sse::scan_replies + } + }] +} + +ad_proc -private notification::apm::delete_sse_delivery_method_impl { + {-impl_name "notification_sse"} +} { + Unregister the NotificationDeliveryMethod service contract implementation for sse. +} { + acs_sc::impl::delete \ + -contract_name "NotificationDeliveryMethod" \ + -impl_name $impl_name +} + +ad_proc -private notification::apm::register_sse_delivery_method { + -impl_id:required +} { + Register the sse delivery method with the notifications service. + + @param impl_id The ID of the NotificationDeliveryMethod service contract implementation. +} { + notification::delivery::new \ + -sc_impl_id $impl_id \ + -short_name "sse" \ + -pretty_name "Server-sent Events" +} + ad_proc -private notification::apm::update_email_delivery_method_impl { -impl_id:required } { Index: openacs-4/packages/notifications/tcl/notification-sse-procs.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/notifications/tcl/notification-sse-procs.tcl,v diff -u --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ openacs-4/packages/notifications/tcl/notification-sse-procs.tcl 19 Dec 2024 16:59:14 -0000 1.1 @@ -0,0 +1,245 @@ +ad_library { + + Notifications Server Sent Events Delivery Method + + @creation-date 2024-12-19 + @author Antonio Pisano +} + +namespace eval notification::sse { + + # + # Needed to update subscribers concurrently. + # + if {![nsv_exists ::notification::sse subscription_mutex]} { + nsv_set ::notification::sse subscription_mutex \ + [ns_mutex create ::notification::sse_subscription] + } + + ad_proc -private subscribe { + subscription_id + } { + Subscribe the current connection channel to notifications. + + This will detach the connection channel from the thread and + abort the script. + } { + set channel [ns_connchan detach] + ns_connchan write $channel [string cat \ + "HTTP/1.1 200 OK\r\n" \ + "Cache-Control: no-cache\r\n" \ + "X-Accel-Buffering': no\r\n" \ + "Content-type: text/event-stream\r\n" \ + "\r\n"] + nsv_lappend \ + ::notification::sse \ + channels-$subscription_id \ + $channel + ad_script_abort + } + + ad_proc -public unsubscribe { + channel + subscription_id + } { + Unsubscribe a channel from notifications. + } { + ns_log notice \ + notification::sse::unsubscribe \ + $subscription_id \ + $channel + + if {[nsv_get ::notification::sse channels-$subscription_id channels]} { + ns_mutex eval [nsv_get ::notification::sse subscription_mutex] { + set idx [lsearch -exact $channels $channel] + nsv_set ::notification::sse channels-$subscription_id \ + [lreplace $channels $idx $idx] + } + } + } + + ad_proc -private channels { + to_user_id + } { + @return list of channels + } { + if {![nsv_get ::notification::sse channels-$to_user_id channels]} { + set channels [list] + } + + return $channels + } + + ad_proc -public send { + from_user_id + to_user_id + reply_object_id + notification_type_id + subject + content_text + content_html + file_ids + } { + Send the notification. + } { + set channels [::notification::sse::channels $to_user_id] + + if {[llength $channels] == 0} { + # + # Nobody listening. We are done. + # + return + } + + if { $content_html eq "" } { + set content $content_text + } else { + set content $content_html + } + + # + # convert relative URLs to fully qualified URLs + # + set content [::ad_html_qualify_links $content] + + set user_locale [::lang::user::site_wide_locale -user_id $to_user_id] + if { $user_locale eq "" } { + set user_locale [::lang::system::site_wide_locale] + } + + set subject [::lang::util::localize $subject $user_locale] + set content [::lang::util::localize $content $user_locale] + + set from_user [::acs_user::get -user_id $from_user_id] + set from_user [dict filter $from_user key user_id first_names last_name email] + + set to_user [::acs_user::get -user_id $to_user_id] + set to_user [dict filter $to_user key user_id first_names last_name email] + + set reply_object [::acs_object::get -object_id $reply_object_id] + set reply_object [dict filter $reply_object key object_id title package_id object_type] + + set notification_type [ns_set array [lindex [db_list_of_ns_sets get_notif_type { + select type_id, short_name, pretty_name, description + from notification_types + where type_id = :notification_type_id + }] 0]] + + # + # We do not expand files right now the same as other entities, + # but we may in the future. + # + + # + # Serialize message as JSON + # + dom createNodeCmd -jsonType NUMBER textNode jsonNumber + dom createNodeCmd -jsonType STRING textNode jsonString + + dom createNodeCmd -jsonType NONE elementNode first_names + dom createNodeCmd -jsonType NONE elementNode last_name + dom createNodeCmd -jsonType NONE elementNode user_id + dom createNodeCmd -jsonType NONE elementNode email + + dom createNodeCmd -jsonType NONE elementNode object_id + dom createNodeCmd -jsonType NONE elementNode title + dom createNodeCmd -jsonType NONE elementNode package_id + dom createNodeCmd -jsonType NONE elementNode object_type + + dom createNodeCmd -jsonType NONE elementNode type_id + dom createNodeCmd -jsonType NONE elementNode short_name + dom createNodeCmd -jsonType NONE elementNode pretty_name + dom createNodeCmd -jsonType NONE elementNode description + + dom createNodeCmd -jsonType NONE elementNode from_user + dom createNodeCmd -jsonType NONE elementNode to_user + dom createNodeCmd -jsonType NONE elementNode reply_object + dom createNodeCmd -jsonType NONE elementNode notification_type + + dom createNodeCmd -jsonType NONE elementNode subject + dom createNodeCmd -jsonType NONE elementNode content + dom createNodeCmd -jsonType ARRAY elementNode file_ids + + set resultJSON [dom createDocumentNode] + $resultJSON appendFromScript { + from_user { + user_id { + jsonNumber [dict get $from_user user_id] + } + foreach key {first_names last_name email} { + $key { + jsonString [dict get $from_user $key] + } + } + } + to_user { + user_id { + jsonNumber [dict get $to_user user_id] + } + foreach key {first_names last_name email} { + $key { + jsonString [dict get $to_user $key] + } + } + } + reply_object { + foreach key {object_id package_id} { + $key { + jsonNumber [dict get $reply_object $key] + } + } + foreach key {title object_type} { + $key { + jsonString [dict get $reply_object $key] + } + } + } + notification_type { + type_id { + jsonNumber [dict get $notification_type type_id] + } + foreach key {short_name pretty_name description} { + $key { + jsonString [dict get $notification_type $key] + } + } + } + subject { + jsonString $subject + } + content { + jsonString $content + } + file_ids [lmap file_id $file_ids { + jsonNumber $file_id + }] + } + + set message [$resultJSON asJSON] + + foreach channel $channels { + try { + ns_connchan write $channel [string cat "data: " $message "\n\n"] + } on error {errmsg} { + ::notification::sse::unsubscribe $channel $to_user_id + } + } + + return $message + } + + ad_proc -private scan_replies {} { + Scan for replies + } { + # + # A noop because there is no reply with SSE. + # + } + +} + +# Local variables: +# mode: tcl +# tcl-indent-level: 4 +# indent-tabs-mode: nil +# End: Index: openacs-4/packages/notifications/www/sse/subscribe.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/notifications/www/sse/subscribe.tcl,v diff -u --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ openacs-4/packages/notifications/www/sse/subscribe.tcl 19 Dec 2024 16:59:14 -0000 1.1 @@ -0,0 +1,10 @@ +ad_page_contract { + + Open an EventSource on this URL to receive all SSE notifications + you have subscribed to. + + @see notification::sse::send + @see https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events +} + +::notification::sse::subscribe [ad_conn user_id]