Sutto-marvin 0.4.0 → 0.8.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. data/bin/marvin +22 -156
  2. data/handlers/keiki_thwopper.rb +21 -0
  3. data/handlers/tweet_tweet.rb +1 -3
  4. data/lib/marvin/abstract_client.rb +75 -189
  5. data/lib/marvin/abstract_parser.rb +9 -11
  6. data/lib/marvin/base.rb +134 -101
  7. data/lib/marvin/client/actions.rb +104 -0
  8. data/lib/marvin/client/default_handlers.rb +97 -0
  9. data/lib/marvin/command_handler.rb +60 -49
  10. data/lib/marvin/console.rb +4 -31
  11. data/lib/marvin/core_commands.rb +30 -12
  12. data/lib/marvin/distributed/client.rb +225 -0
  13. data/lib/marvin/distributed/handler.rb +85 -0
  14. data/lib/marvin/distributed/protocol.rb +88 -0
  15. data/lib/marvin/distributed/server.rb +154 -0
  16. data/lib/marvin/distributed.rb +4 -10
  17. data/lib/marvin/dsl.rb +103 -0
  18. data/lib/marvin/exception_tracker.rb +7 -4
  19. data/lib/marvin/irc/client.rb +127 -99
  20. data/lib/marvin/irc/event.rb +14 -10
  21. data/lib/marvin/irc.rb +0 -1
  22. data/lib/marvin/middle_man.rb +1 -1
  23. data/lib/marvin/parsers/command.rb +10 -8
  24. data/lib/marvin/parsers/prefixes/host_mask.rb +12 -7
  25. data/lib/marvin/parsers/prefixes/server.rb +1 -1
  26. data/lib/marvin/parsers/ragel_parser.rb +59 -52
  27. data/lib/marvin/parsers/ragel_parser.rl +6 -7
  28. data/lib/marvin/parsers/simple_parser.rb +4 -9
  29. data/lib/marvin/parsers.rb +1 -2
  30. data/lib/marvin/settings.rb +29 -79
  31. data/lib/marvin/test_client.rb +20 -26
  32. data/lib/marvin/util.rb +10 -3
  33. data/lib/marvin.rb +42 -39
  34. data/templates/boot.erb +3 -0
  35. data/templates/connections.yml.erb +10 -0
  36. data/templates/debug_handler.erb +5 -0
  37. data/templates/hello_world.erb +10 -0
  38. data/templates/rakefile.erb +15 -0
  39. data/templates/settings.yml.erb +8 -0
  40. data/{config/setup.rb → templates/setup.erb} +8 -10
  41. data/templates/test_helper.erb +17 -0
  42. data/test/abstract_client_test.rb +63 -0
  43. data/test/parser_comparison.rb +2 -2
  44. data/test/parser_test.rb +3 -3
  45. data/test/test_helper.rb +58 -6
  46. metadata +51 -83
  47. data/README.textile +0 -105
  48. data/TUTORIAL.textile +0 -54
  49. data/VERSION.yml +0 -4
  50. data/config/boot.rb +0 -14
  51. data/config/connections.yml.sample +0 -5
  52. data/config/settings.yml.sample +0 -13
  53. data/handlers/logging_handler.rb +0 -89
  54. data/lib/marvin/core_ext.rb +0 -11
  55. data/lib/marvin/daemon.rb +0 -71
  56. data/lib/marvin/data_store.rb +0 -73
  57. data/lib/marvin/dispatchable.rb +0 -99
  58. data/lib/marvin/distributed/dispatch_handler.rb +0 -83
  59. data/lib/marvin/distributed/drb_client.rb +0 -78
  60. data/lib/marvin/distributed/ring_server.rb +0 -41
  61. data/lib/marvin/handler.rb +0 -12
  62. data/lib/marvin/irc/server/abstract_connection.rb +0 -84
  63. data/lib/marvin/irc/server/base_connection.rb +0 -66
  64. data/lib/marvin/irc/server/channel.rb +0 -115
  65. data/lib/marvin/irc/server/named_store.rb +0 -14
  66. data/lib/marvin/irc/server/remote_interface.rb +0 -77
  67. data/lib/marvin/irc/server/user/handle_mixin.rb +0 -140
  68. data/lib/marvin/irc/server/user.rb +0 -5
  69. data/lib/marvin/irc/server/user_connection.rb +0 -134
  70. data/lib/marvin/irc/server/virtual_user_connection.rb +0 -80
  71. data/lib/marvin/irc/server.rb +0 -71
  72. data/lib/marvin/loader.rb +0 -149
  73. data/lib/marvin/logger.rb +0 -86
  74. data/lib/marvin/options.rb +0 -42
  75. data/lib/marvin/parsers/regexp_parser.rb +0 -93
  76. data/lib/marvin/status.rb +0 -72
  77. data/script/client +0 -3
  78. data/script/console +0 -3
  79. data/script/distributed_client +0 -3
  80. data/script/install +0 -1
  81. data/script/ring_server +0 -4
  82. data/script/server +0 -4
  83. data/script/status +0 -3
  84. data/spec/marvin/abstract_client_test.rb +0 -38
  85. data/spec/spec_helper.rb +0 -14
