rbot 0.9.9 → 0.9.10

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 (72) hide show
  1. data/AUTHORS +8 -0
  2. data/ChangeLog +51 -0
  3. data/INSTALL +4 -0
  4. data/README +1 -0
  5. data/REQUIREMENTS +11 -0
  6. data/TODO +2 -0
  7. data/bin/rbot +21 -2
  8. data/data/rbot/languages/german.lang +4 -1
  9. data/data/rbot/languages/russian.lang +75 -0
  10. data/data/rbot/plugins/autoop.rb +42 -51
  11. data/data/rbot/plugins/bans.rb +205 -0
  12. data/data/rbot/plugins/bash.rb +56 -0
  13. data/data/rbot/plugins/chucknorris.rb +74 -0
  14. data/data/rbot/plugins/chucknorris.yml.gz +0 -0
  15. data/data/rbot/plugins/deepthoughts.rb +95 -0
  16. data/data/rbot/plugins/demauro.rb +95 -0
  17. data/data/rbot/plugins/digg.rb +51 -0
  18. data/data/rbot/plugins/figlet.rb +24 -0
  19. data/data/rbot/plugins/forecast.rb +133 -0
  20. data/data/rbot/plugins/freshmeat.rb +13 -7
  21. data/data/rbot/plugins/google.rb +2 -0
  22. data/data/rbot/plugins/grouphug.rb +36 -0
  23. data/data/rbot/plugins/imdb.rb +92 -0
  24. data/data/rbot/plugins/insult.rb +8 -1
  25. data/data/rbot/plugins/iplookup.rb +227 -0
  26. data/data/rbot/plugins/karma.rb +2 -2
  27. data/data/rbot/plugins/keywords.rb +470 -0
  28. data/data/rbot/plugins/lart.rb +132 -146
  29. data/data/rbot/plugins/lastfm.rb +25 -0
  30. data/data/rbot/plugins/markov.rb +204 -0
  31. data/data/rbot/plugins/math.rb +5 -1
  32. data/data/rbot/plugins/nickserv.rb +71 -11
  33. data/data/rbot/plugins/opme.rb +19 -19
  34. data/data/rbot/plugins/quakeauth.rb +2 -2
  35. data/data/rbot/plugins/quotes.rb +40 -25
  36. data/data/rbot/plugins/remind.rb +1 -1
  37. data/data/rbot/plugins/rot13.rb +2 -2
  38. data/data/rbot/plugins/roulette.rb +49 -15
  39. data/data/rbot/plugins/rss.rb +585 -0
  40. data/data/rbot/plugins/rubyurl.rb +39 -0
  41. data/data/rbot/plugins/seen.rb +2 -1
  42. data/data/rbot/plugins/slashdot.rb +5 -5
  43. data/data/rbot/plugins/spell.rb +5 -0
  44. data/data/rbot/plugins/theyfightcrime.rb +121 -0
  45. data/data/rbot/plugins/threat.rb +55 -0
  46. data/data/rbot/plugins/tinyurl.rb +39 -0
  47. data/data/rbot/plugins/topic.rb +204 -0
  48. data/data/rbot/plugins/urban.rb +71 -0
  49. data/data/rbot/plugins/url.rb +399 -4
  50. data/data/rbot/plugins/wow.rb +123 -0
  51. data/data/rbot/plugins/wserver.rb +1 -1
  52. data/data/rbot/templates/levels.rbot +2 -0
  53. data/lib/rbot/auth.rb +207 -96
  54. data/lib/rbot/channel.rb +5 -5
  55. data/lib/rbot/config.rb +125 -24
  56. data/lib/rbot/dbhash.rb +87 -21
  57. data/lib/rbot/httputil.rb +181 -13
  58. data/lib/rbot/ircbot.rb +525 -179
  59. data/lib/rbot/ircsocket.rb +330 -54
  60. data/lib/rbot/message.rb +66 -23
  61. data/lib/rbot/messagemapper.rb +25 -17
  62. data/lib/rbot/plugins.rb +244 -115
  63. data/lib/rbot/post-clean.rb +1 -0
  64. data/lib/rbot/{post-install.rb → post-config.rb} +1 -1
  65. data/lib/rbot/rbotconfig.rb +29 -14
  66. data/lib/rbot/registry.rb +111 -72
  67. data/lib/rbot/rfc2812.rb +208 -197
  68. data/lib/rbot/timer.rb +4 -0
  69. data/lib/rbot/utils.rb +2 -2
  70. metadata +127 -104
  71. data/data/rbot/plugins/rss.rb.disabled +0 -414
  72. data/lib/rbot/keywords.rb +0 -433
data/lib/rbot/httputil.rb CHANGED
@@ -3,8 +3,9 @@ module Utils
3
3
 
4
4
  require 'resolv'
5
5
  require 'net/http'
6
+ require 'net/https'
6
7
  Net::HTTP.version_1_2
7
-
8
+
8
9
  # class for making http requests easier (mainly for plugins to use)
9
10
  # this class can check the bot proxy configuration to determine if a proxy
10
11
  # needs to be used, which includes support for per-url proxy configuration.
@@ -25,9 +26,22 @@ class HttpUtil
25
26
  BotConfig.register BotConfigArrayValue.new('http.proxy_exclude',
26
27
  :default => [],
27
28
  :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")
29
+ BotConfig.register BotConfigIntegerValue.new('http.max_redir',
30
+ :default => 5,
31
+ :desc => "Maximum number of redirections to be used when getting a document")
32
+ BotConfig.register BotConfigIntegerValue.new('http.expire_time',
33
+ :default => 60,
34
+ :desc => "After how many minutes since last use a cached document is considered to be expired")
35
+ BotConfig.register BotConfigIntegerValue.new('http.max_cache_time',
36
+ :default => 60*24,
37
+ :desc => "After how many minutes since first use a cached document is considered to be expired")
38
+ BotConfig.register BotConfigIntegerValue.new('http.no_expire_cache',
39
+ :default => false,
40
+ :desc => "Set this to true if you want the bot to never expire the cached pages")
28
41
 
29
42
  def initialize(bot)
30
43
  @bot = bot
44
+ @cache = Hash.new
31
45
  @headers = {
32
46
  'User-Agent' => "rbot http util #{$version} (http://linuxbrit.co.uk/rbot/)",
33
47
  }
@@ -47,7 +61,7 @@ class HttpUtil
47
61
  begin
48
62
  list.concat Resolv.getaddresses(uri.host)
49
63
  rescue StandardError => err
50
- puts "warning: couldn't resolve host uri.host"
64
+ warning "couldn't resolve host uri.host"
51
65
  end
52
66
 
53
67
  unless @bot.config["http.proxy_exclude"].empty?
@@ -79,7 +93,7 @@ class HttpUtil
79
93
  # uri:: Uri to create a proxy for
80
94
  #
81
95
  # return a net/http Proxy object, which is configured correctly for
82
- # proxying based on the bot's proxy configuration.
96
+ # proxying based on the bot's proxy configuration.
83
97
  # This will include per-url proxy configuration based on the bot config
84
98
  # +http_proxy_include/exclude+ options.
85
99
  def get_proxy(uri)
@@ -97,7 +111,7 @@ class HttpUtil
97
111
  proxy = URI.parse @bot.config["http.proxy_uri"] rescue nil
98
112
  end
99
113
  if proxy
100
- debug "proxy is set to #{proxy.uri}"
114
+ debug "proxy is set to #{proxy.host} port #{proxy.port}"
101
115
  if proxy_required(uri)
102
116
  proxy_host = proxy.host
103
117
  proxy_port = proxy.port
@@ -106,36 +120,190 @@ class HttpUtil
106
120
  end
107
121
  end
108
122
  end
109
-
110
- return Net::HTTP.new(uri.host, uri.port, proxy_host, proxy_port, proxy_user, proxy_port)
123
+
124
+ h = Net::HTTP.new(uri.host, uri.port, proxy_host, proxy_port, proxy_user, proxy_port)
125
+ h.use_ssl = true if uri.scheme == "https"
126
+ return h
111
127
  end
112
128
 
113
129
  # uri:: uri to query (Uri object)
114
130
  # readtimeout:: timeout for reading the response
115
131
  # opentimeout:: timeout for opening the connection
116
132
  #
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)
133
+ # simple get request, returns (if possible) response body following redirs
134
+ # and caching if requested
135
+ # if a block is given, it yields the urls it gets redirected to
136
+ # TODO we really need something to implement proper caching
137
+ def get(uri_or_str, readtimeout=10, opentimeout=5, max_redir=@bot.config["http.max_redir"], cache=false)
138
+ if uri_or_str.class <= URI
139
+ uri = uri_or_str
140
+ else
141
+ uri = URI.parse(uri_or_str.to_s)
142
+ end
143
+
120
144
  proxy = get_proxy(uri)
