-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy pathdictionary.tcl
423 lines (359 loc) · 11.1 KB
/
dictionary.tcl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
# 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."
}