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.
- data/AUTHORS +8 -0
- data/ChangeLog +51 -0
- data/INSTALL +4 -0
- data/README +1 -0
- data/REQUIREMENTS +11 -0
- data/TODO +2 -0
- data/bin/rbot +21 -2
- data/data/rbot/languages/german.lang +4 -1
- data/data/rbot/languages/russian.lang +75 -0
- data/data/rbot/plugins/autoop.rb +42 -51
- data/data/rbot/plugins/bans.rb +205 -0
- data/data/rbot/plugins/bash.rb +56 -0
- data/data/rbot/plugins/chucknorris.rb +74 -0
- data/data/rbot/plugins/chucknorris.yml.gz +0 -0
- data/data/rbot/plugins/deepthoughts.rb +95 -0
- data/data/rbot/plugins/demauro.rb +95 -0
- data/data/rbot/plugins/digg.rb +51 -0
- data/data/rbot/plugins/figlet.rb +24 -0
- data/data/rbot/plugins/forecast.rb +133 -0
- data/data/rbot/plugins/freshmeat.rb +13 -7
- data/data/rbot/plugins/google.rb +2 -0
- data/data/rbot/plugins/grouphug.rb +36 -0
- data/data/rbot/plugins/imdb.rb +92 -0
- data/data/rbot/plugins/insult.rb +8 -1
- data/data/rbot/plugins/iplookup.rb +227 -0
- data/data/rbot/plugins/karma.rb +2 -2
- data/data/rbot/plugins/keywords.rb +470 -0
- data/data/rbot/plugins/lart.rb +132 -146
- data/data/rbot/plugins/lastfm.rb +25 -0
- data/data/rbot/plugins/markov.rb +204 -0
- data/data/rbot/plugins/math.rb +5 -1
- data/data/rbot/plugins/nickserv.rb +71 -11
- data/data/rbot/plugins/opme.rb +19 -19
- data/data/rbot/plugins/quakeauth.rb +2 -2
- data/data/rbot/plugins/quotes.rb +40 -25
- data/data/rbot/plugins/remind.rb +1 -1
- data/data/rbot/plugins/rot13.rb +2 -2
- data/data/rbot/plugins/roulette.rb +49 -15
- data/data/rbot/plugins/rss.rb +585 -0
- data/data/rbot/plugins/rubyurl.rb +39 -0
- data/data/rbot/plugins/seen.rb +2 -1
- data/data/rbot/plugins/slashdot.rb +5 -5
- data/data/rbot/plugins/spell.rb +5 -0
- data/data/rbot/plugins/theyfightcrime.rb +121 -0
- data/data/rbot/plugins/threat.rb +55 -0
- data/data/rbot/plugins/tinyurl.rb +39 -0
- data/data/rbot/plugins/topic.rb +204 -0
- data/data/rbot/plugins/urban.rb +71 -0
- data/data/rbot/plugins/url.rb +399 -4
- data/data/rbot/plugins/wow.rb +123 -0
- data/data/rbot/plugins/wserver.rb +1 -1
- data/data/rbot/templates/levels.rbot +2 -0
- data/lib/rbot/auth.rb +207 -96
- data/lib/rbot/channel.rb +5 -5
- data/lib/rbot/config.rb +125 -24
- data/lib/rbot/dbhash.rb +87 -21
- data/lib/rbot/httputil.rb +181 -13
- data/lib/rbot/ircbot.rb +525 -179
- data/lib/rbot/ircsocket.rb +330 -54
- data/lib/rbot/message.rb +66 -23
- data/lib/rbot/messagemapper.rb +25 -17
- data/lib/rbot/plugins.rb +244 -115
- data/lib/rbot/post-clean.rb +1 -0
- data/lib/rbot/{post-install.rb → post-config.rb} +1 -1
- data/lib/rbot/rbotconfig.rb +29 -14
- data/lib/rbot/registry.rb +111 -72
- data/lib/rbot/rfc2812.rb +208 -197
- data/lib/rbot/timer.rb +4 -0
- data/lib/rbot/utils.rb +2 -2
- metadata +127 -104
- data/data/rbot/plugins/rss.rb.disabled +0 -414
- 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
|
-
|
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.
|
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
|
-
|
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
|
118
|
-
#
|
119
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
-
#
|
55
|
-
attr_reader :
|
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
|
-
|
108
|
-
|
109
|
-
:
|
110
|
-
:
|
111
|
-
|
112
|
-
|
113
|
-
:
|
114
|
-
:
|
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
|
-
|
215
|
+
error "data directory '#{Config::datadir}' not found, did you setup.rb?"
|
120
216
|
exit 2
|
121
217
|
end
|
122
|
-
|
123
|
-
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
|
-
|
240
|
+
log "no #{botclass} directory found, creating from templates.."
|
128
241
|
if FileTest.exist? botclass
|
129
|
-
|
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
|
-
|
148
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
365
|
+
@client[:badnick] = proc {|data|
|
366
|
+
warning "bad nick (#{data[:nick]})"
|
181
367
|
}
|
182
368
|
@client[:ping] = proc {|data|
|
183
|
-
|
184
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
417
|
+
irclog "@ Mode #{modestring} #{targets} by #{sourcenick}", channel
|
230
418
|
}
|
231
419
|
@client[:welcome] = proc {|data|
|
232
|
-
|
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
|
-
|
460
|
+
irclog "@ I set topic \"#{topic}\"", channel
|
273
461
|
else
|
274
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
316
|
-
@socket.
|
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
|
-
|
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
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
rescue
|
337
|
-
|
338
|
-
|
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
|
-
|
573
|
+
|
574
|
+
stop_server_pings
|
343
575
|
@channels.clear
|
344
|
-
@socket.
|
345
|
-
|
346
|
-
|
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
|
360
|
-
|
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
|
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
|
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
|
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
|
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
|
-
|
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
|
-
|
693
|
+
irclog "* #{@nick} #{message}", where
|
409
694
|
elsif (where =~ /^(\S*)!.*$/)
|
410
|
-
|
695
|
+
irclog "* #{@nick}[#{where}] #{message}", $1
|
411
696
|
else
|
412
|
-
|
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+.
|
422
|
-
# channel name, or a nick for private message logging
|
423
|
-
def
|
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
|
-
|
442
|
-
|
443
|
-
|
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.
|
446
|
-
|
447
|
-
|
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
|
-
|
749
|
+
irclog "@ quit (#{message})", v.name
|
450
750
|
}
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
@
|
455
|
-
|
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
|
-
|
462
|
-
|
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
|
-
|
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,
|
782
|
+
# call the save method for bot's config, auth and all plugins
|
474
783
|
def save
|
475
|
-
@
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
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
|
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
|
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
|
-
|
584
|
-
|
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
|
-
|
945
|
+
irclog "* [#{m.sourcenick}(#{m.sourceaddress})] #{m.message}", m.sourcenick
|
606
946
|
else
|
607
|
-
|
947
|
+
irclog "* #{m.sourcenick} #{m.message}", m.target
|
608
948
|
end
|
609
949
|
else
|
610
950
|
if(m.public?)
|
611
|
-
|
951
|
+
irclog "<#{m.sourcenick}> #{m.message}", m.target
|
612
952
|
else
|
613
|
-
|
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
|
-
|
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
|
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
|
-
|
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.
|
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
|
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
|
-
|
1070
|
+
irclog "-=#{@nick}=- #{message}", where
|
729
1071
|
elsif (where =~ /(\S*)!.*/)
|
730
|
-
|
1072
|
+
irclog "[-=#{where}=-] #{message}", $1
|
731
1073
|
else
|
732
|
-
|
1074
|
+
irclog "[-=#{where}=-] #{message}"
|
733
1075
|
end
|
734
1076
|
when "PRIVMSG"
|
735
1077
|
if(where =~ /^#/)
|
736
|
-
|
1078
|
+
irclog "<#{@nick}> #{message}", where
|
737
1079
|
elsif (where =~ /^(\S*)!.*$/)
|
738
|
-
|
1080
|
+
irclog "[msg(#{where})] #{message}", $1
|
739
1081
|
else
|
740
|
-
|
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
|
-
|
1091
|
+
irclog "@ Joined channel #{m.channel}", m.channel
|
750
1092
|
else
|
751
|
-
|
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
|
-
|
1105
|
+
irclog "@ Left channel #{m.channel} (#{m.message})", m.channel
|
764
1106
|
@channels.delete(m.channel)
|
765
1107
|
else
|
766
|
-
|
767
|
-
@channels
|
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
|
-
|
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
|
-
|
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
|
-
|
1143
|
+
debug "topic of channel #{m.channel} is now #{@channels[m.channel].topic}"
|
797
1144
|
end
|
798
1145
|
|
799
|
-
# delegate a privmsg to auth
|
1146
|
+
# delegate a privmsg to auth or plugin handlers
|
800
1147
|
def delegate_privmsg(message)
|
801
|
-
[@auth, @plugins
|
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
|