cinch 1.0.2 → 1.1.0
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/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
|