diff options
Diffstat (limited to 'pixseen')
| -rw-r--r-- | pixseen/pixseen-msggen.tcl | 85 | ||||
| -rw-r--r-- | pixseen/pixseen-msgs/en.msg | 302 | ||||
| -rw-r--r-- | pixseen/pixseen.tcl | 1339 |
3 files changed, 1726 insertions, 0 deletions
diff --git a/pixseen/pixseen-msggen.tcl b/pixseen/pixseen-msggen.tcl new file mode 100644 index 0000000..5766009 --- /dev/null +++ b/pixseen/pixseen-msggen.tcl @@ -0,0 +1,85 @@ +#!/bin/sh +# the next line restarts using tclsh \ +exec tclsh8.5 "$0" "$@" + +encoding system {utf-8} + +set fd [open pixseen.tcl r] +set data [read $fd] +close $fd + +set msgList {} +foreach {- item} [regexp -all -inline -- {\[mc \{([^\}]+)\}} $data] { + if {[lsearch -exact $msgList $item] == -1} { + lappend msgList $item + } +} + +# generate en.msg + +set fd [open pixseen-msgs/en.msg w] +fconfigure $fd -translation lf -encoding {utf-8} +puts $fd "# en.msg - automatically generated by pixseen-msggen.tcl on [clock format [clock seconds] -timezone UTC]\n" +puts $fd "namespace eval ::pixseen \{" +puts $fd "\tvariable lang \"en\"" +foreach item $msgList { + puts $fd "\n\tmcset \$lang \\" + puts $fd "\t\t\{$item\} \\" + puts $fd "\t\t\{$item\}" +} +puts $fd "\n\}" +close $fd + +# generate en_US_bork.msg + +proc chef {args} { + set subs [list {a([nu])} {u\1}\ + {A([nu])} {U\1}\ + {a\Y} e\ + {A\Y} E\ + {en\y} ee\ + {\Yew} oo\ + {\Ye\y} e-a\ + {\ye} i\ + {\yE} I\ + {\Yf} ff\ + {\Yir} ur\ + {(\w+?)i(\w+?)$} {\1ee\2}\ + {\Yow} oo\ + {\yo} oo\ + {\yO} Oo\ + {^the$} zee\ + {^The$} Zee\ + {th\y} t\ + {\Ytion} shun\ + {\Yu} {oo}\ + {\YU} {Oo}\ + v f\ + V F\ + w w\ + W W\ + {([a-z])[.]} {\1. Bork Bork Bork!}] + foreach word $args { + foreach {exp subSpec} $subs { + set word [regsub -all -- $exp $word $subSpec] +# puts "$exp || $subSpec -> $word" + } + lappend retval $word + } + return [join $retval] +} + +set fd [open pixseen-msgs/en_us_bork.msg w] +fconfigure $fd -translation lf -encoding {utf-8} +puts $fd "# en_US_bork.msg - automatically generated by pixseen-msggen.tcl on [clock format [clock seconds] -timezone UTC]\n" +puts $fd "namespace eval ::pixseen \{" +puts $fd "\tvariable lang \"en_us_bork\"" +foreach item $msgList { + puts $fd "\n\tmcset \$lang \\" + puts $fd "\t\t\{$item\} \\" + puts $fd "\t\t\{[chef $item]\}" +} +puts $fd "\n\}" + +close $fd + diff --git a/pixseen/pixseen-msgs/en.msg b/pixseen/pixseen-msgs/en.msg new file mode 100644 index 0000000..cbd1460 --- /dev/null +++ b/pixseen/pixseen-msgs/en.msg @@ -0,0 +1,302 @@ +# en.msg - automatically generated by pixseen-msggen.tcl on Wed Mar 31 12:43:08 UTC 2010 + +namespace eval ::pixseen { + variable lang "en" + + mcset $lang \ + {years} \ + {years} + + mcset $lang \ + {year} \ + {year} + + mcset $lang \ + {months} \ + {months} + + mcset $lang \ + {month} \ + {month} + + mcset $lang \ + {weeks} \ + {weeks} + + mcset $lang \ + {week} \ + {week} + + mcset $lang \ + {days} \ + {days} + + mcset $lang \ + {day} \ + {day} + + mcset $lang \ + {hours} \ + {hours} + + mcset $lang \ + {hour} \ + {hour} + + mcset $lang \ + {minutes} \ + {minutes} + + mcset $lang \ + {minute} \ + {minute} + + mcset $lang \ + {seconds} \ + {seconds} + + mcset $lang \ + {second} \ + {second} + + mcset $lang \ + {%1$s error; %2$s was unable to extract uhost. PLEASE REPORT THIS BUG!} \ + {%1$s error; %2$s was unable to extract uhost. PLEASE REPORT THIS BUG!} + + mcset $lang \ + {%1$s was last seen %2$s ago.} \ + {%1$s was last seen %2$s ago.} + + mcset $lang \ + {%1$s (%2$s) was last seen parting a channel %3$s ago.} \ + {%1$s (%2$s) was last seen parting a channel %3$s ago.} + + mcset $lang \ + {%1$s (%2$s) was last seen parting %3$s %4$s ago.} \ + {%1$s (%2$s) was last seen parting %3$s %4$s ago.} + + mcset $lang \ + {%1$s (%2$s) was last seen parting a channel %3$s ago, stating "%4$s"} \ + {%1$s (%2$s) was last seen parting a channel %3$s ago, stating "%4$s"} + + mcset $lang \ + {%1$s (%2$s) was last seen parting %3$s %4$s ago, stating "%5$s"} \ + {%1$s (%2$s) was last seen parting %3$s %4$s ago, stating "%5$s"} + + mcset $lang \ + {%1$s (%2$s) was last seen joining a channel %3$s ago.} \ + {%1$s (%2$s) was last seen joining a channel %3$s ago.} + + mcset $lang \ + {%1$s (%2$s) was last seen joining %3$s %4$s ago. %1$s is still on %3$s.} \ + {%1$s (%2$s) was last seen joining %3$s %4$s ago. %1$s is still on %3$s.} + + mcset $lang \ + {%1$s (%2$s) was last seen joining %3$s %4$s ago. I don't see %1$s on %3$s now, though.} \ + {%1$s (%2$s) was last seen joining %3$s %4$s ago. I don't see %1$s on %3$s now, though.} + + mcset $lang \ + {%1$s (%2$s) was last seen changing nicks to %4$s on a channel %3$s ago.} \ + {%1$s (%2$s) was last seen changing nicks to %4$s on a channel %3$s ago.} + + mcset $lang \ + {%1$s (%2$s) was last seen changing nicks to %5$s on %3$s %4$s ago.} \ + {%1$s (%2$s) was last seen changing nicks to %5$s on %3$s %4$s ago.} + + mcset $lang \ + {%1$s (%2$s) was last seen changing nicks from %4$s on a channel %3$s ago.} \ + {%1$s (%2$s) was last seen changing nicks from %4$s on a channel %3$s ago.} + + mcset $lang \ + {%1$s (%2$s) was last seen changing nicks from %5$s on %3$s %4$s ago. %1$s is still on %3$s.} \ + {%1$s (%2$s) was last seen changing nicks from %5$s on %3$s %4$s ago. %1$s is still on %3$s.} + + mcset $lang \ + {%1$s (%2$s) was last seen changing nicks from %5$s on %3$s %4$s ago. I don't see %1$s on %3$s now, though.} \ + {%1$s (%2$s) was last seen changing nicks from %5$s on %3$s %4$s ago. I don't see %1$s on %3$s now, though.} + + mcset $lang \ + {%1$s (%2$s) was last seen quitting from a channel %3$s ago.} \ + {%1$s (%2$s) was last seen quitting from a channel %3$s ago.} + + mcset $lang \ + {%1$s (%2$s) was last seen quitting from %3$s %4$s ago.} \ + {%1$s (%2$s) was last seen quitting from %3$s %4$s ago.} + + mcset $lang \ + {%1$s (%2$s) was last seen quitting from %3$s %4$s ago, stating "%5$s"} \ + {%1$s (%2$s) was last seen quitting from %3$s %4$s ago, stating "%5$s"} + + mcset $lang \ + {%1$s (%2$s) was last seen parting a channel due to a netsplit %3$s ago.} \ + {%1$s (%2$s) was last seen parting a channel due to a netsplit %3$s ago.} + + mcset $lang \ + {%1$s (%2$s) was last seen parting %3$s due to a netsplit %4$s ago.} \ + {%1$s (%2$s) was last seen parting %3$s due to a netsplit %4$s ago.} + + mcset $lang \ + {%1$s (%2$s) was last seen rejoining a channel from a netsplit %3$s ago.} \ + {%1$s (%2$s) was last seen rejoining a channel from a netsplit %3$s ago.} + + mcset $lang \ + {%1$s (%2$s) was last seen rejoining %3$s from a netsplit %4$s ago. %1$s is still on %3$s.} \ + {%1$s (%2$s) was last seen rejoining %3$s from a netsplit %4$s ago. %1$s is still on %3$s.} + + mcset $lang \ + {%1$s (%2$s) was last seen rejoining %3$s from a netsplit %4$s ago. I don't see %1$s on %3$s now, though.} \ + {%1$s (%2$s) was last seen rejoining %3$s from a netsplit %4$s ago. I don't see %1$s on %3$s now, though.} + + mcset $lang \ + {%1$s (%2$s) was last seen kicked from a channel %3$s ago.} \ + {%1$s (%2$s) was last seen kicked from a channel %3$s ago.} + + mcset $lang \ + {%1$s (%2$s) was last seen kicked from %3$s by %4$s %5$s ago, with the reason "%6$s"} \ + {%1$s (%2$s) was last seen kicked from %3$s by %4$s %5$s ago, with the reason "%6$s"} + + mcset $lang \ + {%1$s (%2$s) was last seen entering the partyline %3$s ago. %1$s is on the partyline right now.} \ + {%1$s (%2$s) was last seen entering the partyline %3$s ago. %1$s is on the partyline right now.} + + mcset $lang \ + {%1$s (%2$s) was last seen entering the partyline %3$s ago. I don't see %1$s on the partyline now, though.} \ + {%1$s (%2$s) was last seen entering the partyline %3$s ago. I don't see %1$s on the partyline now, though.} + + mcset $lang \ + {%1$s (%2$s) was last seen leaving the partyline %3$s ago. %1$s is on the partyline channel %4$s still.} \ + {%1$s (%2$s) was last seen leaving the partyline %3$s ago. %1$s is on the partyline channel %4$s still.} + + mcset $lang \ + {%1$s (%2$s) was last seen leaving the partyline %3$s ago.} \ + {%1$s (%2$s) was last seen leaving the partyline %3$s ago.} + + mcset $lang \ + {%1$s (%2$s) was last seen entering the partyline on %3$s %4$s ago. %1 is on the partyline right now.} \ + {%1$s (%2$s) was last seen entering the partyline on %3$s %4$s ago. %1 is on the partyline right now.} + + mcset $lang \ + {%1$s (%2$s) was last seen entering the partyline on %3$s %4$s ago. I don't see %1$s on the partyline now, though.} \ + {%1$s (%2$s) was last seen entering the partyline on %3$s %4$s ago. I don't see %1$s on the partyline now, though.} + + mcset $lang \ + {%1$s (%2$s) was last seen leaving the partyline from %3$s %4$s ago. %1$s is on the partyline channel %5$s still.} \ + {%1$s (%2$s) was last seen leaving the partyline from %3$s %4$s ago. %1$s is on the partyline channel %5$s still.} + + mcset $lang \ + {%1$s (%2$s) was last seen leaving the partyline from %3$s %4$s ago.} \ + {%1$s (%2$s) was last seen leaving the partyline from %3$s %4$s ago.} + + mcset $lang \ + {%1$s was last seen marked as away (%2$s) on the partyline %3$s ago. %1$s is on the partyline right now.} \ + {%1$s was last seen marked as away (%2$s) on the partyline %3$s ago. %1$s is on the partyline right now.} + + mcset $lang \ + {%1$s was last seen marked as away (%2$s) on the partyline %3$s ago. I don't see %1$s on the partyline now, though.} \ + {%1$s was last seen marked as away (%2$s) on the partyline %3$s ago. I don't see %1$s on the partyline now, though.} + + mcset $lang \ + {%1$s was last seen returning to the partyline %2$s ago. %1$s is on the partyline right now.} \ + {%1$s was last seen returning to the partyline %2$s ago. %1$s is on the partyline right now.} + + mcset $lang \ + {%1$s was last seen returning to the partyline %2$s ago. I don't see %1$s on the partyline now, though.} \ + {%1$s was last seen returning to the partyline %2$s ago. I don't see %1$s on the partyline now, though.} + + mcset $lang \ + {%1$s error; Unhandled event in %2$s: %3$s} \ + {%1$s error; Unhandled event in %2$s: %3$s} + + mcset $lang \ + {I don't remember seeing %s.} \ + {I don't remember seeing %s.} + + mcset $lang \ + {%1$s SQL error %2$s; %3$s} \ + {%1$s SQL error %2$s; %3$s} + + mcset $lang \ + {SQL error %1$s; %2$s} \ + {SQL error %1$s; %2$s} + + mcset $lang \ + {%s: performing database maintenance...} \ + {%s: performing database maintenance...} + + mcset $lang \ + {Usage: %s} \ + {Usage: %s} + + mcset $lang \ + {%1$s, Usage: %2$s} \ + {%1$s, Usage: %2$s} + + mcset $lang \ + {Go look in a mirror.} \ + {Go look in a mirror.} + + mcset $lang \ + {%s, go look in a mirror.} \ + {%s, go look in a mirror.} + + mcset $lang \ + {You found me!} \ + {You found me!} + + mcset $lang \ + {You found me, %s!} \ + {You found me, %s!} + + mcset $lang \ + {%s is on the channel right now!} \ + {%s is on the channel right now!} + + mcset $lang \ + {%1$s is on the channel right now! %1$s last spoke %2$s ago.} \ + {%1$s is on the channel right now! %1$s last spoke %2$s ago.} + + mcset $lang \ + {That is not a valid nickname.} \ + {That is not a valid nickname.} + + mcset $lang \ + {%s, that is not a valid nickname.} \ + {%s, that is not a valid nickname.} + + mcset $lang \ + {There were no matches to your query.} \ + {There were no matches to your query.} + + mcset $lang \ + {Displaying %1$s of %2$s results:} \ + {Displaying %1$s of %2$s results:} + + mcset $lang \ + {%s: No existing database found, defining SQL schema.} \ + {%s: No existing database found, defining SQL schema.} + + mcset $lang \ + {Fatal Error!} \ + {Fatal Error!} + + mcset $lang \ + {%1$s: FATAL ERROR; SQLite database corrupt, exiting.} \ + {%1$s: FATAL ERROR; SQLite database corrupt, exiting.} + + mcset $lang \ + {%s: Loaded the seen database.} \ + {%s: Loaded the seen database.} + + mcset $lang \ + {%s: Unloaded the seen database.} \ + {%s: Unloaded the seen database.} + + mcset $lang \ + {Error: Invalid seen language "%s".} \ + {Error: Invalid seen language "%s".} + + mcset $lang \ + {Loaded %1$s v%2$s by %3$s} \ + {Loaded %1$s v%2$s by %3$s} + +} diff --git a/pixseen/pixseen.tcl b/pixseen/pixseen.tcl new file mode 100644 index 0000000..11fbed1 --- /dev/null +++ b/pixseen/pixseen.tcl @@ -0,0 +1,1339 @@ +# pixseen.tcl -- +# +# SQLite powered seen script. Keeps track of everyone, based on nickname. +# +# Copyright (c) 2010, Rickard Utgren <rutgren@gmail.com> +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# +# RCS: $Id$ +# +# v1.1 by Pixelz - unreleased +# - Fixed a problem with ValidTable always failing on some older SQLite versions +# - Fixed a problem with the public trigger never showing the syntax help +# - Minor fixes +# - Fixed a problem with glob & regex matching where only the oldest matches would ever be returned +# - Added a setting to change the maximum number of results returned from a query +# - Fixed a problem with Hand2Uhost never returning anything useful +# +# v1.0 by Pixelz - April 5, 2010 +# - Initial release + +# ToDo: +# - add some kind of botnet synching +# - auto-discover pixseen bots on link +# - auto-assign a primary bot for each channel that will answer public requests +# * Possible different routes for inter-bot information sharing: +# * "Synch everything" route: +# - synch databases on link +# - keep synching across the net whenever there's a change. +# - probably do this blindly, to account for IRCDs that doesn't show all joins/parts (ircu) +# - perhaps add some logic to this, so bots dont push info to other bots that are in the same channel (and would see it anyway), at least on non-ircu, non-chanmode-D channels +# * "Ask botnet" route: +# - Don't synch databases +# - Ask other bots for information on each seen request to the bot +# * Considerations: +# * For both of these methods, the clock probably has to fairly synched up, particularly for the "synch everything" route +# - A simple solution: check TS delta on link, and complain loudly if it's too high, or compensate for it somehow +# - Add IRCU support ("hidden" users in +D channels) -> names -d #chan (who #chan cd) +# - OR just tell quakenet users to use thommey's +D patch +# - add option to track every channel separately +# - track channel idle time, this would probably synergize well with the "track each channel separetly" option +# - find out if it's a good idea to [catch] each sql query (catch is slow, is there a better alternative?) +# - test the idx lookup stuff more, I suspect there's a bug in it somewhere +# - perhaps get rid of the daily unused-channels cleanup, and do it the same way as pixinfo.tcl. +# - Make sure this isn't hugely resource intensive first. +# - Add a setting to set the default matching type? OR Apply better logic to it? perhaps assume glob if it contains asterisk, Most Users fail at regex anyway... + + +package require Tcl 8.5 +package require msgcat 1.4.2 +package require eggdrop 1.6 +package require sqlite3 3.3.0;# order by desc was added in this version + +namespace eval ::pixseen { + # path to the database file + variable dbfile {scripts/pixseen.db} + + # Output with NOTICE nick (0) or PRIVMSG #chan (1) + variable outnotc 1 + + # Language + variable defaultLang "en" + + # Maximum number of results to display in public + variable pubResults 3 + + # Maximum number of results to display in private message + variable msgResults 5 + + # Maximum number of results to display in the partyline + variable dccResults 10 + + ## end of settings ## + + # list of locales, if you translate the script, add your translation to this list + variable locales [list "en"] + + namespace import ::msgcat::* + # mcload fails to load _all_ .msg files, so we have to do it manually + foreach f [glob -nocomplain -directory [file join [file dirname [info script]] pixseen-msgs] -type {b c f l} *.msg] { + source -encoding {utf-8} $f + } + unset -nocomplain f + + mclocale $defaultLang + setudef flag {seen} + setudef str {seenlang} + variable ::botnick + variable ::botnet-nick + variable ::nicklen + variable seenFlood + variable seenver {1.1} + variable dbVersion 1 +} + +## utility procs + +proc ::pixseen::validlang {lang} { + variable locales + if {[lsearch -exact -nocase $locales $lang] == -1} { + return 0 + } else { + return 1 + } +} + +# msgcat compatible duration proc +proc ::pixseen::pixduration {seconds} { + set map [list \ + {years} [mc {years}] \ + {year} [mc {year}] \ + {months} [mc {months}] \ + {month} [mc {month}] \ + {weeks} [mc {weeks}] \ + {week} [mc {week}] \ + {days} [mc {days}] \ + {day} [mc {day}] \ + {hours} [mc {hours}] \ + {hour} [mc {hour}] \ + {minutes} [mc {minutes}] \ + {minute} [mc {minute}] \ + {seconds} [mc {seconds}] \ + {second} [mc {second}] \ + ] + string map $map [duration $seconds] +} + +# takes an idx and returns the user@host associated with it. +# ONLY to be called from finduhost. DON'T CALL THIS DIRECTLY! +proc ::pixseen::Idx2Uhost {idx} { + # for some mind-boggling reason, eggdrop doesn't provide the uhost for a + # lot of the partyline binds, so we extract it from dcclist + foreach item [dcclist chat] { + lassign $item i - u; + if {$idx eq $i} { + return $u + } + } + # this should never happen? + putlog [mc {%1$s error; %2$s was unable to extract uhost. PLEASE REPORT THIS BUG!} {pixseen.tcl} {::pixseen::Idx2Uhost}] + return +} + +# takes a handle and returns the user@host associated with it. +# ONLY to be called from finduhost. DON'T CALL THIS DIRECTLY! +proc ::pixseen::Hand2Uhost {botname hand {chan {*}}} { + foreach item [whom $chan] { + lassign $item Hand Botname Uhost Flags Idle Away + if {[string equal -nocase $botname $Botname] && [string equal -nocase $hand $Hand]} { + return $Uhost + } + } + # this should never happen? + putlog [mc {%1$s error; %2$s was unable to extract uhost. PLEASE REPORT THIS BUG!} {pixseen.tcl} {::pixseen::Hand2Uhost}] + return +} + +# figures out the uhost using different methods for different binds +# returns the uhost found, or "" if it can't find one. +proc ::pixseen::finduhost {bind args} { + # chjn isn't listed because that bind actually calls the proc with the uhost + # chon/chof is NOT triggered for remote users + # away/chpt (and chjn) IS triggered for remote users + switch -exact -- $bind { + {chon} - {chof} { + lassign $args idx + return [Idx2Uhost $idx] + } + {chpt} { + lassign $args idx botname hand chan + if {[string equal -nocase $botname ${::botnet-nick}]} { + # this is a local user, grab the uhost based on the idx + return [Idx2Uhost $idx] + } else { + # this is a remote user, make an educated guess + return [Hand2Uhost $botname $hand $chan] + } + } + {away} { + lassign $args idx botname + if {[string equal -nocase $botname ${::botnet-nick}]} { + # this is a local user, grab the uhost based on the idx + return [Idx2Uhost $idx] + } else { + # this is a remote user. it is NOT POSSIBLE to get their uhost + return + } + } + default { + return + } + } +} + +# returns 1 if a module is loaded, 0 if not +proc ::pixseen::modloaded {module} { + if {[lsearch -exact -index 0 [modules] $module] == -1} { + return 0 + } else { + return 1 + } +} + +# returns the name of a partyline channel, or whatever was passed to it +# if there's no name set +proc ::pixseen::partychanname {chan} { + if {(![modloaded assoc]) || ($chan == 0)} { + return $chan + } elseif {![catch {set name [assoc $chan]}]} { + if {$name ne {}} { + return $name + } else { + return $chan + } + } else { + return $chan + } +} + +# returns 1 if the nick is valid on IRC, 0 if not +proc ::pixseen::validnick {nick} { + if {([info exists ::nicklen]) && ($::nicklen > 32)} { + set len $::nicklen + } else { + set len 32 + } + if {[string length $nick] > $len} { + return 0 + # FixMe: make sure these are all of the valid chars + } elseif {![regexp -- {^[a-zA-Z\|\[\]`^\{\}][a-zA-Z0-9\-_\|\[\]`^\{\}\\]*$} $nick]} { + return 0 + } else { + return 1 + } +} + +# returns 1 if a channel is set +secret, 0 if not +proc ::pixseen::issecret {chan} { + if {[validchan $chan] && [channel get $chan secret]} { + return 1 + } else { + return 0 + } +} + +proc ::pixseen::handseen {handle} { + if {![validuser $handle]} { + return + } else { + lassign [getuser $handle LASTON] timestamp location + # the location can be fun things like "linked" or "@botnick" so let's not even go there... + return [mc {%1$s was last seen %2$s ago.} $handle [pixduration [expr {[clock seconds] - $timestamp}]]] + } +} + +proc ::pixseen::lastspoke {nick chan} { + if {[set idle [getchanidle $nick $chan]] == 0} { + return + } else { + # whose great idea was it to return idle time in minutes? o_O + return [pixduration [expr {$idle * 60}]] + } +} + +# returns the channel if the user is logged in to the partyline. if not, returns "" +proc ::pixseen::onpartyline {handle} { + foreach item [whom *] { + lassign $item nick bot uhost flags idle away chan + if {[string equal -nocase $handle $nick]} { return [partychanname $chan] } + } + return +} + +# checks the flood array and removes old timestamps. +# will eventually remove itself if we're not being flooded +proc ::pixseen::RemoveFlood {args} { + variable seenFlood + if {![array exists seenFlood]} { return } + set time [clock seconds] + foreach uhost [array names seenFlood] { + foreach timestamp $seenFlood($uhost) { + # don't append the timestamp if it's older than 60 seconds + if {[expr {$time - 60}] <= $timestamp} { + lappend stampList $timestamp + } + } + if {[info exists stampList]} { + set seenFlood($uhost) $stampList + } else { + array unset seenFlood $uhost + } + } +} + +# returns 1 if we're flooded, 0 if not +proc ::pixseen::checkflood {uhost} { + variable seenFlood + RemoveFlood + # case 1: uhost doesn't exist in the array, we're not being flooded + if {![info exists seenFlood($uhost)]} { + set seenFlood($uhost) [clock seconds] + return 0 + # case 2: the list for this uhost is full, so we're being flooded with 6 lines over 60 seconds + } elseif {[llength $seenFlood($uhost)] >= 6} { + set seenFlood($uhost) [concat "[lrange [lsort -integer $seenFlood($uhost)] 1 end] [clock seconds]"] + return 1 + # case 3: the list for this uhost isn't full, we're not being flooded + } else { + lappend seenFlood($uhost) [clock seconds] + return 0 + } +} + +# Formats seen events for output +proc ::pixseen::formatevent {event nick uhost time chan reason othernick} { + set duration [pixduration [expr {[clock seconds] - $time}]] + set timestamp [clock format $time -format "%Y-%m-%dT%H:%M:%SZ" -gmt 1] + switch -exact -- $event { + {0} {;# part + if {$reason eq {}} { + if {[issecret $chan]} { + return [mc {%1$s (%2$s) was last seen parting a channel %3$s ago (%4$s)} $nick $uhost $duration $timestamp] + } else { + return [mc {%1$s (%2$s) was last seen parting %3$s %4$s ago (%4$s)} $nick $uhost $chan $duration $timestamp] + } + } else { + if {[issecret $chan]} { + return [mc {%1$s (%2$s) was last seen parting a channel %3$s ago (%4$s), stating "%5$s"} $nick $uhost $duration $timestamp $reason] + } else { + return [mc {%1$s (%2$s) was last seen parting %3$s %4$s ago (%4$s), stating "%5$s"} $nick $uhost $chan $duration $timestamp $reason] + } + } + } + {1} {;# join + if {[issecret $chan]} { + return [mc {%1$s (%2$s) was last seen joining a channel %3$s ago (%4$s)} $nick $uhost $duration $timestamp] + } else { + if {[onchan $nick $chan]} { + return [mc {%1$s (%2$s) was last seen joining %3$s %4$s ago (%5$s). %1$s is still on %3$s.} $nick $uhost $chan $duration $timestamp] + } else { + return [mc {%1$s (%2$s) was last seen joining %3$s %4$s ago (%5$s). I don't see %1$s on %3$s now, though.} $nick $uhost $chan $duration $timestamp] + } + } + } + {2} {;# nick (old) + if {[issecret $chan]} { + return [mc {%1$s (%2$s) was last seen changing nicks to %5$s on a channel %3$s ago (%4$s).} $nick $uhost $duration $timestamp $othernick] + } else { + return [mc {%1$s (%2$s) was last seen changing nicks to %6$s on %3$s %4$s ago (%5%s).} $nick $uhost $chan $duration $timestamp $othernick] + } + } + {3} {;# nick (new) + if {[issecret $chan]} { + return [mc {%1$s (%2$s) was last seen changing nicks from %5$s on a channel %3$s ago (%4$s)} $nick $uhost $duration $timestamp $othernick] + } else { + if {[onchan $nick $chan]} { + return [mc {%1$s (%2$s) was last seen changing nicks from %6$s on %3$s %4$s ago (%5$s). %1$s is still on %3$s.} $nick $uhost $chan $duration $timestamp $othernick] + } else { + return [mc {%1$s (%2$s) was last seen changing nicks from %6$s on %3$s %4$s ago (%5$s). I don't see %1$s on %3$s now, though.} $nick $uhost $chan $duration $timestamp $othernick] + } + } + } + {4} {;# sign (quit) + if {[issecret $chan]} { + return [mc {%1$s (%2$s) was last seen quitting from a channel %3$s ago (%4$s)} $nick $uhost $duration $timestamp] + } elseif {$reason eq {}} { + return [mc {%1$s (%2$s) was last seen quitting from %3$s %4$s ago (%5$s)} $nick $uhost $chan $duration $timestamp] + } else { + return [mc {%1$s (%2$s) was last seen quitting from %3$s %4$s ago (%5$s), stating "%6$s"} $nick $uhost $chan $duration $timestamp $reason] + } + } + {5} {;# splt (netsplit) + if {[issecret $chan]} { + return [mc {%1$s (%2$s) was last seen parting a channel due to a netsplit %3$s ago.} $nick $uhost $duration] + } else { + return [mc {%1$s (%2$s) was last seen parting %3$s due to a netsplit %4$s ago.} $nick $uhost $chan $duration] + } + } + {6} {;# rejn (netsplit rejoin) + if {[issecret $chan]} { + return [mc {%1$s (%2$s) was last seen rejoining a channel from a netsplit %3$s ago (%4$s)} $nick $uhost $duration $timestamp] + } else { + if {[onchan $nick $chan]} { + return [mc {%1$s (%2$s) was last seen rejoining %3$s from a netsplit %4$s ago (%5$s). %1$s is still on %3$s.} $nick $uhost $chan $duration $timestamp] + } else { + return [mc {%1$s (%2$s) was last seen rejoining %3$s from a netsplit %4$s ago (%5$s). I don't see %1$s on %3$s now, though.} $nick $uhost $chan $duration $timestamp] + } + } + } + {7} {;# kick + if {[issecret $chan]} { + return [mc {%1$s (%2$s) was last seen kicked from a channel %3$s ago (%4$s)} $nick $uhost $duration $timestamp] + } else { + return [mc {%1$s (%2$s) was last seen kicked from %3$s by %4$s %5$s ago (%6$s), with the reason "%7$s"} $nick $uhost $chan $othernick $duration $timestamp $reason] + } + } + {8} {;# chon (enter partyline) + if {[onpartyline $nick] ne {}} { + return [mc {%1$s (%2$s) was last seen entering the partyline %3$s ago (%4$s). %1$s is on the partyline right now.} $nick $uhost $duration $timestamp] + } else { + return [mc {%1$s (%2$s) was last seen entering the partyline %3$s ago (%4$s). I don't see %1$s on the partyline now, though.} $nick $uhost $duration $timestamp] + } + } + {9} {;# chof (leaves partyline) + if {[set pchan [onpartyline $nick]] ne {}} { + return [mc {%1$s (%2$s) was last seen leaving the partyline %3$s ago (%4$s). %1$s is on the partyline channel %4$s still.} $nick $uhost $duration $pchan $timestamp] + } else { + return [mc {%1$s (%2$s) was last seen leaving the partyline %3$s ago (%4$s)} $nick $uhost $duration $timestamp] + } + } + {10} {;# chjn (joins partyline channel) + if {[onpartyline $nick] ne {}} { + return [mc {%1$s (%2$s) was last seen entering the partyline on %3$s %4$s ago (%5$s). %1 is on the partyline right now.} $nick $uhost [partychanname $chan] $duration $timestamp] + } else { + return [mc {%1$s (%2$s) was last seen entering the partyline on %3$s %4$s ago (%5$s). I don't see %1$s on the partyline now, though.} $nick $uhost [partychanname $chan] $duration $timestamp] + } + } + {11} {;# chpt (parts partyline channel) + if {[set pchan [onpartyline $nick]] ne {}} { + return [mc {%1$s (%2$s) was last seen leaving the partyline from %3$s %4$s ago (%5$s). %1$s is on the partyline channel %6$s still.} $nick $uhost [partychanname $chan] $duration $timestamp $pchan] + } else { + return [mc {%1$s (%2$s) was last seen leaving the partyline from %3$s %4$s ago (%5$s)} $nick $uhost [partychanname $chan] $duration $timestamp] + } + } + {12} {;# away (partyline away) + if {[onpartyline $nick] ne {}} { + return [mc {%1$s was last seen marked as away (%2$s) on the partyline %3$s ago (%4$s). %1$s is on the partyline right now.} $nick $reason $duration $timestamp] + } else { + return [mc {%1$s was last seen marked as away (%2$s) on the partyline %3$s ago (%4$s). I don't see %1$s on the partyline now, though.} $nick $reason $duration $timestamp] + } + } + {13} {;# back (partyline back from away) + if {[onpartyline $nick] ne {}} { + return [mc {%1$s was last seen returning to the partyline %2$s ago (%3$s). %1$s is on the partyline right now.} $nick $duration $timestamp] + } else { + return [mc {%1$s was last seen returning to the partyline %2$s ago (%3$s). I don't see %1$s on the partyline now, though.} $nick $duration $timestamp] + } + } + {14} {;# chmsg (messaged channel) + if {[issecret $chan]} { + return [mc {%1$s (%2$s) was last seen talking on a channel %3$s ago (%4$s)} $nick $uhost $duration $timestamp] + } else { + if {[onchan $nick $chan]} { + return [mc {%1$s (%2$s) was last seen messaging on %3$s %4$s ago (%5$s). %1$s is still on %3$s.} $nick $uhost $chan $duration $timestamp] + } else { + return [mc {%1$s (%2$s) was last seen messaging on %3$s %4$s ago (%5$s). I don't see %1$s on %3$s now, though.} $nick $uhost $chan $duration $timestamp] + } + } + } + default { + putlog [mc {%1$s error; Unhandled event in %2$s: %3$s} {pixseen.tcl} {formatevent} $event] + return [mc {I don't remember seeing %s.} $nick] + } + } +} + +## SQLite functions + +# This is the SQLite collation function, if it's changed, the index has to be rebuilt with REINDEX or it'll cause Weird Behaviour +proc ::pixseen::rfccomp {a b} { + string compare [string map [list \{ \[ \} \] ~ ^ | \\] [string toupper $a]] [string map [list \{ \[ \} \] ~ ^ | \\] [string toupper $b]] +} + +proc ::pixseen::chan2id {chan} { + if {[catch {seendb eval { INSERT OR IGNORE INTO chanTb VALUES(NULL, $chan); }} error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + } + if {[catch {set retval [seendb eval { SELECT chanid FROM chanTb WHERE chan=$chan LIMIT 1 }]} error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + return -code error [mc {SQL error %1$s; %2$s} [seendb errorcode] $error] + } else { + return $retval + } +} + +# SQLite regexp function, squelches regex errors and turn on nocase +proc ::pixseen::pixregexp {args} { + if {[catch {set result [regexp -nocase -- {*}$args]}]} { + return 0 + } else { + return $result + } +} + +## + +proc ::pixseen::dbAdd {nick event timestamp uhost args} { + switch -exact -- $event { + {part} {;# 0 + lassign $args chan reason + if {[catch {seendb eval { INSERT OR REPLACE INTO seenTb VALUES(0, $nick, $uhost, $timestamp, chan2id($chan), $reason, NULL) }} error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + } + } + {join} {;# 1 + lassign $args chan + if {[catch {seendb eval { INSERT OR REPLACE INTO seenTb VALUES(1, $nick, $uhost, $timestamp, chan2id($chan), NULL, NULL) }} error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + } + } + {nick} {;# 2 & 3 + lassign $args chan newnick + if {[catch {seendb eval { + -- old nick; + INSERT OR REPLACE INTO seenTb VALUES(2, $nick, $uhost, $timestamp, chan2id($chan), NULL, $newnick); + -- new nick; + INSERT OR REPLACE INTO seenTb VALUES(3, $newnick, $uhost, $timestamp, chan2id($chan), NULL, $nick); + }} error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + } + } + {sign} {;# 4 (quit) + lassign $args chan reason + if {[catch {seendb eval { INSERT OR REPLACE INTO seenTb VALUES(4, $nick, $uhost, $timestamp, chan2id($chan), $reason, NULL) }} error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + } + } + {splt} {;# 5 + lassign $args chan + if {[catch {seendb eval { INSERT OR REPLACE INTO seenTb VALUES(5, $nick, $uhost, $timestamp, chan2id($chan), NULL, NULL) }} error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + } + } + {rejn} {;# 6 + lassign $args chan + if {[catch {seendb eval { INSERT OR REPLACE INTO seenTb VALUES(6, $nick, $uhost, $timestamp, chan2id($chan), NULL, NULL) }} error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + } + } + {kick} {;# 7 + lassign $args chan reason aggressor + if {[catch {seendb eval { INSERT OR REPLACE INTO seenTb VALUES(7, $nick, $uhost, $timestamp, chan2id($chan), $reason, $aggressor) }} error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + } + } + {chon} {;# 8 (enters partyline) + if {[catch {seendb eval { INSERT OR REPLACE INTO seenTb VALUES(8, $nick, $uhost, $timestamp, NULL, NULL, NULL) }} error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + } + } + {chof} {;# 9 (leaves partyline) + if {[catch {seendb eval { INSERT OR REPLACE INTO seenTb VALUES(9, $nick, $uhost, $timestamp, NULL, NULL, NULL) }} error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + } + } + {chjn} {;# 10 (joins partyline channel) + lassign $args chan botname + if {[catch {seendb eval { INSERT OR REPLACE INTO seenTb VALUES(10, $nick, $uhost, $timestamp, chan2id($chan), NULL, $botname) }} error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + } + } + {chpt} {;# 11 (parts partyline channel) + lassign $args chan botname + if {[catch {seendb eval { INSERT OR REPLACE INTO seenTb VALUES(11, $nick, $uhost, $timestamp, chan2id($chan), NULL, $botname) }} error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + } + } + {away} {;# 12 (partyline away) + lassign $args botname reason + if {[catch {seendb eval { INSERT OR REPLACE INTO seenTb VALUES(12, $nick, $uhost, $timestamp, NULL, $reason, $botname) }} error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + } + } + {back} {;# 13 (partyline back from away) + lassign $args botname + if {[catch {seendb eval { INSERT OR REPLACE INTO seenTb VALUES(13, $nick, $uhost, $timestamp, NULL, NULL, $botname) }} error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + } + } + {chmsg} {;# 14 + lassign $args chan + if {[catch {seendb eval { INSERT OR REPLACE INTO seenTb VALUES(14, $nick, $uhost, $timestamp, chan2id($chan), NULL, NULL) }} error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + } + } + default { + putlog [mc {%1$s error; Unhandled event in %2$s: %3$s} {pixseen.tcl} {dbAdd} $event] + return + } + } + return +} + +## event binds + +proc ::pixseen::PART {nick uhost hand chan msg} { + dbAdd $nick {part} [clock seconds] $uhost $chan $msg + return +} + +proc ::pixseen::JOIN {nick uhost hand chan} { + dbAdd $nick {join} [clock seconds] $uhost $chan + return +} + +proc ::pixseen::NICK {nick uhost hand chan newnick} { + dbAdd $nick {nick} [clock seconds] $uhost $chan $newnick + return +} + +proc ::pixseen::SIGN {nick uhost hand chan reason} { + dbAdd $nick {sign} [clock seconds] $uhost $chan $reason + return +} + +proc ::pixseen::SPLT {nick uhost hand chan} { + dbAdd $nick {splt} [clock seconds] $uhost $chan + return +} + +proc ::pixseen::REJN {nick uhost hand chan} { + dbAdd $nick {rejn} [clock seconds] $uhost $chan + return +} + +proc ::pixseen::KICK {nick uhost hand chan target reason} { + dbAdd $target {kick} [clock seconds] [getchanhost $target $chan] $chan $reason $nick + return +} + +proc ::pixseen::CHON {hand idx} { + if {[set uhost [finduhost {chon} $idx]] ne {}} { + dbAdd $hand {chon} [clock seconds] $uhost + } + return +} + +proc ::pixseen::CHOF {hand idx} { + if {[set uhost [finduhost {chof} $idx]] ne {}} { + dbAdd $hand {chon} [clock seconds] $uhost + } + return +} + +proc ::pixseen::CHJN {botname hand chan flag idx uhost} { + dbAdd $hand {chon} [clock seconds] $uhost $chan $botname + return +} + +proc ::pixseen::CHPT {botname hand idx chan} { + if {[set uhost [finduhost {chpt} $idx $botname $hand $chan]] ne {}} { + dbAdd $hand {chon} [clock seconds] $uhost $chan $botname + } + return +} + +proc ::pixseen::AWAY {botname idx text} { + if {[string equal -nocase $botname ${::botnet-nick}]} { + # this is a local away + if {$text ne {}} { + # user is away + if {[set uhost [finduhost {away} $idx $botname]] ne {}} { + dbAdd [idx2hand $idx] {away} [clock seconds] $uhost $botname $text + } + } else { + # user has returned + if {[set uhost [idx2uhost {away} $idx $botname]] ne {}} { + dbAdd [idx2hand $idx] {back} [clock seconds] $uhost $botname + } + } + } else { + # this is a remote away. It's not possible to figure out the handle, + # let alone the uhost, so we bail out. + return + } + return +} + + +proc ::pixseen::CHMSG {nick uhost hand chan text} { + dbAdd $nick {chmsg} [clock seconds] $uhost $chan + return +} + +## + +# returns a list of: id event nick uhost time chan reason othernick +proc ::pixseen::dbGetNick {target} { + if {[catch {set result [seendb eval { SELECT event, nick, uhost, time, chanTb.chan, reason, othernick FROM seenTb, chanTb ON seenTb.chanid = chanTb.chanid WHERE nick=$target LIMIT 1 }]} error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + return + } + return $result +} + +# returns: a list of nicks matching the pattern +proc ::pixseen::dbSearchGlob {nick uhost chan} { + # transform GLOB syntax into LIKE syntax: + set nick [string map [list "\\" "\\\\" "%" "\%" "_" "\_" "*" "%" "?" "_"] $nick] + set uhost [string map [list "\\" "\\\\" "%" "\%" "_" "\_" "*" "%" "?" "_"] $uhost] + set chan [string map [list "\\" "\\\\" "%" "\%" "_" "\_" "*" "%" "?" "_"] $chan] + if {$nick eq {}} { set nick "*" } + if {$uhost eq {}} { set uhost "*" } + if {[catch { set result [seendb eval { SELECT nick FROM seenTb, chanTb ON seenTb.chanid = chanTb.chanid WHERE nick LIKE $nick ESCAPE '\' AND uhost LIKE $uhost ESCAPE '\' AND chanTb.chan LIKE $chan ESCAPE '\' ORDER BY seenTb.time DESC }] } error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + return + } else { + return $result + } +} + +# returns: a list of nicks matching the pattern +proc ::pixseen::dbSearchRegex {nick uhost chan} { +if {[catch { set result [seendb eval { SELECT nick FROM seenTb, chanTb ON seenTb.chanid = chanTb.chanid WHERE nick REGEXP $nick AND uhost REGEXP $uhost AND chanTb.chan REGEXP $chan ORDER BY seenTb.time DESC }] } error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + return + } else { + return $result + } +} + +# cleans out unused channels from the database +proc ::pixseen::dbCleanup {args} { + putlog [mc {%s: performing database maintenance...} {pixseen.tcl}] + if {[catch {set idList [seendb eval { SELECT chanid FROM chanTb WHERE chanid NOT IN (SELECT chanid FROM seenTb WHERE chanid = chanTb.chanid) }]} error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + } elseif {$idList ne {}} { + foreach id $idList { + if {[catch {seendb eval { DELETE FROM chanTb WHERE chanid=$id }} error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + } + } + } + return +} + +# Parses command arguments. +# Returns: a list of "nick uhost chan mode". +# Mode 0 = exact matching (the default) +# Mode 1 = glob matching +# Mode 2 = regex matching +proc ::pixseen::ParseArgs {text} { + # !seen foobar + # !seen foobar.com + # !seen #foobar + # !seen foobar foobar.com + # !seen foobar #foobar + # !seen foobar.com #foobar + # !seen foobar.com foobar + # !seen #foobar foobar + # !seen #foobar foobar.com + # !seen foobar foobar.com #foobar + + # default to exact mode + set mode 0 + + # grab the switches + set i 0 + foreach item [set arg [split $text]] { + if {$item eq {--}} { + incr i + break + } elseif {[string index $item 0] eq {-}} { + switch -glob -nocase -- $item { + {-e*} { set mode 0 } + {-g*} { set mode 1 } + {-r*} { set mode 2 } + } + incr i + continue + } else { + break + } + } + + if {([set arglen [llength [set arg [lrange $arg $i end]]]] < 1) || ($arglen > 3)} { + return + } elseif {$arglen == 3} { + lassign $arg nick uhost chan + } elseif {$mode == 2} { + lassign $arg nick uhost chan + if {$uhost eq {}} { set uhost {.*} } + if {$chan eq {}} { set chan {.*} } + } else { + set nick {*} + set uhost {*} + set chan {*} + + set NickDone 0 + set ChanDone 0 + set UhostDone 0 + foreach item $arg { + # nick + if {!$NickDone && [regexp -- {^[^#&!+.][^.]*$} $item]} { + set nick $item + set NickDone 1 + continue + # channel + } elseif {!$ChanDone && [string match {[#&!+]*} $item]} { + set chan $item + set ChanDone 1 + continue + # uhost + } elseif {!$UhostDone && [string match {*.*} $item]} { + set uhost $item + set UhostDone 1 + continue + } + } + } + return [list $nick $uhost $chan $mode] +} + + +# output proc +proc ::pixseen::putseen {nick chan notcText {msgText {}}} { + variable outnotc + if {$outnotc == 0} { + puthelp "NOTICE $nick :\[seen\] $notcText" + } elseif {$msgText != {}} { + puthelp "NOTICE $chan :\[seen\] $msgText" + } else { + puthelp "NOTICE $chan :\[seen\] $notcText" + } +} + +# Handle public !seen +# !seen [-exact/-glob/-regex] [--] <nick> [user@host] [channel]} +proc ::pixseen::pubm_seen {nick uhost hand chan text} { + variable defaultLang; variable pubResults + if {![info exists pubResults] || ![string is integer $pubResults]} { set pubResults {3} } + if {![channel get $chan {seen}]} { + return + } elseif {![matchattr $hand f|f $chan]} { + if {[checkflood $uhost] != 0} { + return + } + } + # Set the locale for this channel + if {[validlang [channel get $chan seenlang]]} { mclocale [channel get $chan seenlang] } + if {[set arg [ParseArgs [join [lrange [split $text] 1 end]]]] eq {}} { + putseen $nick $chan [mc {Usage: %s} {!seen [-exact/-glob/-regex] [--] <nick> [user@host] [channel]}] [mc {usage: %1$s} {!seen [-exact/-glob/-regex] <nick>}] + mclocale $defaultLang + return + } else { + lassign $arg Nick Uhost Chan Mode + } + + if {[string equal -nocase $nick $Nick]} { + putseen $nick $chan [mc {Go look in a mirror.}] [mc {%s, go look in a mirror.} $nick] + mclocale $defaultLang + return + } elseif {[string equal -nocase ${::botnick} $Nick]} { + putseen $nick $chan [mc {You found me!}] [mc {You found me, %s!} $nick] + mclocale $defaultLang + return + # Tcldrop supports glob matching for onchan, so check if Nick is valid first +# } elseif {[validnick $Nick] && [onchan $Nick $chan]} { +# if {[set lastspoke [lastspoke $Nick $chan]] eq {}} { +# putseen $nick $chan [mc {%s is on the channel right now!} $Nick] +# } else { +# putseen $nick $chan [mc {%1$s is on the channel right now! %1$s last spoke %2$s ago.} $Nick $lastspoke] +# } +# mclocale $defaultLang +# return +# } + + switch -exact -- $Mode { + {0} {;# exact + if {![validnick $Nick]} { + putseen $nick $chan [mc {That is not a valid nickname.}] [mc {%s, that is not a valid nickname.} $nick] + mclocale $defaultLang + return + } elseif {[set result [dbGetNick $Nick]] eq {}} { + if {[set handseen [handseen $Nick]] ne {}} { + putseen $nick $chan $handseen + mclocale $defaultLang + return 1 + } else { + putseen $nick $chan [mc {I don't remember seeing %s.} $Nick] + mclocale $defaultLang + return + } + } else { + putseen $nick $chan [formatevent {*}$result] + mclocale $defaultLang + return 1 + } + } + {1} {;# glob + set result [dbSearchGlob $Nick $Uhost $Chan] + } + {2} {;# regex + set result [dbSearchRegex $Nick $Uhost $Chan] + } + } + if {$result eq {}} { + if {[set handseen [handseen $Nick]] ne {}} { + putseen $nick $chan $handseen + mclocale $defaultLang + return 1 + } else { + putseen $nick $chan [mc {There were no matches to your query.}] + mclocale $defaultLang + return + } + } else { + if {[set numMatches [llength $result]] > $pubResults} { + putseen $nick $chan [mc {Displaying %1$s of %2$s results:} $pubResults $numMatches] + } else { + putseen $nick $chan [mc {Displaying %1$s of %2$s results:} $numMatches $numMatches] + } + foreach match [lrange $result 0 [expr {$pubResults - 1}]] { + if {$match ne {}} { + putseen $nick $chan [formatevent {*}[dbGetNick $match]] + } + } + mclocale $defaultLang + return 1 + } + mclocale $defaultLang + return +} + +# Handle /msg botnick seen +proc ::pixseen::msgm_seen {nick uhost hand text} { + putlog "\[seen\] ${nick}!${uhost} (hand: ${hand}) requested seen in PM: $text" + variable msgResults + if {![info exists msgResults] || ![string is integer $msgResults]} { set msgResults {5} } + if {![matchattr $hand f]} { + if {[checkflood $uhost] != 0} { + return + } + } + if {[set arg [ParseArgs [join [lrange [split $text] 1 end]]]] eq {}} { + #puthelp "NOTICE $nick :[mc {Usage: %s} {seen [-exact/-glob/-regex] [--] <nick> [user@host] [channel]}]" + puthelp "NOTICE $nick :[mc {Usage: %s} {seen [-exact/-glob/-regex] [--] <nick>}]" + return + } else { + lassign $arg Nick Uhost Chan Mode + } + if {[string equal -nocase $nick $Nick]} { + puthelp "NOTICE $nick :[mc {Go look in a mirror.}]" + return + } elseif {[string equal -nocase ${::botnick} $Nick]} { + puthelp "NOTICE $nick :[mc {You found me!}]" + return + } + switch -exact -- $Mode { + {0} {;# exact + if {![validnick $Nick]} { + puthelp "NOTICE $nick :[mc {That is not a valid nickname.}]" + return + } elseif {[set result [dbGetNick $Nick]] eq {}} { + if {[set handseen [handseen $Nick]] ne {}} { + puthelp "NOTICE $nick :$handseen" + return 1 + } else { + puthelp "NOTICE $nick :[mc {I don't remember seeing %s.} $Nick]" + return + } + } else { + puthelp "NOTICE $nick :[formatevent {*}$result]" + return 1 + } + } + {1} {;# glob + set result [dbSearchGlob $Nick $Uhost $Chan] + } + {2} {;# regex + set result [dbSearchRegex $Nick $Uhost $Chan] + } + } + if {$result eq {}} { + if {[set handseen [handseen $Nick]] ne {}} { + puthelp "NOTICE $nick :$handseen" + return 1 + } else { + puthelp "NOTICE $nick :[mc {There were no matches to your query.}]" + return + } + } else { + if {[set numMatches [llength $result]] > $msgResults} { + puthelp "NOTICE $nick :[mc {Displaying %1$s of %2$s results:} $msgResults $numMatches]" + } else { + puthelp "NOTICE $nick :[mc {Displaying %1$s of %2$s results:} $numMatches $numMatches]" + } + foreach match [lrange $result 0 [expr {$msgResults - 1}]] { + if {$match ne {}} { + puthelp "NOTICE $nick :[formatevent {*}[dbGetNick $match]]" + } + } + return 1 + } + return +} + +# Handle partyline .seen +proc ::pixseen::dcc_seen {hand idx text} { + variable dccResults + if {![info exists dccResults] || ![string is integer $dccResults]} { set dccResults {10} } + if {[set arg [ParseArgs $text]] eq {}} { + putdcc $idx [mc {Usage: %s} {.seen [-exact/-glob/-regex] [--] <nick> [user@host] [channel]}] + return + } else { + lassign $arg Nick Uhost Chan Mode + } + if {[string equal -nocase $hand $Nick]} { + putdcc $idx [mc {Go look in a mirror.}] + return + } elseif {[string equal -nocase ${::botnick} $Nick]} { + putdcc $idx [mc {You found me!}] + return + } + switch -exact -- $Mode { + {0} {;# exact + if {![validnick $Nick]} { + putdcc $idx [mc {That is not a valid nickname.}] + return + } elseif {[set result [dbGetNick $Nick]] eq {}} { + if {[set handseen [handseen $Nick]] ne {}} { + putdcc $idx $handseen + return 1 + } else { + putdcc $idx [mc {I don't remember seeing %s.} $Nick] + return + } + } else { + putdcc $idx [formatevent {*}$result] + return 1 + } + } + {1} {;# glob + set result [dbSearchGlob $Nick $Uhost $Chan] + } + {2} {;# regex + set result [dbSearchRegex $Nick $Uhost $Chan] + } + } + if {$result eq {}} { + if {[set handseen [handseen $Nick]] ne {}} { + putdcc $idx $handseen + return 1 + } else { + putdcc $idx [mc {There were no matches to your query.}] + return + } + } else { + if {[set numMatches [llength $result]] > $dccResults} { + putdcc $idx [mc {Displaying %1$s of %2$s results:} $dccResults $numMatches] + } else { + putdcc $idx [mc {Displaying %1$s of %2$s results:} $numMatches $numMatches] + } + foreach match [lrange $result 0 [expr {$dccResults - 1}]] { + if {$match ne {}} { + putdcc $idx [formatevent {*}[dbGetNick $match]] + } + } + return 1 + } + return +} + +# verifies table information, return 1 if it's valid, 0 if not +proc ::pixseen::ValidTable {table data} { + switch -exact -- $table { + {pixseen} { + # 0 dbVersion INTEGER 1 {} 0 + lassign $data id name type null default primaryKey + if {$id != 0 || $name ne {dbVersion} || $type ne {INTEGER} || $null < 1 || $default ne {} || $primaryKey > 0} { + return 0 + } + } + {seenTb} { + foreach item $data { + lassign $data id name type null default primaryKey + switch -exact -- $id { + {0} { + # 0 event INTEGER 1 {} 0 + if {$name ne {event} || $type ne {INTEGER} || $null < 1 || $default ne {} || $primaryKey > 0} { + return 0 + } + } + {1} { + # 1 nick STRING 1 {} 1 + if {$name ne {nick} || $type ne {STRING} || $null < 1 || $default ne {} || $primaryKey < 1} { + return 0 + } + } + {2} { + # 2 uhost STRING 1 {} 0 + if {$name ne {uhost} || $type ne {STRING} || $null < 1 || $default ne {} || $primaryKey > 0} { + return 0 + } + } + {3} { + # 3 time INTEGER 1 {} 0 + if {$name ne {time} || $type ne {INTEGER} || $null < 1 || $default ne {} || $primaryKey > 0} { + return 0 + } + } + {4} { + # 4 chanid INTEGER 0 {} 0 + if {$name ne {chanid} || $type ne {INTEGER} || $null > 0 || $default ne {} || $primaryKey > 0} { + return 0 + } + } + {5} { + # 5 reason STRING 0 {} 0 + if {$name ne {reason} || $type ne {STRING} || $null > 0 || $default ne {} || $primaryKey > 0} { + return 0 + } + } + {6} { + # 6 othernick STRING 0 {} 0 + if {$name ne {othernick} || $type ne {STRING} || $null > 0 || $default ne {} || $primaryKey > 0} { + return 0 + } + } + default { + return 0 + } + } + + } + } + {chanTb} { + foreach item $data { + lassign $data id name type null default primaryKey + switch -exact -- $id { + {0} { + #0 chanid INTEGER 1 {} 1 + if {$name ne {chanid} || $type ne {INTEGER} || $null < 1 || $default ne {} || $primaryKey < 1} { + return 0 + } + } + {1} { + #1 chan STRING 1 {} 0 + if {$name ne {chan} || $type ne {STRING} || $null < 1 || $default ne {} || $primaryKey > 0} { + return 0 + } + } + default { + return 0 + } + } + } + } + default { + return 0 + } + } + return 1 +} + +# Prepare the database on load +proc ::pixseen::LOAD {args} { + variable dbfile; variable dbVersion + sqlite3 ::pixseen::seendb $dbfile + seendb collate IRCRFC ::pixseen::rfccomp + seendb function chan2id ::pixseen::chan2id + seendb function regexp ::pixseen::pixregexp + if {[catch {set result [seendb eval {SELECT tbl_name FROM sqlite_master}]} error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + } elseif {$result eq {}} { + # there's no tables defined, so we define some + putlog [mc {%s: No existing database found, defining SQL schema.} {pixseen.tcl}] + if {[catch {seendb eval { + -- Create a table and populate it with a version integer in case we need to change the schema in the future. + CREATE TABLE pixseen ( + dbVersion INTEGER UNIQUE NOT NULL + ); + INSERT INTO pixseen VALUES(1); + + -- Create the table where all our seen data goes + CREATE TABLE seenTb ( + event INTEGER NOT NULL, + nick STRING PRIMARY KEY COLLATE IRCRFC UNIQUE NOT NULL, + uhost STRING COLLATE NOCASE NOT NULL, + time INTEGER NOT NULL, + chanid INTEGER, + reason STRING COLLATE NOCASE, + othernick STRING COLLATE IRCRFC + ); + + -- Create the table that holds channel IDs and their real names + CREATE TABLE chanTb ( + chanid INTEGER PRIMARY KEY UNIQUE NOT NULL, + chan STRING UNIQUE NOT NULL COLLATE IRCRFC + ); + }} error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + } + } else { + # There's already data in this database, so we verify the schema + # Verify the table names + if {[catch { set result [seendb eval { SELECT tbl_name FROM sqlite_master WHERE type='table' ORDER BY tbl_name }] } error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + die [mc {Fatal Error!}] + } elseif {[join $result] ne {chanTb pixseen seenTb}} { + putlog [mc {%1$s: FATAL ERROR; SQLite database corrupt, exiting.} {pixseen.tcl}] + die [mc {Fatal Error!}] + + # Verify the pixseen table + } elseif {[catch { set result [seendb eval { PRAGMA table_info(pixseen) }] } error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + die [mc {Fatal Error!}] + } elseif {![ValidTable {pixseen} $result]} { + putlog [mc {%1$s: FATAL ERROR; SQLite database corrupt, exiting.} {pixseen.tcl}] + die [mc {Fatal Error!}] + + # Verify the database version + } elseif {[catch { set result [seendb eval { SELECT dbVersion FROM pixseen LIMIT 1 }] } error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + die [mc {Fatal Error!}] + } elseif {$result != $dbVersion} { + putlog [mc {%1$s: FATAL ERROR; SQLite database corrupt, exiting.} {pixseen.tcl}] + die [mc {Fatal Error!}] + + # Verify the seenTb table + } elseif {[catch { set result [seendb eval { PRAGMA table_info(seenTb) }] } error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + die [mc {Fatal Error!}] + } elseif {![ValidTable {seenTb} $result]} { + putlog [mc {%1$s: FATAL ERROR; SQLite database corrupt, exiting.} {pixseen.tcl}] + die [mc {Fatal Error!}] + + # Verify the chanTb table + } elseif {[catch { set result [seendb eval { PRAGMA table_info(chanTb) }] } error]} { + putlog [mc {%1$s SQL error %2$s; %3$s} {pixseen.tcl} [seendb errorcode] $error] + die [mc {Fatal Error!}] + } elseif {![ValidTable {chanTb} $result]} { + putlog [mc {%1$s: FATAL ERROR; SQLite database corrupt, exiting.} {pixseen.tcl}] + die [mc {Fatal Error!}] + + # Everything is OK! + } else { + # Do some database maintenance + dbCleanup + putlog [mc {%s: Loaded the seen database.} {pixseen.tcl}] + } + } + return +} + +proc ::pixseen::UNLOAD {args} { + seendb close + putlog [mc {%s: Unloaded the seen database.} {pixseen.tcl}] + return +} + +# We have to verify the password here to make sure that the die is successful +proc ::pixseen::msg_die {cmdString op} { + set hand [lindex $cmdString 3] + set pass [lindex $cmdString 4 0] + if {[passwdok $hand $pass]} { + UNLOAD + } + return +} + +# chanset wrapper +# checks the language people set and complains if it's not supported. +proc ::pixseen::dcc_chanset {hand idx param} { + set chan [lindex [set arg [split $param]] 0] + if {![validchan $chan]} { + *DCC:CHANSET $hand $idx $param + return + } + set settings [lrange $arg 1 end] + set found 0 + foreach setting $settings { + if {$found} { + set lang $setting + } elseif {[string equal -nocase $setting {seenlang}]} { + set found 1 + } + } + if {[info exists lang] && ![validlang $lang]} { + putdcc $idx [mc {Error: Invalid seen language "%s".} $lang] + return + } else { + *DCC:CHANSET $hand $idx $param + return + } +} + +# This proc will be renamed to ::*dcc:chanset on load. We call out real +# wrapper from here so that it can stay in the correct namespace +proc ::pixseen::*dcc:chanset {hand idx param} { + ::pixseen::dcc_chanset $hand $idx $param +} + +namespace eval ::pixseen { + # trace die so that we can unload the database properly before the bot exist + if {![info exists SetTraces]} { + trace add execution die enter ::pixseen::UNLOAD + # don't try to trace these on Tcldrop + if {![info exists ::tcldrop]} { + trace add execution *dcc:die enter ::pixseen::UNLOAD + trace add execution *msg:die enter ::pixseen::msg_die + # wrap chanset so we can validate the language people set + # FixMe: add Tcldrop equivalent + rename ::*dcc:chanset ::*DCC:CHANSET + rename ::pixseen::*dcc:chanset ::*dcc:chanset + } + variable SetTraces 1 + } + # load the database if it's not already loaded + if {[info procs seendb] ne {seendb}} { ::pixseen::LOAD } + # unload the database on rehash & restart + bind evnt - {prerehash} ::pixseen::UNLOAD + bind evnt - {prerestart} ::pixseen::UNLOAD + # seen tracking events + bind part - "*" ::pixseen::PART + bind join - "*" ::pixseen::JOIN + bind nick - "*" ::pixseen::NICK + bind sign - "*" ::pixseen::SIGN + bind splt - "*" ::pixseen::SPLT + bind rejn - "*" ::pixseen::REJN + bind kick - "*" ::pixseen::KICK + bind chon - "*" ::pixseen::CHON + bind chof - "*" ::pixseen::CHOF + bind chjn - "*" ::pixseen::CHJN + bind chpt - "*" ::pixseen::CHPT + bind away - "*" ::pixseen::AWAY + bind pubm - "% *" ::pixseen::CHMSG + # triggers + bind pubm - {% !seen *} ::pixseen::pubm_seen + bind pubm - {% !seen} ::pixseen::pubm_seen + bind msgm - {seen *} ::pixseen::msgm_seen + bind msgm - {seen} ::pixseen::msgm_seen + bind dcc - {seen} ::pixseen::dcc_seen + # flood-array cleanup every 10 minutes + bind time - "?0 * * * *" ::pixseen::RemoveFlood + # do some database maintenance once daily + bind evnt - {logfile} ::pixseen::dbCleanup + putlog [mc {Loaded %1$s v%2$s by %3$s} {pixseen.tcl} $seenver {Pixelz}] +} |
