Class: Marvin::AbstractClient

Inherits:
Object
  • Object
show all
Defined in:
lib/marvin/abstract_client.rb,
lib/marvin/client/actions.rb,
lib/marvin/client/default_handlers.rb

Overview

An abstract class implementing (and mixing in) a lot of the default functionality involved on handling an irc connection.

To provide an implementation, you must subclass and implement #send_line, .add_reconnect, and any other loader etc methods.

See Also:

Direct Known Subclasses

Distributed::Client, IRC::Client, TestClient

Constant Summary collapse

@@events =

Set the default values for the variables

[]
@@configuration =
Marvin::Nash.new
@@connections =
[]
@@development =
false

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(opts) ⇒ AbstractClient

Returns a new instance of AbstractClient.



16
17
18
19
20
21
22
23
24
# File 'lib/marvin/abstract_client.rb', line 16

def initialize(opts)
  opts = opts.to_nash if opts.is_a?(Hash)
  @connection_config = opts.dup # Copy the options so we can use them to reconnect.
  @server            = opts.server
  @port              = opts.port
  @default_channels  = opts.channels
  @nicks             = opts.nicks || []
  @pass              = opts.pass
end

Instance Attribute Details

#channelsObject

Returns the value of attribute channels.



27
28
29
# File 'lib/marvin/abstract_client.rb', line 27

def channels
  @channels
end

#connection_configObject

Returns the value of attribute connection_config.



27
28
29
# File 'lib/marvin/abstract_client.rb', line 27

def connection_config
  @connection_config
end

#disconnect_expectedObject

Returns the value of attribute disconnect_expected.



27
28
29
# File 'lib/marvin/abstract_client.rb', line 27

def disconnect_expected
  @disconnect_expected
end

#nicknameObject

Returns the value of attribute nickname.



27
28
29
# File 'lib/marvin/abstract_client.rb', line 27

def nickname
  @nickname
end

#nicksArray<String>

Returns a list available nicks / nicks to try on connect.

Returns:

  • (Array<String>)

    The array of nicks



164
165
166
# File 'lib/marvin/abstract_client.rb', line 164

def nicks
  @nicks
end

#passObject

Returns the value of attribute pass.



27
28
29
# File 'lib/marvin/abstract_client.rb', line 27

def pass
  @pass
end

#portObject

Returns the value of attribute port.



27
28
29
# File 'lib/marvin/abstract_client.rb', line 27

def port
  @port
end

#serverObject

Returns the value of attribute server.



27
28
29
# File 'lib/marvin/abstract_client.rb', line 27

def server
  @server
end

Class Method Details

.configuration=(config) ⇒ Object

Sets the current class-wide settings of this IRC Client to an instance of Marvin::Nash with the properties of a hash / nash passed in.

The new configuration will be normalized before use (namely, it will convert nested items to nashes)

Parameters:

  • config (Marvin::Nash, Hash)

    the new configuration



95
96
97
98
# File 'lib/marvin/abstract_client.rb', line 95

def self.configuration=(config)
  config = Marvin::Nash.new(config.to_hash) unless config.is_a?(Marvin::Nash)
  @@configuration = config.normalized
end

.configure {|an| ... } ⇒ Object

Configure the class - namely, merge the app-wide configuration and if given a block, merge the results in.

Yield Parameters:

  • an (Marvin::Nash)

    empty nash

Yield Returns:

  • (Marvin::Nash)

    any changed settings.



117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/marvin/abstract_client.rb', line 117

def self.configure
  config = Marvin::Nash.new
  config.merge! Marvin::Settings.configuration
  if block_given?
    yield(nash = Marvin::Nash.new)
    config.merge! nash
  end
  @@configuration = config
  # Help is only currently available on an instance NOT running the distributed handler.
  Marvin::CoreCommands.register! unless Marvin::Distributed::Handler.registered?
  @setup = true
end

.setupObject

Conditional configure



