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.
- data/AUTHORS +16 -0
- data/COPYING +21 -0
- data/ChangeLog +418 -0
- data/INSTALL +8 -0
- data/README +44 -0
- data/REQUIREMENTS +34 -0
- data/TODO +5 -0
- data/Usage_en.txt +129 -0
- data/bin/rbot +81 -0
- data/data/rbot/contrib/plugins/figlet.rb +20 -0
- data/data/rbot/contrib/plugins/ri.rb +83 -0
- data/data/rbot/contrib/plugins/stats.rb +232 -0
- data/data/rbot/contrib/plugins/vandale.rb +49 -0
- data/data/rbot/languages/dutch.lang +73 -0
- data/data/rbot/languages/english.lang +75 -0
- data/data/rbot/languages/french.lang +39 -0
- data/data/rbot/languages/german.lang +67 -0
- data/data/rbot/plugins/autoop.rb +68 -0
- data/data/rbot/plugins/autorejoin.rb +16 -0
- data/data/rbot/plugins/cal.rb +15 -0
- data/data/rbot/plugins/dice.rb +81 -0
- data/data/rbot/plugins/eightball.rb +19 -0
- data/data/rbot/plugins/excuse.rb +470 -0
- data/data/rbot/plugins/fish.rb +61 -0
- data/data/rbot/plugins/fortune.rb +22 -0
- data/data/rbot/plugins/freshmeat.rb +98 -0
- data/data/rbot/plugins/google.rb +51 -0
- data/data/rbot/plugins/host.rb +14 -0
- data/data/rbot/plugins/httpd.rb.disabled +35 -0
- data/data/rbot/plugins/insult.rb +258 -0
- data/data/rbot/plugins/karma.rb +85 -0
- data/data/rbot/plugins/lart.rb +181 -0
- data/data/rbot/plugins/math.rb +122 -0
- data/data/rbot/plugins/nickserv.rb +89 -0
- data/data/rbot/plugins/nslookup.rb +43 -0
- data/data/rbot/plugins/opme.rb +19 -0
- data/data/rbot/plugins/quakeauth.rb +51 -0
- data/data/rbot/plugins/quotes.rb +321 -0
- data/data/rbot/plugins/remind.rb +228 -0
- data/data/rbot/plugins/roshambo.rb +54 -0
- data/data/rbot/plugins/rot13.rb +10 -0
- data/data/rbot/plugins/roulette.rb +147 -0
- data/data/rbot/plugins/rss.rb.disabled +414 -0
- data/data/rbot/plugins/seen.rb +89 -0
- data/data/rbot/plugins/slashdot.rb +94 -0
- data/data/rbot/plugins/spell.rb +36 -0
- data/data/rbot/plugins/tube.rb +71 -0
- data/data/rbot/plugins/url.rb +88 -0
- data/data/rbot/plugins/weather.rb +649 -0
- data/data/rbot/plugins/wserver.rb +71 -0
- data/data/rbot/plugins/xmlrpc.rb.disabled +52 -0
- data/data/rbot/templates/keywords.rbot +4 -0
- data/data/rbot/templates/lart/larts +98 -0
- data/data/rbot/templates/lart/praises +5 -0
- data/data/rbot/templates/levels.rbot +30 -0
- data/data/rbot/templates/users.rbot +1 -0
- data/lib/rbot/auth.rb +203 -0
- data/lib/rbot/channel.rb +54 -0
- data/lib/rbot/config.rb +363 -0
- data/lib/rbot/dbhash.rb +112 -0
- data/lib/rbot/httputil.rb +141 -0
- data/lib/rbot/ircbot.rb +808 -0
- data/lib/rbot/ircsocket.rb +185 -0
- data/lib/rbot/keywords.rb +433 -0
- data/lib/rbot/language.rb +69 -0
- data/lib/rbot/message.rb +256 -0
- data/lib/rbot/messagemapper.rb +262 -0
- data/lib/rbot/plugins.rb +291 -0
- data/lib/rbot/post-install.rb +8 -0
- data/lib/rbot/rbotconfig.rb +36 -0
- data/lib/rbot/registry.rb +271 -0
- data/lib/rbot/rfc2812.rb +1104 -0
- data/lib/rbot/timer.rb +201 -0
- data/lib/rbot/utils.rb +83 -0
- data/setup.rb +1360 -0
- 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
|