- Publicity: Public Only All
chat-procs.tcl
Generic chat procs
- Location:
- packages/xowiki/tcl/chat-procs.tcl
- Created:
- 2006-02-02
- Author:
- Gustaf Neumann
- CVS Identification:
$Id: chat-procs.tcl,v 1.57 2024/09/11 06:15:56 gustafn Exp $
Procedures in this file
Detailed information
[ hide source ] | [ make this the default ]Content File Source
xo::library doc { Generic chat procs @creation-date 2006-02-02 @author Gustaf Neumann @cvs-id $Id: chat-procs.tcl,v 1.57 2024/09/11 06:15:56 gustafn Exp $ } namespace eval ::xo { Class create Message -parameter {time user_id msg color {type "message"}} Class create Chat -superclass ::xo::OrderedComposite \ -parameter { chat_id user_id session_id {mode default} {encoder noencode} {timewindow 600} {sweepinterval 60} {login_messages_p t} {logout_messages_p t} {avatar_p t} {conf {}} {message_relay {connchan bgdelivery none}} } Chat instproc init {} { # :log "-- " # # Work through the list of provided message_relays and select a # usable one. # set :mr ::xo::mr::none foreach mr ${:message_relay} { if {[::xo::mr::$mr can_be_used]} { set :mr ::xo::mr::$mr break } } set :now [clock clicks -milliseconds] if {![info exists :user_id]} { # # Chat may be instantiated outside xowiki, where ::xo::cc is # assumed to exist. # ::xo::ConnectionContext require set :user_id [ad_conn user_id] set :requester [::xo::cc requester] if {${:user_id} == 0} { # # Maybe the user_id was timed out, so fall potentially back to # the untrusted_user_id (which might be as well 0). # set :user_id [::xo::cc get_user_id] } # # Keep always the original user_id # set :original_user_id ${:user_id} if {${:user_id} == 0} { # # Overwrite the user_id with the requester. This increases # backward compatibility and eases handling of the identifier # for the user. # set :user_id ${:requester} } } if {![info exists :session_id]} { set :session_id [ad_conn session_id] } set cls [:info class] set :array $cls-${:chat_id} # # The basic nsv (typically ::chat::Chat) is hit quite frequently # on busy sites. So reduce these these hits. # Something to consider: We could/should do this actually in an # init-script. The only advantage by this construct is to start # the scheduled proc only when a chat is started. # acs::per_thread_cache eval -key chat-initialized-$cls { if {![nsv_exists $cls initialized]} { :log "-- initialize $cls" $cls initialize_nsvs ::acs::clusterwide nsv_set $cls initialized \ [ad_schedule_proc \ -thread "t" ${:sweepinterval} $cls sweep_all_chats] } } if {![nsv_exists ${:array}-seen newest]} { ::acs::clusterwide nsv_set ${:array}-seen newest 0 } if {![nsv_exists ${:array}-color idx]} { ::acs::clusterwide nsv_set ${:array}-color idx 0 } if {![nsv_array exists ${:array}-anonymous_ids]} { ::acs::clusterwide nsv_set ${:array}-anonymous_ids . . } if {${:user_id} != 0 || [:session_id] != 0} { :init_user_color } :set_options } Chat instproc set_options {} { # Any supplied conf we are going to save and apply to any other # instance of this chat created in the future. if {[llength ${:conf}] > 0} { ::acs::clusterwide nsv_array set ${:array}-conf ${:conf} } dict for {key value} [nsv_array get ${:array}-conf] { ::acs::clusterwide nsv_set ${:array}-options $key $value set :$key $value } } Chat instproc register_nsvs {msg_id user_id msg color secs} { # Tell the system we are back again, in case we were auto logged out if { ![nsv_exists ${:array}-login $user_id] } { ::acs::clusterwide nsv_set ${:array}-login $user_id [clock seconds] } ::acs::clusterwide nsv_set ${:array} $msg_id [list ${:now} $secs $user_id $msg $color] ::acs::clusterwide nsv_set ${:array}-seen newest ${:now} ::acs::clusterwide nsv_set ${:array}-seen last $secs ::acs::clusterwide nsv_set ${:array}-last-activity $user_id ${:now} } Chat instproc add_msg {{-get_new:boolean true} {-uid ""} msg} { # :log "--chat adding $msg" set user_id [expr {$uid ne "" ? $uid : ${:user_id}}] set color [:user_color $user_id] set msg [ns_quotehtml $msg] # :log "-- msg=$msg" :broadcast_msg [Message new -volatile -time [clock seconds] \ -user_id $user_id -color $color -msg $msg] :register_nsvs ${:now}.$user_id $user_id $msg $color [clock seconds] # # This in any case a valid result, but only needed for the polling # interface # if {$get_new} { :get_new } } Chat instproc current_message_valid {} { expr { [info exists :user_id] && ${:user_id} != -1 } } Chat instproc active_user_list {} { nsv_array get ${:array}-login } Chat instproc nr_active_users {} { expr { [llength [nsv_array get ${:array}-login]] / 2 } } Chat instproc last_activity {} { if { [:nsv_get ${:array}-seen last ts]} { return [clock format $ts -format "%d.%m.%y %H:%M:%S"] } else { return "-" } } Chat instproc check_age {key ago} { if {$ago > ${:timewindow}} { ::acs::clusterwide nsv_unset ${:array} $key #:log "--c unsetting $key" return 0 } return 1 } if {[ns_info name] eq "NaviServer"} { Chat instproc nsv_get {array key v_value} { :upvar $v_value value return [::nsv_get $array $key value] } } else { Chat instproc nsv_get {array key v_value} { if {[::nsv_exists $array $key]} { :upvar $v_value value set value [::nsv_get $array $key] return 1 } else { return 0 } } } Chat instproc get_new {} { if {![:nsv_get ${:array}-seen ${:session_id} last]} { set last 0 } if {[nsv_get ${:array}-seen newest] > $last} { #:log "--c must check ${:session_id}: [nsv_get ${:array}-seen newest] > $last" foreach {key value} [nsv_array get ${:array}] { lassign $value timestamp secs user msg color if {$timestamp > $last} { # # add the message to the ordered composite. # :add [Message new -time $secs -user_id $user -msg $msg -color $color] } else { :check_age $key [expr {(${:now} - $timestamp) / 1000}] } } ::acs::clusterwide nsv_set ${:array}-seen ${:session_id} ${:now} # :log "--chat setting session_id ${:session_id}: ${:now}" } else { # :log "--chat nothing new for ${:session_id}" } :render } Chat instproc get_all {} { foreach {key value} [nsv_array get ${:array}] { lassign $value timestamp secs user msg color if {[:check_age $key [expr {(${:now} - $timestamp) / 1000}]]} { :add [Message new -time $secs -user_id $user -msg $msg -color $color] } } #:log "--chat setting session_id ${:session_id}: ${:now}" ::acs::clusterwide nsv_set ${:array}-seen ${:session_id} ${:now} :render } Chat instproc sweeper {} { #:log "--core-chat starting" foreach {user timestamp} [nsv_array get ${:array}-last-activity] { set ago [expr {(${:now} - $timestamp) / 1000}] #ns_log notice "--core-chat Checking: now=${:now}, timestamp=$timestamp, ago=$ago" if {$ago > 300} { :logout -user_id $user -msg "auto logout" # ns_log warning "-user_id $user auto logout" ${:mr} sweep chat-${:chat_id} } } :broadcast_msg [Message new -volatile -type "users" -time [clock seconds]] #:log "-- ending" } Chat instproc logout {{-user_id ""} {-msg ""}} { set user_id [expr {$user_id ne "" ? $user_id : ${:user_id}}] ns_log notice "--core-chat User $user_id logging out of chat" if {${:logout_messages_p}} { if {$msg eq ""} {set msg [_ xowiki.chat_has_left_the_room].} :add_msg -uid $user_id -get_new false $msg } # These values could already not be here. Just ignore when we don't # find them try { ::acs::clusterwide nsv_unset -nocomplain ${:array}-login $user_id } try { ::acs::clusterwide nsv_unset -nocomplain ${:array}-color $user_id } try { ::acs::clusterwide nsv_unset -nocomplain ${:array}-last-activity $user_id } } Chat instproc init_user_color {} { if { [nsv_exists ${:array}-color ${:user_id}] } { return } else { set colors [::parameter::get -parameter UserColors -default [[:info class] set colors]] # ns_log notice "getting colors of [:info class] = [info exists colors]" set color [lindex $colors [expr { [nsv_get ${:array}-color idx] % [llength $colors] }]] ::acs::clusterwide nsv_set ${:array}-color ${:user_id} $color ::acs::clusterwide nsv_incr ${:array}-color idx } } Chat instproc get_users {} { return [:json_encode_msg [Message new -volatile -type "users" -time [clock seconds]]] } Chat instproc user_active {user_id} { # was the user already active? #:log "--chat login already active? [nsv_exists ${:array}-last-activity $user_id]" return [nsv_exists ${:array}-last-activity $user_id] } Chat instproc login {} { :log "--chat login mode=${:mode}" if {${:login_messages_p} && ![:user_active ${:user_id}]} { :add_msg -uid ${:user_id} -get_new false [_ xowiki.chat_has_entered_the_room] } elseif {${:user_id} > 0 && ![nsv_exists ${:array}-login ${:user_id}]} { # give some proof of our presence to the chat system when we # don't issue the login message ::acs::clusterwide nsv_set ${:array}-login ${:user_id} [clock seconds] ::acs::clusterwide nsv_set ${:array}-last-activity ${:user_id} ${:now} } :encoder noencode #:log "--chat setting session_id ${:session_id}: ${:now} mode=${:mode}" return [:get_all] } Chat instproc user_color { user_id } { if { ![:nsv_get ${:array}-color $user_id color] } { :log "warning: Cannot find user color for chat (${:array}-color $user_id)!" set color [lindex [[:info class] set colors] 0] } return $color } Chat instproc usable_screen_name { screen_name requester } { if {[nsv_get ${:array}-anonymous_ids $screen_name seenRequester]} { if {$seenRequester eq $requester} { # # We have this screen name already assigned to this requester. # #ns_log notice "check screen name for $requester in ${:array}-anonymous_ids -> later time" return 1 } else { #ns_log notice "check screen name for $requester in ${:array}-anonymous_ids -> not usable <$seenRequester != $requester>" return 0 } } # # We saw this screen name the first time. # #ns_log notice "check screen name for $requester in ${:array}-anonymous_ids -> first time" nsv_set ${:array}-anonymous_ids $screen_name $requester return 1 } Chat instproc user_name { user_id } { # # Map the provided user_id (which might be numeric or an IP # address) to a screen name, which might be the configured screen # name, the username, or of the form userXXX. # #:log "user_name for $user_id" if {![nsf::is int32 $user_id]} { # # The user_id is a requester (e.g. IPv4 or IPv6 address) # set requester $user_id if {[::acs::icanuse "ns_hash"]} { set hash [ns_hash $requester] set screen_name user[expr {$hash % 1000}] if {![:usable_screen_name $screen_name $requester]} { # # Collision: we have this screen_name already for a # different requester. # for {set i 1} {$i < 200} {incr i} { set screen_name user[expr {$hash % 1000}]$i if {[:usable_screen_name $screen_name $requester]} { break } } } } else { set screen_name $requester } } elseif {$user_id > 0} { # # True user_id # set screen_name [acs_user::get_user_info -user_id $user_id -element screen_name] if {$screen_name eq ""} { set screen_name [person::name -person_id $user_id] } } elseif { $user_id == 0 } { set screen_name "Nobody" } else { # # This might be triggered during background processing. # set screen_name "System" } #:log "user_name for $user_id -> $screen_name" return $screen_name } Chat instproc urlencode {string} {ns_urlencode $string} Chat instproc noencode {string} {set string} Chat instproc encode {string} {my [:encoder] $string} Chat instproc json_encode {string} { string map [list \n \\n \" \\\" ' {\\'} \\ \\\\] $string } Chat instproc json_encode_msg {msg} { set type [$msg type] switch $type { "message" { set message [$msg msg] set user_id [$msg user_id] set user [:user_name $user_id] set color [$msg color] set timestamp [clock format [$msg time] -format {[%H:%M:%S]}] foreach var {message user timestamp color user_id} { set $var [:json_encode [set $var]] } return [subst {{"type": "$type", "message": "$message", "timestamp": "$timestamp", "user": "$user", "color": "$color", "user_id": "$user_id"}\n}] } "users" { set message [list] foreach {user_id timestamp} [:active_user_list] { if {$user_id < 0} continue set timestamp [clock format [expr {[clock seconds] - $timestamp}] -format "%H:%M:%S" -gmt 1] set user [:user_name $user_id] set color [:user_color $user_id] foreach var {user timestamp color user_id} { set $var [:json_encode [set $var]] } lappend message [subst {{"timestamp": "$timestamp", "user": "$user", "color": "$color", "user_id": "$user_id"}}] } set message "\[[join $message ,]\]" return [subst {{"type": "$type", "chat_id": "${:chat_id}", "message": $message}\n}] } } } Chat instproc broadcast_msg {msg} { #:log "--chat broadcast_msg" ${:mr} send_to_subscriber chat-${:chat_id} [:json_encode_msg $msg] } Chat instproc subscribe {-uid} { set user_id [expr {[info exists uid] ? $uid : ${:user_id}}] set color [:user_color $user_id] #ns_log notice "--CHAT [self] subscribe chat-${:chat_id} -mode ${:mode} via <${:mr}>" ${:mr} subscribe chat-${:chat_id} -mode ${:mode} } Chat instproc render {} { :orderby time set result [list] # Piggyback the users list in every rendering, this way we don't # need a separate ajax request for the polling interface. :add [Message new -type "users" -time [clock seconds]] foreach child [:children] { lappend result [:json_encode_msg $child] } return "\[[join $result ,]\]" } ############################################################################ # Chat meta class, since we need to define general class-specific methods ############################################################################ Class create ChatClass -superclass ::xotcl::Class ChatClass method sweep_all_chats {} { #:log "-- starting" foreach nsv [nsv_names "[self]-*-seen"] { if { [regexp "[self]-(\[0-9\]+)-seen" $nsv _ chat_id] } { #:log "--Chat_id $chat_id" :new -volatile -chat_id $chat_id -user_id 0 -session_id 0 -init -sweeper } } #:log "-- ending" } ChatClass method initialize_nsvs {} { # empty stub for subclasses to extend } ChatClass method flush_messages {-chat_id:required} { set array "[self]-$chat_id" ::acs::clusterwide nsv_unset -nocomplain $array ::acs::clusterwide nsv_unset -nocomplain $array-seen ::acs::clusterwide nsv_unset -nocomplain $array-last-activity } ChatClass method init {} { # default setting is set19 from http://www.graphviz.org/doc/info/colors.html # per parameter settings in the chat package are available (param UserColors) set :colors [list #1b9e77 #d95f02 #7570b3 #e7298a #66a61e #e6ab02 #a6761d #666666] } } namespace eval ::xowiki { ::xo::ChatClass create Chat -superclass ::xo::Chat ::xo::ChatClass proc is_chat_p {class} { return [expr {[nsf::is object $class] && [$class class] eq [self]}] } ::xo::ChatClass ad_instproc login { -chat_id:required {-skin "classic"} {-package_id ""} {-mode ""} {-path ""} {-avatar_p:boolean true} {-force_login_p:boolean false} -login_messages_p:boolean -logout_messages_p:boolean -timewindow } { Logs into a chat } { #:log "--chat" if {![ns_conn isconnected]} { return } if {$force_login_p} { auth::require_login } # This might come in handy to get resources from the chat package # if we want to have e.g. a separate css. # set package_key [apm_package_key_from_id $package_id] # set resources_path /resources/${package_key} template::head::add_css -href /resources/xowiki/chat-skins/chat-$skin.css # # Check, whether we have the tcllibthread and a sufficiently new # AOLserver/NaviServer supporting bgdelivery transfers. When this # is missing, we must force the mode to polling. # if {[info commands ::thread::mutex] eq "" || [catch {ns_conn contentsentlength}]} { set mode polling } set session_id [ad_conn session_id].[clock seconds] set base_url [export_vars -base /shared/ajax/chat -no_empty { {id $chat_id} {s $session_id} {class "[self]"} mode }] # get LinkRegex parameter from the chat package set link_regex [::parameter::get_global_value \ -package_key "chat" \ -parameter "LinkRegex"] # Should we add a full screen link to the chat? set fs_link_p true # Should we display avatars? (JavaScript can only take 'true' or 'false' as boolean values) if {$avatar_p} { set show_avatar true } else { set show_avatar false } template::head::add_javascript -script "const linkRegex = \"${link_regex}\";" -order 19 template::head::add_javascript -script "const show_avatar = $show_avatar;" -order 20 template::head::add_javascript -src /resources/xowiki/chat-skins/chat-$skin.js -order 22 template::head::add_javascript -src /resources/xowiki/chat.js -order 30 #:log "--CHAT mode=$mode" set html "" if {[apm_package_installed_p chat]} { set message_label [_ xowiki.chat_message] set send_label [_ xowiki.chat_Send_Refresh] } else { set message_label "Message" set send_label "Send" } # TODO: it is currently not possible to embed multiple chats in # the same page. append html [subst { <div id='xowiki-chat'> <div id='xowiki-chat-messages-and-form'> <div id='xowiki-chat-messages'></div> <div id='xowiki-chat-messages-form-block'> <form id='xowiki-chat-messages-form' action='#'> <input type='text' placeholder="$message_label" name='msg' id='xowiki-chat-send' autocomplete="off" /> <button id='xowiki-chat-send-button' type='submit'>$send_label</button> </form> </div> </div> <div id='xowiki-chat-users'></div> </div> }] set conf [dict create] foreach var {force_login_p login_messages_p logout_messages_p timewindow} { if {[info exists $var]} { dict set conf $var [set $var] } } :create c1 \ -chat_id $chat_id \ -session_id $session_id \ -mode $mode \ -conf $conf \ -destroy_on_cleanup #:log "--CHAT created c1 with mode=$mode" append html [subst { <span id="xowiki-my-user-id" style="display:none;">[c1 set user_id]</span> }] set js "" set data [c1 login] if {$data ne ""} { append js [subst -nocommands { let data = $data; for (var i = 0; i < data.length; i++) { renderData(data[i]); } }] } if {$fs_link_p} { append js {addFullScreenLink();} } append js {addSendPic();} #:log "--CHAT create HTML for mode=$mode" append js [subst { chatSubscribe('$base_url'); }] # # A chat may be embedded later in the page's lifecycle, e.g. when # it is extracted from a template. The javascript to subscribe # should trigger when the markup becomes part of the DOM. # append html [subst { <script nonce="[security::csp::nonce]"> $js </script> }] template::add_refresh_on_history_handler return $html } } ::xo::library source_dependent # # Local variables: # mode: tcl # tcl-indent-level: 2 # indent-tabs-mode: nil # End: