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