107
108
109
110
# File 'lib/marvin/abstract_client.rb', line 107

def self.setup
  return if setup?
  configure
end

.setup?Boolean

Check if if the cient class has been setup yet

Returns:

  • (Boolean)

    is the client class setup



102
103
104
# File 'lib/marvin/abstract_client.rb', line 102

def self.setup?
  @setup ||= false
end

Instance Method Details

#action(target, message) ⇒ Object

Does a CTCP action in a channel (equiv. to doing /me in most IRC clients) e.g.

action "#marvin-testing", "is about to sleep"
action "SuttoL", "is about to sleep"


83
84
85
86
87
# File 'lib/marvin/client/actions.rb', line 83

def action(target, message)
  command :privmsg, target, "\01ACTION #{message.strip}\01"
  dispatch :outgoing_action, :target => target, :message => message
  logger.info "Action sent to #{target} - #{message}"
end

#command(name, *args) ⇒ Object

Sends a specified command to the server. Takes name (e.g. :privmsg) and all of the args. Very simply formats them as a string correctly and calls send_data with the results.



10
11
12
13
14
15
16
# File 'lib/marvin/client/actions.rb', line 10

def command(name, *args)
  # First, get the appropriate command
  name = name.to_s.upcase
  args = args.flatten
  args << util.last_param(args.pop)
  send_line "#{name} #{args.compact.join(" ").strip}\r\n"
end

#default_channelsArray<String>

Returns a list of default channels to join on connect

Returns:

  • (Array<String>)

    an array of channel names that are joined on connect



145
146
147
# File 'lib/marvin/abstract_client.rb', line 145

def default_channels
  @default_channels ||= []
end

#default_channels=(channels) ⇒ Object

Sets the list of default channels to join

Parameters:

  • channels (Array<String>)

    the array of channel names



151
152
153
# File 'lib/marvin/abstract_client.rb', line 151

def default_channels=(channels)
  @default_channels = channels.to_a.map { |c| c.to_s }
end

#handle_client_connected(opts = {}) ⇒ Object

The default handler for all things initialization-related on the client. Usually, this will send the user command, set out nick, join all of the channels / rooms we wish to be in and if a password is specified in the configuration, it will also attempt to identify us.



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# File 'lib/marvin/client/default_handlers.rb', line 11

def handle_client_connected(opts = {})
  logger.info "About to handle client connected"
  # If the pass is set
  unless pass.blank?
    logger.info "Sending pass for connection"
    command :pass, pass
  end
  # IRC Connection is establish so we send all the required commands to the server.
  logger.info "Setting default nickname"
  nick nicks.shift
  logger.info "Sending user command"
  command :user, configuration.user, "0", "*", configuration.name
rescue Exception => e
  Marvin::ExceptionTracker.log(e)
end

#handle_incoming_error(opts = {}) ⇒ Object

Make sure we show user server errors



88
89
90
91
92
# File 'lib/marvin/client/default_handlers.rb', line 88

def handle_incoming_error(opts = {})
  if opts[:message].present?
    logger.error "Server ERROR Message: #{opts[:message]}"
  end
end

#handle_incoming_join(opts = {}) ⇒ Object

Only record joins when you’ve successfully joined the channel.



80
81
82
83
84
85
# File 'lib/marvin/client/default_handlers.rb', line 80

def handle_incoming_join(opts = {})
  if opts[:nick] == @nickname
    channels << opts[:target]
    logger.info "Successfully joined channel #{opts[:target]}"
  end
end

#handle_incoming_numeric(opts = {}) ⇒ Object

TODO: Get the correct mapping for a given Code.



39
40
41
42
43
44
45
46
47
48
49
# File 'lib/marvin/client/default_handlers.rb', line 39

