| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423 |
- # vim: expandtab
- #
- # This script makes the bot talk a bit. You can teach it terms to respond to. It
- # also has random responses if it sees its nick mentioned.
- #
- # This is a heavily modified version of dictionary.tcl 2.7 by perpleXa.
- #
- # To enable the script on a channel type (partyline):
- # .chanset #channel +dictionary
- #
- # Dictionary
- # Copyright (C) 2004-2007 perpleXa
- # http://perplexa.ugug.org / #perpleXa on QuakeNet
- #
- # Redistribution, with or without modification, are permitted provided
- # that redistributions retain the above copyright notice, this condition
- # and the following disclaimer.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY, to the extent permitted by law; without
- # even the implied warranty of MERCHANTABILITY or FITNESS FOR A
- # PARTICULAR PURPOSE.
- namespace eval dictionary {
- # Definition file. The format is a tcl dict.
- variable term_file "scripts/dbase/dictionary.db"
- # File containing nicks to not respond to. Newline separated.
- variable skip_nick_file "scripts/dictionary_skip_nicks.txt"
- # File containing chatty responses.
- #
- # These are really just random phrases
- # for the bot to respond with assuming it has been addressed in some way and
- # has nothing really to say about it. Newline separated.
- variable chatty_responses_file "scripts/dictionary_chatty_list.txt"
- # Time to not respond to the same word in the same channel. This is
- # so we don't respond to the same word in quick succession.
- variable throttle_time [expr 10*60]
- # Dictionary terms.
- #
- # Each key is a term and associates with another dict.
- #
- # The sub-dict has keys:
- # - def, the definition
- # - include_term_in_def, which controls whether we output "<term> is <def>"
- # or just "<def>"
- variable terms [dict create]
- # Nicks to not respond to terms for. e.g., bots.
- variable skip_nicks [list]
- variable chatty_responses [list]
- # Dict with keys <channel><term> with values containing the unixtime the last
- # time the term was output, if any.
- #
- # This is for throttling term outputs.
- variable flood [dict create]
- bind pubm -|- "*" ::dictionary::public
- bind pubm -|- "*" ::dictionary::publearn
- setudef flag dictionary
- }
- # Respond to terms in the channel
- proc ::dictionary::public {nick host hand chan argv} {
- variable flood
- variable terms
- variable throttle_time
- variable skip_nicks
- global botnick
- if {![channel get $chan dictionary]} {
- return
- }
- # Ignore cases of '<botnick>:' because those are commands to us. We deal with
- # them in a different proc.
- if {[::dictionary::is_addressing_bot $argv $botnick]} {
- return
- }
- # If the person saying something has a nick that is one we skip, we're done.
- foreach skip_nick $skip_nicks {
- if {[string equal -nocase $nick $skip_nick]} {
- return
- }
- }
- # Look for a word we know about for us to respond to.
- set term ""
- foreach word [dict keys $terms] {
- if {[::dictionary::string_contains_term $argv $word]} {
- set term $word
- break
- }
- }
- # If they didn't say a term we know something about, then the only response
- # we'll send is if they said our name. Send them a chatty response if so.
- if {$term == ""} {
- if {[::dictionary::string_contains_term $argv $botnick]} {
- set response [::dictionary::get_chatty_response $nick]
- putserv "PRIVMSG $chan :$response"
- }
- return
- }
- # They said a word we know something about. We'll potentially output the
- # definition.
- set term_dict [dict get $terms $term]
- # We throttle how often we output the term's definition.
- set flood_key $chan$term
- if {![dict exists $flood $flood_key]} {
- dict set flood $flood_key 0
- }
- set last_term_output_time [dict get $flood $flood_key]
- if {[unixtime] - $last_term_output_time <= $throttle_time} {
- return
- }
- dict set flood $flood_key [unixtime]
- # Output the definition. Note that terms get output differently depending on
- # how they were added.
- set def [dict get $term_dict def]
- if {[dict get $term_dict include_term_in_def]} {
- puthelp "PRIVMSG $chan :$term is $def"
- return
- }
- puthelp "PRIVMSG $chan :$def"
- }
- # Public trigger. This handles commands such as setting, deleting, and listing
- # terms the bot knows about.
- proc ::dictionary::publearn {nick host hand chan argv} {
- global botnick
- variable terms
- if {![channel get $chan dictionary]} {
- return
- }
- set argv [stripcodes "uacgbr" $argv]
- set argv [string trim $argv]
- # We only respond if we are directly addressed (botnick: ). This indicates
- # someone is giving us a command.
- if {![::dictionary::is_addressing_bot $argv $botnick]} {
- return
- }
- if {![regexp -nocase -- {^\S+\s+(.+)} $argv -> rest]} {
- set response [::dictionary::get_negative_response $nick]
- putserv "PRIVMSG $chan :$response"
- return
- }
- # Delete a term. <botnick>: forget <term>
- #
- # Note this means we can't set a term using the "is" syntax (e.g. forget blah
- # is x).
- if {[regexp -nocase -- {^forget\s+(.+)} $rest -> term]} {
- if {![dict exists $terms $term]} {
- set response [::dictionary::get_negative_response $nick]
- putserv "PRIVMSG $chan :I don't know `$term'."
- return
- }
- set def [dict get $terms $term def]
- dict unset terms $term
- ::dictionary::save
- putserv "PRIVMSG $chan :I forgot `$term'. (It was `$def'.)"
- return
- }
- if {[regexp -nocase -- {^remember this:\s+(.+)} $rest -> response]} {
- lappend ::dictionary::chatty_responses $response
- if {[catch {::dictionary::list_to_file $::dictionary::chatty_responses \
- $::dictionary::chatty_responses_file} err]} {
- putserv "PRIVMSG $chan :Error! $err"
- return
- }
- putserv "PRIVMSG $chan :OK, $nick."
- return
- }
- # Set a term. <botnick>: <term> is <definition>
- if {[regexp -nocase -- {^(.+?)\s+is\s+(.+)$} $rest -> term def]} {
- if {[dict exists $terms $term]} {
- set def [dict get $terms $term def]
- putserv "PRIVMSG $chan :`$term' is already `$def'"
- return
- }
- dict set terms $term [dict create \
- def $def \
- include_term_in_def 1 \
- ]
- ::dictionary::save
- set response [::dictionary::get_affirmative_response $nick]
- putserv "PRIVMSG $chan :$response"
- return
- }
- # Set a term. <botnick>: <term>, <definition>
- if {[regexp -nocase -- {^(.+?)\s*,\s+(.+)$} $rest -> term def]} {
- if {[dict exists $terms $term]} {
- set def [dict get $terms $term def]
- putserv "PRIVMSG $chan :`$term' is already `$def'"
- return
- }
- dict set terms $term [dict create \
- def $def \
- include_term_in_def 0 \
- ]
- ::dictionary::save
- set response [::dictionary::get_affirmative_response $nick]
- putserv "PRIVMSG $chan :$response"
- return
- }
- # Message the nick all terms we have
- if {[string tolower $rest] == "listem"} {
- foreach term [lsort -dictionary [dict keys $terms]] {
- set def [dict get $terms $term def]
- puthelp "PRIVMSG $nick :$term: $def"
- }
- return
- }
- if {[string tolower $rest] == "braindump"} {
- set i 1
- foreach response $::dictionary::chatty_responses {
- puthelp "PRIVMSG $nick :$i. $response"
- incr i
- }
- return
- }
- set response [::dictionary::get_chatty_response $nick]
- putserv "PRIVMSG $chan :$response"
- }
- # Return 1 if the given line is addressing the bot.
- #
- # This is the case if the line is of the form:
- # <botnick>:
- #
- # For example if the bot's nick is:
- # bot: Hi there
- #
- # This is checked case insensitively.
- proc ::dictionary::is_addressing_bot {text botnick} {
- set text [string trim $text]
- set text [string tolower $text]
- set prefix [string tolower $botnick]
- append prefix :
- set idx [string first $prefix $text]
- return [expr $idx == 0]
- }
- # Return 1 if the string contains the term. This is tested case insensitively.
- #
- # The term is present only if it is by itself surrounded whitespace or
- # punctuation.
- #
- # e.g. if the term is 'test' then these strings contain it:
- #
- # hi test hi
- # hi test, hi
- # test
- #
- # But these do not:
- #
- # hi testing hi
- # hitest
- proc ::dictionary::string_contains_term {s term} {
- set term_lc [string tolower $term]
- set term_quoted [::dictionary::quotemeta $term_lc]
- set re {\m}
- append re $term_quoted
- append re {\M}
- return [regexp -nocase -- $re $s]
- }
- # Escape/quote metacharacters so that the string becomes suitable for placing in
- # a regular expression. This makes it so any regex metacharacter is quoted.
- #
- # See http://stackoverflow.com/questions/4346750/regular-expression-literal-text-span/4352893#4352893
- proc ::dictionary::quotemeta {s} {
- return [regsub -all {\W} $s {\\&}]
- }
- proc ::dictionary::get_affirmative_response {nick} {
- return "OK, $nick"
- }
- proc ::dictionary::get_negative_response {nick} {
- return "Shut up."
- }
- proc ::dictionary::get_chatty_response {nick} {
- set n [llength $::dictionary::chatty_responses]
- if {$n == 0} {
- return "Hi."
- }
- set idx [expr int($n*rand())]
- set response [lindex $::dictionary::chatty_responses $idx]
- return [regsub -all -- "%%nick%%" $response $nick]
- }
- # Load the term database from our data file.
- proc ::dictionary::load_terms {} {
- variable term_file
- variable terms
- set terms [dict create]
- if {[catch {open $term_file "r"} fp]} {
- return
- }
- set terms [read -nonewline $fp]
- close $fp
- set count [llength [dict keys $terms]]
- return $count
- }
- # Load contents of a file into a list.
- #
- # Each line of the file is made into one element in the list.
- #
- # Blank lines are skipped.
- #
- # Path: Path to the file to open
- #
- # Returns: If we do not find the file or we can't open it then we return an
- # empty list.
- proc ::dictionary::file_contents_to_list {path} {
- if {![file exists $path]} {
- return [list]
- }
- if {[catch {open $path r} fp]} {
- return [list]
- }
- set content [read -nonewline $fp]
- close $fp
- set l [list]
- foreach line [split $content "\n"] {
- set line [string trim $line]
- if {[string length $line] == 0} {
- continue
- }
- lappend l $line
- }
- return $l
- }
- proc ::dictionary::list_to_file {l path} {
- set fh [open $path w]
- foreach e $l {
- puts $fh $e
- }
- close $fh
- }
- # Load a list of nicks to skip from a data file.
- proc ::dictionary::load_skip_nicks {} {
- set ::dictionary::skip_nicks [::dictionary::file_contents_to_list \
- $::dictionary::skip_nick_file]
- }
- # Load chatty responses from data file.
- proc ::dictionary::load_chatty_responses {} {
- set ::dictionary::chatty_responses [::dictionary::file_contents_to_list \
- $::dictionary::chatty_responses_file]
- }
- # Load data from our data files into memory.
- proc ::dictionary::load {args} {
- set term_count [::dictionary::load_terms]
- ::dictionary::load_skip_nicks
- ::dictionary::load_chatty_responses
- return $term_count
- }
- # Save the terms and definitions to the data file.
- proc ::dictionary::save {} {
- variable term_file
- variable terms
- if {![file isdirectory [file dirname $term_file]]} {
- file mkdir [file dirname $term_file]
- }
- set fp [open $term_file w]
- puts -nonewline $fp $terms
- close $fp
- }
- set ::dictionary::count [::dictionary::load]
- if {$::dictionary::count == 1} {
- putlog "dictionary.tcl loaded. $::dictionary::count term."
- } else {
- putlog "dictionary.tcl loaded. $::dictionary::count terms."
- }
|