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