Class: Butler::IRC::Client

Inherits:
Object
  • Object
show all
Includes:
Log::Comfort
Defined in:
lib/butler/irc/client.rb,
lib/butler/irc/client/filter.rb,
lib/butler/irc/client/listener.rb,
lib/butler/irc/client/listenerlist.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
      irc_client.irc.notice("Welcome to #{message.channel}!", message.from)
  end
  puts "received: #{message}"
}
puts "If this point is reached, client has ended"

Defined Under Namespace

Modules: Filter Classes: Listener, ListenerList, 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

Arguments:

* server: the server to connect to


98
99
100
101
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
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/butler/irc/client.rb', line 98

def initialize(server, options={}, &on_disconnect)
	options              = DefaultOptions.merge(options)
	@disconnect_callback = 
	@logger              = nil
	@users               = UserList.new(self)
	@channels            = ChannelList.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
	
	@listener            = ListenerList.new
	@event_loop          = Queue.new
	@thread_read         = nil
	@myself              = @users.myself
	
	subscribe(:PING, 100) { |listener, message| @irc.pong(message.pong) }
	subscribe(:ISUPPORT) { |listener, message|
		isupport = {}
		@parser.isupport.to_hash.each { |key, value|
			isupport[key.to_s] = value
		}
		message.support.each { |key,value|
			isupport[key.downcase] = value
		}
		if message.support.has_key?("PREFIX") then
			isupport["prefixes"] = message.support["PREFIX"].values.join('')
		end
		@parser.reset(isupport)
		@irc.write_with_eol("CAPAB IDENTIFY-MSG") if @parser.isupport.capab and !@parser.msg_identify
	}
	subscribe(:RPL_IDENTIFY_MSG) { |listener, message|
		@parser.msg_identify = true
	}
end

Instance Attribute Details

#channel_charsetObject (readonly)

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



76
77
78
# File 'lib/butler/irc/client.rb', line 76

def channel_charset
  @channel_charset
end

#channelsObject (readonly)

The channels the client participates



79
80
81
# File 'lib/butler/irc/client.rb', line 79

def channels
  @channels
end

#client_charsetObject

The charset the client uses



68
69
70
# File 'lib/butler/irc/client.rb', line 68

def client_charset
  @client_charset
end

#disconnect_callbackObject

A callback invoked on disconnects, accepts one argument: reason, which can be

* :quit:       you told the client to quit
* :disconnect: the server disconnected your client
* :error:      an error occurred in the read-thread (shouldn't happen)


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

def disconnect_callback
  @disconnect_callback
end

#ircObject (readonly)

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



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

def irc
  @irc
end

#myselfObject (readonly)

The user representing the bot



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

def myself
  @myself
end

#server_charsetObject

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



72
73
74
# File 'lib/butler/irc/client.rb', line 72

def server_charset
  @server_charset
end

#usersObject (readonly)

The users visible to the client



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

def users
  @users
end

Instance Method Details

#dispatch(message) ⇒ Object

dispatch a message



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

def dispatch(message)
	message.transcode!(@channel_charset[message.channel], @client_charset)
	@listener.synchronized_each_for(message.symbol) { |listener|
		listener.call(message)
	}
end

#event_loop(priority = -1)) ⇒ Object



324
325
326
327
328
329
330
331
332
333
# File 'lib/butler/irc/client.rb', line 324

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)


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

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:



356
357
358
359
360
361
362
# File 'lib/butler/irc/client.rb', line 356

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



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

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

#load_command_set(*sets) ⇒ Object

Load an additional command-set for @parser



143
144
145
# File 'lib/butler/irc/client.rb', line 143

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

#login(nick, user, real, serverpass = nil) ⇒ Object

login under nick, user, real



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

def (nick, user, real, serverpass=nil)
	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, serverpass)
		queue.shift
	}
	true
ensure
	queue.unsubscribe if queue
	nick_change.unsubscribe if nick_change
end

#on_disconnect(reason) ⇒ Object

Called on disconnects, see disconnect_callback attribute documentation



320
321
322
# File 'lib/butler/irc/client.rb', line 320

def on_disconnect(reason)
	@disconnect_callback.call(reasons) if @disconnect_callback
end

#process(message) ⇒ Object

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



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

def process(message)
	dispatch(@parser.server_message(message))
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



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

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

#subscribe(symbols = nil, priority = 0, *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



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

def subscribe(symbols=nil, priority=0, *args, &callback)
	listener = Listener.new(symbols, priority, *args, &callback)
	@listener.subscribe(listener)
	listener
end

#subscribe_listener(listener) ⇒ Object

subscribe a Listener instance (or anything that emulates its interface)



159
160
161
# File 'lib/butler/irc/client.rb', line 159

def subscribe_listener(listener)
	@listener.subscribe(listener)
end

#terminate(stop_reading = true) ⇒ Object

terminate all processing and reading, see Client#quit



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

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



335
336
337
# File 'lib/butler/irc/client.rb', line 335

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



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

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

#unsubscribe(listener) ⇒ Object

unsubscribe a listener



164
165
166
# File 'lib/butler/irc/client.rb', line 164

def unsubscribe(listener)
	@listener.unsubscribe(listener)
end

#wait_for(symbol, timeout = nil, opt = {}, &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



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

def wait_for(symbol, timeout=nil, opt={}, &test)
	listener = nil
	timeout(timeout) {
		queue    = Queue.new
		listener = subscribe(symbol) { |l, m| queue.push(m) }
		opt[:prepare].call if opt.has_key?(:prepare)
		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)


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
282
# File 'lib/butler/irc/client.rb', line 239

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