data/lib/marvin/base.rb CHANGED
@@ -1,131 +1,147 @@
1
- require 'ostruct'
2
-
3
1
  module Marvin
4
- # A Client Handler
2
+
3
+ def self.handler_parent_classes
4
+ @@handler_parent_classes ||= Hash.new { |h,k| h[k] = Set.new }
5
+ end
6
+
5
7
  class Base
8
+ is :loggable
6
9
 
7
- cattr_accessor :logger
8
- # Set the default logger
9
- self.logger ||= Marvin::Logger
10
-
11
- attr_accessor :client, :target, :from, :options, :logger
12
- class_inheritable_accessor :registered_handlers
13
- self.registered_handlers = {}
14
-
15
- def initialize
16
- self.registered_handlers ||= {}
17
- self.logger ||= Marvin::Logger
10
+ @@handlers = Hash.new do |h,k|
11
+ h[k] = Hash.new { |h2, k2| h2[k2] = [] }
18
12
  end
19
13
 
14
+ attr_accessor :client, :target, :from, :options
15
+
20
16
  class << self
17
+
18
+ def registered?
19
+ @registered ||= false
20
+ end
21
+
22
+ def registered=(value)
23
+ @registered = !!value
24
+ end
21
25
 
22
- def event_handlers_for(message_name, direct = true)
23
- return [] if self == Marvin::Base
24
- rh = (self.registered_handlers ||= {})
25
- rh[self.name] ||= {}
26
- rh[self.name][message_name] ||= []
27
- if direct
28
- found_handlers = rh[self.name][message_name]
29
- found_handlers += self.superclass.event_handlers_for(message_name)
30
- return found_handlers
31
- else
32
- return rh[self.name][message_name]
26
+ # Returns an array of all handlers associated with
27
+ # a specific event name (e.g. :incoming_message)
28
+ def event_handlers_for(message_name)
29
+ message_name = message_name.to_sym
30
+ items = []
31
+ klass = self
32
+ while klass != Object
33
+ items += @@handlers[klass][message_name]
34
+ klass = klass.superclass
33
35
  end
36
+ items
34
37
  end
35
38
 
36
- # Registers a block or method to be called when
37
- # a given event is occured.
38
- # == Examples
39
- #
40
- # on_event :incoming_message, :process_commands
41
- #
42
- # Will call process_commands the current object
43
- # every time incoming message is called.
44
- #
45
- # on_event :incoming_message do
46
- # Marvin::Logger.debug ">> #{options.inspect}"
47
- # end
48
- #
49
- # Will invoke the block every time an incoming message
50
- # is processed.
39
+ # Registers a block to be used as an event handler. The first
40
+ # argument is always the name of the event and the second
41
+ # is either a method name (e.g. :my_awesome_method) or
42
+ # a block (which is instance_evaled)
51
43
  def on_event(name, method_name = nil, &blk)
52
- # If the name is set and it responds to :to_sym
53
- # and no block was passed in.
54
- blk = proc { self.send(method_name.to_sym) } if method_name.respond_to?(:to_sym) && blk.blank?
55
- self.event_handlers_for(name, false) << blk
44
+ blk = proc { self.send(method_name) } if method_name.present?
45
+ @@handlers[self][name] << blk
56
46
  end
57
47
 
48
+ # Like on_event but instead of taking an event name it takes
49
+ # either a number or a name - corresponding to an IRC numeric
50
+ # reply.
58
51
  def on_numeric(value, method_name = nil, &blk)
