Class: Butler::IRC::Client

Inherits:
Object
  • Object
show all
Includes:
Log::Comfort
Defined in:
lib/butler/irc/client.rb

Overview

Description

Wraps Butler::IRC::Socket, providing methods to be aware of the environment. It parses messages, keeps track of users and channels, is aware of events, responds to pings, provides simple methods for requests (e.g. whois, banlist, …) and offers dispatchers for messages and scheduled events. Butler::IRC::Client uses a separated thread for reading and dispatching.

Synopsis

irc_client	= Butler::IRC::Client.new('irc.server.com', :port => 6667, :server_charset => 'utf-8')

irc_client.subscribe(:QUIT) { |listener, message| “#messagemessage.frommessage.from.nick has left us…” }

puts "Whois 'nickname':", irc_client.whois("nickname")
puts *irc_client.banlist("#channel")
irc_client.event_loop { |message|
  case message
    when /#{irc_client.myself.nick}[,:]/
      message.answer("#{message.from.nick}, you spoke to me?")
    when :JOIN
      message.from.notice("Welcome to #{message.channel}!")
  end
  puts "received: #{message}"
}
puts "If this point is reached, client has ended"

Direct Known Subclasses

Bot

Defined Under Namespace

Modules: Filter Classes: Listener, Terminate

Constant Summary collapse

DefaultTimeout =

The timeout defaults

{
	:login   => 150,
}
DefaultOptions =

Defaults for the opts argument in Butler::Bot.new

{
	:client_charset  => 'utf-8',
	:server_charset  => 'utf-8',
	:channel_charset => {},
	:timeout         => {},
}

Instance Attribute Summary collapse

Attributes included from Log::Comfort

#logger

Instance Method Summary collapse

Methods included from Log::Comfort

#debug, #error, #exception, #fail, #info, #log, #warn

Constructor Details

#initialize(server, options = {}, &on_disconnect) ⇒ Client

Returns a new instance of Client.



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/butler/irc/client.rb', line 104

def initialize(server, options={}, &on_disconnect)
	options          = DefaultOptions.merge(options)
	@logger          = nil
	@users           = Users.new(self)
	@channels        = Channels.new(self)
	@users.channels  = @channels
	@channels.users  = @users
	@parser          = Parser.new(self, @users, @channels, "rfc2812", "generic")
	
	@client_charset  = options.delete(:client_charset)
	@server_charset  = options.delete(:server_charset)
	# proc needed because @server_charset might change
	@channel_charset = Hash.new { |h,k| @server_charset }.merge(options.delete(:channel_charset))
	
	@timeout         = DefaultTimeout.merge(options.delete(:timeout))

	@irc             = Socket.new(options.delete(:server) || server, options) # the socket, all methods to the socket are wrapped
	
	@listen          = Hash.new { |h,k| h[k] = [] }
	@listener        = {}
	@event_loop      = Queue.new
	@dispatch_lock   = Mutex.new
	@thread_read     = nil
	@myself          = @users.myself
	
	subscribe(:PING, 100) { |listener, message| @irc.pong(message.pong) }
end

Instance Attribute Details

#channel_charsetObject (readonly)

The charsets of individual channel, only set if they differ from server_charset



93
94
95
# File 'lib/butler/irc/client.rb', line 93

def channel_charset
  @channel_charset
end

#channelsObject (readonly)

Returns the value of attribute channels.



95
96
97
# File 'lib/butler/irc/client.rb', line 95

def channels
  @channels
end

#client_charsetObject

The charset the client uses



85
86
87
# File 'lib/butler/irc/client.rb', line 85

def client_charset
  @client_charset
end

#ircObject (readonly)

The Butler::IRC::Socket, most send commands have to be used on this



99
100
101
# File 'lib/butler/irc/client.rb', line 99

def irc
  @irc
end

#myselfObject (readonly)

The user representing the bot



102
103
104
# File 'lib/butler/irc/client.rb', line 102

def myself
  @myself
end

#server_charsetObject

The charset the server needs messages sent to him and sends message to this client in.



89
90
91
# File 'lib/butler/irc/client.rb', line 89

def server_charset
  @server_charset
end

#usersObject (readonly)

Returns the value of attribute users.



96
97
98
# File 'lib/butler/irc/client.rb', line 96

def users
  @users
end

Instance Method Details

#event_loop(priority = -1)) ⇒ Object



319
320
321
322
323
324
325
326
327
328
# File 'lib/butler/irc/client.rb', line 319

def event_loop(priority=-1)
	if block_given?
		filter(nil, priority, @event_loop)
		while message = @event_loop.shift; yield(message); end
	else
		sleep
	end
ensure
	@event_loop.unsubscribe if block_given?
end

#filter(symbol, priority = 0, queue = Queue.new) ⇒ Object

listens for all Messages with symbol (optionally passing a test given as block) and pushes them onto the Queue returns the Queue, extended with Filter. You are responsible to unsubscribe it (call Queue#unsubscribe on it)

Raises:

  • (ArgumentError)


192
193
194
195
196
197
198
199
200
201
202
# File 'lib/butler/irc/client.rb', line 192

def filter(symbol, priority=0, queue=Queue.new)
	raise ArgumentError, "Invalid Queue #{queue}:#{queue.class}" unless queue.respond_to?(:push)
	listener = if block_given? then
		subscribe(symbol, priority) { |l, message| queue.push(message) if yield(message) }
	else
		subscribe(symbol, priority) { |l, message| queue.push(message) }
	end
	queue.extend Filter
	queue.listener = listener
	queue
end

#inspectObject

:nodoc:



350
351
352
353
354
355
356
# File 'lib/butler/irc/client.rb', line 350

def inspect # :nodoc:
	"#<%s:0x%08x irc=%s>" %  [
		self.class,
		object_id << 1,
		@irc.inspect
	]
end

#join(*args) ⇒ Object

Same as IRC::Socket#join, but will do a who on every joined channel



230
231
232
233
234
# File 'lib/butler/irc/client.rb', line 230

def join(*args)
	@irc.join(*args).each { |channel|
		@irc.who(channel)
	}
end

#load_command_set(*sets) ⇒ Object

Load an additional command-set for @parser



133
134
135
# File 'lib/butler/irc/client.rb', line 133

def load_command_set(*sets)
	@parser.commands.load(*sets)
end

#login(nick, user, real) ⇒ Object

login under nick, user, real



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/butler/irc/client.rb', line 205

def (nick, user, real)
	queue, nick_change = nil, nil
	number = 0
	timeout(@timeout[:login]) {
		@irc.connect
		@users.create_myself(nick, user, real)
		@myself = @users.myself
		queue = filter(:RPL_WELCOME)
		filter(:ERR_NOMOTD, 0, queue)
		nick_change = subscribe(:ERR_NICKNAMEINUSE) {
			change = "[#{number+=1}]#{nick}"
			@irc.nick(change)
			@myself.nick = change
		}
		@thread_read = Thread.new(&method(:thread_read))
		@irc.(nick, user, real)
		queue.shift
	}
	true
ensure
	queue.unsubscribe if queue
	nick_change.unsubscribe if nick_change
end

#on_disconnect(reason) ⇒ Object



316
317
# File 'lib/butler/irc/client.rb', line 316

def on_disconnect(reason)
end

#process(message) ⇒ Object

process a Butler::IRC::Message, normally fed from thread_read.



335
336
337
338
339
340
341
342
343
344
345
346
347
348
# File 'lib/butler/irc/client.rb', line 335

def process(message)
	message = @parser.server_message(message)
	message.transcode!(@channel_charset[message.channel], @client_charset)
	@dispatch_lock.synchronize {
		@listen[nil].each { |listener| listener.callback.call(listener, message) }
		if @listen.has_key?(message.symbol)
			@listen[message.symbol].each { |listener| listener.callback.call(listener, message, *listener.args) }
		end
	}
rescue Terminate, Errno::EPIPE => error
	raise error # on these errors we got to get out of the loop
rescue Exception => error
	exception(error) # those errors are logged, reading goes on
end

#quit(reason = nil) ⇒ Object

Sends quit message to server, terminates connection



284
285
286
287
288
# File 'lib/butler/irc/client.rb', line 284

def quit(reason=nil)
	@irc.quit(reason)
	terminate
	@irc.close
end

#subscribe(symbol = nil, priority = 0, id = nil, *args, &callback) ⇒ Object

callback is called whenever a message with Message#symbol == symbol (or on every message if symbol is nil) priority may be any numeric, higher priority is dispatched to first, lower priority later returns an Butler::IRC::Client::Listener



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/butler/irc/client.rb', line 142

def subscribe(symbol=nil, priority=0, id=nil, *args, &callback)
	id              ||= callback
	raise "#{id} already subscribed" if @listener.has_key?(id)
	listener          = Listener.new(priority, callback, args) { |item|
		@dispatch_lock.synchronize {
			@listener.delete(id)
			@listen[symbol].delete(item)
			@listen.delete(symbol) if @listen[symbol].empty?
		}
	}
	@dispatch_lock.synchronize {
		@listener[id]     = listener
		@listen[symbol]  << listener
		@listen[symbol].sort_by { |l| -l.priority }
	}
	listener
end

#terminate(stop_reading = true) ⇒ Object

terminate all processing and reading, see Client#quit



292
293
294
295
# File 'lib/butler/irc/client.rb', line 292

def terminate(stop_reading=true)
	@thread_read.raise Terminate if stop_reading and @thread_read and @thread_read.alive?
	terminate_event_loop
end

#terminate_event_loopObject



330
331
332
# File 'lib/butler/irc/client.rb', line 330

def terminate_event_loop
	@event_loop.push(nil) if @event_loop
end

#thread_readObject

this thread is responsible for reading the servers messages and dispatching them to responders normally this thread is alive as long as the client is connected to the server



300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/butler/irc/client.rb', line 300

def thread_read
	while message = @irc.read; process(message); end
	on_disconnect(:disconnect)
rescue Terminate => e		# got the termination signal
	info("Terminating read thread")
	on_disconnect(:quit)
rescue Errno::EPIPE => error	# irc server closed connection
	exception(error)
	on_disconnect(:disconnect)
rescue Exception => error
	exception(error)
	on_disconnect(:error)
ensure
	@irc.close
end

#unsubscribe(id) ⇒ Object

unsubscribe a listener by id



161
162
163
164
165
166
167
# File 'lib/butler/irc/client.rb', line 161

def unsubscribe(id)
	@dispatch_lock.synchronize {
		@listener.delete(id)
		@listen[symbol].delete(item)
		@listen.delete(symbol) if @listen[symbol].empty?
	}
end

#wait_for(symbol, timeout = nil, &test) ⇒ Object

blocks current thread until a Message with symbol (optionally passing a test given as block) is received, returns the message received that matches. returns nil if it times out before a match



173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/butler/irc/client.rb', line 173

def wait_for(symbol, timeout=nil, &test)
	timeout(timeout) {
		queue    = Queue.new
		listener = subscribe(symbol) { |l, m| queue.push(m) }
		begin
			message = queue.shift
		end until block_given? ? yield(message) : true
		message
	}
rescue Timeout::Error
	return nil
ensure
	listener.unsubscribe
end

#whois(user) ⇒ Object

Do a whois on nick Returns an Butler::IRC::Whois-Struct

Raises:

  • (ArgumentError)


238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/butler/irc/client.rb', line 238

def whois(user)
	nick	= user.to_str.strip_user_prefixes
	raise ArgumentError, "Invalid nick #{nick.inspect}" unless nick.valid_nickname?
	queue = Queue.new
	whois	= Whois.new
	whois.exists = true
	[
		:RPL_WHOISUSER,
		:RPL_WHOISSERVER,
		:RPL_WHOISIDLE,
		:RPL_ENDOFWHOIS,
		:RPL_UNIQOPIS,
		:RPL_WHOISCHANNELS,
		:RPL_IDENTIFIED_TO_SERVICES,
		:ERR_NOSUCHNICK,
		:RPL_REGISTERED_INFO
	].each { |reply| filter(reply, 1, queue) }
	@irc.whois(nick)
	until (message = queue.shift).symbol == :RPL_ENDOFWHOIS
		case message.symbol
			when :ERR_NOSUCHNICK
				whois.exists     = false
			when :RPL_WHOISUSER
				whois.exists     = true
				whois.nick       = message.nick
				whois.user       = message.user
				whois.host       = message.host
				whois.real       = message.real
			when :RPL_WHOISSERVER
			when :RPL_WHOISIDLE
				whois.exists   	 = true
				whois.signon	   = message.signon_time
				whois.idle		   = message.seconds_idle
			when :RPL_UNIQOPIS
			when :RPL_WHOISCHANNELS
				whois.exists     = true
				whois.channels   = message.channels
			when :RPL_REGISTERED_INFO
				whois.exists     = true
				whois.registered = true
		end
	end
	return whois
end