rbot 0.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. data/AUTHORS +16 -0
  2. data/COPYING +21 -0
  3. data/ChangeLog +418 -0
  4. data/INSTALL +8 -0
  5. data/README +44 -0
  6. data/REQUIREMENTS +34 -0
  7. data/TODO +5 -0
  8. data/Usage_en.txt +129 -0
  9. data/bin/rbot +81 -0
  10. data/data/rbot/contrib/plugins/figlet.rb +20 -0
  11. data/data/rbot/contrib/plugins/ri.rb +83 -0
  12. data/data/rbot/contrib/plugins/stats.rb +232 -0
  13. data/data/rbot/contrib/plugins/vandale.rb +49 -0
  14. data/data/rbot/languages/dutch.lang +73 -0
  15. data/data/rbot/languages/english.lang +75 -0
  16. data/data/rbot/languages/french.lang +39 -0
  17. data/data/rbot/languages/german.lang +67 -0
  18. data/data/rbot/plugins/autoop.rb +68 -0
  19. data/data/rbot/plugins/autorejoin.rb +16 -0
  20. data/data/rbot/plugins/cal.rb +15 -0
  21. data/data/rbot/plugins/dice.rb +81 -0
  22. data/data/rbot/plugins/eightball.rb +19 -0
  23. data/data/rbot/plugins/excuse.rb +470 -0
  24. data/data/rbot/plugins/fish.rb +61 -0
  25. data/data/rbot/plugins/fortune.rb +22 -0
  26. data/data/rbot/plugins/freshmeat.rb +98 -0
  27. data/data/rbot/plugins/google.rb +51 -0
  28. data/data/rbot/plugins/host.rb +14 -0
  29. data/data/rbot/plugins/httpd.rb.disabled +35 -0
  30. data/data/rbot/plugins/insult.rb +258 -0
  31. data/data/rbot/plugins/karma.rb +85 -0
  32. data/data/rbot/plugins/lart.rb +181 -0
  33. data/data/rbot/plugins/math.rb +122 -0
  34. data/data/rbot/plugins/nickserv.rb +89 -0
  35. data/data/rbot/plugins/nslookup.rb +43 -0
  36. data/data/rbot/plugins/opme.rb +19 -0
  37. data/data/rbot/plugins/quakeauth.rb +51 -0
  38. data/data/rbot/plugins/quotes.rb +321 -0
  39. data/data/rbot/plugins/remind.rb +228 -0
  40. data/data/rbot/plugins/roshambo.rb +54 -0
  41. data/data/rbot/plugins/rot13.rb +10 -0
  42. data/data/rbot/plugins/roulette.rb +147 -0
  43. data/data/rbot/plugins/rss.rb.disabled +414 -0
  44. data/data/rbot/plugins/seen.rb +89 -0
  45. data/data/rbot/plugins/slashdot.rb +94 -0
  46. data/data/rbot/plugins/spell.rb +36 -0
  47. data/data/rbot/plugins/tube.rb +71 -0
  48. data/data/rbot/plugins/url.rb +88 -0
  49. data/data/rbot/plugins/weather.rb +649 -0
  50. data/data/rbot/plugins/wserver.rb +71 -0
  51. data/data/rbot/plugins/xmlrpc.rb.disabled +52 -0
  52. data/data/rbot/templates/keywords.rbot +4 -0
  53. data/data/rbot/templates/lart/larts +98 -0
  54. data/data/rbot/templates/lart/praises +5 -0
  55. data/data/rbot/templates/levels.rbot +30 -0
  56. data/data/rbot/templates/users.rbot +1 -0
  57. data/lib/rbot/auth.rb +203 -0
  58. data/lib/rbot/channel.rb +54 -0
  59. data/lib/rbot/config.rb +363 -0
  60. data/lib/rbot/dbhash.rb +112 -0
  61. data/lib/rbot/httputil.rb +141 -0
  62. data/lib/rbot/ircbot.rb +808 -0
  63. data/lib/rbot/ircsocket.rb +185 -0
  64. data/lib/rbot/keywords.rb +433 -0
  65. data/lib/rbot/language.rb +69 -0
  66. data/lib/rbot/message.rb +256 -0
  67. data/lib/rbot/messagemapper.rb +262 -0
  68. data/lib/rbot/plugins.rb +291 -0
  69. data/lib/rbot/post-install.rb +8 -0
  70. data/lib/rbot/rbotconfig.rb +36 -0
  71. data/lib/rbot/registry.rb +271 -0
  72. data/lib/rbot/rfc2812.rb +1104 -0
  73. data/lib/rbot/timer.rb +201 -0
  74. data/lib/rbot/utils.rb +83 -0
  75. data/setup.rb +1360 -0
  76. metadata +129 -0
