cinch 1.0.2 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +25 -44
- data/examples/basic/autovoice.rb +1 -1
- data/examples/basic/join_part.rb +0 -4
- data/examples/plugins/autovoice.rb +2 -5
- data/examples/plugins/google.rb +1 -2
- data/examples/plugins/hooks.rb +36 -0
- data/examples/plugins/lambdas.rb +35 -0
- data/examples/plugins/last_nick.rb +24 -0
- data/examples/plugins/multiple_matches.rb +1 -10
- data/examples/plugins/own_events.rb +37 -0
- data/examples/plugins/timer.rb +22 -0
- data/examples/plugins/url_shorten.rb +1 -1
- data/lib/cinch.rb +50 -1
- data/lib/cinch/ban.rb +5 -2
- data/lib/cinch/bot.rb +360 -193
- data/lib/cinch/cache_manager.rb +15 -0
- data/lib/cinch/callback.rb +6 -0
- data/lib/cinch/channel.rb +150 -96
- data/lib/cinch/channel_manager.rb +26 -0
- data/lib/cinch/constants.rb +6 -4
- data/lib/cinch/exceptions.rb +9 -0
- data/lib/cinch/irc.rb +197 -82
- data/lib/cinch/logger/formatted_logger.rb +8 -8
- data/lib/cinch/logger/zcbot_logger.rb +37 -0
- data/lib/cinch/mask.rb +17 -3
- data/lib/cinch/message.rb +14 -7
- data/lib/cinch/message_queue.rb +8 -4
- data/lib/cinch/mode_parser.rb +56 -0
- data/lib/cinch/pattern.rb +45 -0
- data/lib/cinch/plugin.rb +129 -34
- data/lib/cinch/rubyext/string.rb +4 -4
- data/lib/cinch/syncable.rb +8 -0
- data/lib/cinch/user.rb +68 -13
- data/lib/cinch/user_manager.rb +60 -0
- metadata +17 -35
- data/Rakefile +0 -66
- data/lib/cinch/PLANNED +0 -4
- data/spec/bot_spec.rb +0 -5
- data/spec/channel_spec.rb +0 -5
- data/spec/cinch_spec.rb +0 -5
- data/spec/irc_spec.rb +0 -5
- data/spec/message_spec.rb +0 -5
- data/spec/plugin_spec.rb +0 -5
- data/spec/spec.opts +0 -2
- data/spec/spec_helper.rb +0 -8
- data/spec/user_spec.rb +0 -5
data/lib/cinch/bot.rb
CHANGED
@@ -25,19 +25,24 @@ require "cinch/ban"
|
|
25
25
|
require "cinch/mask"
|
26
26
|
require "cinch/isupport"
|
27
27
|
require "cinch/plugin"
|
28
|
+
require "cinch/pattern"
|
29
|
+
require "cinch/mode_parser"
|
30
|
+
require "cinch/cache_manager"
|
31
|
+
require "cinch/channel_manager"
|
32
|
+
require "cinch/user_manager"
|
28
33
|
|
29
34
|
module Cinch
|
30
35
|
|
31
36
|
class Bot
|
32
37
|
# @return [Config]
|
33
|
-
|
38
|
+
attr_reader :config
|
34
39
|
# @return [IRC]
|
35
|
-
|
40
|
+
attr_reader :irc
|
36
41
|
# @return [Logger]
|
37
42
|
attr_accessor :logger
|
38
43
|
# @return [Array<Channel>] All channels the bot currently is in
|
39
44
|
attr_reader :channels
|
40
|
-
# @return [String]
|
45
|
+
# @return [String] the bot's hostname
|
41
46
|
attr_reader :host
|
42
47
|
# @return [Mask]
|
43
48
|
attr_reader :mask
|
@@ -47,18 +52,34 @@ module Cinch
|
|
47
52
|
attr_reader :realname
|
48
53
|
# @return [Time]
|
49
54
|
attr_reader :signed_on_at
|
55
|
+
# @return [Array<Plugin>] All registered plugins
|
56
|
+
attr_reader :plugins
|
57
|
+
# @return [Array<Thread>]
|
58
|
+
# @api private
|
59
|
+
attr_reader :handler_threads
|
60
|
+
# @return [Boolean] whether the bot is in the process of disconnecting
|
61
|
+
attr_reader :quitting
|
62
|
+
# @return [UserManager]
|
63
|
+
attr_reader :user_manager
|
64
|
+
# @return [ChannelManager]
|
65
|
+
attr_reader :channel_manager
|
66
|
+
# @return [Boolean]
|
67
|
+
# @api private
|
68
|
+
attr_accessor :last_connection_was_successful
|
69
|
+
|
70
|
+
# @group Helper methods
|
50
71
|
|
51
72
|
# Helper method for turning a String into a {Channel} object.
|
52
73
|
#
|
53
74
|
# @param [String] channel a channel name
|
54
75
|
# @return [Channel] a {Channel} object
|
55
76
|
# @example
|
56
|
-
# on :message, /^please join (#.+)$/ do |target|
|
77
|
+
# on :message, /^please join (#.+)$/ do |m, target|
|
57
78
|
# Channel(target).join
|
58
79
|
# end
|
59
80
|
def Channel(channel)
|
60
81
|
return channel if channel.is_a?(Channel)
|
61
|
-
|
82
|
+
@channel_manager.find_ensured(channel)
|
62
83
|
end
|
63
84
|
|
64
85
|
# Helper method for turning a String into an {User} object.
|
@@ -66,75 +87,21 @@ module Cinch
|
|
66
87
|
# @param [String] user a user's nickname
|
67
88
|
# @return [User] an {User} object
|
68
89
|
# @example
|
69
|
-
# on :message, /^tell me everything about (.+)$/ do |target|
|
90
|
+
# on :message, /^tell me everything about (.+)$/ do |m, target|
|
70
91
|
# user = User(target)
|
71
|
-
# reply "%s is named %s and connects from %s" % [user.nick, user.name, user.host]
|
92
|
+
# m.reply "%s is named %s and connects from %s" % [user.nick, user.name, user.host]
|
72
93
|
# end
|
73
94
|
def User(user)
|
74
95
|
return user if user.is_a?(User)
|
75
|
-
|
76
|
-
end
|
77
|
-
|
78
|
-
# @return [void]
|
79
|
-
# @see Logger#debug
|
80
|
-
def debug(msg)
|
81
|
-
@logger.debug(msg)
|
82
|
-
end
|
83
|
-
|
84
|
-
# @return [Boolean]
|
85
|
-
def strict?
|
86
|
-
@config.strictness == :strict
|
87
|
-
end
|
88
|
-
|
89
|
-
# @yield
|
90
|
-
def initialize(&b)
|
91
|
-
@logger = Logger::FormattedLogger.new($stderr)
|
92
|
-
@events = {}
|
93
|
-
@config = OpenStruct.new({
|
94
|
-
:server => "localhost",
|
95
|
-
:port => 6667,
|
96
|
-
:ssl => false,
|
97
|
-
:password => nil,
|
98
|
-
:nick => "cinch",
|
99
|
-
:realname => "cinch",
|
100
|
-
:verbose => true,
|
101
|
-
:messages_per_second => 0.5,
|
102
|
-
:server_queue_size => 10,
|
103
|
-
:strictness => :forgiving,
|
104
|
-
:message_split_start => '... ',
|
105
|
-
:message_split_end => ' ...',
|
106
|
-
:max_messages => nil,
|
107
|
-
:plugins => OpenStruct.new({
|
108
|
-
:plugins => [],
|
109
|
-
:prefix => "!",
|
110
|
-
:options => Hash.new {|h,k| h[k] = {}},
|
111
|
-
}),
|
112
|
-
:channels => [],
|
113
|
-
:encoding => Encoding.default_external,
|
114
|
-
})
|
115
|
-
|
116
|
-
@semaphores_mutex = Mutex.new
|
117
|
-
@semaphores = Hash.new { |h,k| h[k] = Mutex.new }
|
118
|
-
@plugins = []
|
119
|
-
@callback = Callback.new(self)
|
120
|
-
@channels = []
|
121
|
-
|
122
|
-
on :connect do
|
123
|
-
bot.config.channels.each do |channel|
|
124
|
-
bot.join channel
|
125
|
-
end
|
126
|
-
end
|
127
|
-
|
128
|
-
instance_eval(&b) if block_given?
|
96
|
+
@user_manager.find_ensured(user)
|
129
97
|
end
|
130
98
|
|
131
|
-
#
|
132
|
-
# nothing else but yielding {Bot#config}, but it makes for a nice DSL.
|
99
|
+
# Define helper methods in the context of the bot.
|
133
100
|
#
|
134
|
-
# @
|
101
|
+
# @yield Expects a block containing method definitions
|
135
102
|
# @return [void]
|
136
|
-
def
|
137
|
-
|
103
|
+
def helpers(&b)
|
104
|
+
Callback.class_eval(&b)
|
138
105
|
end
|
139
106
|
|
140
107
|
# Since Cinch uses threads, all handlers can be run
|
@@ -175,56 +142,6 @@ module Cinch
|
|
175
142
|
semaphore.synchronize(&block)
|
176
143
|
end
|
177
144
|
|
178
|
-
# Registers a handler.
|
179
|
-
#
|
180
|
-
# @param [String, Symbol, Integer] event the event to match. Available
|
181
|
-
# events are all IRC commands in lowercase as symbols, all numeric
|
182
|
-
# replies, and the following:
|
183
|
-
#
|
184
|
-
# - :channel (a channel message)
|
185
|
-
# - :private (a private message)
|
186
|
-
# - :message (both channel and private messages)
|
187
|
-
# - :error (handling errors, use a numeric error code as `match`)
|
188
|
-
# - :ctcp (ctcp requests, use a ctcp command as `match`)
|
189
|
-
#
|
190
|
-
# @param [Regexp, String, Integer] match every message of the
|
191
|
-
# right event will be checked against this argument and the event
|
192
|
-
# will only be called if it matches
|
193
|
-
#
|
194
|
-
# @yieldparam [String] *args each capture group of the regex will
|
195
|
-
# be one argument to the block. It is optional to accept them,
|
196
|
-
# though
|
197
|
-
#
|
198
|
-
# @return [void]
|
199
|
-
def on(event, regexps = [], *args, &block)
|
200
|
-
regexps = [*regexps]
|
201
|
-
regexps = [//] if regexps.empty?
|
202
|
-
|
203
|
-
event = event.to_sym
|
204
|
-
|
205
|
-
regexps.map! do |regexp|
|
206
|
-
case regexp
|
207
|
-
when String, Integer
|
208
|
-
if event == :ctcp
|
209
|
-
/^#{Regexp.escape(regexp.to_s)}(?:$| .+)/
|
210
|
-
else
|
211
|
-
/^#{Regexp.escape(regexp.to_s)}$/
|
212
|
-
end
|
213
|
-
else
|
214
|
-
regexp
|
215
|
-
end
|
216
|
-
end
|
217
|
-
(@events[event] ||= []) << [regexps, args, block]
|
218
|
-
end
|
219
|
-
|
220
|
-
# Define helper methods in the context of the bot.
|
221
|
-
#
|
222
|
-
# @yield Expects a block containing method definitions
|
223
|
-
# @return [void]
|
224
|
-
def helpers(&b)
|
225
|
-
Callback.class_eval(&b)
|
226
|
-
end
|
227
|
-
|
228
145
|
# Stop execution of the current {#on} handler.
|
229
146
|
#
|
230
147
|
# @return [void]
|
@@ -232,6 +149,9 @@ module Cinch
|
|
232
149
|
throw :halt
|
233
150
|
end
|
234
151
|
|
152
|
+
# @endgroup
|
153
|
+
# @group Sending messages
|
154
|
+
|
235
155
|
# Sends a raw message to the server.
|
236
156
|
#
|
237
157
|
# @param [String] command The message to send.
|
@@ -246,18 +166,20 @@ module Cinch
|
|
246
166
|
#
|
247
167
|
# @param [String] recipient the recipient
|
248
168
|
# @param [String] text the message to send
|
169
|
+
# @param [Boolean] notice Use NOTICE instead of PRIVMSG?
|
249
170
|
# @return [void]
|
250
171
|
# @see Channel#send
|
251
172
|
# @see User#send
|
252
173
|
# @see #safe_msg
|
253
|
-
def msg(recipient, text)
|
174
|
+
def msg(recipient, text, notice = false)
|
254
175
|
text = text.to_s
|
255
176
|
split_start = @config.message_split_start || ""
|
256
177
|
split_end = @config.message_split_end || ""
|
178
|
+
command = notice ? "NOTICE" : "PRIVMSG"
|
257
179
|
|
258
180
|
text.split(/\r\n|\r|\n/).each do |line|
|
259
|
-
|
260
|
-
maxlength =
|
181
|
+
maxlength = 510 - (":" + " #{command} " + " :").size
|
182
|
+
maxlength = maxlength - self.mask.to_s.length - recipient.to_s.length
|
261
183
|
maxlength_without_end = maxlength - split_end.bytesize
|
262
184
|
|
263
185
|
if line.bytesize > maxlength
|
@@ -273,16 +195,29 @@ module Cinch
|
|
273
195
|
splitted << line
|
274
196
|
splitted[0, (@config.max_messages || splitted.size)].each do |string|
|
275
197
|
string.tr!("\u00A0", " ") # clean string from any non-breaking spaces
|
276
|
-
raw("
|
198
|
+
raw("#{command} #{recipient} :#{string}")
|
277
199
|
end
|
278
200
|
else
|
279
|
-
raw("
|
201
|
+
raw("#{command} #{recipient} :#{line}")
|
280
202
|
end
|
281
203
|
end
|
282
204
|
end
|
283
205
|
alias_method :privmsg, :msg
|
284
206
|
alias_method :send, :msg
|
285
207
|
|
208
|
+
# Sends a NOTICE to a recipient (a channel or user).
|
209
|
+
# You should be using {Channel#notice} and {User#notice} instead.
|
210
|
+
#
|
211
|
+
# @param [String] recipient the recipient
|
212
|
+
# @param [String] text the message to send
|
213
|
+
# @return [void]
|
214
|
+
# @see Channel#notice
|
215
|
+
# @see User#notice
|
216
|
+
# @see #safe_notice
|
217
|
+
def notice(recipient, text)
|
218
|
+
msg(recipient, text, true)
|
219
|
+
end
|
220
|
+
|
286
221
|
# Like {#msg}, but remove any non-printable characters from
|
287
222
|
# `text`. The purpose of this method is to send text of untrusted
|
288
223
|
# sources, like other users or feeds.
|
@@ -302,6 +237,19 @@ module Cinch
|
|
302
237
|
alias_method :safe_privmsg, :safe_msg
|
303
238
|
alias_method :safe_send, :safe_msg
|
304
239
|
|
240
|
+
# Like {#safe_msg} but for notices.
|
241
|
+
#
|
242
|
+
# @return (see #safe_msg)
|
243
|
+
# @param (see #safe_msg)
|
244
|
+
# @see #safe_notice
|
245
|
+
# @see #notice
|
246
|
+
# @see User#safe_notice
|
247
|
+
# @see Channel#safe_notice
|
248
|
+
# @todo (see #safe_msg)
|
249
|
+
def safe_notice(recipient, text)
|
250
|
+
msg(recipient, Cinch.filter_string(text), true)
|
251
|
+
end
|
252
|
+
|
305
253
|
# Invoke an action (/me) in/to a recipient (a channel or user).
|
306
254
|
# You should be using {Channel#action} and {User#action} instead.
|
307
255
|
#
|
@@ -332,112 +280,336 @@ module Cinch
|
|
332
280
|
action(recipient, Cinch.filter_string(text))
|
333
281
|
end
|
334
282
|
|
335
|
-
#
|
283
|
+
# @endgroup
|
284
|
+
# @group Events & Plugins
|
285
|
+
|
286
|
+
# Registers a handler.
|
287
|
+
#
|
288
|
+
# @param [String, Symbol, Integer] event the event to match. Available
|
289
|
+
# events are all IRC commands in lowercase as symbols, all numeric
|
290
|
+
# replies, and the following:
|
291
|
+
#
|
292
|
+
# - :channel (a channel message)
|
293
|
+
# - :private (a private message)
|
294
|
+
# - :message (both channel and private messages)
|
295
|
+
# - :error (handling errors, use a numeric error code as `match`)
|
296
|
+
# - :ctcp (ctcp requests, use a ctcp command as `match`)
|
297
|
+
#
|
298
|
+
# @param [Regexp, String, Integer] match every message of the
|
299
|
+
# right event will be checked against this argument and the event
|
300
|
+
# will only be called if it matches
|
301
|
+
#
|
302
|
+
# @yieldparam [String] *args each capture group of the regex will
|
303
|
+
# be one argument to the block. It is optional to accept them,
|
304
|
+
# though
|
336
305
|
#
|
337
|
-
# @param [String, Channel] channel either the name of a channel or a {Channel} object
|
338
|
-
# @param [String] key optionally the key of the channel
|
339
306
|
# @return [void]
|
340
|
-
|
341
|
-
|
342
|
-
|
307
|
+
def on(event, regexps = [], *args, &block)
|
308
|
+
regexps = [*regexps]
|
309
|
+
regexps = [//] if regexps.empty?
|
310
|
+
|
311
|
+
event = event.to_sym
|
312
|
+
|
313
|
+
regexps.map! do |regexp|
|
314
|
+
pattern = case regexp
|
315
|
+
when Pattern
|
316
|
+
regexp
|
317
|
+
when Regexp
|
318
|
+
Pattern.new(nil, regexp, nil)
|
319
|
+
else
|
320
|
+
if event == :ctcp
|
321
|
+
Pattern.new(/^/, /#{Regexp.escape(regexp.to_s)}(?:$| .+)/)
|
322
|
+
else
|
323
|
+
Pattern.new(/^/, /#{Regexp.escape(regexp.to_s)}/, /$/)
|
324
|
+
end
|
325
|
+
end
|
326
|
+
debug "[on handler] Registering handler with pattern `#{pattern.inspect}`, reacting on `#{event}`"
|
327
|
+
pattern
|
328
|
+
end
|
329
|
+
(@events[event] ||= []) << [regexps, args, block]
|
343
330
|
end
|
344
331
|
|
345
|
-
#
|
346
|
-
#
|
347
|
-
#
|
348
|
-
# @param [
|
332
|
+
# @param [Symbol] event The event type
|
333
|
+
# @param [Message, nil] msg The message which is responsible for
|
334
|
+
# and attached to the event, or nil.
|
335
|
+
# @param [Array] *arguments A list of additional arguments to pass
|
336
|
+
# to event handlers
|
349
337
|
# @return [void]
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
338
|
+
def dispatch(event, msg = nil, *arguments)
|
339
|
+
if handlers = find(event, msg)
|
340
|
+
handlers.each do |handler|
|
341
|
+
regexps, args, block = *handler
|
342
|
+
# calling Message#match multiple times is not a problem
|
343
|
+
# because we cache the result
|
344
|
+
if msg
|
345
|
+
regexp = regexps.find { |rx|
|
346
|
+
msg.match(rx.to_r(msg), event)
|
347
|
+
}
|
348
|
+
captures = msg.match(regexp.to_r(msg), event).captures
|
349
|
+
else
|
350
|
+
captures = []
|
351
|
+
end
|
354
352
|
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
@config.nick
|
353
|
+
invoke(block, args, msg, captures, arguments)
|
354
|
+
end
|
355
|
+
end
|
359
356
|
end
|
360
357
|
|
361
|
-
|
362
|
-
|
358
|
+
# Register all plugins from `@config.plugins.plugins`.
|
359
|
+
#
|
360
|
+
# @return [void]
|
361
|
+
def register_plugins
|
362
|
+
@config.plugins.plugins.each do |plugin|
|
363
|
+
register_plugin(plugin)
|
364
|
+
end
|
363
365
|
end
|
364
366
|
|
365
|
-
|
366
|
-
|
367
|
+
# Registers a plugin.
|
368
|
+
#
|
369
|
+
# @param [Class<Plugin>] plugin The plugin class to register
|
370
|
+
# @return [void]
|
371
|
+
def register_plugin(plugin)
|
372
|
+
@plugins << plugin.new(self)
|
367
373
|
end
|
368
374
|
|
369
|
-
|
370
|
-
|
371
|
-
User(nick).__send__(attr)
|
372
|
-
end
|
373
|
-
end
|
375
|
+
# @endgroup
|
376
|
+
# @group Bot Control
|
374
377
|
|
375
|
-
#
|
378
|
+
# This method is used to set a bot's options. It indeed does
|
379
|
+
# nothing else but yielding {Bot#config}, but it makes for a nice DSL.
|
376
380
|
#
|
377
|
-
# @
|
378
|
-
# @
|
379
|
-
def
|
380
|
-
|
381
|
-
raise Exceptions::NickTooLong, new_nick
|
382
|
-
end
|
383
|
-
@config.nick = new_nick
|
384
|
-
raw "NICK #{new_nick}"
|
381
|
+
# @yieldparam [Struct] config the bot's config
|
382
|
+
# @return [void]
|
383
|
+
def configure(&block)
|
384
|
+
@callback.instance_exec(@config, &block)
|
385
385
|
end
|
386
386
|
|
387
387
|
# Disconnects from the server.
|
388
388
|
#
|
389
|
+
# @param [String] message The quit message to send while quitting
|
389
390
|
# @return [void]
|
390
391
|
def quit(message = nil)
|
392
|
+
@quitting = true
|
391
393
|
command = message ? "QUIT :#{message}" : "QUIT"
|
392
394
|
raw command
|
393
395
|
end
|
394
396
|
|
397
|
+
# Connects the bot to a server.
|
398
|
+
#
|
399
|
+
# @param [Boolean] plugins Automatically register plugins from
|
400
|
+
# `@config.plugins.plugins`?
|
401
|
+
# @return [void]
|
395
402
|
# Connects the bot to a server.
|
396
403
|
#
|
397
404
|
# @param [Boolean] plugins Automatically register plugins from
|
398
405
|
# `@config.plugins.plugins`?
|
399
406
|
# @return [void]
|
400
407
|
def start(plugins = true)
|
408
|
+
@reconnects = 0
|
401
409
|
register_plugins if plugins
|
402
|
-
|
403
|
-
|
404
|
-
|
410
|
+
|
411
|
+
begin
|
412
|
+
@user_manager.each do |user|
|
413
|
+
user.in_whois = false
|
414
|
+
user.unsync_all
|
415
|
+
end # reset state of all users
|
416
|
+
|
417
|
+
@channel_manager.each do |channel|
|
418
|
+
channel.unsync_all
|
419
|
+
end # reset state of all channels
|
420
|
+
|
421
|
+
@logger.debug "Connecting to #{@config.server}:#{@config.port}"
|
422
|
+
@irc = IRC.new(self)
|
423
|
+
@irc.connect
|
424
|
+
|
425
|
+
if @config.reconnect && !@quitting
|
426
|
+
# double the delay for each unsuccesful reconnection attempt
|
427
|
+
if @last_connection_was_successful
|
428
|
+
@reconnects = 0
|
429
|
+
@last_connection_was_successful = false
|
430
|
+
else
|
431
|
+
@reconnects += 1
|
432
|
+
end
|
433
|
+
|
434
|
+
# Sleep for a few seconds before reconnecting to prevent being
|
435
|
+
# throttled by the IRC server
|
436
|
+
wait = 2**@reconnects
|
437
|
+
@logger.debug "Waiting #{wait} seconds before reconnecting"
|
438
|
+
sleep wait
|
439
|
+
end
|
440
|
+
end while @config.reconnect and not @quitting
|
405
441
|
end
|
406
442
|
|
407
|
-
#
|
443
|
+
# @endgroup
|
444
|
+
# @group Channel Control
|
445
|
+
|
446
|
+
# Join a channel.
|
408
447
|
#
|
448
|
+
# @param [String, Channel] channel either the name of a channel or a {Channel} object
|
449
|
+
# @param [String] key optionally the key of the channel
|
409
450
|
# @return [void]
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
end
|
451
|
+
# @see Channel#join
|
452
|
+
def join(channel, key = nil)
|
453
|
+
Channel(channel).join(key)
|
414
454
|
end
|
415
455
|
|
416
|
-
#
|
456
|
+
# Part a channel.
|
417
457
|
#
|
418
|
-
# @param [
|
458
|
+
# @param [String, Channel] channel either the name of a channel or a {Channel} object
|
459
|
+
# @param [String] reason an optional reason/part message
|
419
460
|
# @return [void]
|
420
|
-
|
421
|
-
|
461
|
+
# @see Channel#part
|
462
|
+
def part(channel, reason = nil)
|
463
|
+
Channel(channel).part(reason)
|
464
|
+
end
|
465
|
+
|
466
|
+
# @endgroup
|
467
|
+
|
468
|
+
# (see Logger::Logger#debug)
|
469
|
+
# @see Logger::Logger#debug
|
470
|
+
def debug(msg)
|
471
|
+
@logger.debug(msg)
|
472
|
+
end
|
473
|
+
|
474
|
+
# @return [Boolean] True if the bot reports ISUPPORT violations as
|
475
|
+
# exceptions.
|
476
|
+
def strict?
|
477
|
+
@config.strictness == :strict
|
478
|
+
end
|
479
|
+
|
480
|
+
# @yield
|
481
|
+
def initialize(&b)
|
482
|
+
@logger = Logger::FormattedLogger.new($stderr)
|
483
|
+
@events = {}
|
484
|
+
@config = OpenStruct.new({
|
485
|
+
:server => "localhost",
|
486
|
+
:port => 6667,
|
487
|
+
:ssl => OpenStruct.new({
|
488
|
+
:use => false,
|
489
|
+
:verify => false,
|
490
|
+
:client_cert => nil,
|
491
|
+
:ca_path => "/etc/ssl/certs",
|
492
|
+
}),
|
493
|
+
:password => nil,
|
494
|
+
:nick => "cinch",
|
495
|
+
:nicks => nil,
|
496
|
+
:realname => "cinch",
|
497
|
+
:verbose => true,
|
498
|
+
:messages_per_second => 0.5,
|
499
|
+
:server_queue_size => 10,
|
500
|
+
:strictness => :forgiving,
|
501
|
+
:message_split_start => '... ',
|
502
|
+
:message_split_end => ' ...',
|
503
|
+
:max_messages => nil,
|
504
|
+
:plugins => OpenStruct.new({
|
505
|
+
:plugins => [],
|
506
|
+
:prefix => /^!/,
|
507
|
+
:suffix => nil,
|
508
|
+
:options => Hash.new {|h,k| h[k] = {}},
|
509
|
+
}),
|
510
|
+
:channels => [],
|
511
|
+
:encoding => :irc,
|
512
|
+
:reconnect => true,
|
513
|
+
:local_host => nil,
|
514
|
+
:timeouts => OpenStruct.new({
|
515
|
+
:read => 240,
|
516
|
+
:connect => 10,
|
517
|
+
}),
|
518
|
+
:ping_interval => 120,
|
519
|
+
})
|
520
|
+
|
521
|
+
@semaphores_mutex = Mutex.new
|
522
|
+
@semaphores = Hash.new { |h,k| h[k] = Mutex.new }
|
523
|
+
@plugins = []
|
524
|
+
@callback = Callback.new(self)
|
525
|
+
@channels = []
|
526
|
+
@handler_threads = []
|
527
|
+
@quitting = false
|
528
|
+
|
529
|
+
@user_manager = UserManager.new(self)
|
530
|
+
@channel_manager = ChannelManager.new(self)
|
531
|
+
|
532
|
+
on :connect do
|
533
|
+
bot.config.channels.each do |channel|
|
534
|
+
bot.join channel
|
535
|
+
end
|
536
|
+
end
|
537
|
+
|
538
|
+
instance_eval(&b) if block_given?
|
422
539
|
end
|
423
540
|
|
541
|
+
# The bot's nickname.
|
542
|
+
# @overload nick=(new_nick)
|
543
|
+
# @raise [Exceptions::NickTooLong] Raised if the bot is
|
544
|
+
# operating in {#strict? strict mode} and the new nickname is
|
545
|
+
# too long
|
546
|
+
# @return [String]
|
547
|
+
# @overload nick
|
548
|
+
# @return [String]
|
549
|
+
# @return [String]
|
550
|
+
attr_accessor :nick
|
551
|
+
undef_method "nick"
|
552
|
+
undef_method "nick="
|
553
|
+
def nick
|
554
|
+
@config.nick
|
555
|
+
end
|
556
|
+
|
557
|
+
def nick=(new_nick)
|
558
|
+
if new_nick.size > @irc.isupport["NICKLEN"] && strict?
|
559
|
+
raise Exceptions::NickTooLong, new_nick
|
560
|
+
end
|
561
|
+
@config.nick = new_nick
|
562
|
+
raw "NICK #{new_nick}"
|
563
|
+
end
|
564
|
+
|
565
|
+
# Try to create a free nick, first by cycling through all
|
566
|
+
# available alternatives and then by appending underscores.
|
567
|
+
#
|
568
|
+
# @param [String] base The base nick to start trying from
|
424
569
|
# @api private
|
425
|
-
# @return
|
426
|
-
def
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
570
|
+
# @return String
|
571
|
+
def generate_next_nick(base = nil)
|
572
|
+
nicks = @config.nicks || []
|
573
|
+
|
574
|
+
if base
|
575
|
+
# if `base` is not in our list of nicks to try, assume that it's
|
576
|
+
# custom and just append an underscore
|
577
|
+
if !nicks.include?(base)
|
578
|
+
return base + "_"
|
579
|
+
else
|
580
|
+
# if we have a base, try the next nick or append an
|
581
|
+
# underscore if no more nicks are left
|
582
|
+
new_index = nicks.index(base) + 1
|
583
|
+
if nicks[new_index]
|
584
|
+
return nicks[new_index]
|
435
585
|
else
|
436
|
-
|
586
|
+
return base + "_"
|
437
587
|
end
|
438
|
-
|
439
|
-
invoke(block, args, msg, captures)
|
440
588
|
end
|
589
|
+
else
|
590
|
+
# if we have no base, try the first possible nick
|
591
|
+
new_nick = @config.nicks ? @config.nicks.first : @config.nick
|
592
|
+
end
|
593
|
+
end
|
594
|
+
|
595
|
+
# @return [Boolean] True if the bot is using SSL to connect to the
|
596
|
+
# server.
|
597
|
+
def secure?
|
598
|
+
@config[:ssl] == true || (@config[:ssl].is_a?(Hash) && @config[:ssl][:use])
|
599
|
+
end
|
600
|
+
|
601
|
+
# This method is only provided in order to give Bot and User a
|
602
|
+
# common interface.
|
603
|
+
#
|
604
|
+
# @return [false] Always returns `false`.
|
605
|
+
# @see User#unknown? See User#unknown? for the method's real use.
|
606
|
+
def unknown?
|
607
|
+
false
|
608
|
+
end
|
609
|
+
|
610
|
+
[:host, :mask, :user, :realname, :signed_on_at, :secure?].each do |attr|
|
611
|
+
define_method(attr) do
|
612
|
+
User(nick).__send__(attr)
|
441
613
|
end
|
442
614
|
end
|
443
615
|
|
@@ -450,28 +622,23 @@ module Cinch
|
|
450
622
|
|
451
623
|
events.select { |regexps|
|
452
624
|
regexps.first.any? { |regexp|
|
453
|
-
msg.match(regexp, type)
|
625
|
+
msg.match(regexp.to_r(msg), type)
|
454
626
|
}
|
455
627
|
}
|
456
628
|
end
|
457
629
|
end
|
458
630
|
|
459
|
-
def invoke(block, args, msg, match)
|
460
|
-
|
461
|
-
|
462
|
-
# 1 defined number of args, send only those
|
463
|
-
bargs = case block.arity <=> 0
|
464
|
-
when -1; match
|
465
|
-
when 0; []
|
466
|
-
when 1; match[0..block.arity-1 - args.size]
|
467
|
-
end
|
468
|
-
Thread.new do
|
631
|
+
def invoke(block, args, msg, match, arguments)
|
632
|
+
bargs = match + arguments
|
633
|
+
@handler_threads << Thread.new do
|
469
634
|
begin
|
470
635
|
catch(:halt) do
|
471
636
|
@callback.instance_exec(msg, *args, *bargs, &block)
|
472
637
|
end
|
473
638
|
rescue => e
|
474
639
|
@logger.log_exception(e)
|
640
|
+
ensure
|
641
|
+
@handler_threads.delete Thread.current
|
475
642
|
end
|
476
643
|
end
|
477
644
|
end
|