def handle_incoming_numeric(opts = {})
  case opts[:code]
    when Marvin::IRC::Replies[:RPL_WELCOME]
      handle_welcome
    when Marvin::IRC::Replies[:ERR_NICKNAMEINUSE]
      handle_nick_taken
  end
  code = opts[:code].to_i
  args = Marvin::Util.arguments(opts[:data])
  dispatch :incoming_numeric_processed, :code => code, :data => args
end

#handle_incoming_ping(opts = {}) ⇒ Object

The default response for PING’s - it simply replies with a PONG.



32
33
34
35
# File 'lib/marvin/client/default_handlers.rb', line 32

def handle_incoming_ping(opts = {})
  logger.info "Received Incoming Ping - Handling with a PONG"
  pong(opts[:data])
end

#handle_nick_takenObject

The default handler for when a users nickname is taken on on the server. It will attempt to get the nicknickname from the nicknames part of the configuration (if available) and will then call #nick to change the nickname.



65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/marvin/client/default_handlers.rb', line 65

def handle_nick_taken
  logger.info "Nickname '#{nickname}' on #{server} taken, trying next." 
  logger.info "Available Nicknames: #{nicks.empty? ? "None" : nicks.join(", ")}"
  if !nicks.empty?
    logger.info "Getting next nickname to switch"
    next_nick = nicks.shift # Get the next nickname
    logger.info "Attemping to set nickname to '#{next_nick}'"
    nick next_nick
  else
    logger.error "No Nicknames available - QUITTING"
    quit
  end
end

#handle_welcomeObject



51
52
53
54
55
56
57
58
59
# File 'lib/marvin/client/default_handlers.rb', line 51

def handle_welcome
  logger.info "Welcome received from server"
  # If a password is specified, we will attempt to message
  # NickServ to identify ourselves.
  say ":IDENTIFY #{self.configuration.password}", "NickServ" if configuration.password.present?
  # Join the default channels IF they're already set
  # Note that Marvin::IRC::Client.connect will set them AFTER this stuff is run.
  join default_channels
end

#host_with_portString

Returns the irc server and port

Returns:

  • (String)

    server port in “server:port” format



157
158
159
# File 'lib/marvin/abstract_client.rb', line 157

def host_with_port
  @host_with_port ||= "#{server}:#{port}"
end

#join(*channels_to_join) ⇒ Object

Join one or more channels on the current server e.g.

client.join "#marvin-testing"
client.join ["#marvin-testing", "#rubyonrails"]
client.join "#marvin-testing", "#rubyonrails"


23
24
25
26
27
28
29
# File 'lib/marvin/client/actions.rb', line 23

def join(*channels_to_join)
  channels_to_join = channels_to_join.flatten.map { |c| util.channel_name(c) }
  # If you're joining multiple channels at once, we join them together
  command :JOIN, channels_to_join.join(",")
  channels_to_join.each { |channel| dispatch :outgoing_join, :target => channel }
  logger.info "Sent JOIN for channels #{channels_to_join.join(", ")}"
end

#msg(target, message) ⇒ Object

Sends a message to a target (either a channel or a user) e.g.

msg "#marvin-testing", "Hello there!"
msg "SuttoL", "Hey, I'm playing with marvin!"


73
74
75
76
77
# File 'lib/marvin/client/actions.rb', line 73

def msg(target, message)
  command :privmsg, target, message
  dispatch :outgoing_message, :target => target, :message => message
  logger.info "Message #{target} - #{message}"
end

#nick(new_nick) ⇒ Object



95
96
97
98
99
100
101
# File 'lib/marvin/client/actions.rb', line 95

def nick(new_nick)
  logger.info "Changing nick to #{new_nick}"
  command :nick, new_nick
  @nickname = new_nick
  dispatch :outgoing_nick, :new_nick => new_nick
  logger.info "Nick changed to #{new_nick}"
end

#part(channel, reason = nil) ⇒ Object

Parts a channel, with an optional reason e.g.

part "#marvin-testing"
part "#marvin-testing", "Ninjas stole by felafel"


35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/marvin/client/actions.rb', line 35