@@ -0,0 +1,185 @@
1
+ module Irc
2
+
3
+ require 'socket'
4
+ require 'thread'
5
+ require 'rbot/timer'
6
+
7
+ # wrapped TCPSocket for communication with the server.
8
+ # emulates a subset of TCPSocket functionality
9
+ class IrcSocket
10
+ # total number of lines sent to the irc server
11
+ attr_reader :lines_sent
12
+
13
+ # total number of lines received from the irc server
14
+ attr_reader :lines_received
15
+
16
+ # delay between lines sent
17
+ attr_reader :sendq_delay
18
+
19
+ # max lines to burst
20
+ attr_reader :sendq_burst
21
+
22
+ # server:: server to connect to
23
+ # port:: IRCd port
24
+ # host:: optional local host to bind to (ruby 1.7+ required)
25
+ # create a new IrcSocket
26
+ def initialize(server, port, host, sendq_delay=2, sendq_burst=4)
27
+ @timer = Timer::Timer.new
28
+ @timer.add(0.2) do
29
+ spool
30
+ end
31
+ @server = server.dup
32
+ @port = port.to_i
33
+ @host = host
34
+ @spooler = false
35
+ @lines_sent = 0
36
+ @lines_received = 0
37
+ if sendq_delay
38
+ @sendq_delay = sendq_delay.to_f
39
+ else
40
+ @sendq_delay = 2
41
+ end
42
+ @last_send = Time.new - @sendq_delay
43
+ @burst = 0
44
+ if sendq_burst
45
+ @sendq_burst = sendq_burst.to_i
46
+ else
47
+ @sendq_burst = 4
48
+ end
49
+ end
50
+
51
+ # open a TCP connection to the server
52
+ def connect
53
+ if(@host)
54
+ begin
55
+ @sock=TCPSocket.new(@server, @port, @host)
56
+ rescue ArgumentError => e
57
+ $stderr.puts "Your version of ruby does not support binding to a "
58
+ $stderr.puts "specific local address, please upgrade if you wish "
59
+ $stderr.puts "to use HOST = foo"
60
+ $stderr.puts "(this option has been disabled in order to continue)"
61
+ @sock=TCPSocket.new(@server, @port)
62
+ end
63
+ else
64
+ @sock=TCPSocket.new(@server, @port)
65
+ end
66
+ @qthread = false
67
+ @qmutex = Mutex.new
68
+ @sendq = Array.new
69
+ end
70
+
71
+ def sendq_delay=(newfreq)
72
+ debug "changing sendq frequency to #{newfreq}"
73
+ @qmutex.synchronize do
74
+ @sendq_delay = newfreq
75
+ if newfreq == 0
76
+ clearq
77
+ @timer.stop
78
+ else
79
+ @timer.start
80
+ end
81
+ end
82
+ end
83
+
84
+ def sendq_burst=(newburst)
85
+ @qmutex.synchronize do
86
+ @sendq_burst = newburst
87
+ end
88
+ end
89
+
90
+ # used to send lines to the remote IRCd
91
+ # message: IRC message to send
92
+ def puts(message)
93
+ @qmutex.synchronize do
94
+ # debug "In puts - got mutex"
95
+ puts_critical(message)
96
+ end
97
+ end
98
+
99
+ # get the next line from the server (blocks)
100
+ def gets
101
+ reply = @sock.gets
102
+ @lines_received += 1
103
+ reply.strip! if reply
104
+ debug "RECV: #{reply.inspect}"
105
+ reply
106
+ end
107
+
108
+ def queue(msg)
109
+ if @sendq_delay > 0
110
+ @qmutex.synchronize do
111
+ @sendq.push msg
112
+ end
113
+ @timer.start
114
+ else
115
+ # just send it if queueing is disabled
116
+ self.puts(msg)
117
+ end
118
+ end
119
+
120
+ # pop a message off the queue, send it
121
+ def spool
122
+ if @sendq.empty?
123
+ @timer.stop
124
+ return
125
+ end
126
+ now = Time.new
127
+ if (now >= (@last_send + @sendq_delay))
128
+ # reset burst counter after @sendq_delay has passed
129
+ @burst = 0
130
+ debug "in spool, resetting @burst"
131
+ elsif (@burst >= @sendq_burst)
132
+ # nope. can't send anything, come back to us next tick...
133
+ @timer.start
134
+ return
135
+ end
136
+ @qmutex.synchronize do
137
+ debug "(can send #{@sendq_burst - @burst} lines, there are #{@sendq.length} to send)"
138
+ (@sendq_burst - @burst).times do
139
+ break if @sendq.empty?
140
+ puts_critical(@sendq.shift)
141
+ end
142
+ end
143
+ if @sendq.empty?
144
+ @timer.stop
145
+ end
146
+ end
147
+
148
+ def clearq
149
+ unless @sendq.empty?
150
+ @qmutex.synchronize do
151
+ @sendq.clear
152
+ end
153
+ end
154
+ end
155
+
156
+ # flush the TCPSocket
157
+ def flush
158
+ @sock.flush
159
+ end
160
+
161
+ # Wraps Kernel.select on the socket
162
+ def select(timeout=nil)
163
+ Kernel.select([@sock], nil, nil, timeout)
164
+ end
165
+
166
+ # shutdown the connection to the server
167
+ def shutdown(how=2)
168
+ @sock.shutdown(how)
169
+ end
170
+
171
+ private
172
+
173
+ # same as puts, but expects to be called with a mutex held on @qmutex
174
+ def puts_critical(message)
175
+ # debug "in puts_critical"
176
+ debug "SEND: #{message.inspect}"
177
+ @sock.send(message + "\n",0)
178
+ @last_send = Time.new
179
+ @lines_sent += 1
180
+ @burst += 1
181
+ end
182
+
183
+ end
184
+
185
+ end
@@ -0,0 +1,433 @@
1
+ require 'pp'
2
+
3
+ module Irc
4
+
5
+ # Keyword class
6
+ #
7
+ # Encapsulates a keyword ("foo is bar" is a keyword called foo, with type
8
+ # is, and has a single value of bar).
9
+ # Keywords can have multiple values, to_s() will choose one at random
10
+ class Keyword
11
+
12
+ # type of keyword (e.g. "is" or "are")
13
+ attr_reader :type
14
+
15
+ # type:: type of keyword (e.g "is" or "are")
16
+ # values:: array of values
17
+ #
18
+ # create a keyword of type +type+ with values +values+
19
+ def initialize(type, values)
20
+ @type = type.downcase
21
+ @values = values
22
+ end
23
+
24
+ # pick a random value for this keyword and return it
25
+ def to_s
26
+ if(@values.length > 1)
27
+ Keyword.unescape(@values[rand(@values.length)])
28
+ else
29
+ Keyword.unescape(@values[0])
30
+ end
31
+ end
32
+
33
+ # describe the keyword (show all values without interpolation)
34
+ def desc
35
+ @values.join(" | ")
36
+ end
37
+
38
+ # return the keyword in a stringified form ready for storage
39
+ def dump
40
+ @type + "/" + Keyword.unescape(@values.join("<=or=>"))
41
+ end
42
+
43
+ # deserialize the stringified form to an object
44
+ def Keyword.restore(str)
45
+ if str =~ /^(\S+?)\/(.*)$/
46
+ type = $1
47
+ vals = $2.split("<=or=>")
48
+ return Keyword.new(type, vals)
49
+ end
50
+ return nil
51
+ end
52
+
53
+ # values:: array of values to add
54
+ # add values to a keyword
55
+ def <<(values)
56
+ if(@values.length > 1 || values.length > 1)
57
+ values.each {|v|
58
+ @values << v
59
+ }
60
+ else
61
+ @values[0] += " or " + values[0]
62
+ end
63
+ end
64
+
65
+ # unescape special words/characters in a keyword
66
+ def Keyword.unescape(str)
67
+ str.gsub(/\\\|/, "|").gsub(/ \\is /, " is ").gsub(/ \\are /, " are ").gsub(/\\\?(\s*)$/, "?\1")
68
+ end
69
+
70
+ # escape special words/characters in a keyword
71
+ def Keyword.escape(str)
72
+ str.gsub(/\|/, "\\|").gsub(/ is /, " \\is ").gsub(/ are /, " \\are ").gsub(/\?(\s*)$/, "\\?\1")
73
+ end
74
+ end
75
+
76
+ # keywords class.
77
+ #
78
+ # Handles all that stuff like "bot: foo is bar", "bot: foo?"
79
+ #
80
+ # Fallback after core and auth have had a look at a message and refused to
81
+ # handle it, checks for a keyword command or lookup, otherwise the message
82
+ # is delegated to plugins
83
+ class Keywords
84
+ BotConfig.register BotConfigBooleanValue.new('keyword.listen',
85
+ :default => false,
86
+ :desc => "Should the bot listen to all chat and attempt to automatically detect keywords? (e.g. by spotting someone say 'foo is bar')")
87
+ BotConfig.register BotConfigBooleanValue.new('keyword.address',
88
+ :default => true,
89
+ :desc => "Should the bot require that keyword lookups are addressed to it? If not, the bot will attempt to lookup foo if someone says 'foo?' in channel")
90
+
91
+ # create a new Keywords instance, associated to bot +bot+
92
+ def initialize(bot)
93
+ @bot = bot
94
+ @statickeywords = Hash.new
95
+ upgrade_data
96
+ @keywords = DBTree.new bot, "keyword"
97
+
98
+ scan
99
+
100
+ # import old format keywords into DBHash
101
+ if(File.exist?("#{@bot.botclass}/keywords.rbot"))
102
+ puts "auto importing old keywords.rbot"
103
+ IO.foreach("#{@bot.botclass}/keywords.rbot") do |line|
104
+ if(line =~ /^(.*?)\s*<=(is|are)?=?>\s*(.*)$/)
105
+ lhs = $1
106
+ mhs = $2
107
+ rhs = $3
108
+ mhs = "is" unless mhs
109
+ rhs = Keyword.escape rhs
110
+ values = rhs.split("<=or=>")
111
+ @keywords[lhs] = Keyword.new(mhs, values).dump
112
+ end
113
+ end
114
+ File.delete("#{@bot.botclass}/keywords.rbot")
115
+ end
116
+ end
117
+
118
+ # drop static keywords and reload them from files, picking up any new
119
+ # keyword files that have been added
120
+ def rescan
121
+ @statickeywords = Hash.new
122
+ scan
123
+ end
124
+
125
+ # load static keywords from files, picking up any new keyword files that
126
+ # have been added
127
+ def scan
128
+ # first scan for old DBHash files, and convert them
129
+ Dir["#{@bot.botclass}/keywords/*"].each {|f|
130
+ next unless f =~ /\.db$/
131
+ puts "upgrading keyword db #{f} (rbot 0.9.5 or prior) database format"
132
+ newname = f.gsub(/\.db$/, ".kdb")
133
+ old = BDB::Hash.open f, nil,
134
+ "r+", 0600, "set_pagesize" => 1024,
135
+ "set_cachesize" => [0, 32 * 1024, 0]
136
+ new = BDB::CIBtree.open newname, nil,
137
+ BDB::CREATE | BDB::EXCL | BDB::TRUNCATE,
138
+ 0600, "set_pagesize" => 1024,
139
+ "set_cachesize" => [0, 32 * 1024, 0]
140
+ old.each {|k,v|
141
+ new[k] = v
142
+ }
143
+ old.close
144
+ new.close
145
+ File.delete(f)
146
+ }
147
+
148
+ # then scan for current DBTree files, and load them
149
+ Dir["#{@bot.botclass}/keywords/*"].each {|f|
150
+ next unless f =~ /\.kdb$/
151
+ hsh = DBTree.new @bot, f, true
152
+ key = File.basename(f).gsub(/\.kdb$/, "")
153
+ debug "keywords module: loading DBTree file #{f}, key #{key}"
154
+ @statickeywords[key] = hsh
155
+ }
156
+
157
+ # then scan for non DB files, and convert/import them and delete
158
+ Dir["#{@bot.botclass}/keywords/*"].each {|f|
159
+ next if f =~ /\.kdb$/
160
+ next if f =~ /CVS$/
161
+ puts "auto converting keywords from #{f}"
162
+ key = File.basename(f)
163
+ unless @statickeywords.has_key?(key)
164
+ @statickeywords[key] = DBHash.new @bot, "#{f}.db", true
165
+ end
166
+ IO.foreach(f) {|line|
167
+ if(line =~ /^(.*?)\s*<?=(is|are)?=?>\s*(.*)$/)
168
+ lhs = $1
169
+ mhs = $2
170
+ rhs = $3
171
+ # support infobot style factfiles, by fixing them up here
172
+ rhs.gsub!(/\$who/, "<who>")
173
+ mhs = "is" unless mhs
174
+ rhs = Keyword.escape rhs
175
+ values = rhs.split("<=or=>")
176
+ @statickeywords[key][lhs] = Keyword.new(mhs, values).dump
177
+ end
178
+ }
179
+ File.delete(f)
180
+ @statickeywords[key].flush
181
+ }
182
+ end
183
+
184
+ # upgrade data files found in old rbot formats to current
185
+ def upgrade_data
186
+ if File.exist?("#{@bot.botclass}/keywords.db")
187
+ puts "upgrading old keywords (rbot 0.9.5 or prior) database format"
188
+ old = BDB::Hash.open "#{@bot.botclass}/keywords.db", nil,
189
+ "r+", 0600, "set_pagesize" => 1024,
190
+ "set_cachesize" => [0, 32 * 1024, 0]
191
+ new = BDB::CIBtree.open "#{@bot.botclass}/keyword.db", nil,
192
+ BDB::CREATE | BDB::EXCL | BDB::TRUNCATE,
193
+ 0600, "set_pagesize" => 1024,
194
+ "set_cachesize" => [0, 32 * 1024, 0]
195
+ old.each {|k,v|
196
+ new[k] = v
197
+ }
198
+ old.close
199
+ new.close
200
+ File.delete("#{@bot.botclass}/keywords.db")
201
+ end
202
+ end
203
+
204
+ # save dynamic keywords to file
205
+ def save
206
+ @keywords.flush
207
+ end
208
+ def oldsave
209
+ File.open("#{@bot.botclass}/keywords.rbot", "w") do |file|
210
+ @keywords.each do |key, value|
211
+ file.puts "#{key}<=#{value.type}=>#{value.dump}"
212
+ end
213
+ end
214
+ end
215
+
216
+ # lookup keyword +key+, return it or nil
217
+ def [](key)
218
+ debug "keywords module: looking up key #{key}"
219
+ if(@keywords.has_key?(key))
220
+ return Keyword.restore(@keywords[key])
221
+ else
222
+ # key name order for the lookup through these
223
+ @statickeywords.keys.sort.each {|k|
224
+ v = @statickeywords[k]
225
+ if v.has_key?(key)
226
+ return Keyword.restore(v[key])
227
+ end
228
+ }
229
+ end
230
+ return nil
231
+ end
232
+
233
+ # does +key+ exist as a keyword?
234
+ def has_key?(key)
235
+ if @keywords.has_key?(key) && Keyword.restore(@keywords[key]) != nil
236
+ return true
237
+ end
238
+ @statickeywords.each {|k,v|
239
+ if v.has_key?(key) && Keyword.restore(v[key]) != nil
240
+ return true
241
+ end
242
+ }
243
+ return false
244
+ end
245
+
246
+ # m:: PrivMessage containing message info
247
+ # key:: key being queried
248
+ # dunno:: optional, if true, reply "dunno" if +key+ not found
249
+ #
250
+ # handle a message asking about a keyword
251
+ def keyword(m, key, dunno=true)
252
+ unless(kw = self[key])
253
+ m.reply @bot.lang.get("dunno") if (dunno)
254
+ return
255
+ end
256
+ response = kw.to_s
257
+ response.gsub!(/<who>/, m.sourcenick)
258
+ if(response =~ /^<reply>\s*(.*)/)
259
+ m.reply "#$1"
260
+ elsif(response =~ /^<action>\s*(.*)/)
261
+ @bot.action m.replyto, "#$1"
262
+ elsif(m.public? && response =~ /^<topic>\s*(.*)/)
263
+ topic = $1
264
+ @bot.topic m.target, topic
265
+ else
266
+ m.reply "#{key} #{kw.type} #{response}"
267
+ end
268
+ end
269
+
270
+
271
+ # m:: PrivMessage containing message info
272
+ # target:: channel/nick to tell about the keyword
273
+ # key:: key being queried
274
+ #
275
+ # handle a message asking the bot to tell someone about a keyword
276
+ def keyword_tell(m, target, key)
277
+ unless(kw = self[key])
278
+ @bot.say m.sourcenick, @bot.lang.get("dunno_about_X") % key
279
+ return
280
+ end
281
+ response = kw.to_s
282
+ response.gsub!(/<who>/, m.sourcenick)
283
+ if(response =~ /^<reply>\s*(.*)/)
284
+ @bot.say target, "#{m.sourcenick} wanted me to tell you: (#{key}) #$1"
285
+ m.reply "okay, I told #{target}: (#{key}) #$1"
286
+ elsif(response =~ /^<action>\s*(.*)/)
287
+ @bot.action target, "#$1 (#{m.sourcenick} wanted me to tell you)"
288
+ m.reply "okay, I told #{target}: * #$1"
289
+ else
290
+ @bot.say target, "#{m.sourcenick} wanted me to tell you that #{key} #{kw.type} #{response}"
291
+ m.reply "okay, I told #{target} that #{key} #{kw.type} #{response}"
292
+ end
293
+ end
294
+
295
+ # handle a message which alters a keyword
296
+ # like "foo is bar", or "no, foo is baz", or "foo is also qux"
297
+ def keyword_command(sourcenick, target, lhs, mhs, rhs, quiet=false)
298
+ debug "got keyword command #{lhs}, #{mhs}, #{rhs}"
299
+ overwrite = false
300
+ overwrite = true if(lhs.gsub!(/^no,\s*/, ""))
301
+ also = true if(rhs.gsub!(/^also\s+/, ""))
302
+ values = rhs.split(/\s+\|\s+/)
303
+ lhs = Keyword.unescape lhs
304
+ if(overwrite || also || !has_key?(lhs))
305
+ if(also && has_key?(lhs))
306
+ kw = self[lhs]
307
+ kw << values
308
+ @keywords[lhs] = kw.dump
309
+ else
310
+ @keywords[lhs] = Keyword.new(mhs, values).dump
311
+ end
312
+ @bot.okay target if !quiet
313
+ elsif(has_key?(lhs))
314
+ kw = self[lhs]
315
+ @bot.say target, "but #{lhs} #{kw.type} #{kw.desc}" if kw && !quiet
316
+ end
317
+ end
318
+
319
+ # return help string for Keywords with option topic +topic+
320
+ def help(topic="")
321
+ case topic
322
+ when "overview"
323
+ return "set: <keyword> is <definition>, overide: no, <keyword> is <definition>, add to definition: <keyword> is also <definition>, random responses: <keyword> is <definition> | <definition> [| ...], plurals: <keyword> are <definition>, escaping: \\is, \\are, \\|, specials: <reply>, <action>, <who>"
324
+ when "set"
325
+ return "set => <keyword> is <definition>"
326
+ when "plurals"
327
+ return "plurals => <keywords> are <definition>"
328
+ when "override"
329
+ return "overide => no, <keyword> is <definition>"
330
+ when "also"
331
+ return "also => <keyword> is also <definition>"
332
+ when "random"
333
+ return "random responses => <keyword> is <definition> | <definition> [| ...]"
334
+ when "get"
335
+ return "asking for keywords => (with addressing) \"<keyword>?\", (without addressing) \"'<keyword>\""
336
+ when "tell"
337
+ return "tell <nick> about <keyword> => if <keyword> is known, tell <nick>, via /msg, its definition"
338
+ when "forget"
339
+ return "forget <keyword> => forget fact <keyword>"
340
+ when "keywords"
341
+ return "keywords => show current keyword counts"
342
+ when "<reply>"
343
+ return "<reply> => normal response is \"<keyword> is <definition>\", but if <definition> begins with <reply>, the response will be \"<definition>\""
344
+ when "<action>"
345
+ return "<action> => makes keyword respnse \"/me <definition>\""
346
+ when "<who>"
347
+ return "<who> => replaced with questioner in reply"
348
+ when "<topic>"
349
+ return "<topic> => respond by setting the topic to the rest of the definition"
350
+ when "search"
351
+ return "keywords search [--all] [--full] <regexp> => search keywords for <regexp>. If --all is set, search static keywords too, if --full is set, search definitions too."
352
+ else
353
+ return "Keyword module (Fact learning and regurgitation) topics: overview, set, plurals, override, also, random, get, tell, forget, keywords, keywords search, <reply>, <action>, <who>, <topic>"
354
+ end
355
+ end
356
+
357
+ # privmsg handler
358
+ def privmsg(m)
359
+ return if m.replied?
360
+ if(m.address?)
361
+ if(!(m.message =~ /\\\?\s*$/) && m.message =~ /^(.*\S)\s*\?\s*$/)
362
+ keyword m, $1 if(@bot.auth.allow?("keyword", m.source, m.replyto))
363
+ elsif(m.message =~ /^(.*?)\s+(is|are)\s+(.*)$/)
364
+ keyword_command(m.sourcenick, m.replyto, $1, $2, $3) if(@bot.auth.allow?("keycmd", m.source, m.replyto))
365
+ elsif (m.message =~ /^tell\s+(\S+)\s+about\s+(.+)$/)
366
+ keyword_tell(m, $1, $2) if(@bot.auth.allow?("keyword", m.source, m.replyto))
367
+ elsif (m.message =~ /^forget\s+(.*)$/)
368
+ key = $1
369
+ if((@bot.auth.allow?("keycmd", m.source, m.replyto)) && @keywords.has_key?(key))
370
+ @keywords.delete(key)
371
+ @bot.okay m.replyto
372
+ end
373
+ elsif (m.message =~ /^keywords$/)
374
+ if(@bot.auth.allow?("keyword", m.source, m.replyto))
375
+ length = 0
376
+ @statickeywords.each {|k,v|
377
+ length += v.length
378
+ }
379
+ m.reply "There are currently #{@keywords.length} keywords, #{length} static facts defined."
380
+ end
381
+ elsif (m.message =~ /^keywords search\s+(.*)$/)
382
+ str = $1
383
+ all = false
384
+ all = true if str.gsub!(/--all\s+/, "")
385
+ full = false
386
+ full = true if str.gsub!(/--full\s+/, "")
387
+
388
+ re = Regexp.new(str, Regexp::IGNORECASE)
389
+ if(@bot.auth.allow?("keyword", m.source, m.replyto))
390
+ matches = Array.new
391
+ @keywords.each {|k,v|
392
+ kw = Keyword.restore(v)
393
+ if re.match(k) || (full && re.match(kw.desc))
394
+ matches << [k,kw]
395
+ end
396
+ }
397
+ if all
398
+ @statickeywords.each {|k,v|
399
+ v.each {|kk,vv|
400
+ kw = Keyword.restore(vv)
401
+ if re.match(kk) || (full && re.match(kw.desc))
402
+ matches << [kk,kw]
403
+ end
404
+ }
405
+ }
406
+ end
407
+ if matches.length == 1
408
+ rkw = matches[0]
409
+ m.reply "#{rkw[0]} #{rkw[1].type} #{rkw[1].desc}"
410
+ elsif matches.length > 0
411
+ i = 0
412
+ matches.each {|rkw|
413
+ m.reply "[#{i+1}/#{matches.length}] #{rkw[0]} #{rkw[1].type} #{rkw[1].desc}"
414
+ i += 1
415
+ break if i == 3
416
+ }
417
+ else
418
+ m.reply "no keywords match #{str}"
419
+ end
420
+ end
421
+ end
422
+ else
423
+ # in channel message, not to me
424
+ if(m.message =~ /^'(.*)$/ || (!@bot.config["keyword.address"] && m.message =~ /^(.*\S)\s*\?\s*$/))
425
+ keyword m, $1, false if(@bot.auth.allow?("keyword", m.source))
426
+ elsif(@bot.config["keyword.listen"] == true && (m.message =~ /^(.*?)\s+(is|are)\s+(.*)$/))
427
+ # TODO MUCH more selective on what's allowed here
428
+ keyword_command(m.sourcenick, m.replyto, $1, $2, $3, true) if(@bot.auth.allow?("keycmd", m.source))
429
+ end
430
+ end
431
+ end
432
+ end
433
+ end