rbot 0.9.9

Sign up to get free protection for your applications and to get access to all the features.
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