59
- if value.is_a?(Numeric)
60
- new_value = "%03d" % value
61
- else
62
- new_value = Marvin::IRC::Replies[value]
63
- end
64
- if new_value.nil?
65
- logger.error "The numeric '#{value}' was undefined"
66
- else
67
- blk = proc { self.send(method_name.to_sym) } if method_name.respond_to?(:to_sym) && blk.blank?
68
- self.event_handlers_for(:"incoming_numeric_#{new_value}", false) << blk
69
- end
52
+ value = value.is_a?(Numeric) ? ("%03d" % value) : Marvin::IRC::Replies[value]
53
+ on_event(:"incoming_numeric_#{new_value}", method_name, &blk) if value.present?
70
54
  end
71
55
 
72
- # Register's in the IRC Client callback chain.
73
- def register!(parent = Marvin::Settings.default_client)
56
+ # Register this specific handler on the IRC handler.
57
+ def register!(parent = Marvin::Settings.client)
74
58
  return if self == Marvin::Base # Only do it for sub-classes.
75
- parent.register_handler self.new
59
+ parent.register_handler self.new unless parent.handlers.any? { |h| h.class == self }
60
+ Marvin.handler_parent_classes[self.name] << parent
76
61
  end
77
-
78
- def uses_datastore(datastore_name, local_name)
79
- if Marvin::Loader.type == :distributed_client
80
- Marvin::Logger.warn "Using datastores inside of a distributed client is a bad idea, mmmkay?"
62
+
63
+ def reloading!
64
+ Marvin.handler_parent_classes[self.name].each do |dispatcher|
65
+ parent_handlers = dispatcher.handlers
66
+ related = parent_handlers.select { |h| h.class == self }
67
+ related.each do |h|
68
+ h.handle(:reloading, {})
69
+ dispatcher.delete_handler(h)
70
+ end
81
71
  end
82
- cattr_accessor local_name.to_sym
83
- self.send("#{local_name}=", Marvin::DataStore.new(datastore_name))
84
- rescue Exception => e
85
- logger.debug "Exception in datastore declaration - #{e.inspect}"
86
72
  end
87
73
 
74
+ def reloaded!
75
+ Marvin.handler_parent_classes[self.name].each do |dispatcher|
76
+ before = dispatcher.handlers
77
+ register!(dispatcher)
78
+ after = dispatcher.handlers
79
+ (after - before).each { |h| h.handle(:reloaded, {}) }
80
+ end
81
+
82
+ end
83
+
88
84
  end
89
85
 
90
- # Given an incoming message, handle it appropriatly.
91
86
  def handle(message, options)
92
- begin
93
- self.setup_defaults(options)
94
- h = self.class.event_handlers_for(message)
95
- h.each { |handle| self.instance_eval(&handle) }
96
- rescue Exception => e
97
- logger.fatal "Exception processing handle #{message}"
98
- Marvin::ExceptionTracker.log(e)
99
- end
87
+ dup._handle(message, options)
88
+ end
89
+
90
+ # Given an incoming message, handle it appropriately by getting all
91
+ # associated event handlers. It also logs any exceptions (aslong as
92
+ # they raised by halt)
93
+ def _handle(message, options)
94
+ setup_details(options)
95
+ h = self.class.event_handlers_for(message)
96
+ h.each { |eh| self.instance_eval(&eh) }
97
+ rescue Exception => e
98
+ # Pass on halt_handler_processing events.
99
+ raise e if e.is_a?(Marvin::HaltHandlerProcessing)
100
+ logger.fatal "Exception processing handler for #{message.inspect}"
101
+ Marvin::ExceptionTracker.log(e)
102
+ ensure
103
+ reset_details
100
104
  end
101
105
 
106
+ # The default handler for numerics. mutates them into a more
107
+ # friendly version of themselves. It will also pass through
108
+ # the original incoming_numeric event.
102
109
  def handle_incoming_numeric(opts)