121
145
  proxy.open_timeout = opentimeout
122
146
  proxy.read_timeout = readtimeout
123
-
147
+
124
148
  begin
125
149
  proxy.start() {|http|
150
+ yield uri.request_uri() if block_given?
126
151
  resp = http.get(uri.request_uri(), @headers)
127
- if resp.code == "200"
152
+ case resp
153
+ when Net::HTTPSuccess
154
+ if cache && !(resp.key?('cache-control') && resp['cache-control']=='must-revalidate')
155
+ k = uri.to_s
156
+ @cache[k] = Hash.new
157
+ @cache[k][:body] = resp.body
158
+ @cache[k][:last_mod] = Time.httpdate(resp['last-modified']) if resp.key?('last-modified')
159
+ if resp.key?('date')
160
+ @cache[k][:first_use] = Time.httpdate(resp['date'])
161
+ @cache[k][:last_use] = Time.httpdate(resp['date'])
162
+ else
163
+ now = Time.new
164
+ @cache[k][:first_use] = now
165
+ @cache[k][:last_use] = now
166
+ end
167
+ @cache[k][:count] = 1
168
+ end
128
169
  return resp.body
170
+ when Net::HTTPRedirection
171
+ debug "Redirecting #{uri} to #{resp['location']}"
172
+ yield resp['location'] if block_given?
173
+ if max_redir > 0
174
+ return get( URI.parse(resp['location']), readtimeout, opentimeout, max_redir-1, cache)
175
+ else
176
+ warning "Max redirection reached, not going to #{resp['location']}"
177
+ end
178
+ else
179
+ debug "HttpUtil.get return code #{resp.code} #{resp.body}"
180
+ end
181
+ return nil
182
+ }
183
+ rescue StandardError, Timeout::Error => e
184
+ error "HttpUtil.get exception: #{e.inspect}, while trying to get #{uri}"
185
+ debug e.backtrace.join("\n")
186
+ end
187
+ return nil
188
+ end
189
+
190
+ # just like the above, but only gets the head
191
+ def head(uri_or_str, readtimeout=10, opentimeout=5, max_redir=@bot.config["http.max_redir"])
192
+ if uri_or_str.class <= URI
193
+ uri = uri_or_str
194
+ else
195
+ uri = URI.parse(uri_or_str.to_s)
196
+ end
197
+
198
+ proxy = get_proxy(uri)
199
+ proxy.open_timeout = opentimeout
200
+ proxy.read_timeout = readtimeout
201
+
202
+ begin
203
+ proxy.start() {|http|
204
+ yield uri.request_uri() if block_given?
205
+ resp = http.head(uri.request_uri(), @headers)
206
+ case resp
207
+ when Net::HTTPSuccess
208
+ return resp
209
+ when Net::HTTPRedirection
210
+ debug "Redirecting #{uri} to #{resp['location']}"
211
+ yield resp['location'] if block_given?
212
+ if max_redir > 0
213
+ return head( URI.parse(resp['location']), readtimeout, opentimeout, max_redir-1)
214
+ else
215
+ warning "Max redirection reached, not going to #{resp['location']}"
216
+ end
129
217
  else
130
- puts "HttpUtil.get return code #{resp.code} #{resp.body}"
218
+ debug "HttpUtil.head return code #{resp.code}"
131
219
  end
132
220
  return nil
133
221
  }
134
222
  rescue StandardError, Timeout::Error => e
135
- $stderr.puts "HttpUtil.get exception: #{e}, while trying to get #{uri}"
223
+ error "HttpUtil.head exception: #{e.inspect}, while trying to get #{uri}"
224
+ debug e.backtrace.join("\n")
136
225
  end
137
226
  return nil
138
227
  end
228
+
229
+ # gets a page from the cache if it's still (assumed to be) valid
230
+ # TODO remove stale cached pages, except when called with noexpire=true
231
+ def get_cached(uri_or_str, readtimeout=10, opentimeout=5,
232
+ max_redir=@bot.config['http.max_redir'],
233
+ noexpire=@bot.config['http.no_expire_cache'])
234
+ if uri_or_str.class <= URI
235
+ uri = uri_or_str
236
+ else
237
+ uri = URI.parse(uri_or_str.to_s)
238
+ end
239
+
240
+ k = uri.to_s
241
+ if !@cache.key?(k)
242
+ remove_stale_cache unless noexpire
243
+ return get(uri, readtimeout, opentimeout, max_redir, true)
244
+ end
245
+ now = Time.new
246
+ begin
247
+ # See if the last-modified header can be used
248
+ # Assumption: the page was not modified if both the header
249
+ # and the cached copy have the last-modified value, and it's the same time
250
+ # If only one of the cached copy and the header have the value, or if the
251
+ # value is different, we assume that the cached copyis invalid and therefore
252
+ # get a new one.
253
+ # On our first try, we tested for last-modified in the webpage first,
254
+ # and then on the local cache. however, this is stupid (in general),
255
+ # so we only test for the remote page if the local copy had the header
256
+ # in the first place.
257
+ if @cache[k].key?(:last_mod)
258
+ h = head(uri, readtimeout, opentimeout, max_redir)
259
+ if h.key?('last-modified')
260
+ if Time.httpdate(h['last-modified']) == @cache[k][:last_mod]
261
+ if h.key?('date')
262
+ @cache[k][:last_use] = Time.httpdate(h['date'])
263
+ else
264
+ @cache[k][:last_use] = now
265
+ end
266
+ @cache[k][:count] += 1
267
+ return @cache[k][:body]
268
+ end
269
+ remove_stale_cache unless noexpire
270
+ return get(uri, readtimeout, opentimeout, max_redir, true)
271
+ end
272
+ remove_stale_cache unless noexpire
273
+ return get(uri, readtimeout, opentimeout, max_redir, true)
274
+ end
275
+ rescue => e
276
+ warning "Error #{e.inspect} getting the page #{uri}, using cache"
277
+ debug e.backtrace.join("\n")
278
+ return @cache[k][:body]
279
+ end
280
+ # If we still haven't returned, we are dealing with a non-redirected document
281
+ # that doesn't have the last-modified attribute
282
+ debug "Could not use last-modified attribute for URL #{uri}, guessing cache validity"
283
+ if noexpire or !expired?(@cache[k], now)
284
+ @cache[k][:count] += 1
285
+ @cache[k][:last_use] = now
286
+ debug "Using cache"
287
+ return @cache[k][:body]
288
+ end
289
+ debug "Cache expired, getting anew"
290
+ @cache.delete(k)
291
+ remove_stale_cache unless noexpire
292
+ return get(uri, readtimeout, opentimeout, max_redir, true)
293
+ end
294
+
295
+ def expired?(hash, time)
296
+ (time - hash[:last_use] > @bot.config['http.expire_time']*60) or
297
+ (time - hash[:first_use] > @bot.config['http.max_cache_time']*60)
298
+ end
299
+
300
+ def remove_stale_cache
301
+ now = Time.new
302
+ @cache.reject! { |k, val|
303
+ !val.key?(:last_modified) && expired?(val, now)
304
+ }
305
+ end
306
+
139
307
  end
140
308
  end
141
309
  end
data/lib/rbot/ircbot.rb CHANGED
@@ -1,21 +1,77 @@
1
1
  require 'thread'
2
+
2
3
  require 'etc'
3
4
  require 'fileutils'
5
+ require 'logger'
4
6
 
