dictionary.tcl 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. # vim: expandtab
  2. #
  3. # This script makes the bot talk a bit. You can teach it terms to respond to. It
  4. # also has random responses if it sees its nick mentioned.
  5. #
  6. # This is a heavily modified version of dictionary.tcl 2.7 by perpleXa.
  7. #
  8. # To enable the script on a channel type (partyline):
  9. # .chanset #channel +dictionary
  10. #
  11. # Dictionary
  12. # Copyright (C) 2004-2007 perpleXa
  13. # http://perplexa.ugug.org / #perpleXa on QuakeNet
  14. #
  15. # Redistribution, with or without modification, are permitted provided
  16. # that redistributions retain the above copyright notice, this condition
  17. # and the following disclaimer.
  18. #
  19. # This program is distributed in the hope that it will be useful,
  20. # but WITHOUT ANY WARRANTY, to the extent permitted by law; without
  21. # even the implied warranty of MERCHANTABILITY or FITNESS FOR A
  22. # PARTICULAR PURPOSE.
  23. namespace eval dictionary {
  24. # Definition file. The format is a tcl dict.
  25. variable term_file "scripts/dbase/dictionary.db"
  26. # File containing nicks to not respond to. Newline separated.
  27. variable skip_nick_file "scripts/dictionary_skip_nicks.txt"
  28. # File containing affirmative responses. Newline separated.
  29. variable affirmative_responses_file "scripts/dictionary_affirmative_list.txt"
  30. # File containing negative responses. Newline separated.
  31. variable negative_responses_file "scripts/dictionary_negative_list.txt"
  32. # File containing chatty responses.
  33. #
  34. # These are really just random phrases
  35. # for the bot to respond with assuming it has been addressed in some way and
  36. # has nothing really to say about it. Newline separated.
  37. variable chatty_responses_file "scripts/dictionary_chatty_list.txt"
  38. # Time to not respond to the same word in the same channel. This is
  39. # so we don't respond to the same word in quick succession.
  40. variable throttle_time [expr 10*60]
  41. # Dictionary terms.
  42. #
  43. # Each key is a term and associates with another dict.
  44. #
  45. # The sub-dict has keys:
  46. # - def, the definition
  47. # - include_term_in_def, which controls whether we output "<term> is <def>"
  48. # or just "<def>"
  49. variable terms [dict create]
  50. # Nicks to not respond to terms for. e.g., bots.
  51. variable skip_nicks [list]
  52. variable affirmative_responses [list]
  53. variable negative_responses [list]
  54. variable chatty_responses [list]
  55. # Dict with keys <channel><term> with values containing the unixtime the last
  56. # time the term was output, if any.
  57. #
  58. # This is for throttling term outputs.
  59. variable flood [dict create]
  60. bind pubm -|- "*" ::[namespace current]::public
  61. bind pubm -|- "*" ::[namespace current]::publearn
  62. bind evnt -|- "save" ::[namespace current]::save
  63. setudef flag dictionary
  64. }
  65. # Respond to terms in the channel
  66. proc ::dictionary::public {nick host hand chan argv} {
  67. variable flood
  68. variable terms
  69. variable throttle_time
  70. variable skip_nicks
  71. global botnick
  72. if {![channel get $chan dictionary]} {
  73. return
  74. }
  75. # Ignore cases of '<botnick>:' because those are commands to us. We deal with
  76. # them in a different proc.
  77. if {[::dictionary::is_addressing_bot $argv $botnick]} {
  78. return
  79. }
  80. # If the person saying something has a nick that is one we skip, we're done.
  81. foreach skip_nick $skip_nicks {
  82. if {[string equal -nocase $nick $skip_nick]} {
  83. return
  84. }
  85. }
  86. # Look for a word we know about for us to respond to.
  87. set term ""
  88. foreach word [dict keys $terms] {
  89. if {[::dictionary::string_contains_term $argv $word]} {
  90. set term $word
  91. break
  92. }
  93. }
  94. # If they didn't say a term we know something about, then the only response
  95. # we'll send is if they said our name. Send them a chatty response if so.
  96. if {$term == ""} {
  97. if {[::dictionary::string_contains_term $argv $botnick]} {
  98. set response [::dictionary::get_chatty_response $nick]
  99. putserv "PRIVMSG $chan :$response"
  100. }
  101. return
  102. }
  103. # They said a word we know something about. We'll potentially output the
  104. # definition.
  105. set term_dict [dict get $terms $term]
  106. # We throttle how often we output the term's definition.
  107. set flood_key $chan$term
  108. if {![dict exists $flood $flood_key]} {
  109. dict set flood $flood_key 0
  110. }
  111. set last_term_output_time [dict get $flood $flood_key]
  112. if {[unixtime] - $last_term_output_time <= $throttle_time} {
  113. return
  114. }
  115. dict set flood $flood_key [unixtime]
  116. # Output the definition. Note that terms get output differently depending on
  117. # how they were added.
  118. set def [dict get $term_dict def]
  119. if {[dict get $term_dict include_term_in_def]} {
  120. puthelp "PRIVMSG $chan :$term is $def"
  121. return
  122. }
  123. puthelp "PRIVMSG $chan :$def"
  124. }
  125. # Public trigger. This handles commands such as setting, deleting, and listing
  126. # terms the bot knows about.
  127. proc ::dictionary::publearn {nick host hand chan argv} {
  128. global botnick
  129. variable terms
  130. if {![channel get $chan dictionary]} {
  131. return
  132. }
  133. set argv [stripcodes "uacgbr" $argv]
  134. set argv [string trim $argv]
  135. # We only respond if we are directly addressed (botnick: ). This indicates
  136. # someone is giving us a command.
  137. if {![::dictionary::is_addressing_bot $argv $botnick]} {
  138. return
  139. }
  140. if {![regexp -nocase -- {^\S+\s+(.+)} $argv -> rest]} {
  141. set response [::dictionary::get_negative_response $nick]
  142. putserv "PRIVMSG $chan :$response"
  143. return
  144. }
  145. # Delete a term. <botnick>: forget <term>
  146. #
  147. # Note this means we can't set a term using the "is" syntax (e.g. forget blah
  148. # is x).
  149. if {[regexp -nocase -- {^forget\s+(.+)} $rest -> term]} {
  150. if {![dict exists $terms $term]} {
  151. set response [::dictionary::get_negative_response $nick]
  152. putserv "PRIVMSG $chan :I don't know `$term'."
  153. return
  154. }
  155. set def [dict get $terms $term def]
  156. dict unset terms $term
  157. ::dictionary::save
  158. putserv "PRIVMSG $chan :I forgot `$term'. (It was `$def'.)"
  159. return
  160. }
  161. # Set a term. <botnick>: <term> is <definition>
  162. if {[regexp -nocase -- {^(.+?)\s+is\s+(.+)$} $rest -> term def]} {
  163. if {[dict exists $terms $term]} {
  164. set def [dict get $terms $term def]
  165. putserv "PRIVMSG $chan :`$term' is already `$def'"
  166. return
  167. }
  168. dict set terms $term [dict create \
  169. def $def \
  170. include_term_in_def 1 \
  171. ]
  172. ::dictionary::save
  173. set response [::dictionary::get_affirmative_response $nick]
  174. putserv "PRIVMSG $chan :$response"
  175. return
  176. }
  177. # Set a term. <botnick>: <term>, <definition>
  178. if {[regexp -nocase -- {^(.+?)\s*,\s+(.+)$} $rest -> term def]} {
  179. if {[dict exists $terms $term]} {
  180. set def [dict get $terms $term def]
  181. putserv "PRIVMSG $chan :`$term' is already `$def'"
  182. return
  183. }
  184. dict set terms $term [dict create \
  185. def $def \
  186. include_term_in_def 0 \
  187. ]
  188. ::dictionary::save
  189. set response [::dictionary::get_affirmative_response $nick]
  190. putserv "PRIVMSG $chan :$response"
  191. return
  192. }
  193. # Message the nick all terms we have
  194. if {[string tolower $rest] == "listem"} {
  195. foreach term [lsort -dictionary [dict keys $terms]] {
  196. set def [dict get $terms $term def]
  197. puthelp "PRIVMSG $nick :$term: $def"
  198. }
  199. return
  200. }
  201. set response [::dictionary::get_chatty_response $nick]
  202. putserv "PRIVMSG $chan :$response"
  203. }
  204. # Return 1 if the given line is addressing the bot.
  205. #
  206. # This is the case if the line is of the form:
  207. # <botnick>:
  208. #
  209. # For example if the bot's nick is:
  210. # bot: Hi there
  211. #
  212. # This is checked case insensitively.
  213. proc ::dictionary::is_addressing_bot {text botnick} {
  214. set text [string trim $text]
  215. set text [string tolower $text]
  216. set prefix [string tolower $botnick]
  217. append prefix :
  218. set idx [string first $prefix $text]
  219. return [expr $idx == 0]
  220. }
  221. # Return 1 if the string contains the term. This is tested case insensitively.
  222. #
  223. # The term is present only if it is by itself surrounded whitespace or
  224. # punctuation.
  225. #
  226. # e.g. if the term is 'test' then these strings contain it:
  227. #
  228. # hi test hi
  229. # hi test, hi
  230. # test
  231. #
  232. # But these do not:
  233. #
  234. # hi testing hi
  235. # hitest
  236. proc ::dictionary::string_contains_term {s term} {
  237. set term_lc [string tolower $term]
  238. set term_quoted [::dictionary::quotemeta $term_lc]
  239. # \m matches at the beginning of a word, \M at the end.
  240. return [regexp -nocase -- \\m$term_quoted\\M $s]
  241. }
  242. # Escape/quote metacharacters so that the string becomes suitable for placing in
  243. # a regular expression. This makes it so any regex metacharacter is quoted.
  244. #
  245. # See http://stackoverflow.com/questions/4346750/regular-expression-literal-text-span/4352893#4352893
  246. proc ::dictionary::quotemeta {s} {
  247. return [regsub -all {\W} $s {\\&}]
  248. }
  249. proc ::dictionary::get_random_response {responses nick} {
  250. # We assume we have responses in the list.
  251. set response_index [rand [llength $responses]]
  252. set response [lindex $responses $response_index]
  253. return [regsub -all -- "%%nick%%" $response $nick]
  254. }
  255. proc ::dictionary::get_affirmative_response {nick} {
  256. if {[llength $::dictionary::affirmative_responses] == 0} {
  257. return "OK."
  258. }
  259. return [::dictionary::get_random_response \
  260. $::dictionary::affirmative_responses $nick]
  261. }
  262. proc ::dictionary::get_negative_response {nick} {
  263. if {[llength $::dictionary::negative_responses] == 0} {
  264. return "No."
  265. }
  266. return [::dictionary::get_random_response \
  267. $::dictionary::negative_responses $nick]
  268. }
  269. proc ::dictionary::get_chatty_response {nick} {
  270. if {[llength $::dictionary::chatty_responses] == 0} {
  271. return "Hi."
  272. }
  273. return [::dictionary::get_random_response \
  274. $::dictionary::chatty_responses $nick]
  275. }
  276. # Load the term database from our data file.
  277. proc ::dictionary::load_terms {} {
  278. variable term_file
  279. variable terms
  280. set terms [dict create]
  281. if {[catch {open $term_file "r"} fp]} {
  282. return
  283. }
  284. set terms [read -nonewline $fp]
  285. close $fp
  286. set count [llength [dict keys $terms]]
  287. return $count
  288. }
  289. # Load contents of a file into a list.
  290. #
  291. # Each line of the file is made into one element in the list.
  292. #
  293. # Blank lines are skipped.
  294. #
  295. # Path: Path to the file to open
  296. #
  297. # Returns: If we do not find the file or we can't open it then we return an
  298. # empty list.
  299. proc ::dictionary::file_contents_to_list {path} {
  300. if {![file exists $path]} {
  301. return [list]
  302. }
  303. if {[catch {open $path r} fp]} {
  304. return [list]
  305. }
  306. set content [read -nonewline $fp]
  307. close $fp
  308. set l [list]
  309. foreach line [split $content "\n"] {
  310. set line [string trim $line]
  311. if {[string length $line] == 0} {
  312. continue
  313. }
  314. lappend l $line
  315. }
  316. return $l
  317. }
  318. # Load a list of nicks to skip from a data file.
  319. proc ::dictionary::load_skip_nicks {} {
  320. set ::dictionary::skip_nicks [::dictionary::file_contents_to_list \
  321. $::dictionary::skip_nick_file]
  322. }
  323. # Load affirmative responses from data file.
  324. proc ::dictionary::load_affirmative_responses {} {
  325. set ::dictionary::affirmative_responses [::dictionary::file_contents_to_list \
  326. $::dictionary::affirmative_responses_file]
  327. }
  328. # Load negative responses from data file.
  329. proc ::dictionary::load_negative_responses {} {
  330. set ::dictionary::negative_responses [::dictionary::file_contents_to_list \
  331. $::dictionary::negative_responses_file]
  332. }
  333. # Load chatty responses from data file.
  334. proc ::dictionary::load_chatty_responses {} {
  335. set ::dictionary::chatty_responses [::dictionary::file_contents_to_list \
  336. $::dictionary::chatty_responses_file]
  337. }
  338. # Load data from our data files into memory.
  339. proc ::dictionary::load {args} {
  340. set term_count [::dictionary::load_terms]
  341. ::dictionary::load_skip_nicks
  342. ::dictionary::load_affirmative_responses
  343. ::dictionary::load_negative_responses
  344. ::dictionary::load_chatty_responses
  345. return $term_count
  346. }
  347. # Save the terms and definitions to the data file.
  348. proc ::dictionary::write_db {} {
  349. variable term_file
  350. variable terms
  351. if {![file isdirectory [file dirname $term_file]]} {
  352. file mkdir [file dirname $term_file]
  353. }
  354. set fp [open $term_file w]
  355. puts -nonewline $fp $terms
  356. close $fp
  357. }
  358. # Handle save events. Write out our data files.
  359. proc ::dictionary::save {args} {
  360. ::dictionary::write_db
  361. }
  362. set ::dictionary::count [::dictionary::load]
  363. putlog "dictionary.tcl loaded. $::dictionary::count term(s)."