103
- self.handle(:incoming_numeric, opts)
104
- name = :"incoming_numeric_#{options.code}"
105
- events = self.class.event_handlers_for(name)
106
- logger.debug "Dispatching #{events.size} events for #{name}"
107
- events.each { |eh| self.instance_eval(&eh) }
110
+ handle(:incoming_numeric, opts)
111
+ handle(:"incoming_numeric_#{opts[:code]}", opts)
112
+ end
113
+
114
+ # msg sends the given text to the current target, be it
115
+ # either a channel or a specific user.
116
+ def msg(message, target = self.target)
117
+ client.msg(target, message)
108
118
  end
109
119
 
110
- def say(message, target = self.target)
111
- client.msg target, message
120
+ alias say msg
121
+
122
+ def action(message, target = self.target)
123
+ client.action(target, message)
112
124
  end
113
125
 
114
- def pm(target, message)
115
- say(target, message)
126
+ # A conditional version of message that will only send the message
127
+ # if the target / from is a user. To do this, it uses from_channel?
128
+ def pm(message, target)
129
+ say(message, target) unless from_channel?(target)
116
130
  end
117
131
 
132
+ # Replies to a message. if it was received in a channel, it will
133
+ # use the standard irc "Name: text" convention for replying whilst
134
+ # if it was in a direct message it sends it as is.
118
135
  def reply(message)
119
136
  if from_channel?
120
- say "#{self.from}: #{message}"
137
+ say("#{from}: #{message}")
121
138
  else
122
- say message, self.from # Default back to pm'ing the user
139
+ say(message, from)
123
140
  end
124
141
  end
125
142
 
126
143
  def ctcp(message)
127
- return if from_channel? # Must be from user
128
- say "\01#{message}\01", self.from
144
+ say("\01#{message}\01", from) if !from_channel?
129
145
  end
130
146
 
131
147
  # Request information
@@ -133,29 +149,46 @@ module Marvin
133
149
  # reflects whether or not the current message / previous message came
134
150
  # from a user via pm.
135
151
  def from_user?
136
- self.target && !from_channel?
152
+ !from_channel?
137
153
  end
138
154
 