5
7
  $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
8
+ $daemonize = false unless $daemonize
9
+
10
+ $dateformat = "%Y/%m/%d %H:%M:%S"
11
+ $logger = Logger.new($stderr)
12
+ $logger.datetime_format = $dateformat
13
+ $logger.level = $cl_loglevel if $cl_loglevel
14
+ $logger.level = 0 if $debug
15
+
16
+ def rawlog(level, message=nil, who_pos=1)
17
+ call_stack = caller
18
+ if call_stack.length > who_pos
19
+ who = call_stack[who_pos].sub(%r{(?:.+)/([^/]+):(\d+)(:in .*)?}) { "#{$1}:#{$2}#{$3}" }
20
+ else
21
+ who = "(unknown)"
22
+ end
23
+ # Output each line. To distinguish between separate messages and multi-line
24
+ # messages originating at the same time, we blank #{who} after the first message
25
+ # is output.
26
+ message.to_s.each_line { |l|
27
+ $logger.add(level, l.chomp, who)
28
+ who.gsub!(/./," ")
29
+ }
30
+ end
31
+
32
+ def log_session_start
33
+ $logger << "\n\n=== #{botclass} session started on #{Time.now.strftime($dateformat)} ===\n\n"
34
+ end
35
+
36
+ def log_session_end
37
+ $logger << "\n\n=== #{botclass} session ended on #{Time.now.strftime($dateformat)} ===\n\n"
38
+ end
39
+
40
+ def debug(message=nil, who_pos=1)
41
+ rawlog(Logger::Severity::DEBUG, message, who_pos)
10
42
  end
11
43
 
44
+ def log(message=nil, who_pos=1)
45
+ rawlog(Logger::Severity::INFO, message, who_pos)
46
+ end
47
+
48
+ def warning(message=nil, who_pos=1)
49
+ rawlog(Logger::Severity::WARN, message, who_pos)
50
+ end
51
+
52
+ def error(message=nil, who_pos=1)
53
+ rawlog(Logger::Severity::ERROR, message, who_pos)
54
+ end
55
+
56
+ def fatal(message=nil, who_pos=1)
57
+ rawlog(Logger::Severity::FATAL, message, who_pos)
58
+ end
59
+
60
+ debug "debug test"
61
+ log "log test"
62
+ warning "warning test"
63
+ error "error test"
64
+ fatal "fatal test"
65
+
66
+ # The following global is used for the improved signal handling.
67
+ $interrupted = 0
68
+
12
69
  # these first
13
70
  require 'rbot/rbotconfig'
14
71
  require 'rbot/config'
15
72
  require 'rbot/utils'
16
73
 
17
74
  require 'rbot/rfc2812'
18
- require 'rbot/keywords'
19
75
  require 'rbot/ircsocket'
20
76
  require 'rbot/auth'
21
77
  require 'rbot/timer'
@@ -34,25 +90,25 @@ module Irc
34
90
  class IrcBot
35
91
  # the bot's current nickname
36
92
  attr_reader :nick
37
-
93
+
38
94
  # the bot's IrcAuth data
39
95
  attr_reader :auth
40
-
96
+
41
97
  # the bot's BotConfig data
42
98
  attr_reader :config
43
-
99
+
44
100
  # the botclass for this bot (determines configdir among other things)
45
101
  attr_reader :botclass
46
-
102
+
47
103
  # used to perform actions periodically (saves configuration once per minute
48
104
  # by default)
49
105
  attr_reader :timer
50
-
106
+
51
107
  # bot's Language data
52
108
  attr_reader :lang
53
109
 
54
- # bot's configured addressing prefixes
55
- attr_reader :addressing_prefixes
110
+ # capabilities info for the server
111
+ attr_reader :capabilities
56
112
 
57
113
  # channel info for channels the bot is in
58
114
  attr_reader :channels
@@ -65,6 +121,9 @@ class IrcBot
65
121
  # and restore objects in their own namespaces.)
66
122
  attr_reader :registry
67
123
 
124
+ # bot's plugins. This is an instance of class Plugins
125
+ attr_reader :plugins
126
+
68
127
  # bot's httputil help object, for fetching resources via http. Sets up
69
128
  # proxies etc as defined by the bot configuration/environment
70
129
  attr_reader :httputil
@@ -72,13 +131,14 @@ class IrcBot
72
131
  # create a new IrcBot with botclass +botclass+
73
132
  def initialize(botclass, params = {})
74
133
  # BotConfig for the core bot
134
+ # TODO should we split socket stuff into ircsocket, etc?
75
135
  BotConfig.register BotConfigStringValue.new('server.name',
76
136
  :default => "localhost", :requires_restart => true,
77
137
  :desc => "What server should the bot connect to?",
78
138
  :wizard => true)
79
139
  BotConfig.register BotConfigIntegerValue.new('server.port',
80
140
  :default => 6667, :type => :integer, :requires_restart => true,
81
- :desc => "What port should the bot connect to?",
141
+ :desc => "What port should the bot connect to?",
82
142
  :validate => Proc.new {|v| v > 0}, :wizard => true)
