Class: Butler::IRC::Client

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

Constructor Details

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

Returns a new instance of Client.



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

def initialize(server, options={}, &on_disconnect)
	options          = DefaultOptions.merge(options)
	@users           = Users.new(self)
	@channels        = Channels.new(self)
	@users.channels  = @channels
	@channels.users  = @users
	@parser          = Parser.new(self, @users, @channels)
	
	@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(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



91
92
93
# File 'lib/butler/irc/client.rb', line 91

def channel_charset
  @channel_charset
end

#channelsObject (readonly)

Returns the value of attribute channels.



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

def channels
  @channels
end

#client_charsetObject

The charset the client uses



83
84
85
# File 'lib/butler/irc/client.rb', line 83

def client_charset
  @client_charset
end

#ircObject (readonly)

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



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

def irc
  @irc
end

#myselfObject (readonly)

The user representing the bot



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

def myself
  @myself
end

#server_charsetObject

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



87
88
89
# File 'lib/butler/irc/client.rb', line 87

def server_charset
  @server_charset
end

#usersObject (readonly)

Returns the value of attribute users.



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

def users
  @users
end

Instance Method Details

#event_loop(priority = -1)) ⇒ Object



311
312
313
314
315
316
317
318
319
320
# File 'lib/butler/irc/client.rb', line 311

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)


184
185
186
187
188
189
190
191
192
193
194
# File 'lib/butler/irc/client.rb', line 184

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:



342
343
344
345
346
347
348
# File 'lib/butler/irc/client.rb', line 342

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



222
223
224
225
226
# File 'lib/butler/irc/client.rb', line 222

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

#login(nick, user, real) ⇒ Object

login under nick, user, real



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/butler/irc/client.rb', line 197

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



308
309
# File 'lib/butler/irc/client.rb', line 308

def on_disconnect(reason)
end

#process(message) ⇒ Object

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



327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'lib/butler/irc/client.rb', line 327

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



276
277
278
279
280
# File 'lib/butler/irc/client.rb', line 276

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



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/butler/irc/client.rb', line 134

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



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

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



322
323
324
# File 'lib/butler/irc/client.rb', line 322

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



292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/butler/irc/client.rb', line 292

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



153
154
155
156
157
158
159
# File 'lib/butler/irc/client.rb', line 153

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



165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/butler/irc/client.rb', line 165

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)


230
231
232
233
234
235
236
237
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
# File 'lib/butler/irc/client.rb', line 230

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