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,141 @@
1
+ module Irc
2
+ module Utils
3
+
4
+ require 'resolv'
5
+ require 'net/http'
6
+ Net::HTTP.version_1_2
7
+
8
+ # class for making http requests easier (mainly for plugins to use)
9
+ # this class can check the bot proxy configuration to determine if a proxy
10
+ # needs to be used, which includes support for per-url proxy configuration.
11
+ class HttpUtil
12
+ BotConfig.register BotConfigBooleanValue.new('http.use_proxy',
13
+ :default => false, :desc => "should a proxy be used for HTTP requests?")
14
+ BotConfig.register BotConfigStringValue.new('http.proxy_uri', :default => false,
15
+ :desc => "Proxy server to use for HTTP requests (URI, e.g http://proxy.host:port)")
16
+ BotConfig.register BotConfigStringValue.new('http.proxy_user',
17
+ :default => nil,
18
+ :desc => "User for authenticating with the http proxy (if required)")
19
+ BotConfig.register BotConfigStringValue.new('http.proxy_pass',
20
+ :default => nil,
21
+ :desc => "Password for authenticating with the http proxy (if required)")
22
+ BotConfig.register BotConfigArrayValue.new('http.proxy_include',
23
+ :default => [],
24
+ :desc => "List of regexps to check against a URI's hostname/ip to see if we should use the proxy to access this URI. All URIs are proxied by default if the proxy is set, so this is only required to re-include URIs that might have been excluded by the exclude list. e.g. exclude /.*\.foo\.com/, include bar\.foo\.com")
25
+ BotConfig.register BotConfigArrayValue.new('http.proxy_exclude',
26
+ :default => [],
27
+ :desc => "List of regexps to check against a URI's hostname/ip to see if we should use avoid the proxy to access this URI and access it directly")
28
+
29
+ def initialize(bot)
30
+ @bot = bot
31
+ @headers = {
32
+ 'User-Agent' => "rbot http util #{$version} (http://linuxbrit.co.uk/rbot/)",
33
+ }
34
+ end
35
+
36
+ # if http_proxy_include or http_proxy_exclude are set, then examine the
37
+ # uri to see if this is a proxied uri
38
+ # the in/excludes are a list of regexps, and each regexp is checked against
39
+ # the server name, and its IP addresses
40
+ def proxy_required(uri)
41
+ use_proxy = true
42
+ if @bot.config["http.proxy_exclude"].empty? && @bot.config["http.proxy_include"].empty?
43
+ return use_proxy
44
+ end
45
+
46
+ list = [uri.host]
47
+ begin
48
+ list.concat Resolv.getaddresses(uri.host)
49
+ rescue StandardError => err
50
+ puts "warning: couldn't resolve host uri.host"
51
+ end
52
+
53
+ unless @bot.config["http.proxy_exclude"].empty?
54
+ re = @bot.config["http.proxy_exclude"].collect{|r| Regexp.new(r)}
55
+ re.each do |r|
56
+ list.each do |item|
57
+ if r.match(item)
58
+ use_proxy = false
59
+ break
60
+ end
61
+ end
62
+ end
63
+ end
64
+ unless @bot.config["http.proxy_include"].empty?
65
+ re = @bot.config["http.proxy_include"].collect{|r| Regexp.new(r)}
66
+ re.each do |r|
67
+ list.each do |item|
68
+ if r.match(item)
69
+ use_proxy = true
70
+ break
71
+ end
72
+ end
73
+ end
74
+ end
75
+ debug "using proxy for uri #{uri}?: #{use_proxy}"
76
+ return use_proxy
77
+ end
78
+
79
+ # uri:: Uri to create a proxy for
80
+ #
81
+ # return a net/http Proxy object, which is configured correctly for
82
+ # proxying based on the bot's proxy configuration.
83
+ # This will include per-url proxy configuration based on the bot config
84
+ # +http_proxy_include/exclude+ options.
85
+ def get_proxy(uri)
86
+ proxy = nil
87
+ proxy_host = nil
88
+ proxy_port = nil
89
+ proxy_user = nil
90
+ proxy_pass = nil
91
+
92
+ if @bot.config["http.use_proxy"]
93
+ if (ENV['http_proxy'])
94
+ proxy = URI.parse ENV['http_proxy'] rescue nil
95
+ end
96
+ if (@bot.config["http.proxy_uri"])
97
+ proxy = URI.parse @bot.config["http.proxy_uri"] rescue nil
98
+ end
99
+ if proxy
100
+ debug "proxy is set to #{proxy.uri}"
101
+ if proxy_required(uri)
102
+ proxy_host = proxy.host
103
+ proxy_port = proxy.port
104
+ proxy_user = @bot.config["http.proxy_user"]
105
+ proxy_pass = @bot.config["http.proxy_pass"]
106
+ end
107
+ end
108
+ end
109
+
110
+ return Net::HTTP.new(uri.host, uri.port, proxy_host, proxy_port, proxy_user, proxy_port)
111
+ end
112
+
113
+ # uri:: uri to query (Uri object)
114
+ # readtimeout:: timeout for reading the response
115
+ # opentimeout:: timeout for opening the connection
116
+ #
117
+ # simple get request, returns response body if the status code is 200 and
118
+ # the request doesn't timeout.
119
+ def get(uri, readtimeout=10, opentimeout=5)
120
+ proxy = get_proxy(uri)
121
+ proxy.open_timeout = opentimeout
122
+ proxy.read_timeout = readtimeout
123
+
124
+ begin
125
+ proxy.start() {|http|
126
+ resp = http.get(uri.request_uri(), @headers)
127
+ if resp.code == "200"
128
+ return resp.body
129
+ else
130
+ puts "HttpUtil.get return code #{resp.code} #{resp.body}"
131
+ end
132
+ return nil
133
+ }
134
+ rescue StandardError, Timeout::Error => e
135
+ $stderr.puts "HttpUtil.get exception: #{e}, while trying to get #{uri}"
136
+ end
137
+ return nil
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,808 @@
1
+ require 'thread'
2
+ require 'etc'
3
+ require 'fileutils'
4
+
5
+ $debug = false unless $debug
6
+ # print +message+ if debugging is enabled
7
+ def debug(message=nil)
8
+ print "DEBUG: #{message}\n" if($debug && message)
9
+ #yield
10
+ end
11
+
12
+ # these first
13
+ require 'rbot/rbotconfig'
14
+ require 'rbot/config'
15
+ require 'rbot/utils'
16
+
17
+ require 'rbot/rfc2812'
18
+ require 'rbot/keywords'
19
+ require 'rbot/ircsocket'
20
+ require 'rbot/auth'
21
+ require 'rbot/timer'
22
+ require 'rbot/plugins'
23
+ require 'rbot/channel'
24
+ require 'rbot/message'
25
+ require 'rbot/language'
26
+ require 'rbot/dbhash'
27
+ require 'rbot/registry'
28
+ require 'rbot/httputil'
29
+
30
+ module Irc
31
+
32
+ # Main bot class, which manages the various components, receives messages,
33
+ # handles them or passes them to plugins, and contains core functionality.
34
+ class IrcBot
35
+ # the bot's current nickname
36
+ attr_reader :nick
37
+
38
+ # the bot's IrcAuth data
39
+ attr_reader :auth
40
+
41
+ # the bot's BotConfig data
42
+ attr_reader :config
43
+
44
+ # the botclass for this bot (determines configdir among other things)
45
+ attr_reader :botclass
46
+
47
+ # used to perform actions periodically (saves configuration once per minute
48
+ # by default)
49
+ attr_reader :timer
50
+
51
+ # bot's Language data
52
+ attr_reader :lang
53
+
54
+ # bot's configured addressing prefixes
55
+ attr_reader :addressing_prefixes
56
+
57
+ # channel info for channels the bot is in
58
+ attr_reader :channels
59
+
60
+ # bot's irc socket
61
+ attr_reader :socket
62
+
63
+ # bot's object registry, plugins get an interface to this for persistant
64
+ # storage (hash interface tied to a bdb file, plugins use Accessors to store
65
+ # and restore objects in their own namespaces.)
66
+ attr_reader :registry
67
+
68
+ # bot's httputil help object, for fetching resources via http. Sets up
69
+ # proxies etc as defined by the bot configuration/environment
70
+ attr_reader :httputil
71
+
72
+ # create a new IrcBot with botclass +botclass+
73
+ def initialize(botclass, params = {})
74
+ # BotConfig for the core bot
75
+ BotConfig.register BotConfigStringValue.new('server.name',
76
+ :default => "localhost", :requires_restart => true,
77
+ :desc => "What server should the bot connect to?",
78
+ :wizard => true)
79
+ BotConfig.register BotConfigIntegerValue.new('server.port',
80
+ :default => 6667, :type => :integer, :requires_restart => true,
81
+ :desc => "What port should the bot connect to?",
82
+ :validate => Proc.new {|v| v > 0}, :wizard => true)
83
+ BotConfig.register BotConfigStringValue.new('server.password',
84
+ :default => false, :requires_restart => true,
85
+ :desc => "Password for connecting to this server (if required)",
86
+ :wizard => true)
87
+ BotConfig.register BotConfigStringValue.new('server.bindhost',
88
+ :default => false, :requires_restart => true,
89
+ :desc => "Specific local host or IP for the bot to bind to (if required)",
90
+ :wizard => true)
91
+ BotConfig.register BotConfigIntegerValue.new('server.reconnect_wait',
92
+ :default => 5, :validate => Proc.new{|v| v >= 0},
93
+ :desc => "Seconds to wait before attempting to reconnect, on disconnect")
94
+ BotConfig.register BotConfigStringValue.new('irc.nick', :default => "rbot",
95
+ :desc => "IRC nickname the bot should attempt to use", :wizard => true,
96
+ :on_change => Proc.new{|bot, v| bot.sendq "NICK #{v}" })
97
+ BotConfig.register BotConfigStringValue.new('irc.user', :default => "rbot",
98
+ :requires_restart => true,
99
+ :desc => "local user the bot should appear to be", :wizard => true)
100
+ BotConfig.register BotConfigArrayValue.new('irc.join_channels',
101
+ :default => [], :wizard => true,
102
+ :desc => "What channels the bot should always join at startup. List multiple channels using commas to separate. If a channel requires a password, use a space after the channel name. e.g: '#chan1, #chan2, #secretchan secritpass, #chan3'")
103
+ BotConfig.register BotConfigIntegerValue.new('core.save_every',
104
+ :default => 60, :validate => Proc.new{|v| v >= 0},
105
+ # TODO change timer via on_change proc
106
+ :desc => "How often the bot should persist all configuration to disk (in case of a server crash, for example")
107
+ BotConfig.register BotConfigFloatValue.new('server.sendq_delay',
108
+ :default => 2.0, :validate => Proc.new{|v| v >= 0},
109
+ :desc => "(flood prevention) the delay between sending messages to the server (in seconds)",
110
+ :on_change => Proc.new {|bot, v| bot.socket.sendq_delay = v })
111
+ BotConfig.register BotConfigIntegerValue.new('server.sendq_burst',
112
+ :default => 4, :validate => Proc.new{|v| v >= 0},
113
+ :desc => "(flood prevention) max lines to burst to the server before throttling. Most ircd's allow bursts of up 5 lines, with non-burst limits of 512 bytes/2 seconds",
114
+ :on_change => Proc.new {|bot, v| bot.socket.sendq_burst = v })
115
+
116
+ @argv = params[:argv]
117
+
118
+ unless FileTest.directory? Config::datadir
119
+ puts "data directory '#{Config::datadir}' not found, did you install.rb?"
120
+ exit 2
121
+ end
122
+
123
+ botclass = "/home/#{Etc.getlogin}/.rbot" unless botclass
124
+ @botclass = botclass.gsub(/\/$/, "")
125
+
126
+ unless FileTest.directory? botclass
127
+ puts "no #{botclass} directory found, creating from templates.."
128
+ if FileTest.exist? botclass
129
+ puts "Error: file #{botclass} exists but isn't a directory"
130
+ exit 2
131
+ end
132
+ FileUtils.cp_r Config::datadir+'/templates', botclass
133
+ end
134
+
135
+ Dir.mkdir("#{botclass}/logs") unless File.exist?("#{botclass}/logs")
136
+
137
+ @startup_time = Time.new
138
+ @config = BotConfig.new(self)
139
+ @timer = Timer::Timer.new(1.0) # only need per-second granularity
140
+ @registry = BotRegistry.new self
141
+ @timer.add(@config['core.save_every']) { save } if @config['core.save_every']
142
+ @channels = Hash.new
143
+ @logs = Hash.new
144
+
145
+ @httputil = Utils::HttpUtil.new(self)
146
+ @lang = Language::Language.new(@config['core.language'])
147
+ @keywords = Keywords.new(self)
148
+ @auth = IrcAuth.new(self)
149
+
150
+ Dir.mkdir("#{botclass}/plugins") unless File.exist?("#{botclass}/plugins")
151
+ @plugins = Plugins::Plugins.new(self, ["#{botclass}/plugins"])
152
+
153
+ @socket = IrcSocket.new(@config['server.name'], @config['server.port'], @config['server.bindhost'], @config['server.sendq_delay'], @config['server.sendq_burst'])
154
+ @nick = @config['irc.nick']
155
+ if @config['core.address_prefix']
156
+ @addressing_prefixes = @config['core.address_prefix'].split(" ")
157
+ else
158
+ @addressing_prefixes = Array.new
159
+ end
160
+
161
+ @client = IrcClient.new
162
+ @client[:privmsg] = proc { |data|
163
+ message = PrivMessage.new(self, data[:source], data[:target], data[:message])
164
+ onprivmsg(message)
165
+ }
166
+ @client[:notice] = proc { |data|
167
+ message = NoticeMessage.new(self, data[:source], data[:target], data[:message])
168
+ # pass it off to plugins that want to hear everything
169
+ @plugins.delegate "listen", message
170
+ }
171
+ @client[:motd] = proc { |data|
172
+ data[:motd].each_line { |line|
173
+ log "MOTD: #{line}", "server"
174
+ }
175
+ }
176
+ @client[:nicktaken] = proc { |data|
177
+ nickchg "#{data[:nick]}_"
178
+ }
179
+ @client[:badnick] = proc {|data|
180
+ puts "WARNING, bad nick (#{data[:nick]})"
181
+ }
182
+ @client[:ping] = proc {|data|
183
+ # (jump the queue for pongs)
184
+ @socket.puts "PONG #{data[:pingid]}"
185
+ }
186
+ @client[:nick] = proc {|data|
187
+ sourcenick = data[:sourcenick]
188
+ nick = data[:nick]
189
+ m = NickMessage.new(self, data[:source], data[:sourcenick], data[:nick])
190
+ if(sourcenick == @nick)
191
+ debug "my nick is now #{nick}"
192
+ @nick = nick
193
+ end
194
+ @channels.each {|k,v|
195
+ if(v.users.has_key?(sourcenick))
196
+ log "@ #{sourcenick} is now known as #{nick}", k
197
+ v.users[nick] = v.users[sourcenick]
198
+ v.users.delete(sourcenick)
199
+ end
200
+ }
201
+ @plugins.delegate("listen", m)
202
+ @plugins.delegate("nick", m)
203
+ }
204
+ @client[:quit] = proc {|data|
205
+ source = data[:source]
206
+ sourcenick = data[:sourcenick]
207
+ sourceurl = data[:sourceaddress]
208
+ message = data[:message]
209
+ m = QuitMessage.new(self, data[:source], data[:sourcenick], data[:message])
210
+ if(data[:sourcenick] =~ /#{@nick}/i)
211
+ else
212
+ @channels.each {|k,v|
213
+ if(v.users.has_key?(sourcenick))
214
+ log "@ Quit: #{sourcenick}: #{message}", k
215
+ v.users.delete(sourcenick)
216
+ end
217
+ }
218
+ end
219
+ @plugins.delegate("listen", m)
220
+ @plugins.delegate("quit", m)
221
+ }
222
+ @client[:mode] = proc {|data|
223
+ source = data[:source]
224
+ sourcenick = data[:sourcenick]
225
+ sourceurl = data[:sourceaddress]
226
+ channel = data[:channel]
227
+ targets = data[:targets]
228
+ modestring = data[:modestring]
229
+ log "@ Mode #{modestring} #{targets} by #{sourcenick}", channel
230
+ }
231
+ @client[:welcome] = proc {|data|
232
+ log "joined server #{data[:source]} as #{data[:nick]}", "server"
233
+ debug "I think my nick is #{@nick}, server thinks #{data[:nick]}"
234
+ if data[:nick] && data[:nick].length > 0
235
+ @nick = data[:nick]
236
+ end
237
+
238
+ @plugins.delegate("connect")
239
+
240
+ @config['irc.join_channels'].each {|c|
241
+ debug "autojoining channel #{c}"
242
+ if(c =~ /^(\S+)\s+(\S+)$/i)
243
+ join $1, $2
244
+ else
245
+ join c if(c)
246
+ end
247
+ }
248
+ }
249
+ @client[:join] = proc {|data|
250
+ m = JoinMessage.new(self, data[:source], data[:channel], data[:message])
251
+ onjoin(m)
252
+ }
253
+ @client[:part] = proc {|data|
254
+ m = PartMessage.new(self, data[:source], data[:channel], data[:message])
255
+ onpart(m)
256
+ }
257
+ @client[:kick] = proc {|data|
258
+ m = KickMessage.new(self, data[:source], data[:target],data[:channel],data[:message])
259
+ onkick(m)
260
+ }
261
+ @client[:invite] = proc {|data|
262
+ if(data[:target] =~ /^#{@nick}$/i)
263
+ join data[:channel] if (@auth.allow?("join", data[:source], data[:sourcenick]))
264
+ end
265
+ }
266
+ @client[:changetopic] = proc {|data|
267
+ channel = data[:channel]
268
+ sourcenick = data[:sourcenick]
269
+ topic = data[:topic]
270
+ timestamp = data[:unixtime] || Time.now.to_i
271
+ if(sourcenick == @nick)
272
+ log "@ I set topic \"#{topic}\"", channel
273
+ else
274
+ log "@ #{sourcenick} set topic \"#{topic}\"", channel
275
+ end
276
+ m = TopicMessage.new(self, data[:source], data[:channel], timestamp, data[:topic])
277
+
278
+ ontopic(m)
279
+ @plugins.delegate("listen", m)
280
+ @plugins.delegate("topic", m)
281
+ }
282
+ @client[:topic] = @client[:topicinfo] = proc {|data|
283
+ channel = data[:channel]
284
+ m = TopicMessage.new(self, data[:source], data[:channel], data[:unixtime], data[:topic])
285
+ ontopic(m)
286
+ }
287
+ @client[:names] = proc {|data|
288
+ channel = data[:channel]
289
+ users = data[:users]
290
+ unless(@channels[channel])
291
+ puts "bug: got names for channel '#{channel}' I didn't think I was in\n"
292
+ exit 2
293
+ end
294
+ @channels[channel].users.clear
295
+ users.each {|u|
296
+ @channels[channel].users[u[0].sub(/^[@&~+]/, '')] = ["mode", u[1]]
297
+ }
298
+ }
299
+ @client[:unknown] = proc {|data|
300
+ #debug "UNKNOWN: #{data[:serverstring]}"
301
+ log data[:serverstring], ":unknown"
302
+ }
303
+ end
304
+
305
+ # connect the bot to IRC
306
+ def connect
307
+ trap("SIGTERM") { quit }
308
+ trap("SIGHUP") { quit }
309
+ trap("SIGINT") { quit }
310
+ begin
311
+ @socket.connect
312
+ rescue => e
313
+ raise "failed to connect to IRC server at #{@config['server.name']} #{@config['server.port']}: " + e
314
+ end
315
+ @socket.puts "PASS " + @config['server.password'] if @config['server.password']
316
+ @socket.puts "NICK #{@nick}\nUSER #{@config['irc.user']} 4 #{@config['server.name']} :Ruby bot. (c) Tom Gilbert"
317
+ end
318
+
319
+ # begin event handling loop
320
+ def mainloop
321
+ while true
322
+ connect
323
+ @timer.start
324
+
325
+ begin
326
+ while true
327
+ if @socket.select
328
+ break unless reply = @socket.gets
329
+ @client.process reply
330
+ end
331
+ end
332
+ rescue TimeoutError, SocketError => e
333
+ puts "network exception: connection closed: #{e}"
334
+ puts e.backtrace.join("\n")
335
+ @socket.close # now we reconnect
336
+ rescue => e # TODO be selective, only grab Network errors
337
+ puts "unexpected exception: connection closed: #{e}"
338
+ puts e.backtrace.join("\n")
339
+ exit 2
340
+ end
341
+
342
+ puts "disconnected"
343
+ @channels.clear
344
+ @socket.clearq
345
+
346
+ puts "waiting to reconnect"
347
+ sleep @config['server.reconnect_wait']
348
+ end
349
+ end
350
+
351
+ # type:: message type
352
+ # where:: message target
353
+ # message:: message text
354
+ # send message +message+ of type +type+ to target +where+
355
+ # Type can be PRIVMSG, NOTICE, etc, but those you should really use the
356
+ # relevant say() or notice() methods. This one should be used for IRCd
357
+ # extensions you want to use in modules.
358
+ def sendmsg(type, where, message)
359
+ # limit it 440 chars + CRLF.. so we have to split long lines
360
+ left = 440 - type.length - where.length - 3
361
+ begin
362
+ if(left >= message.length)
363
+ sendq("#{type} #{where} :#{message}")
364
+ log_sent(type, where, message)
365
+ return
366
+ end
367
+ line = message.slice!(0, left)
368
+ lastspace = line.rindex(/\s+/)
369
+ if(lastspace)
370
+ message = line.slice!(lastspace, line.length) + message
371
+ message.gsub!(/^\s+/, "")
372
+ end
373
+ sendq("#{type} #{where} :#{line}")
374
+ log_sent(type, where, line)
375
+ end while(message.length > 0)
376
+ end
377
+
378
+ # queue an arbitraty message for the server
379
+ def sendq(message="")
380
+ # temporary
381
+ @socket.queue(message)
382
+ end
383
+
384
+ # send a notice message to channel/nick +where+
385
+ def notice(where, message)
386
+ message.each_line { |line|
387
+ line.chomp!
388
+ next unless(line.length > 0)
389
+ sendmsg("NOTICE", where, line)
390
+ }
391
+ end
392
+
393
+ # say something (PRIVMSG) to channel/nick +where+
394
+ def say(where, message)
395
+ message.to_s.gsub(/[\r\n]+/, "\n").each_line { |line|
396
+ line.chomp!
397
+ next unless(line.length > 0)
398
+ unless((where =~ /^#/) && (@channels.has_key?(where) && @channels[where].quiet))
399
+ sendmsg("PRIVMSG", where, line)
400
+ end
401
+ }
402
+ end
403
+
404
+ # perform a CTCP action with message +message+ to channel/nick +where+
405
+ def action(where, message)
406
+ sendq("PRIVMSG #{where} :\001ACTION #{message}\001")
407
+ if(where =~ /^#/)
408
+ log "* #{@nick} #{message}", where
409
+ elsif (where =~ /^(\S*)!.*$/)
410
+ log "* #{@nick}[#{where}] #{message}", $1
411
+ else
412
+ log "* #{@nick}[#{where}] #{message}", where
413
+ end
414
+ end
415
+
416
+ # quick way to say "okay" (or equivalent) to +where+
417
+ def okay(where)
418
+ say where, @lang.get("okay")
419
+ end
420
+
421
+ # log message +message+ to a file determined by +where+. +where+ can be a
422
+ # channel name, or a nick for private message logging
423
+ def log(message, where="server")
424
+ message.chomp!
425
+ stamp = Time.now.strftime("%Y/%m/%d %H:%M:%S")
426
+ unless(@logs.has_key?(where))
427
+ @logs[where] = File.new("#{@botclass}/logs/#{where}", "a")
428
+ @logs[where].sync = true
429
+ end
430
+ @logs[where].puts "[#{stamp}] #{message}"
431
+ #debug "[#{stamp}] <#{where}> #{message}"
432
+ end
433
+
434
+ # set topic of channel +where+ to +topic+
435
+ def topic(where, topic)
436
+ sendq "TOPIC #{where} :#{topic}"
437
+ end
438
+
439
+ # disconnect from the server and cleanup all plugins and modules
440
+ def shutdown(message = nil)
441
+ trap("SIGTERM", "DEFAULT")
442
+ trap("SIGHUP", "DEFAULT")
443
+ trap("SIGINT", "DEFAULT")
444
+ message = @lang.get("quit") if (message.nil? || message.empty?)
445
+ @socket.clearq
446
+ save
447
+ @plugins.cleanup
448
+ @channels.each_value {|v|
449
+ log "@ quit (#{message})", v.name
450
+ }
451
+ @socket.puts "QUIT :#{message}"
452
+ @socket.flush
453
+ @socket.shutdown
454
+ @registry.close
455
+ puts "rbot quit (#{message})"
456
+ end
457
+
458
+ # message:: optional IRC quit message
459
+ # quit IRC, shutdown the bot
460
+ def quit(message=nil)
461
+ shutdown(message)
462
+ exit 0
463
+ end
464
+
465
+ # totally shutdown and respawn the bot
466
+ def restart
467
+ shutdown("restarting, back in #{@config['server.reconnect_wait']}...")
468
+ sleep @config['server.reconnect_wait']
469
+ # now we re-exec
470
+ exec($0, *@argv)
471
+ end
472
+
473
+ # call the save method for bot's config, keywords, auth and all plugins
474
+ def save
475
+ @registry.flush
476
+ @config.save
477
+ @keywords.save
478
+ @auth.save
479
+ @plugins.save
480
+ end
481
+
482
+ # call the rescan method for the bot's lang, keywords and all plugins
483
+ def rescan
484
+ @lang.rescan
485
+ @plugins.rescan
486
+ @keywords.rescan
487
+ end
488
+
489
+ # channel:: channel to join
490
+ # key:: optional channel key if channel is +s
491
+ # join a channel
492
+ def join(channel, key=nil)
493
+ if(key)
494
+ sendq "JOIN #{channel} :#{key}"
495
+ else
496
+ sendq "JOIN #{channel}"
497
+ end
498
+ end
499
+
500
+ # part a channel
501
+ def part(channel, message="")
502
+ sendq "PART #{channel} :#{message}"
503
+ end
504
+
505
+ # attempt to change bot's nick to +name+
506
+ # FIXME
507
+ # if rbot is already taken, this happens:
508
+ # <giblet> rbot_, nick rbot
509
+ # --- rbot_ is now known as rbot__
510
+ # he should of course just keep his existing nick and report the error :P
511
+ def nickchg(name)
512
+ sendq "NICK #{name}"
513
+ end
514
+
515
+ # changing mode
516
+ def mode(channel, mode, target)
517
+ sendq "MODE #{channel} #{mode} #{target}"
518
+ end
519
+
520
+ # m:: message asking for help
521
+ # topic:: optional topic help is requested for
522
+ # respond to online help requests
523
+ def help(topic=nil)
524
+ topic = nil if topic == ""
525
+ case topic
526
+ when nil
527
+ helpstr = "help topics: core, auth, keywords"
528
+ helpstr += @plugins.helptopics
529
+ helpstr += " (help <topic> for more info)"
530
+ when /^core$/i
531
+ helpstr = corehelp
532
+ when /^core\s+(.+)$/i
533
+ helpstr = corehelp $1
534
+ when /^auth$/i
535
+ helpstr = @auth.help
536
+ when /^auth\s+(.+)$/i
537
+ helpstr = @auth.help $1
538
+ when /^keywords$/i
539
+ helpstr = @keywords.help
540
+ when /^keywords\s+(.+)$/i
541
+ helpstr = @keywords.help $1
542
+ else
543
+ unless(helpstr = @plugins.help(topic))
544
+ helpstr = "no help for topic #{topic}"
545
+ end
546
+ end
547
+ return helpstr
548
+ end
549
+
550
+ # returns a string describing the current status of the bot (uptime etc)
551
+ def status
552
+ secs_up = Time.new - @startup_time
553
+ uptime = Utils.secs_to_string secs_up
554
+ return "Uptime #{uptime}, #{@plugins.length} plugins active, #{@registry.length} items stored in registry, #{@socket.lines_sent} lines sent, #{@socket.lines_received} received."
555
+ end
556
+
557
+
558
+ private
559
+
560
+ # handle help requests for "core" topics
561
+ def corehelp(topic="")
562
+ case topic
563
+ when "quit"
564
+ return "quit [<message>] => quit IRC with message <message>"
565
+ when "restart"
566
+ return "restart => completely stop and restart the bot (including reconnect)"
567
+ when "join"
568
+ return "join <channel> [<key>] => join channel <channel> with secret key <key> if specified. #{@nick} also responds to invites if you have the required access level"
569
+ when "part"
570
+ return "part <channel> => part channel <channel>"
571
+ when "hide"
572
+ return "hide => part all channels"
573
+ when "save"
574
+ return "save => save current dynamic data and configuration"
575
+ when "rescan"
576
+ return "rescan => reload modules and static facts"
577
+ when "nick"
578
+ return "nick <nick> => attempt to change nick to <nick>"
579
+ when "say"
580
+ return "say <channel>|<nick> <message> => say <message> to <channel> or in private message to <nick>"
581
+ when "action"
582
+ return "action <channel>|<nick> <message> => does a /me <message> to <channel> or in private message to <nick>"
583
+ when "topic"
584
+ return "topic <channel> <message> => set topic of <channel> to <message>"
585
+ when "quiet"
586
+ return "quiet [in here|<channel>] => with no arguments, stop speaking in all channels, if \"in here\", stop speaking in this channel, or stop speaking in <channel>"
587
+ when "talk"
588
+ return "talk [in here|<channel>] => with no arguments, resume speaking in all channels, if \"in here\", resume speaking in this channel, or resume speaking in <channel>"
589
+ when "version"
590
+ return "version => describes software version"
591
+ when "botsnack"
592
+ return "botsnack => reward #{@nick} for being good"
593
+ when "hello"
594
+ return "hello|hi|hey|yo [#{@nick}] => greet the bot"
595
+ else
596
+ return "Core help topics: quit, restart, config, join, part, hide, save, rescan, nick, say, action, topic, quiet, talk, version, botsnack, hello"
597
+ end
598
+ end
599
+
600
+ # handle incoming IRC PRIVMSG +m+
601
+ def onprivmsg(m)
602
+ # log it first
603
+ if(m.action?)
604
+ if(m.private?)
605
+ log "* [#{m.sourcenick}(#{m.sourceaddress})] #{m.message}", m.sourcenick
606
+ else
607
+ log "* #{m.sourcenick} #{m.message}", m.target
608
+ end
609
+ else
610
+ if(m.public?)
611
+ log "<#{m.sourcenick}> #{m.message}", m.target
612
+ else
613
+ log "[#{m.sourcenick}(#{m.sourceaddress})] #{m.message}", m.sourcenick
614
+ end
615
+ end
616
+
617
+ # pass it off to plugins that want to hear everything
618
+ @plugins.delegate "listen", m
619
+
620
+ if(m.private? && m.message =~ /^\001PING\s+(.+)\001/)
621
+ notice m.sourcenick, "\001PING #$1\001"
622
+ log "@ #{m.sourcenick} pinged me"
623
+ return
624
+ end
625
+
626
+ if(m.address?)
627
+ case m.message
628
+ when (/^join\s+(\S+)\s+(\S+)$/i)
629
+ join $1, $2 if(@auth.allow?("join", m.source, m.replyto))
630
+ when (/^join\s+(\S+)$/i)
631
+ join $1 if(@auth.allow?("join", m.source, m.replyto))
632
+ when (/^part$/i)
633
+ part m.target if(m.public? && @auth.allow?("join", m.source, m.replyto))
634
+ when (/^part\s+(\S+)$/i)
635
+ part $1 if(@auth.allow?("join", m.source, m.replyto))
636
+ when (/^quit(?:\s+(.*))?$/i)
637
+ quit $1 if(@auth.allow?("quit", m.source, m.replyto))
638
+ when (/^restart$/i)
639
+ restart if(@auth.allow?("quit", m.source, m.replyto))
640
+ when (/^hide$/i)
641
+ join 0 if(@auth.allow?("join", m.source, m.replyto))
642
+ when (/^save$/i)
643
+ if(@auth.allow?("config", m.source, m.replyto))
644
+ save
645
+ m.okay
646
+ end
647
+ when (/^nick\s+(\S+)$/i)
648
+ nickchg($1) if(@auth.allow?("nick", m.source, m.replyto))
649
+ when (/^say\s+(\S+)\s+(.*)$/i)
650
+ say $1, $2 if(@auth.allow?("say", m.source, m.replyto))
651
+ when (/^action\s+(\S+)\s+(.*)$/i)
652
+ action $1, $2 if(@auth.allow?("say", m.source, m.replyto))
653
+ when (/^topic\s+(\S+)\s+(.*)$/i)
654
+ topic $1, $2 if(@auth.allow?("topic", m.source, m.replyto))
655
+ when (/^mode\s+(\S+)\s+(\S+)\s+(.*)$/i)
656
+ mode $1, $2, $3 if(@auth.allow?("mode", m.source, m.replyto))
657
+ when (/^ping$/i)
658
+ say m.replyto, "pong"
659
+ when (/^rescan$/i)
660
+ if(@auth.allow?("config", m.source, m.replyto))
661
+ m.okay
662
+ rescan
663
+ end
664
+ when (/^quiet$/i)
665
+ if(auth.allow?("talk", m.source, m.replyto))
666
+ m.okay
667
+ @channels.each_value {|c| c.quiet = true }
668
+ end
669
+ when (/^quiet in (\S+)$/i)
670
+ where = $1
671
+ if(auth.allow?("talk", m.source, m.replyto))
672
+ m.okay
673
+ where.gsub!(/^here$/, m.target) if m.public?
674
+ @channels[where].quiet = true if(@channels.has_key?(where))
675
+ end
676
+ when (/^talk$/i)
677
+ if(auth.allow?("talk", m.source, m.replyto))
678
+ @channels.each_value {|c| c.quiet = false }
679
+ m.okay
680
+ end
681
+ when (/^talk in (\S+)$/i)
682
+ where = $1
683
+ if(auth.allow?("talk", m.source, m.replyto))
684
+ where.gsub!(/^here$/, m.target) if m.public?
685
+ @channels[where].quiet = false if(@channels.has_key?(where))
686
+ m.okay
687
+ end
688
+ when (/^status\??$/i)
689
+ m.reply status if auth.allow?("status", m.source, m.replyto)
690
+ when (/^registry stats$/i)
691
+ if auth.allow?("config", m.source, m.replyto)
692
+ m.reply @registry.stat.inspect
693
+ end
694
+ when (/^(help\s+)?config(\s+|$)/)
695
+ @config.privmsg(m)
696
+ when (/^(version)|(introduce yourself)$/i)
697
+ say m.replyto, "I'm a v. #{$version} rubybot, (c) Tom Gilbert - http://linuxbrit.co.uk/rbot/"
698
+ when (/^help(?:\s+(.*))?$/i)
699
+ say m.replyto, help($1)
700
+ #TODO move these to a "chatback" plugin
701
+ when (/^(botsnack|ciggie)$/i)
702
+ say m.replyto, @lang.get("thanks_X") % m.sourcenick if(m.public?)
703
+ say m.replyto, @lang.get("thanks") if(m.private?)
704
+ when (/^(hello|howdy|hola|salut|bonjour|sup|niihau|hey|hi(\W|$)|yo(\W|$)).*/i)
705
+ say m.replyto, @lang.get("hello_X") % m.sourcenick if(m.public?)
706
+ say m.replyto, @lang.get("hello") if(m.private?)
707
+ else
708
+ delegate_privmsg(m)
709
+ end
710
+ else
711
+ # stuff to handle when not addressed
712
+ case m.message
713
+ when (/^\s*(hello|howdy|hola|salut|bonjour|sup|niihau|hey|hi(\W|$)|yo(\W|$))[\s,-.]+#{@nick}$/i)
714
+ say m.replyto, @lang.get("hello_X") % m.sourcenick
715
+ when (/^#{@nick}!*$/)
716
+ say m.replyto, @lang.get("hello_X") % m.sourcenick
717
+ else
718
+ @keywords.privmsg(m)
719
+ end
720
+ end
721
+ end
722
+
723
+ # log a message. Internal use only.
724
+ def log_sent(type, where, message)
725
+ case type
726
+ when "NOTICE"
727
+ if(where =~ /^#/)
728
+ log "-=#{@nick}=- #{message}", where
729
+ elsif (where =~ /(\S*)!.*/)
730
+ log "[-=#{where}=-] #{message}", $1
731
+ else
732
+ log "[-=#{where}=-] #{message}"
733
+ end
734
+ when "PRIVMSG"
735
+ if(where =~ /^#/)
736
+ log "<#{@nick}> #{message}", where
737
+ elsif (where =~ /^(\S*)!.*$/)
738
+ log "[msg(#{where})] #{message}", $1
739
+ else
740
+ log "[msg(#{where})] #{message}", where
741
+ end
742
+ end
743
+ end
744
+
745
+ def onjoin(m)
746
+ @channels[m.channel] = IRCChannel.new(m.channel) unless(@channels.has_key?(m.channel))
747
+ if(m.address?)
748
+ debug "joined channel #{m.channel}"
749
+ log "@ Joined channel #{m.channel}", m.channel
750
+ else
751
+ log "@ #{m.sourcenick} joined channel #{m.channel}", m.channel
752
+ @channels[m.channel].users[m.sourcenick] = Hash.new
753
+ @channels[m.channel].users[m.sourcenick]["mode"] = ""
754
+ end
755
+
756
+ @plugins.delegate("listen", m)
757
+ @plugins.delegate("join", m)
758
+ end
759
+
760
+ def onpart(m)
761
+ if(m.address?)
762
+ debug "left channel #{m.channel}"
763
+ log "@ Left channel #{m.channel} (#{m.message})", m.channel
764
+ @channels.delete(m.channel)
765
+ else
766
+ log "@ #{m.sourcenick} left channel #{m.channel} (#{m.message})", m.channel
767
+ @channels[m.channel].users.delete(m.sourcenick)
768
+ end
769
+
770
+ # delegate to plugins
771
+ @plugins.delegate("listen", m)
772
+ @plugins.delegate("part", m)
773
+ end
774
+
775
+ # respond to being kicked from a channel
776
+ def onkick(m)
777
+ if(m.address?)
778
+ debug "kicked from channel #{m.channel}"
779
+ @channels.delete(m.channel)
780
+ log "@ You have been kicked from #{m.channel} by #{m.sourcenick} (#{m.message})", m.channel
781
+ else
782
+ @channels[m.channel].users.delete(m.sourcenick)
783
+ log "@ #{m.target} has been kicked from #{m.channel} by #{m.sourcenick} (#{m.message})", m.channel
784
+ end
785
+
786
+ @plugins.delegate("listen", m)
787
+ @plugins.delegate("kick", m)
788
+ end
789
+
790
+ def ontopic(m)
791
+ @channels[m.channel] = IRCChannel.new(m.channel) unless(@channels.has_key?(m.channel))
792
+ @channels[m.channel].topic = m.topic if !m.topic.nil?
793
+ @channels[m.channel].topic.timestamp = m.timestamp if !m.timestamp.nil?
794
+ @channels[m.channel].topic.by = m.source if !m.source.nil?
795
+
796
+ debug "topic of channel #{m.channel} is now #{@channels[m.channel].topic}"
797
+ end
798
+
799
+ # delegate a privmsg to auth, keyword or plugin handlers
800
+ def delegate_privmsg(message)
801
+ [@auth, @plugins, @keywords].each {|m|
802
+ break if m.privmsg(message)
803
+ }
804
+ end
805
+
806
+ end
807
+
808
+ end