dictionary.tcl 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  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. # Binds
  63. bind pubm -|- "*" ::[namespace current]::public
  64. bind pubm -|- "*" ::[namespace current]::publearn
  65. bind evnt -|- "save" ::[namespace current]::save
  66. # Miscellaneous
  67. setudef flag dictionary
  68. namespace export *
  69. }
  70. # respond to terms in the channel
  71. proc ::dictionary::public {nick host hand chan argv} {
  72. variable flood
  73. variable terms
  74. variable throttle_time
  75. variable skip_nicks
  76. global botnick
  77. if {![channel get $chan dictionary]} {
  78. return
  79. }
  80. # ignore cases of '<mynick>:' because those are commands to us.
  81. if {[lindex [split $argv] 0] == "$botnick:"} {
  82. return
  83. }
  84. # if the nick is one to skip, then get out now.
  85. foreach skip_nick $skip_nicks {
  86. if {[string equal -nocase $nick $skip_nick]} {
  87. return
  88. }
  89. }
  90. # look for a word we know about.
  91. foreach word [dict keys $terms] {
  92. if {[string match -nocase "* $word *" $argv] \
  93. || [string match -nocase "$word *" $argv] \
  94. || [string match -nocase "* $word" $argv] \
  95. || [string match -nocase "$word" $argv]} \
  96. {
  97. set term $word
  98. break
  99. }
  100. }
  101. if {![info exists term]} {
  102. return
  103. }
  104. set term_dict [dict get $terms $term]
  105. # check if we've responded to the word recently so we don't
  106. # keep saying it.
  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 def.
  117. set def [dict get $term_dict def]
  118. # terms get output differently depending on how they were
  119. # added.
  120. if {[dict get $term_dict include_term_in_def]} {
  121. puthelp "PRIVMSG $chan :$term is $def"
  122. return
  123. }
  124. puthelp "PRIVMSG $chan :$def"
  125. }
  126. # public trigger. this handles interaction to set/clear terms
  127. # and for responding directly by the bot.
  128. proc ::dictionary::publearn {nick host hand chan argv} {
  129. global botnick
  130. variable terms
  131. if {![channel get $chan dictionary]} {
  132. return
  133. }
  134. set argv [stripcodes "uacgbr" $argv]
  135. # we only respond to the case where the message starts
  136. # with '<botnick>:'
  137. if {[lindex [split $argv] 0] != "$botnick:"} {
  138. return
  139. }
  140. # try to set a term.
  141. # this can be done by: <botnick>: <term> is <definition>
  142. # and: <botnick>: <term>, <definition>
  143. if {([lsearch $argv "is"] >= 0 && [llength $argv] >= 4) \
  144. || ([string first "," $argv]>-1 && [llength $argv] >= 3)} \
  145. {
  146. # <botnick>: <term> is <definition
  147. set include_term_in_def 1
  148. if {[lsearch $argv "is"] >= 0 && [string first "," $argv] < 0} {
  149. set term [lrange [split $argv] 1 [expr [lsearch $argv "is"] - 1]]
  150. set description "[lrange [split $argv] [expr [lsearch $argv "is"] + 1] end]"
  151. # <botnick>: <term>, <definition>
  152. } else {
  153. set term [lrange [split $argv] 1 end]
  154. set term [string range $term 0 [expr [string first "," $term] - 1]]
  155. set include_term_in_def 0
  156. set description "[string range $argv [expr [string first "," $argv] + 2] end]"
  157. }
  158. if {[dict exists $terms $term]} {
  159. set term_dict [dict get $terms $term]
  160. set def [dict get $term_dict def]
  161. putserv "PRIVMSG $chan :$term is already $def"
  162. return
  163. }
  164. set term [string trim $term]
  165. set description [string trim $description]
  166. if {[string length $term] == 0 || [string length $description] == 0} {
  167. set response [::dictionary::get_negative_response $nick]
  168. putserv "PRIVMSG $chan :$response"
  169. return
  170. }
  171. # set it, and send a random success response.
  172. set term_dict [dict create]
  173. dict set term_dict def $description
  174. dict set term_dict include_term_in_def $include_term_in_def
  175. dict set terms $term $term_dict
  176. set response [::dictionary::get_affirmative_response $nick]
  177. putserv "PRIVMSG $chan :$response"
  178. return
  179. }
  180. # delete a term. <botnick>: forget <term>
  181. if {[lindex [split $argv] 1] == "forget" && [llength $argv] >= 3} {
  182. set term [lrange [split $argv] 2 end]
  183. # if it does not exist, then send a random deny response.
  184. if {![dict exists $terms $term]} {
  185. set response [::dictionary::get_negative_response $nick]
  186. putserv "PRIVMSG $chan :$response"
  187. return
  188. }
  189. dict unset terms $term
  190. putserv "PRIVMSG $chan :I forgot $term."
  191. return
  192. }
  193. # message the nick all terms we have
  194. if {[lindex [split $argv] 1] == "listem" && [llength $argv] == 2} {
  195. foreach term [lsort -dictionary [dict keys $terms]] {
  196. set term_dict [dict get $terms $term]
  197. set def [dict get $term_dict def]
  198. puthelp "PRIVMSG $nick :$term: $def"
  199. }
  200. return
  201. }
  202. # unknown command. send a random chatty response.
  203. set response [::dictionary::get_chatty_response $nick]
  204. putserv "PRIVMSG $chan :$response"
  205. }
  206. proc ::dictionary::get_random_response {responses nick} {
  207. # we assume we have responses in the list.
  208. set response_index [rand [llength $responses]]
  209. set response [lindex $responses $response_index]
  210. # replace %%nick%% with %%nick%% if present.
  211. return [regsub -all -- "%%nick%%" $response $nick]
  212. }
  213. proc ::dictionary::get_affirmative_response {nick} {
  214. if {[llength $::dictionary::affirmative_responses] == 0} {
  215. return "OK."
  216. }
  217. return [::dictionary::get_random_response \
  218. $::dictionary::affirmative_responses $nick]
  219. }
  220. proc ::dictionary::get_negative_response {nick} {
  221. if {[llength $::dictionary::negative_responses] == 0} {
  222. return "No."
  223. }
  224. return [::dictionary::get_random_response \
  225. $::dictionary::negative_responses $nick]
  226. }
  227. proc ::dictionary::get_chatty_response {nick} {
  228. if {[llength $::dictionary::chatty_responses] == 0} {
  229. return "Hi."
  230. }
  231. return [::dictionary::get_random_response \
  232. $::dictionary::chatty_responses $nick]
  233. }
  234. # load the term database from our data file.
  235. proc ::dictionary::load_terms {} {
  236. variable term_file
  237. variable terms
  238. set terms [dict create]
  239. if {[catch {open $term_file "r"} fp]} {
  240. return
  241. }
  242. set terms [read -nonewline $fp]
  243. close $fp
  244. set count [llength [dict keys $terms]]
  245. return $count
  246. }
  247. # load contents of a file into a list.
  248. # each line of the file is made into one element in the list.
  249. # blank lines are skipped.
  250. #
  251. # path: path to the file to open
  252. #
  253. # returns: if we do not find the file or we can't open it then we return an
  254. # empty list.
  255. proc ::dictionary::file_contents_to_list {path} {
  256. if {![file exists $path]} {
  257. return [list]
  258. }
  259. if {[catch {open $path r} fp]} {
  260. return [list]
  261. }
  262. set content [read -nonewline $fp]
  263. close $fp
  264. set l [list]
  265. foreach line [split $content "\n"] {
  266. set line [string trim $line]
  267. if {[string length $line] == 0} {
  268. continue
  269. }
  270. lappend l $line
  271. }
  272. return $l
  273. }
  274. # load a list of nicks to skip from a data file.
  275. #
  276. # returns: void
  277. proc ::dictionary::load_skip_nicks {} {
  278. set ::dictionary::skip_nicks [::dictionary::file_contents_to_list \
  279. $::dictionary::skip_nick_file]
  280. }
  281. # load affirmative responses from data file.
  282. #
  283. # returns: void
  284. proc ::dictionary::load_affirmative_responses {} {
  285. set ::dictionary::affirmative_responses [::dictionary::file_contents_to_list \
  286. $::dictionary::affirmative_responses_file]
  287. }
  288. # load negative responses from data file.
  289. #
  290. # returns: void
  291. proc ::dictionary::load_negative_responses {} {
  292. set ::dictionary::negative_responses [::dictionary::file_contents_to_list \
  293. $::dictionary::negative_responses_file]
  294. }
  295. # load chatty responses from data file.
  296. #
  297. # returns: void
  298. proc ::dictionary::load_chatty_responses {} {
  299. set ::dictionary::chatty_responses [::dictionary::file_contents_to_list \
  300. $::dictionary::chatty_responses_file]
  301. }
  302. # load data from our data files into memory.
  303. proc ::dictionary::load {args} {
  304. # the term database.
  305. set term_count [::dictionary::load_terms]
  306. # nicks to skip.
  307. ::dictionary::load_skip_nicks
  308. # responses.
  309. ::dictionary::load_affirmative_responses
  310. ::dictionary::load_negative_responses
  311. ::dictionary::load_chatty_responses
  312. return $term_count
  313. }
  314. # save the term/definitions to the data file.
  315. proc ::dictionary::write_db {} {
  316. variable term_file
  317. variable terms
  318. if {![file isdirectory [file dirname $term_file]]} {
  319. file mkdir [file dirname $term_file]
  320. }
  321. set fp [open $term_file w]
  322. puts -nonewline $fp $terms
  323. close $fp
  324. }
  325. # handle save events. write out our data files.
  326. proc ::dictionary::save {args} {
  327. # term database.
  328. ::dictionary::write_db
  329. }
  330. set ::dictionary::count [::dictionary::load]
  331. putlog "dictionary.tcl loaded. $::dictionary::count term(s)."