83
143
  BotConfig.register BotConfigStringValue.new('server.password',
84
144
  :default => false, :requires_restart => true,
@@ -91,6 +151,23 @@ class IrcBot
91
151
  BotConfig.register BotConfigIntegerValue.new('server.reconnect_wait',
92
152
  :default => 5, :validate => Proc.new{|v| v >= 0},
93
153
  :desc => "Seconds to wait before attempting to reconnect, on disconnect")
154
+ BotConfig.register BotConfigFloatValue.new('server.sendq_delay',
155
+ :default => 2.0, :validate => Proc.new{|v| v >= 0},
156
+ :desc => "(flood prevention) the delay between sending messages to the server (in seconds)",
157
+ :on_change => Proc.new {|bot, v| bot.socket.sendq_delay = v })
158
+ BotConfig.register BotConfigIntegerValue.new('server.sendq_burst',
159
+ :default => 4, :validate => Proc.new{|v| v >= 0},
160
+ :desc => "(flood prevention) max lines to burst to the server before throttling. Most ircd's allow bursts of up 5 lines",
161
+ :on_change => Proc.new {|bot, v| bot.socket.sendq_burst = v })
162
+ BotConfig.register BotConfigStringValue.new('server.byterate',
163
+ :default => "400/2", :validate => Proc.new{|v| v.match(/\d+\/\d/)},
164
+ :desc => "(flood prevention) max bytes/seconds rate to send the server. Most ircd's have limits of 512 bytes/2 seconds",
165
+ :on_change => Proc.new {|bot, v| bot.socket.byterate = v })
166
+ BotConfig.register BotConfigIntegerValue.new('server.ping_timeout',
167
+ :default => 30, :validate => Proc.new{|v| v >= 0},
168
+ :on_change => Proc.new {|bot, v| bot.start_server_pings},
169
+ :desc => "reconnect if server doesn't respond to PING within this many seconds (set to 0 to disable)")
170
+
94
171
  BotConfig.register BotConfigStringValue.new('irc.nick', :default => "rbot",
95
172
  :desc => "IRC nickname the bot should attempt to use", :wizard => true,
96
173
  :on_change => Proc.new{|bot, v| bot.sendq "NICK #{v}" })
@@ -100,65 +177,173 @@ class IrcBot
100
177
  BotConfig.register BotConfigArrayValue.new('irc.join_channels',
101
178
  :default => [], :wizard => true,
102
179
  :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'")
180
+ BotConfig.register BotConfigArrayValue.new('irc.ignore_users',
181
+ :default => [],
182
+ :desc => "Which users to ignore input from. This is mainly to avoid bot-wars triggered by creative people")
183
+
103
184
  BotConfig.register BotConfigIntegerValue.new('core.save_every',
104
185
  :default => 60, :validate => Proc.new{|v| v >= 0},
105
186
  # 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 })
187
+ :desc => "How often the bot should persist all configuration to disk (in case of a server crash, for example)")
188
+
189
+ BotConfig.register BotConfigBooleanValue.new('core.run_as_daemon',
190
+ :default => false, :requires_restart => true,
191
+ :desc => "Should the bot run as a daemon?")
192
+
193
+ BotConfig.register BotConfigStringValue.new('log.file',
194
+ :default => false, :requires_restart => true,
195
+ :desc => "Name of the logfile to which console messages will be redirected when the bot is run as a daemon")
196
+ BotConfig.register BotConfigIntegerValue.new('log.level',
197
+ :default => 1, :requires_restart => false,
198
+ :validate => Proc.new { |v| (0..5).include?(v) },
199
+ :on_change => Proc.new { |bot, v|
200
+ $logger.level = v
201
+ },
202
+ :desc => "The minimum logging level (0=DEBUG,1=INFO,2=WARN,3=ERROR,4=FATAL) for console messages")
203
+ BotConfig.register BotConfigIntegerValue.new('log.keep',
204
+ :default => 1, :requires_restart => true,
205
+ :validate => Proc.new { |v| v >= 0 },
206
+ :desc => "How many old console messages logfiles to keep")
207
+ BotConfig.register BotConfigIntegerValue.new('log.max_size',
208
+ :default => 10, :requires_restart => true,
209
+ :validate => Proc.new { |v| v > 0 },
210
+ :desc => "Maximum console messages logfile size (in megabytes)")
115
211
 
116
212
  @argv = params[:argv]
117
213
 
118
214
  unless FileTest.directory? Config::datadir
119
- puts "data directory '#{Config::datadir}' not found, did you install.rb?"
215
+ error "data directory '#{Config::datadir}' not found, did you setup.rb?"
120
216
  exit 2
121
217
  end
122
-
123
- botclass = "/home/#{Etc.getlogin}/.rbot" unless botclass
218
+
219
+ unless botclass and not botclass.empty?
220
+ # We want to find a sensible default.
221
+ # * On POSIX systems we prefer ~/.rbot for the effective uid of the process
222
+ # * On Windows (at least the NT versions) we want to put our stuff in the
223
+ # Application Data folder.
224
+ # We don't use any particular O/S detection magic, exploiting the fact that
225
+ # Etc.getpwuid is nil on Windows
226
+ if Etc.getpwuid(Process::Sys.geteuid)
227
+ botclass = Etc.getpwuid(Process::Sys.geteuid)[:dir].dup
228
+ else
229
+ if ENV.has_key?('APPDATA')
230
+ botclass = ENV['APPDATA'].dup
231
+ botclass.gsub!("\\","/")
232
+ end
233
+ end
234
+ botclass += "/.rbot"
235
+ end
236
+ botclass = File.expand_path(botclass)
124
237
  @botclass = botclass.gsub(/\/$/, "")
125
238
 
126
239
  unless FileTest.directory? botclass
127
- puts "no #{botclass} directory found, creating from templates.."
240
+ log "no #{botclass} directory found, creating from templates.."
128
241
  if FileTest.exist? botclass
129
- puts "Error: file #{botclass} exists but isn't a directory"
242
+ error "file #{botclass} exists but isn't a directory"
130
243
  exit 2
131
244
  end
132
245
  FileUtils.cp_r Config::datadir+'/templates', botclass
133
246
  end
134
-
247
+
135
248
  Dir.mkdir("#{botclass}/logs") unless File.exist?("#{botclass}/logs")
249
+ Dir.mkdir("#{botclass}/registry") unless File.exist?("#{botclass}/registry")
136
250
 
251
+ @ping_timer = nil
252
+ @pong_timer = nil
253
+ @last_ping = nil
137
254
  @startup_time = Time.new
138
255
  @config = BotConfig.new(self)
256
+
257
+ if @config['core.run_as_daemon']
258
+ $daemonize = true
259
+ end
260
+
261
+ @logfile = @config['log.file']
262
+ if @logfile.class!=String || @logfile.empty?
263
+ @logfile = "#{botclass}/#{File.basename(botclass).gsub(/^\.+/,'')}.log"
264
+ end
265
+
266
+ # See http://blog.humlab.umu.se/samuel/archives/000107.html
267
+ # for the backgrounding code
268
+ if $daemonize
269
+ begin
270
+ exit if fork
271
+ Process.setsid
272
+ exit if fork
273
+ rescue NotImplementedError
274
+ warning "Could not background, fork not supported"
275
+ rescue => e
276
+ warning "Could not background. #{e.inspect}"
277
+ end
278
+ Dir.chdir botclass
279
+ # File.umask 0000 # Ensure sensible umask. Adjust as needed.
280
+ log "Redirecting standard input/output/error"
281
+ begin
282
+ STDIN.reopen "/dev/null"
283
+ rescue Errno::ENOENT
284
+ # On Windows, there's not such thing as /dev/null
285
+ STDIN.reopen "NUL"
286
+ end
287
+ def STDOUT.write(str=nil)
288
+ log str, 2
289
+ return str.to_s.length
290
+ end
291
+ def STDERR.write(str=nil)
292
+ if str.to_s.match(/:\d+: warning:/)
293
+ warning str, 2
294
+ else
295
+ error str, 2
296
+ end
297
+ return str.to_s.length
298
+ end
299
+ end
300
+
301
+ # Set the new logfile and loglevel. This must be done after the daemonizing
302
+ $logger = Logger.new(@logfile, @config['log.keep'], @config['log.max_size']*1024*1024)
303
+ $logger.datetime_format= $dateformat
304
+ $logger.level = @config['log.level']
305
+ $logger.level = $cl_loglevel if $cl_loglevel
306
+ $logger.level = 0 if $debug
307
+
308
+ log_session_start
309
+
139
310
  @timer = Timer::Timer.new(1.0) # only need per-second granularity
140
311
  @registry = BotRegistry.new self
312
+ @save_mutex = Mutex.new
141
313
  @timer.add(@config['core.save_every']) { save } if @config['core.save_every']
142
314
  @channels = Hash.new
143
315
  @logs = Hash.new
144
-
145
316
  @httputil = Utils::HttpUtil.new(self)
146
317
  @lang = Language::Language.new(@config['core.language'])
147
- @keywords = Keywords.new(self)
148
- @auth = IrcAuth.new(self)
318
+ begin
319
+ @auth = IrcAuth.new(self)
320
+ rescue => e
321
+ fatal e.inspect
322
+ fatal e.backtrace.join("\n")
323
+ log_session_end
324
+ exit 2
325
+ end
149
326
 
150
327
  Dir.mkdir("#{botclass}/plugins") unless File.exist?("#{botclass}/plugins")
151
328
  @plugins = Plugins::Plugins.new(self, ["#{botclass}/plugins"])
152
329
 
153
330
  @socket = IrcSocket.new(@config['server.name'], @config['server.port'], @config['server.bindhost'], @config['server.sendq_delay'], @config['server.sendq_burst'])
154
331
  @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
-
332
+
161
333
  @client = IrcClient.new
334
+ @client[:isupport] = proc { |data|
335
+ if data[:capab]
336
+ sendq "CAPAB IDENTIFY-MSG"
337
+ end
338
+ }
339
+ @client[:datastr] = proc { |data|
340
+ debug data.inspect
341
+ if data[:text] == "IDENTIFY-MSG"
342
+ @capabilities["identify-msg".to_sym] = true
343
+ else
344
+ debug "Not handling RPL_DATASTR #{data[:servermessage]}"
345
+ end
346
+ }
162
347
  @client[:privmsg] = proc { |data|
163
348
  message = PrivMessage.new(self, data[:source], data[:target], data[:message])
164
349
  onprivmsg(message)
@@ -170,18 +355,21 @@ class IrcBot
170
355
  }
171
356
  @client[:motd] = proc { |data|
172
357
  data[:motd].each_line { |line|
173
- log "MOTD: #{line}", "server"
358
+ irclog "MOTD: #{line}", "server"
174
359
  }
175
360
  }
176
- @client[:nicktaken] = proc { |data|
361
+ @client[:nicktaken] = proc { |data|
177
362
  nickchg "#{data[:nick]}_"
363
+ @plugins.delegate "nicktaken", data[:nick]
178
364
  }
179
- @client[:badnick] = proc {|data|
180
- puts "WARNING, bad nick (#{data[:nick]})"
365
+ @client[:badnick] = proc {|data|
366
+ warning "bad nick (#{data[:nick]})"
181
367
  }
182
368
  @client[:ping] = proc {|data|
183
- # (jump the queue for pongs)
184
- @socket.puts "PONG #{data[:pingid]}"
369
+ @socket.queue "PONG #{data[:pingid]}"
370
+ }
371
+ @client[:pong] = proc {|data|
372
+ @last_ping = nil
185
373
  }
186
374
  @client[:nick] = proc {|data|
187
375
  sourcenick = data[:sourcenick]
@@ -193,7 +381,7 @@ class IrcBot
193
381
  end
194
382
  @channels.each {|k,v|
195
383
  if(v.users.has_key?(sourcenick))
196
- log "@ #{sourcenick} is now known as #{nick}", k
384
+ irclog "@ #{sourcenick} is now known as #{nick}", k
197
385
  v.users[nick] = v.users[sourcenick]
198
386
  v.users.delete(sourcenick)
199
387
  end
@@ -207,11 +395,11 @@ class IrcBot
207
395
  sourceurl = data[:sourceaddress]
208
396
  message = data[:message]
209
397
  m = QuitMessage.new(self, data[:source], data[:sourcenick], data[:message])
210
- if(data[:sourcenick] =~ /#{@nick}/i)
398
+ if(data[:sourcenick] =~ /#{Regexp.escape(@nick)}/i)
211
399
  else
212
400
  @channels.each {|k,v|
213
401
  if(v.users.has_key?(sourcenick))
214
- log "@ Quit: #{sourcenick}: #{message}", k
402
+ irclog "@ Quit: #{sourcenick}: #{message}", k
215
403
  v.users.delete(sourcenick)
216
404
  end
217
405
  }
@@ -226,10 +414,10 @@ class IrcBot
226
414
  channel = data[:channel]
227
415
  targets = data[:targets]
228
416
  modestring = data[:modestring]
229
- log "@ Mode #{modestring} #{targets} by #{sourcenick}", channel
417
+ irclog "@ Mode #{modestring} #{targets} by #{sourcenick}", channel
230
418
  }
231
419
  @client[:welcome] = proc {|data|
232
- log "joined server #{data[:source]} as #{data[:nick]}", "server"
420
+ irclog "joined server #{data[:source]} as #{data[:nick]}", "server"
233
421
  debug "I think my nick is #{@nick}, server thinks #{data[:nick]}"
234
422
  if data[:nick] && data[:nick].length > 0
235
423
  @nick = data[:nick]
@@ -255,11 +443,11 @@ class IrcBot
255
443
  onpart(m)
256
444
  }
257
445
  @client[:kick] = proc {|data|
258
- m = KickMessage.new(self, data[:source], data[:target],data[:channel],data[:message])
446
+ m = KickMessage.new(self, data[:source], data[:target],data[:channel],data[:message])
259
447
  onkick(m)
260
448
  }
261
449
  @client[:invite] = proc {|data|
262
- if(data[:target] =~ /^#{@nick}$/i)
450
+ if(data[:target] =~ /^#{Regexp.escape(@nick)}$/i)
263
451
  join data[:channel] if (@auth.allow?("join", data[:source], data[:sourcenick]))
264
452
  end
265
453
  }
@@ -269,9 +457,9 @@ class IrcBot
269
457
  topic = data[:topic]
270
458
  timestamp = data[:unixtime] || Time.now.to_i
271
459
  if(sourcenick == @nick)
272
- log "@ I set topic \"#{topic}\"", channel
460
+ irclog "@ I set topic \"#{topic}\"", channel
273
461
  else
274
- log "@ #{sourcenick} set topic \"#{topic}\"", channel
462
+ irclog "@ #{sourcenick} set topic \"#{topic}\"", channel
275
463
  end
276
464
  m = TopicMessage.new(self, data[:source], data[:channel], timestamp, data[:topic])
277
465
 
@@ -288,66 +476,117 @@ class IrcBot
288
476
  channel = data[:channel]
289
477
  users = data[:users]
290
478
  unless(@channels[channel])
291
- puts "bug: got names for channel '#{channel}' I didn't think I was in\n"
292
- exit 2
479
+ warning "got names for channel '#{channel}' I didn't think I was in\n"
480
+ # exit 2
293
481
  end
294
482
  @channels[channel].users.clear
295
483
  users.each {|u|
296
484
  @channels[channel].users[u[0].sub(/^[@&~+]/, '')] = ["mode", u[1]]
297
485
  }
486
+ @plugins.delegate "names", data[:channel], data[:users]
298
487
  }
299
488
  @client[:unknown] = proc {|data|
300
489
  #debug "UNKNOWN: #{data[:serverstring]}"
301
- log data[:serverstring], ":unknown"
490
+ irclog data[:serverstring], ".unknown"
302
491
  }
303
492
  end
304
493
 
494
+ def got_sig(sig)
495
+ debug "received #{sig}, queueing quit"
496
+ $interrupted += 1
497
+ debug "interrupted #{$interrupted} times"
498
+ if $interrupted >= 5
499
+ debug "drastic!"
500
+ log_session_end
501
+ exit 2
502
+ elsif $interrupted >= 3
503
+ debug "quitting"
504
+ quit
505
+ end
506
+ end
507
+
305
508
  # connect the bot to IRC
306
509
  def connect
307
- trap("SIGTERM") { quit }
308
- trap("SIGHUP") { quit }
309
- trap("SIGINT") { quit }
310
510
  begin
511
+ trap("SIGINT") { got_sig("SIGINT") }
512
+ trap("SIGTERM") { got_sig("SIGTERM") }
513
+ trap("SIGHUP") { got_sig("SIGHUP") }
514
+ rescue ArgumentError => e
515
+ debug "failed to trap signals (#{e.inspect}): running on Windows?"
516
+ rescue => e
517
+ debug "failed to trap signals: #{e.inspect}"
518
+ end
519
+ begin
520
+ quit if $interrupted > 0
311
521
  @socket.connect
312
- rescue => e
313
- raise "failed to connect to IRC server at #{@config['server.name']} #{@config['server.port']}: " + e
522
+ rescue => e
523
+ raise e.class, "failed to connect to IRC server at #{@config['server.name']} #{@config['server.port']}: " + e
314
524
  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"
525
+ @socket.emergency_puts "PASS " + @config['server.password'] if @config['server.password']
526
+ @socket.emergency_puts "NICK #{@nick}\nUSER #{@config['irc.user']} 4 #{@config['server.name']} :Ruby bot. (c) Tom Gilbert"
527
+ @capabilities = Hash.new
528
+ start_server_pings
317
529
  end
318
530
 
319
531
  # begin event handling loop
320
532
  def mainloop
321
533
  while true
322
- connect
323
- @timer.start
324
-
325
534
  begin
326
- while true
535
+ quit if $interrupted > 0
536
+ connect
537
+ @timer.start
538
+
539
+ while @socket.connected?
327
540
  if @socket.select
328
541
  break unless reply = @socket.gets
329
542
  @client.process reply
330
543
  end
544
+ quit if $interrupted > 0
331
545
  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")
546
+
547
+ # I despair of this. Some of my users get "connection reset by peer"
548
+ # exceptions that ARENT SocketError's. How am I supposed to handle
549
+ # that?
550
+ rescue SystemExit
551
+ log_session_end
552
+ exit 0
553
+ rescue Errno::ETIMEDOUT, TimeoutError, SocketError => e
554
+ error "network exception: #{e.class}: #{e}"
555
+ debug e.backtrace.join("\n")
556
+ rescue BDB::Fatal => e
557
+ fatal "fatal bdb error: #{e.class}: #{e}"
558
+ fatal e.backtrace.join("\n")
559
+ DBTree.stats
560
+ # Why restart? DB problems are serious stuff ...
561
+ # restart("Oops, we seem to have registry problems ...")
562
+ log_session_end
563
+ exit 2
564
+ rescue Exception => e
565
+ error "non-net exception: #{e.class}: #{e}"
566
+ error e.backtrace.join("\n")
567
+ rescue => e
568
+ fatal "unexpected exception: #{e.class}: #{e}"
569
+ fatal e.backtrace.join("\n")
570
+ log_session_end
339
571
  exit 2
340
572
  end
341
-
342
- puts "disconnected"
573
+
574
+ stop_server_pings
343
575
  @channels.clear
344
- @socket.clearq
345
-
346
- puts "waiting to reconnect"
576
+ if @socket.connected?
577
+ @socket.clearq
578
+ @socket.shutdown
579
+ end
580
+
581
+ log "disconnected"
582
+
583
+ quit if $interrupted > 0
584
+
585
+ log "waiting to reconnect"
347
586
  sleep @config['server.reconnect_wait']
348
587
  end
349
588
  end
350
-
589
+
351
590
  # type:: message type
352
591
  # where:: message target
353
592
  # message:: message text
@@ -355,12 +594,16 @@ class IrcBot
355
594
  # Type can be PRIVMSG, NOTICE, etc, but those you should really use the
356
595
  # relevant say() or notice() methods. This one should be used for IRCd
357
596
  # 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
597
+ def sendmsg(type, where, message, chan=nil, ring=0)
598
+ # limit it according to the byterate, splitting the message
599
+ # taking into consideration the actual message length
600
+ # and all the extra stuff
601
+ # TODO allow something to do for commands that produce too many messages
602
+ # TODO example: math 10**10000
603
+ left = @socket.bytes_per - type.length - where.length - 4
361
604
  begin
362
605
  if(left >= message.length)
363
- sendq("#{type} #{where} :#{message}")
606
+ sendq "#{type} #{where} :#{message}", chan, ring
364
607
  log_sent(type, where, message)
365
608
  return
366
609
  end
@@ -370,46 +613,88 @@ class IrcBot
370
613
  message = line.slice!(lastspace, line.length) + message
371
614
  message.gsub!(/^\s+/, "")
372
615
  end
373
- sendq("#{type} #{where} :#{line}")
616
+ sendq "#{type} #{where} :#{line}", chan, ring
374
617
  log_sent(type, where, line)
375
618
  end while(message.length > 0)
376
619
  end
377
620
 
378
621
  # queue an arbitraty message for the server
379
- def sendq(message="")
622
+ def sendq(message="", chan=nil, ring=0)
380
623
  # temporary
381
- @socket.queue(message)
624
+ @socket.queue(message, chan, ring)
382
625
  end
383
626
 
384
627
  # send a notice message to channel/nick +where+
385
- def notice(where, message)
628
+ def notice(where, message, mchan=nil, mring=-1)
629
+ if mchan == ""
630
+ chan = where
631
+ else
632
+ chan = mchan
633
+ end
634
+ if mring < 0
635
+ if where =~ /^#/
636
+ ring = 2
637
+ else
638
+ ring = 1
639
+ end
640
+ else
641
+ ring = mring
642
+ end
386
643
  message.each_line { |line|
387
644
  line.chomp!
388
645
  next unless(line.length > 0)
389
- sendmsg("NOTICE", where, line)
646
+ sendmsg "NOTICE", where, line, chan, ring
390
647
  }
391
648
  end
392
649
 
393
650
  # say something (PRIVMSG) to channel/nick +where+
394
- def say(where, message)
651
+ def say(where, message, mchan="", mring=-1)
652
+ if mchan == ""
653
+ chan = where
654
+ else
655
+ chan = mchan
656
+ end
657
+ if mring < 0
658
+ if where =~ /^#/
659
+ ring = 2
660
+ else
661
+ ring = 1
662
+ end
663
+ else
664
+ ring = mring
665
+ end
395
666
  message.to_s.gsub(/[\r\n]+/, "\n").each_line { |line|
396
667
  line.chomp!
397
668
  next unless(line.length > 0)
398
669
  unless((where =~ /^#/) && (@channels.has_key?(where) && @channels[where].quiet))
399
- sendmsg("PRIVMSG", where, line)
670
+ sendmsg "PRIVMSG", where, line, chan, ring
400
671
  end
401
672
  }
402
673
  end
403
674
 
404
675
  # perform a CTCP action with message +message+ to channel/nick +where+
405
- def action(where, message)
406
- sendq("PRIVMSG #{where} :\001ACTION #{message}\001")
676
+ def action(where, message, mchan="", mring=-1)
677
+ if mchan == ""
678
+ chan = where
679
+ else
680
+ chan = mchan
681
+ end
682
+ if mring < 0
683
+ if where =~ /^#/
684
+ ring = 2
685
+ else
686
+ ring = 1
687
+ end
688
+ else
689
+ ring = mring
690
+ end
691
+ sendq "PRIVMSG #{where} :\001ACTION #{message}\001", chan, ring
407
692
  if(where =~ /^#/)
408
- log "* #{@nick} #{message}", where
693
+ irclog "* #{@nick} #{message}", where
409
694
  elsif (where =~ /^(\S*)!.*$/)
410
- log "* #{@nick}[#{where}] #{message}", $1
695
+ irclog "* #{@nick}[#{where}] #{message}", $1
411
696
  else
412
- log "* #{@nick}[#{where}] #{message}", where
697
+ irclog "* #{@nick}[#{where}] #{message}", where
413
698
  end
414
699
  end
415
700
 
@@ -418,11 +703,12 @@ class IrcBot
418
703
  say where, @lang.get("okay")
419
704
  end
420
705
 
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!
706
+ # log IRC-related message +message+ to a file determined by +where+.
707
+ # +where+ can be a channel name, or a nick for private message logging
708
+ def irclog(message, where="server")
709
+ message = message.chomp
425
710
  stamp = Time.now.strftime("%Y/%m/%d %H:%M:%S")
711
+ where = where.gsub(/[:!?$*()\/\\<>|"']/, "_")
426
712
  unless(@logs.has_key?(where))
427
713
  @logs[where] = File.new("#{@botclass}/logs/#{where}", "a")
428
714
  @logs[where].sync = true
@@ -430,93 +716,111 @@ class IrcBot
430
716
  @logs[where].puts "[#{stamp}] #{message}"
431
717
  #debug "[#{stamp}] <#{where}> #{message}"
432
718
  end
433
-
719
+
434
720
  # set topic of channel +where+ to +topic+
435
721
  def topic(where, topic)
436
- sendq "TOPIC #{where} :#{topic}"
722
+ sendq "TOPIC #{where} :#{topic}", where, 2
437
723
  end
438
724
 
439
725
  # disconnect from the server and cleanup all plugins and modules
440
726
  def shutdown(message = nil)
441
- trap("SIGTERM", "DEFAULT")
442
- trap("SIGHUP", "DEFAULT")
443
- trap("SIGINT", "DEFAULT")
727
+ debug "Shutting down ..."
728
+ ## No we don't restore them ... let everything run through
729
+ # begin
730
+ # trap("SIGINT", "DEFAULT")
731
+ # trap("SIGTERM", "DEFAULT")
732
+ # trap("SIGHUP", "DEFAULT")
733
+ # rescue => e
734
+ # debug "failed to restore signals: #{e.inspect}\nProbably running on windows?"
735
+ # end
444
736
  message = @lang.get("quit") if (message.nil? || message.empty?)
445
- @socket.clearq
446
- save
447
- @plugins.cleanup
737
+ if @socket.connected?
738
+ debug "Clearing socket"
739
+ @socket.clearq
740
+ debug "Sending quit message"
741
+ @socket.emergency_puts "QUIT :#{message}"
742
+ debug "Flushing socket"
743
+ @socket.flush
744
+ debug "Shutting down socket"
745
+ @socket.shutdown
746
+ end
747
+ debug "Logging quits"
448
748
  @channels.each_value {|v|
449
- log "@ quit (#{message})", v.name
749
+ irclog "@ quit (#{message})", v.name
450
750
  }
451
- @socket.puts "QUIT :#{message}"
452
- @socket.flush
453
- @socket.shutdown
454
- @registry.close
455
- puts "rbot quit (#{message})"
751
+ debug "Saving"
752
+ save
753
+ debug "Cleaning up"
754
+ @plugins.cleanup
755
+ # debug "Closing registries"
756
+ # @registry.close
757
+ debug "Cleaning up the db environment"
758
+ DBTree.cleanup_env
759
+ log "rbot quit (#{message})"
456
760
  end
457
-
761
+
458
762
  # message:: optional IRC quit message
459
763
  # quit IRC, shutdown the bot
460
764
  def quit(message=nil)
461
- shutdown(message)
462
- exit 0
765
+ begin
766
+ shutdown(message)
767
+ ensure
768
+ exit 0
769
+ end
463
770
  end
464
771
 
465
772
  # totally shutdown and respawn the bot
466
- def restart
467
- shutdown("restarting, back in #{@config['server.reconnect_wait']}...")
773
+ def restart(message = false)
774
+ msg = message ? message : "restarting, back in #{@config['server.reconnect_wait']}..."
775
+ shutdown(msg)
468
776
  sleep @config['server.reconnect_wait']
469
777
  # now we re-exec
778
+ # Note, this fails on Windows
470
779
  exec($0, *@argv)
471
780
  end
472
781
 
473
- # call the save method for bot's config, keywords, auth and all plugins
782
+ # call the save method for bot's config, auth and all plugins
474
783
  def save
475
- @registry.flush
476
- @config.save
477
- @keywords.save
478
- @auth.save
479
- @plugins.save
784
+ @save_mutex.synchronize do
785
+ @config.save
786
+ @auth.save
787
+ @plugins.save
788
+ DBTree.cleanup_logs
789
+ end
480
790
  end
481
791
 
482
- # call the rescan method for the bot's lang, keywords and all plugins
792
+ # call the rescan method for the bot's lang and all plugins
483
793
  def rescan
484
794
  @lang.rescan
485
795
  @plugins.rescan
486
- @keywords.rescan
487
796
  end
488
-
797
+
489
798
  # channel:: channel to join
490
799
  # key:: optional channel key if channel is +s
491
800
  # join a channel
492
801
  def join(channel, key=nil)
493
802
  if(key)
494
- sendq "JOIN #{channel} :#{key}"
803
+ sendq "JOIN #{channel} :#{key}", channel, 2
495
804
  else
496
- sendq "JOIN #{channel}"
805
+ sendq "JOIN #{channel}", channel, 2
497
806
  end
498
807
  end
499
808
 
500
809
  # part a channel
501
810
  def part(channel, message="")
502
- sendq "PART #{channel} :#{message}"
811
+ sendq "PART #{channel} :#{message}", channel, 2
503
812
  end
504
813
 
505
814
  # 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
815
  def nickchg(name)
512
816
  sendq "NICK #{name}"
513
817
  end
514
818
 
515
819
  # changing mode
516
820
  def mode(channel, mode, target)
517
- sendq "MODE #{channel} #{mode} #{target}"
821
+ sendq "MODE #{channel} #{mode} #{target}", channel, 2
518
822
  end
519
-
823
+
520
824
  # m:: message asking for help
521
825
  # topic:: optional topic help is requested for
522
826
  # respond to online help requests
@@ -524,7 +828,7 @@ class IrcBot
524
828
  topic = nil if topic == ""
525
829
  case topic
526
830
  when nil
527
- helpstr = "help topics: core, auth, keywords"
831
+ helpstr = "help topics: core, auth"
528
832
  helpstr += @plugins.helptopics
529
833
  helpstr += " (help <topic> for more info)"
530
834
  when /^core$/i
@@ -535,10 +839,6 @@ class IrcBot
535
839
  helpstr = @auth.help
536
840
  when /^auth\s+(.+)$/i
537
841
  helpstr = @auth.help $1
538
- when /^keywords$/i
539
- helpstr = @keywords.help
540
- when /^keywords\s+(.+)$/i
541
- helpstr = @keywords.help $1
542
842
  else
543
843
  unless(helpstr = @plugins.help(topic))
544
844
  helpstr = "no help for topic #{topic}"
@@ -551,9 +851,49 @@ class IrcBot
551
851
  def status
552
852
  secs_up = Time.new - @startup_time
553
853
  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."
854
+ # return "Uptime #{uptime}, #{@plugins.length} plugins active, #{@registry.length} items stored in registry, #{@socket.lines_sent} lines sent, #{@socket.lines_received} received."
855
+ return "Uptime #{uptime}, #{@plugins.length} plugins active, #{@socket.lines_sent} lines sent, #{@socket.lines_received} received."
856
+ end
857
+
858
+ # we'll ping the server every 30 seconds or so, and expect a response
859
+ # before the next one come around..
860
+ def start_server_pings
861
+ stop_server_pings
862
+ return unless @config['server.ping_timeout'] > 0
863
+ # we want to respond to a hung server within 30 secs or so
864
+ @ping_timer = @timer.add(30) {
865
+ @last_ping = Time.now
866
+ @socket.queue "PING :rbot"
867
+ }
868
+ @pong_timer = @timer.add(10) {
869
+ unless @last_ping.nil?
870
+ diff = Time.now - @last_ping
871
+ unless diff < @config['server.ping_timeout']
872
+ debug "no PONG from server for #{diff} seconds, reconnecting"
873
+ begin
874
+ @socket.shutdown
875
+ rescue
876
+ debug "couldn't shutdown connection (already shutdown?)"
877
+ end
878
+ @last_ping = nil
879
+ raise TimeoutError, "no PONG from server in #{diff} seconds"
880
+ end
881
+ end
882
+ }
555
883
  end
556
884
 
885
+ def stop_server_pings
886
+ @last_ping = nil
887
+ # stop existing timers if running
888
+ unless @ping_timer.nil?
889
+ @timer.remove @ping_timer
890
+ @ping_timer = nil
891
+ end
892
+ unless @pong_timer.nil?
893
+ @timer.remove @pong_timer
894
+ @pong_timer = nil
895
+ end
896
+ end
557
897
 
558
898
  private
559
899
 
@@ -580,8 +920,8 @@ class IrcBot
580
920
  return "say <channel>|<nick> <message> => say <message> to <channel> or in private message to <nick>"
581
921
  when "action"
582
922
  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>"
923
+ # when "topic"
924
+ # return "topic <channel> <message> => set topic of <channel> to <message>"
585
925
  when "quiet"
586
926
  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
927
  when "talk"
@@ -602,28 +942,31 @@ class IrcBot
602
942
  # log it first
603
943
  if(m.action?)
604
944
  if(m.private?)
605
- log "* [#{m.sourcenick}(#{m.sourceaddress})] #{m.message}", m.sourcenick
945
+ irclog "* [#{m.sourcenick}(#{m.sourceaddress})] #{m.message}", m.sourcenick
606
946
  else
607
- log "* #{m.sourcenick} #{m.message}", m.target
947
+ irclog "* #{m.sourcenick} #{m.message}", m.target
608
948
  end
609
949
  else
610
950
  if(m.public?)
611
- log "<#{m.sourcenick}> #{m.message}", m.target
951
+ irclog "<#{m.sourcenick}> #{m.message}", m.target
612
952
  else
613
- log "[#{m.sourcenick}(#{m.sourceaddress})] #{m.message}", m.sourcenick
953
+ irclog "[#{m.sourcenick}(#{m.sourceaddress})] #{m.message}", m.sourcenick
614
954
  end
615
955
  end
616
956
 
957
+ @config['irc.ignore_users'].each { |mask| return if Irc.netmaskmatch(mask,m.source) }
958
+
617
959
  # pass it off to plugins that want to hear everything
618
960
  @plugins.delegate "listen", m
619
961
 
620
962
  if(m.private? && m.message =~ /^\001PING\s+(.+)\001/)
621
963
  notice m.sourcenick, "\001PING #$1\001"
622
- log "@ #{m.sourcenick} pinged me"
964
+ irclog "@ #{m.sourcenick} pinged me"
623
965
  return
624
966
  end
625
967
 
626
968
  if(m.address?)
969
+ delegate_privmsg(m)
627
970
  case m.message
628
971
  when (/^join\s+(\S+)\s+(\S+)$/i)
629
972
  join $1, $2 if(@auth.allow?("join", m.source, m.replyto))
@@ -635,8 +978,8 @@ class IrcBot
635
978
  part $1 if(@auth.allow?("join", m.source, m.replyto))
636
979
  when (/^quit(?:\s+(.*))?$/i)
637
980
  quit $1 if(@auth.allow?("quit", m.source, m.replyto))
638
- when (/^restart$/i)
639
- restart if(@auth.allow?("quit", m.source, m.replyto))
981
+ when (/^restart(?:\s+(.*))?$/i)
982
+ restart $1 if(@auth.allow?("quit", m.source, m.replyto))
640
983
  when (/^hide$/i)
641
984
  join 0 if(@auth.allow?("join", m.source, m.replyto))
642
985
  when (/^save$/i)
@@ -650,16 +993,19 @@ class IrcBot
650
993
  say $1, $2 if(@auth.allow?("say", m.source, m.replyto))
651
994
  when (/^action\s+(\S+)\s+(.*)$/i)
652
995
  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))
996
+ # when (/^topic\s+(\S+)\s+(.*)$/i)
997
+ # topic $1, $2 if(@auth.allow?("topic", m.source, m.replyto))
655
998
  when (/^mode\s+(\S+)\s+(\S+)\s+(.*)$/i)
656
999
  mode $1, $2, $3 if(@auth.allow?("mode", m.source, m.replyto))
657
1000
  when (/^ping$/i)
658
1001
  say m.replyto, "pong"
659
1002
  when (/^rescan$/i)
660
1003
  if(@auth.allow?("config", m.source, m.replyto))
661
- m.okay
1004
+ m.reply "saving ..."
1005
+ save
1006
+ m.reply "rescanning ..."
662
1007
  rescan
1008
+ m.reply "done. #{@plugins.status(true)}"
663
1009
  end
664
1010
  when (/^quiet$/i)
665
1011
  if(auth.allow?("talk", m.source, m.replyto))
@@ -704,18 +1050,14 @@ class IrcBot
704
1050
  when (/^(hello|howdy|hola|salut|bonjour|sup|niihau|hey|hi(\W|$)|yo(\W|$)).*/i)
705
1051
  say m.replyto, @lang.get("hello_X") % m.sourcenick if(m.public?)
706
1052
  say m.replyto, @lang.get("hello") if(m.private?)
707
- else
708
- delegate_privmsg(m)
709
1053
  end
710
1054
  else
711
1055
  # stuff to handle when not addressed
712
1056
  case m.message
713
- when (/^\s*(hello|howdy|hola|salut|bonjour|sup|niihau|hey|hi(\W|$)|yo(\W|$))[\s,-.]+#{@nick}$/i)
1057
+ when (/^\s*(hello|howdy|hola|salut|bonjour|sup|niihau|hey|hi|yo(\W|$))[\s,-.]+#{Regexp.escape(@nick)}$/i)
714
1058
  say m.replyto, @lang.get("hello_X") % m.sourcenick
715
- when (/^#{@nick}!*$/)
1059
+ when (/^#{Regexp.escape(@nick)}!*$/)
716
1060
  say m.replyto, @lang.get("hello_X") % m.sourcenick
717
- else
718
- @keywords.privmsg(m)
719
1061
  end
720
1062
  end
721
1063
  end
@@ -725,19 +1067,19 @@ class IrcBot
725
1067
  case type
726
1068
  when "NOTICE"
727
1069
  if(where =~ /^#/)
728
- log "-=#{@nick}=- #{message}", where
1070
+ irclog "-=#{@nick}=- #{message}", where
729
1071
  elsif (where =~ /(\S*)!.*/)
730
- log "[-=#{where}=-] #{message}", $1
1072
+ irclog "[-=#{where}=-] #{message}", $1
731
1073
  else
732
- log "[-=#{where}=-] #{message}"
1074
+ irclog "[-=#{where}=-] #{message}"
733
1075
  end
734
1076
  when "PRIVMSG"
735
1077
  if(where =~ /^#/)
736
- log "<#{@nick}> #{message}", where
1078
+ irclog "<#{@nick}> #{message}", where
737
1079
  elsif (where =~ /^(\S*)!.*$/)
738
- log "[msg(#{where})] #{message}", $1
1080
+ irclog "[msg(#{where})] #{message}", $1
739
1081
  else
740
- log "[msg(#{where})] #{message}", where
1082
+ irclog "[msg(#{where})] #{message}", where
741
1083
  end
742
1084
  end
743
1085
  end
@@ -746,9 +1088,9 @@ class IrcBot
746
1088
  @channels[m.channel] = IRCChannel.new(m.channel) unless(@channels.has_key?(m.channel))
747
1089
  if(m.address?)
748
1090
  debug "joined channel #{m.channel}"
749
- log "@ Joined channel #{m.channel}", m.channel
1091
+ irclog "@ Joined channel #{m.channel}", m.channel
750
1092
  else
751
- log "@ #{m.sourcenick} joined channel #{m.channel}", m.channel
1093
+ irclog "@ #{m.sourcenick} joined channel #{m.channel}", m.channel
752
1094
  @channels[m.channel].users[m.sourcenick] = Hash.new
753
1095
  @channels[m.channel].users[m.sourcenick]["mode"] = ""
754
1096
  end
@@ -760,13 +1102,18 @@ class IrcBot
760
1102
  def onpart(m)
761
1103
  if(m.address?)
762
1104
  debug "left channel #{m.channel}"
763
- log "@ Left channel #{m.channel} (#{m.message})", m.channel
1105
+ irclog "@ Left channel #{m.channel} (#{m.message})", m.channel
764
1106
  @channels.delete(m.channel)
765
1107
  else
766
- log "@ #{m.sourcenick} left channel #{m.channel} (#{m.message})", m.channel
767
- @channels[m.channel].users.delete(m.sourcenick)
1108
+ irclog "@ #{m.sourcenick} left channel #{m.channel} (#{m.message})", m.channel
1109
+ if @channels.has_key?(m.channel)
1110
+ @channels[m.channel].users.delete(m.sourcenick)
1111
+ else
1112
+ warning "got part for channel '#{channel}' I didn't think I was in\n"
1113
+ # exit 2
1114
+ end
768
1115
  end
769
-
1116
+
770
1117
  # delegate to plugins
771
1118
  @plugins.delegate("listen", m)
772
1119
  @plugins.delegate("part", m)
@@ -777,10 +1124,10 @@ class IrcBot
777
1124
  if(m.address?)
778
1125
  debug "kicked from channel #{m.channel}"
779
1126
  @channels.delete(m.channel)
780
- log "@ You have been kicked from #{m.channel} by #{m.sourcenick} (#{m.message})", m.channel
1127
+ irclog "@ You have been kicked from #{m.channel} by #{m.sourcenick} (#{m.message})", m.channel
781
1128
  else
782
1129
  @channels[m.channel].users.delete(m.sourcenick)
783
- log "@ #{m.target} has been kicked from #{m.channel} by #{m.sourcenick} (#{m.message})", m.channel
1130
+ irclog "@ #{m.target} has been kicked from #{m.channel} by #{m.sourcenick} (#{m.message})", m.channel
784
1131
  end
785
1132
 
786
1133
  @plugins.delegate("listen", m)
@@ -793,16 +1140,15 @@ class IrcBot
793
1140
  @channels[m.channel].topic.timestamp = m.timestamp if !m.timestamp.nil?
794
1141
  @channels[m.channel].topic.by = m.source if !m.source.nil?
795
1142
 
796
- debug "topic of channel #{m.channel} is now #{@channels[m.channel].topic}"
1143
+ debug "topic of channel #{m.channel} is now #{@channels[m.channel].topic}"
797
1144
  end
798
1145
 
799
- # delegate a privmsg to auth, keyword or plugin handlers
1146
+ # delegate a privmsg to auth or plugin handlers
800
1147
  def delegate_privmsg(message)
801
- [@auth, @plugins, @keywords].each {|m|
1148
+ [@auth, @plugins].each {|m|
802
1149
  break if m.privmsg(message)
803
1150
  }
804
1151
  end
805
-
806
1152
  end
807
1153
 
808
1154
  end