139
- # Determines whether the previous message was inside a channel.
140
- def from_channel?
141
- self.target && [?#, ?&].include?(self.target[0])
155
+ # Determines whether a given target (defaulting to the target of the
156
+ # last message was in a channel)
157
+ def from_channel?(target = self.target)
158
+ target.present? && target =~ /^[\&\#]/
142
159
  end
143
160
 
144
161
  def addressed?
145
- self.from_user? || options.message =~ /^#{self.client.nickname.downcase}:\s+/i
162
+ from_user? || options.message =~ /^#{client.nickname.downcase}:\s+/i
163
+ end
164
+
165
+ # A Perennial automagical helper for dispatch
166
+ def registered=(value)
167
+ self.class.registered = value
168
+ end
169
+
170
+ protected
171
+
172
+ # Initializes details for the current cycle - in essence, this makes the
173
+ # details of the current request available.
174
+ def setup_details(options)
175
+ @options = options.is_a?(Marvin::Nash) ? options : Marvin::Nash.new(options.to_hash)
176
+ @target = @options.target if @options.target?
177
+ @from = @options.nick if @options.nick?
146
178
  end
147
179
 
148
- def setup_defaults(options)
149
- self.options = options.is_a?(OpenStruct) ? options : OpenStruct.new(options)
150
- self.target = options[:target] if options.has_key?(:target)
151
- self.from = options[:nick] if options.has_key?(:nick)
180
+ def reset_details
181
+ @options = nil
182
+ @target = nil
183
+ @from = nil
152
184
  end
153
185
 
154
- # Halt's on the handler, used to prevent
155
- # other handlers also responding to the same
156
- # message more than once.
186
+ # Halt can be called during the handle / process. Doing so
187
+ # prevents any more handlers in the handler chain from being
188
+ # called. It's kind of like return but it works across all
189
+ # handlers, not just the current one.
157
190
  def halt!
158
- raise HaltHandlerProcessing
191
+ raise Marvin::HaltHandlerProcessing
159
192
  end
160
193
 
161
194
  end
@@ -0,0 +1,104 @@
1
+ module Marvin
2
+ class AbstractClient
3
+
4
+ ## General IRC Functions / Actions
5
+
6
+ # Sends a specified command to the server.
7
+ # Takes name (e.g. :privmsg) and all of the args.
8
+ # Very simply formats them as a string correctly
9
+ # and calls send_data with the results.
10
+ def command(name, *args)
11
+ # First, get the appropriate command
12
+ name = name.to_s.upcase
13
+ args = args.flatten
14
+ args << util.last_param(args.pop)
15
+ send_line "#{name} #{args.compact.join(" ").strip}\r\n"
16
+ end
17
+
18
+ # Join one or more channels on the current server
19
+ # e.g.
20
+ # client.join "#marvin-testing"
21
+ # client.join ["#marvin-testing", "#rubyonrails"]
22
+ # client.join "#marvin-testing", "#rubyonrails"
23
+ def join(*channels_to_join)
24
+ channels_to_join = channels_to_join.flatten.map { |c| util.channel_name(c) }
25
+ # If you're joining multiple channels at once, we join them together
26
+ command :JOIN, channels_to_join.join(",")
27
+ channels_to_join.each { |channel| dispatch :outgoing_join, :target => channel }
28
+ logger.info "Sent JOIN for channels #{channels_to_join.join(", ")}"
29
+ end
30
+
31
+ # Parts a channel, with an optional reason
32
+ # e.g.
33
+ # part "#marvin-testing"
34
+ # part "#marvin-testing", "Ninjas stole by felafel"
35
+ def part(channel, reason = nil)
36
+ channel = util.channel_name(channel)
37
+ # Send the command anyway, even if we're not a
38
+ # a recorded member something might of happened.
39
+ command :part, channel, reason
40
+ if channels.include?(channel)
41
+ dispatch :outgoing_part, :target => channel, :reason => reason
42
+ logger.info "Parted channel #{channel} - #{reason.present? ? reason : "Non given"}"
43
+ else
44
+ logger.warn "Parted channel #{channel} but wasn't recorded as member of channel"
45
+ end
46
+ end
47
+
48
+ # Quites from a server, first parting all channels if a second
49
+ # argument is passed as true
50
+ # e.g.
51
+ # quit
52
+ # quit "Going to grab some z's"
53
+ def quit(reason = nil, part_before_quit = false)
54
+ @disconnect_expected = true
55
+ # If the user wants to part before quitting, they should
56
+ # pass a second, true, parameter
57
+ if part_before_quit
58
+ logger.info "Preparing to part from channels before quitting"
59
+ channels.to_a.each { |chan| part(chan, reason) }
60
+ logger.info "Parted from all channels, quitting"
61
+ end
62
+ command :quit, reason
63
+ dispatch :outgoing_quit
64
+ # Remove the connections from the pool
65
+ connections.delete(self)
66
+ logger.info "Quit from #{host_with_port}"
67
+ end
68
+
69
+ # Sends a message to a target (either a channel or a user)
70
+ # e.g.
71
+ # msg "#marvin-testing", "Hello there!"
72
+ # msg "SuttoL", "Hey, I'm playing with marvin!"
73
+ def msg(target, message)
74
+ command :privmsg, target, message
75
+ dispatch :outgoing_message, :target => target, :message => message
76
+ logger.info "Message #{target} - #{message}"
77
+ end
78
+
79
+ # Does a CTCP action in a channel (equiv. to doing /me in most IRC clients)
80
+ # e.g.
81
+ # action "#marvin-testing", "is about to sleep"
82
+ # action "SuttoL", "is about to sleep"
83
+ def action(target, message)
84
+ command :privmsg, target, "\01ACTION #{message.strip}\01"
85
+ dispatch :outgoing_action, :target => target, :message => message
86
+ logger.info "Action sent to #{target} - #{message}"
87
+ end
88
+
89
+ def pong(data)
90
+ command :pong, data
91
+ dispatch :outgoing_pong
92
+ logger.info "PONG sent to #{host_with_port} w/ data - #{data}"
93
+ end
94
+
95
+ def nick(new_nick)
96
+ logger.info "Changing nick to #{new_nick}"
97
+ command :nick, new_nick
98
+ @nickname = new_nick
99
+ dispatch :outgoing_nick, :new_nick => new_nick
100
+ logger.info "Nick changed to #{new_nick}"
101
+ end
102
+
103
+ end
104
+ end
@@ -0,0 +1,97 @@
1
+ module Marvin
2
+ class AbstractClient
3
+
4
+ # Default handlers
5
+
6
+ # The default handler for all things initialization-related
7
+ # on the client. Usually, this will send the user command,
8
+ # set out nick, join all of the channels / rooms we wish
9
+ # to be in and if a password is specified in the configuration,
10
+ # it will also attempt to identify us.
11
+ def handle_client_connected(opts = {})
12
+ logger.info "About to handle client connected"
13
+ # If the pass is set
14
+ unless pass.blank?
15
+ logger.info "Sending pass for connection"
16
+ command :pass, pass
17
+ end
18
+ # IRC Connection is establish so we send all the required commands to the server.
19
+ logger.info "Setting default nickname"
20
+ nick nicks.shift
21
+ logger.info "Sending user command"
22
+ command :user, configuration.user, "0", "*", configuration.name
23
+ rescue Exception => e
24
+ Marvin::ExceptionTracker.log(e)
25
+ end
26
+
27
+ # handle a bunch of default events that happen at a connection
28
+ # level instead of a per-app level.
29
+
30
+ # The default response for PING's - it simply replies
31
+ # with a PONG.
32
+ def handle_incoming_ping(opts = {})
33
+ logger.info "Received Incoming Ping - Handling with a PONG"
34
+ pong(opts[:data])
35
+ end
36
+
37
+ # TODO: Get the correct mapping for a given
38
+ # Code.
39
+ def handle_incoming_numeric(opts = {})
40
+ case opts[:code]
41
+ when Marvin::IRC::Replies[:RPL_WELCOME]
42
+ handle_welcome
43
+ when Marvin::IRC::Replies[:ERR_NICKNAMEINUSE]
44
+ handle_nick_taken
45
+ when Marvin::IRC::Replies[:RPL_TOPIC]
46
+ handle_channel_topic
47
+ end
48
+ code = opts[:code].to_i
49
+ args = Marvin::Util.arguments(opts[:data])
50
+ dispatch :incoming_numeric_processed, :code => code, :data => args
51
+ end
52
+
53
+ def handle_welcome
54
+ logger.info "Welcome received from server"
55
+ # If a password is specified, we will attempt to message
56
+ # NickServ to identify ourselves.
57
+ say ":IDENTIFY #{self.configuration.password}", "NickServ" if configuration.password.present?
58
+ # Join the default channels IF they're already set
59
+ # Note that Marvin::IRC::Client.connect will set them AFTER this stuff is run.
60
+ join default_channels
61
+ end
62
+
63
+ # The default handler for when a users nickname is taken on
64
+ # on the server. It will attempt to get the nicknickname from
65
+ # the nicknames part of the configuration (if available) and
66
+ # will then call #nick to change the nickname.
67
+ def handle_nick_taken
68
+ logger.info "Nickname '#{nickname}' on #{server} taken, trying next."
69
+ logger.info "Available Nicknames: #{nicks.empty? ? "None" : nicks.join(", ")}"
70
+ if !nicks.empty?
71
+ logger.info "Getting next nickname to switch"
72
+ next_nick = nicks.shift # Get the next nickname
73
+ logger.info "Attemping to set nickname to '#{next_nick}'"
74
+ nick next_nick
75
+ else
76
+ logger.fatal "No Nicknames available - QUITTING"
77
+ quit
78
+ end
79
+ end
80
+
81
+ # Only record joins when you've successfully joined the channel.
82
+ def handle_incoming_join(opts = {})
83
+ if opts[:nick] == @nickname
84
+ channels << opts[:target]
85
+ logger.info "Successfully joined channel #{opts[:target]}"
86
+ end
87
+ end
88
+
89
+ # Make sure we show user server errors
90
+ def handle_incoming_error(opts = {})
91
+ if opts[:message].present?
92
+ logger.info "Got ERROR Message: #{opts[:message]}"
93
+ end
94
+ end
95
+
96
+ end
97
+ end
@@ -1,80 +1,91 @@
1
1
  require 'set'
2
2
 
3
3
  module Marvin
4
-
5
- # A Simple Marvin handler based on processing
6
- # commands, similar in design to MatzBot.
7
4
  class CommandHandler < Base
8
5
 
9
- class_inheritable_accessor :exposed_methods, :command_prefix
10
- cattr_accessor :descriptions, :last_description, :exposed_method_names
6
+ class_inheritable_accessor :command_prefix
7
+ self.command_prefix = ""
11
8
 
12
- self.command_prefix = ""
13
- self.exposed_methods = Set.new
14
- self.descriptions = {}
15
- self.exposed_method_names = Set.new
9
+ @@exposed_method_mapping = Hash.new { |h,k| h[k] = [] }
10
+ @@method_descriptions = Hash.new { |h,k| h[k] = {} }
11
+ @@registered_classes = Set.new
16
12
 
17
13
  class << self
18
14
 
15
+ def command(name, method_desc = nil, &blk)
16
+ exposes name
17
+ desc method_desc unless method_desc.blank?
18
+ define_method(name, &blk)
19
+ end
20
+
21
+ def prefix_is(p)
22
+ self.command_prefix = p
23
+ end
24
+
19
25
  def exposes(*args)
20
- names = args.map { |a| a.to_sym }.flatten
21
- self.exposed_methods += names
22
- self.exposed_method_names += names
26
+ args.each { |name| @@exposed_method_mapping[self] << name.to_sym }
27
+ end
28
+
29
+ def exposed_methods
30
+ methods = []
31
+ klass = self
32
+ while klass != Object
33
+ methods += @@exposed_method_mapping[klass]
34
+ klass = klass.superclass
35
+ end
36
+ return methods.uniq.compact
37
+ end
38
+
39
+ def prefix_regexp
40
+ /^#{command_prefix}/
41
+ end
42
+
43
+ def desc(description)
44
+ @last_description = description
45
+ end
46
+
47
+ def exposed_name(method)
48
+ "#{command_prefix}#{method}"
23
49
  end
24
50
 
25
51
  end
26
52
 
27
- on_event :incoming_message do
28
- logger.debug "Incoming message"
29
- check_for_commands
30
- end
53
+ on_event :incoming_message, :check_for_commands
31
54
 
32
55
  def check_for_commands
56
+ message = options.message.to_s.strip
33
57
  data, command = nil, nil
34
- if self.from_channel?
35
- logger.debug "Processing command in channel"
36
- split_message = options.message.split(" ", 3)
37
- prefix = split_message.shift
38
- # Return if in channel and it isn't address to the user.
39
- return unless prefix == "#{self.client.nickname}:"
40
- command, data = split_message # Set remaining.
58
+ if from_channel?
59
+ name, command, data = message.split(/\s+/, 2)
60
+ return if name !~ /^#{client.nickname}:/i
41
61
  else
42
- command, data = options.message.split(" ", 2)
62
+ command, data = message.splt(/\s+/, 2)
43
63
  end
44
- # Double check for sanity
45
- return if command.blank?
46
- command_name = extract_command_name(command)
47
- unless command_name.nil?
48
- logger.debug "Command Exists - processing"
49
- # Dispatch the command.
50
- self.send(command_name, data.to_s.split(" ")) if self.respond_to?(command_name)
64
+ data ||= ""
65
+ if (command_name = extract_command_name(command)).present?
66
+ logger.info "Processing command '#{command_name}' for #{from}"
67
+ send(command_name, data.to_s) if respond_to?(command_name)
51
68
  end
52
69
  end
53
70
 
54
71
  def extract_command_name(command)
55
- prefix_length = self.command_prefix.to_s.length
56
- has_prefix = command[0...prefix_length] == self.command_prefix.to_s
57
- if has_prefix
58
- method_name = command[prefix_length..-1].to_s.underscore.to_sym
59
- return method_name if self.exposed_methods.to_a.include?(method_name)
72
+ re = self.class.prefix_regexp
73
+ if command =~ re
74
+ method_name = command.gsub(re, "").underscore.to_sym
75
+ return method_name if self.class.exposed_methods.include?(method_name)
60
76
  end
61
77
  end
62
78
 
63
- class << self
64
-
65
- def desc(desc)
66
- self.last_description = desc
67
- end
68
-
69
- def method_added(name)
70
- unless last_description.blank?
71
- descriptions[name.to_sym] = self.last_description
72
- self.last_description = nil
73
- end
79
+ def exposed_name(name)
80
+ self.class.exposed_name(name)
81
+ end
82
+
83
+ def self.method_added(name)
84
+ if @last_description.present?
85
+ @@method_descriptions[self][name.to_sym] = @last_description
86
+ @last_description = nil
74
87
  end
75
-
76
88
  end
77
89
 
78
90
  end
79
-
80
91
  end