Sutto-marvin 0.4.0 → 0.8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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