namespace eval statusd { ######################################################################## # Copyright ©2011 lee8oi@gmail.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # http://www.gnu.org/licenses/ # # Statusd v0.3.7(10.24.11) # by: # github link: https://github.com/lee8oi/statusd/blob/master/statusd.tcl # # Status oriented rebuild of Seend v1.8.3 # # ------------------------------------------------------------------- # # Statusd is a reimagining of Seend script designed to track users by storing # thier latest activity & hostmask. Activities include joined, parted, kicked, # quit, nick change, spoke, and action. If status is not found script will check # channels nick list. If nick isn't in channel then a pattern search will be # performed instead and results displayed. Script also includes the ability # to search for nicks by hostmask using the host parameter. As well as an # automatic backup system that saves on .die, restart, timed intervals. # # *Configuration options can be changed using the dcc command 'statusd'. # Configurable options include all listed in configuration section. # # Initial channel setup: # (starts logging and enables public status command. Run in partyline.) # .chanset #channel +statusd # # Public command syntax: # !status ?channel? # !status host # # DCC command syntax: # (omitting value and/or option displays help) # .statusd set ?option? ?value? # .statusd ?load|store? # # Example Usage: # (public) # !status lee8oi # lee8oi spoke in #dukelovett 4 minutes 21 seconds ago. Message: # I thought it was a great idea so I got started.tcl paste # !status lee8oi #dukelovett2 # lee8oi joined #dukelovett2 5 minutes 59 seconds ago. # !status host *lee* # *lee* host matches the following nicks: lee8oi # # (DCC/partyline) # .statusd set # Configurable options: trigger backupfile # interval logbackups use_current_chan. Setting config without a value shows # help. ie: '.statusd set trigger' for trigger help. # .statusd set trigger # Usage: .statusd set trigger # .statusd set trigger !seen # Statusd trigger changed to: !seen # # # Thanks: drsprite. This script was concieved from your suggestions & comments. # # Updates: # v0.3 # 1.Added configuration backup system. Script configuration settings are now # saved & restored through the backup system as well as with '.statusd load' # and '.statusd store' commands via dcc/partyline. # 2.Removed backup_trigger & its configuration options since backups can be # performed via dcc/partyline. Also a couple other minor fixes. # 3.Script now checks if nick is in channel when a status is not found. If # nick is not in channel then a pattern search will be performed and results # returned. # 4.Fixed bug that caused nick patterns with * symbols to trigger 'currently in # chan' responses. # 5.Modified pattern search to also search channel nick list for matching names. # 6.Added chancheck function to validate specified channels. This fixes a bug # related to users specifying channels that the bot has not joined. # 7.Fixed configuration system to eliminate the extra binds leftover after # changing command trigger & configuration via partyline. # # ------------------------------------------------------------------- # Configuration Defaults: # # *Set command trigger # +-------+ variable trigger !seen # +-------+ # # *Set backupfile location/name # +--------------------------+ variable backupfile "scripts/statusdData.tcl" # +--------------------------+ # # *Set configfile location/name # +--------------------------+ variable configfile "scripts/statusdConfig.tcl" # +--------------------------+ # # *Set backup interval time (in mins) # +---+ variable interval 5 # +---+ # # *Log backup messages # 1 = on, 0 = off # +---+ variable logbackups 0 # +---+ # # *Use current channel if no channel specified(channel only) # -Off defaults to using the channel with most recent activity. # 1 = on, 0 = off # +---+ variable use_current_chan 1 # +---+ # ######################################################################## variable timeseen variable lastmsg variable status variable statustime variable statustext variable lastchan variable nickcase variable nickhost variable ver "0.3.7" setudef flag statusd proc config_load {args} { if {[file exists [set ::statusd::configfile]]} { source [set ::statusd::configfile] } bind msg - [set ::statusd::trigger] ::statusd::msg_show_status bind pub - [set ::statusd::trigger] ::statusd::show_status } } ::statusd::config_load bind dcc n statusd ::statusd::dcc_proc bind pubm - * ::statusd::status_logger_pubm bind sign - * ::statusd::status_logger_sign bind part - * ::statusd::status_logger_part bind join - * ::statusd::status_logger_join bind kick - * ::statusd::status_logger_kick bind nick - * ::statusd::status_logger_nick bind evnt - prerestart ::statusd::prerestart bind evnt - loaded ::statusd::loaded bind ctcp - "ACTION" ::statusd::status_logger_action if {![info exists ::statusd_dietrace]} { # .die trigger. do backup trace add execution *dcc:die enter ::statusd::backup_data trace add execution *msg:die enter ::statusd::backup_data } if {![info exists statusd_timer_running]} { # no existing timer. start new one. timer [set ::statusd::interval] ::statusd::timer_proc set statusd_timer_running 1 } namespace eval statusd { proc prerestart {type} { # prerestart trigger. do backup. ::statusd::backup_data if {[set ::statusd::logbackups]} { # logging is enabled. putlog "Status backup performed." } } proc config_store {args} { set fs [open [set ::statusd::configfile] w+] # create variable lines and 'array set' lines using array data. foreach arr {trigger backupfile configfile interval\ logbackups use_current_chan} { set arg "::statusd::${arr}" set arg2 [set ${arg}] if {[string is integer $arg2]} { puts $fs "variable $arr $arg2" } else { puts $fs "variable $arr \"$arg2\"" } } close $fs; } proc loaded {type} { # bot loaded trigger do restore. if {[file exists [set ::statusd::backupfile]]} { source [set ::statusd::backupfile] } ::statusd::config_load } proc timer_proc {args} { # call self at timed intervals. do backup ::statusd::backup_data timer [set ::statusd::interval] ::statusd::timer_proc return 1 } proc backup_data {args} { # backup to file set fs [open [set ::statusd::backupfile] w+] # create variable lines and 'array set' lines using array data. foreach arr {status nickcase lastchan statustime statustext nickhost} { set arrg "::statusd::${arr}" puts $fs "variable $arrg" puts $fs "array set $arr [list [array get [set arrg]]]" } close $fs; ::statusd::config_store if {[set ::statusd::logbackups]} { # logging is enabled. putlog "Status backup performed." } } proc chancheck {channel} { if {[catch {channel info $channel} err]} { set checkvar 0 } else { set checkvar 1 } return $checkvar } proc search_names {searchterm channel} { variable ::statusd::nickcase variable ::statusd::status set channel [string tolower $channel] set lterm [string tolower $searchterm] set namelist [array names nickcase "\*${lterm}\*"] if {$channel != ""} { set fromchanlist [::statusd::search_nicklist "\*$searchterm\*" $channel "no"] } else { set fromchanlist "" } if { $namelist != "" || $fromchanlist != "" } { # names matching pattern exist. set newlist "[string trimleft $fromchanlist]" if {[regexp {^\#} $channel]} { foreach elem $namelist { if {[info exists status($elem,$channel)]} { append newlist " " $nickcase($elem) } } if {$newlist != ""} { set result "'${searchterm}' matches from $channel: [lsort -unique $newlist]" } else { set result "'${searchterm}' not found." } } elseif {$channel == ""} { foreach elem $namelist { if {[info exists nickcase($elem)]} { append newlist " " $nickcase($elem) } } if {$newlist != ""} { set result "'${searchterm}' matches: $newlist" } else { set result "'${searchterm}' not found." } } } else { set result "'${searchterm}' not found." } return $result } proc search_nicklist {nick chan check} { set newlist "" set nicklist [split [chanlist $chan]] if {$check == "yes"} { set stripnick [string map {\* ""} $nick] } else { set stripnick $nick } foreach elem $nicklist { if {[string match -nocase $stripnick $elem]} { append newlist " " $elem } } if {$check == "yes"} { if {$newlist != "" } { return "true" } } else { return $newlist } } proc search_hosts {searchterm} { variable ::statusd::nickhost variable ::statusd::nickcase set namelist [array names nickhost] set hostlist "" set elemname "" set elemval "" foreach elem $namelist { set lelem [string tolower $elem] set elemname [set nickcase($lelem)] set elemval $nickhost($elem) if {[string match $searchterm $elemval]} { append hostlist " " $elemname } } if { $hostlist != "" } { set result "'${searchterm}' host matches the following nicks:$hostlist" } else { set result "'${searchterm}' not found." } return $result } proc set_status {nick userhost channel status text} { set lnick [string tolower $nick] set lchan [string tolower $channel] set ::statusd::status($lnick,$lchan) $status set ::statusd::statustext($lnick,$lchan) $text set ::statusd::statustime($lnick,$lchan) [clock seconds] set ::statusd::nickcase($lnick) $nick set ::statusd::nickhost($lnick) $userhost set ::statusd::lastchan($lnick) $lchan } proc get_status {nick channel} { set lnick [string tolower $nick] set lchan [string tolower $channel] set lstatus [set ::statusd::status($lnick,$lchan)] set ltext [set ::statusd::statustext($lnick,$lchan)] set ltime [set ::statusd::statustime($lnick,$lchan)] set ncase [set ::statusd::nickcase($lnick)] set durat [duration [expr {[clock seconds] - $ltime}]] if {$lstatus == "Quit"} { set result "$ncase was last seen quitting $durat ago. $ltext" } elseif {$lstatus == "Parted"} { set result "$ncase was last seen Parting from $channel $durat ago." } elseif {$lstatus == "Joined"} { set result "$ncase joined $channel $durat ago." } elseif {$lstatus == "Kicked"} { set result "$ncase was kicked from $channel $durat ago. Reason: $ltext" } elseif {$lstatus == "Nick Change"} { set result "$ncase was last seen changing name $ltext $durat ago." } elseif {$lstatus == "Spoke"} { set result "$ncase spoke in $channel $durat ago. Message: $ltext" } elseif {$lstatus == "Action"} { set result "$ncase was seen acting in $channel $durat ago: * $ncase $ltext" } else { set result "Status error. Unknown status type." } return $result } proc msg_show_status {nick userhost handle text} { #Status check via msg/query set arg1 [lindex [split $text] 0] set arg2 [lindex [split $text] 1] set larg1 [string tolower $arg1] variable ::statusd::status variable ::statusd::lastchan if {$arg2 != "" && [regexp {^\#} $arg2]} { #2nd arg is channel if {[::statusd::chancheck $arg2]} { if {[info exists status($larg1,$arg2)]} { #status available. set vstatus [::statusd::get_status $arg1 $arg2] } else { set listvar [::statusd::search_nicklist $arg1 $arg2 "check"] if {$listvar == "true"} { set vstatus "$arg1 is currently in $arg2" } else { set vstatus [::statusd::search_names $arg1 $arg2] } } } else { set vstatus "invalid channel" } putserv "PRIVMSG $nick :$vstatus" } elseif {$larg1 == "host"} { if {$arg2 != ""} { set vstatus [::statusd::search_hosts $arg2] } else { set pubcom [set ::statusd::trigger] set vstatus "Usage: $pubcom host " } putserv "PRIVMSG $nick :$vstatus" } elseif {$arg2 == "" && $arg1 != ""} { #no channel specified. nick only. if {[info exists lastchan($larg1)]} { set lastch [set lastchan($larg1)] } else { set lastch "#NOCHAN" } if {[info exists status($larg1,$lastch)]} { #status available set vstatus [::statusd::get_status $arg1 $lastch] } else { set searchresults [::statusd::search_names $arg1 ""] set vstatus "No status found. Specify a channel to check nick list.\ $searchresults" } putserv "PRIVMSG $nick :$vstatus" } else { #no args provided. variable ::statusd::ver set pubcom [set ::statusd::trigger] putserv "PRIVMSG $nick :Usage: $pubcom |or| $pubcom host " } } proc show_status {nick userhost handle channel text} { #status check via channel if {[channel get $channel statusd]} { set arg1 [lindex [split $text] 0] set arg2 [lindex [split $text] 1] set larg1 [string tolower $arg1] variable ::statusd::status if {$arg2 != "" && [regexp {^\#} $arg2]} { #2nd arg is channel if {[::statusd::chancheck $arg2]} { if {[info exists status($larg1,$arg2)]} { #status available. set vstatus [::statusd::get_status $arg1 $arg2] } else { #fall back to pattern search. set listvar [::statusd::search_nicklist $arg1 $arg2 "yes"] if {$listvar == "true"} { set vstatus "$arg1 is currently in $arg2" } else { set vstatus [::statusd::search_names $arg1 $arg2] } } } else { set vstatus "invalid channel" } putserv "PRIVMSG $channel :$vstatus" } elseif {$larg1 == "host"} { if {$arg2 != ""} { set vstatus [::statusd::search_hosts $arg2] } else { set pubcom [set ::statusd::trigger] set vstatus "Usage: $pubcom host " } putserv "PRIVMSG $channel :$vstatus" } elseif {$arg2 == "" && $arg1 != ""} { #no channel specified. nick only. if {[set ::statusd::use_current_chan]} { #use current chan set chanvar $channel } else { #use most recent channel if {[info exists ::statusd::lastchan($larg1)]} { #Use recorded last channel. set chanvar [set ::statusd::lastchan($larg1)] } else { #fall back to current channel. set chanvar $channel } } if {[info exists status($larg1,$chanvar)]} { #status available set vstatus [::statusd::get_status $arg1 $chanvar] } else { set listvar [::statusd::search_nicklist $arg1 $chanvar "yes"] if {$listvar == "true"} { set vstatus "$arg1 is currently in $chanvar" } else { set vstatus [::statusd::search_names $arg1 $chanvar] } } putserv "PRIVMSG $channel :$vstatus" } else { #no args provided. variable ::statusd::ver set pubcom [set ::statusd::trigger] putserv "PRIVMSG $channel :Usage: $pubcom ?channel? |or| $pubcom host " } } } proc dcc_proc {handle idx text} { set textarr [split $text] set text1 [string tolower [lindex $textarr 0]] set text2 [string tolower [lindex $textarr 1]] set text3 [string tolower [lindex $textarr 2]] switch $text1 { "" { putdcc $idx "Usage: .statusd ?set|load|store?" } "store" { ::statusd::backup_data putdcc $idx "Statusd backup performed." } "load" { ::statusd::loaded "" putdcc $idx "Statusd configuration loaded." } "set" { switch $text2 { "" { putdcc $idx "Configurable options are: trigger, \ backupfile, interval, logbackups,\ use_current_chan. Setting config without a value shows help.\ ie: '.statusd set trigger' for trigger help." } "trigger" { if {$text3 != ""} { unbind msg - [set ::statusd::trigger] ::statusd::msg_show_status unbind pub - [set ::statusd::trigger] ::statusd::show_status set ::statusd::trigger $text3 bind msg - [set ::statusd::trigger] ::statusd::msg_show_status bind pub - [set ::statusd::trigger] ::statusd::show_status putdcc $idx "Statusd trigger changed to: $text3" } else { putdcc $idx "Usage: .statusd set trigger " } } "backupfile" { if {$text3 != ""} { set ::statusd::backupfile "[lindex $textarr 2]" putdcc $idx "Statusd backupfile changed to: [lindex $textarr 2]" } else { putdcc $idx "Usage: .statusd set backupfile location/filename" } } "interval" { if {$text3 != "" && [string is integer $text3]} { set ::statusd::interval $text3 putdcc $idx "Statusd interval changed to: $text3" } else { putdcc $idx "Usage: .statusd set interval " } } "logbackups" { if {$text3 != "" && [string is integer $text3]} { if {$text3 == 1 || $text3 == 0} { set ::statusd::logbackups $text3 putdcc $idx "Statusd logbackups changed to: $text3" } else { putdcc $idx "Use 1 for on. 0 for off." } } else { putdcc $idx "Usage: .statusd set logbackups <1|0>" } } "use_current_chan" { if {$text3 != "" && [string is integer $text3]} { if {$text3 == 1 || $text3 == 0} { set ::statusd::use_current_chan $text3 putdcc $idx "Statusd use_current_chan changed to: $text3" } else { putdcc $idx "Use 1 for on. 0 for off." } } else { putdcc $idx "Usage: .statusd set use_current_chan <1|0>" } } default { putdcc $idx "Configurable options are: trigger, \ backupfile, interval, logbackups,\ use_current_chan. Setting config without a value shows help.\ ie: '.statusd set trigger' for trigger help." } } } default { putdcc $idx "Usage: .statusd ?set|load|store?" } } } proc status_logger_pubm {nick userhost handle channel text} { #public channel message logger. if {[channel get $channel statusd]} { set arg1 [lindex [split $text] 0] if {$arg1 != [set ::statusd::trigger]} { # not a status request. Ok to save. ::statusd::set_status $nick $userhost $channel "Spoke" $text } } } proc status_logger_sign {nick userhost handle channel text} { if {[channel get $channel statusd]} { ::statusd::set_status $nick $userhost $channel "Quit" $text } } proc status_logger_part {nick userhost handle channel text} { if {[channel get $channel statusd]} { ::statusd::set_status $nick $userhost $channel "Parted" "" } } proc status_logger_join {nick userhost handle channel} { if {[channel get $channel statusd]} { ::statusd::set_status $nick $userhost $channel "Joined" "" } } proc status_logger_kick {nick userhost handle channel target reason} { if {[channel get $channel statusd]} { set ltarg [string tolower $target] if {[info exists ::statusd::nickhost($ltarg)] && [set ::statusd::nickhost($ltarg)] != "none"} { set curmask [set ::statusd::nickhost($ltarg)] } else { set curmask "none" } ::statusd::set_status $target $curmask $channel "Kicked" $reason } } proc status_logger_nick {nick userhost handle channel newnick} { if {[channel get $channel statusd]} { ::statusd::set_status $nick $userhost $channel "Nick Change" "to $newnick" ::statusd::set_status $newnick $userhost $channel "Nick Change" "from $nick" } } proc status_logger_action { nick userhost hand dest keyword text } { if {[string index $dest 0] != "#"} { return 0 } if {[channel get $dest statusd]} { ::statusd::set_status $nick $userhost $dest "Action" $text } } } putlog "Statusd [set ::statusd::ver] loaded."