dictionary.tcl 12 KB

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