def part(channel, reason = nil)
  channel = util.channel_name(channel)
  # Send the command anyway, even if we're not a
  # a recorded member something might of happened.
  command :part, channel, reason
  if channels.include?(channel)
    dispatch :outgoing_part, :target => channel, :reason => reason
    logger.info "Parted channel #{channel} - #{reason.present? ? reason : "Non given"}"
  else
    logger.warn "Parted channel #{channel} but wasn't recorded as member of channel"
  end
end

#pong(data) ⇒ Object



89
90
91
92
93
# File 'lib/marvin/client/actions.rb', line 89

def pong(data)
  command :pong, data
  dispatch :outgoing_pong
  logger.info "PONG sent to #{host_with_port} w/ data - #{data}"
end

#pre_dispatchingObject

Before dispatching, check if we need to reload and setup handlers correctly.



82
83
84
85
# File 'lib/marvin/abstract_client.rb', line 82

def pre_dispatching
  process_development
  setup_handlers
end

#process_connectObject

Initializes the instance variables used for the current connection, dispatching a :client_connected event once it has finished. During this process, it will call #client= on each handler if they respond to it.

See Also:



43
44
45
46
47
48
49
50
# File 'lib/marvin/abstract_client.rb', line 43

def process_connect
  self.class.setup
  logger.info "Initializing the current instance"
  @channels = []
  connections << self
  logger.info "Dispatching the default :client_connected event"
  dispatch :client_connected
end

#process_developmentObject

If @@development is true, We’ll attempt to reload any changed files (namely, handlers).s



77
78
79
# File 'lib/marvin/abstract_client.rb', line 77

def process_development
  Marvin::Reloading.reload! if @@development
end

#process_disconnectObject

Handles a lost connection / disconnect, stopping the loader if it’s the last connection (purposely terminated) otherwise scheduling a reconnection

See Also:

  • Loader.stop!


56
57
58
59
60
61
62
63
64
65
66
# File 'lib/marvin/abstract_client.rb', line 56

def process_disconnect
  logger.info "Handling disconnect for #{host_with_port}"
  connections.delete(self)
  dispatch :client_disconnected
  if @disconnect_expected
    Marvin::Loader.stop! if connections.blank?
  else
    logger.warn "Unexpectly lost connection to server; adding reconnect"
    self.class.add_reconnect @connection_config
  end
end

#quit(reason = nil, part_before_quit = false) ⇒ Object

Quites from a server, first parting all channels if a second argument is passed as true e.g.

quit
quit "Going to grab some z's"


53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/marvin/client/actions.rb', line 53

def quit(reason = nil, part_before_quit = false)
  @disconnect_expected = true
  # If the user wants to part before quitting, they should
  # pass a second, true, parameter
  if part_before_quit
    logger.info "Preparing to part from channels before quitting"
    channels.to_a.each { |chan| part(chan, reason) }
    logger.info "Parted from all channels, quitting"
  end
  command :quit, reason
  dispatch :outgoing_quit
  # Remove the connections from the pool
  connections.delete(self)
  logger.info  "Quit from #{host_with_port}"
end

#receive_line(line) ⇒ Object

Receives a raw line (without EOL characters) and dispatch the incoming_line event. Once that’s done, parse the line and dispatch an event if present.

Parameters:

  • line (String)

    the incoming line



137
138
139
140
141
# File 'lib/marvin/abstract_client.rb', line 137

def receive_line(line)
  dispatch :incoming_line, :line => line
  event = Marvin::Settings.parser.parse(line)
  dispatch(event.to_incoming_event_name, event.to_hash) unless event.nil?  
end

#setup_handlersObject

Iterates over handles and calls client= when defined. Used before dispatching in order to ensure each hander has the correct client. Note that this needs to be improved soon.



71
72
73
# File 'lib/marvin/abstract_client.rb', line 71

def setup_handlers
  handlers.each { |h| h.client = self if h.respond_to?(:client=) }
end