marvin 0.8.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/bin/marvin +33 -0
  2. data/handlers/debug_handler.rb +5 -0
  3. data/handlers/hello_world.rb +9 -0
  4. data/handlers/keiki_thwopper.rb +21 -0
  5. data/handlers/simple_logger.rb +24 -0
  6. data/handlers/tweet_tweet.rb +19 -0
  7. data/lib/marvin.rb +56 -0
  8. data/lib/marvin/abstract_client.rb +146 -0
  9. data/lib/marvin/abstract_parser.rb +29 -0
  10. data/lib/marvin/base.rb +195 -0
  11. data/lib/marvin/client/actions.rb +104 -0
  12. data/lib/marvin/client/default_handlers.rb +97 -0
  13. data/lib/marvin/command_handler.rb +91 -0
  14. data/lib/marvin/console.rb +50 -0
  15. data/lib/marvin/core_commands.rb +49 -0
  16. data/lib/marvin/distributed.rb +8 -0
  17. data/lib/marvin/distributed/client.rb +225 -0
  18. data/lib/marvin/distributed/handler.rb +85 -0
  19. data/lib/marvin/distributed/protocol.rb +88 -0
  20. data/lib/marvin/distributed/server.rb +154 -0
  21. data/lib/marvin/dsl.rb +103 -0
  22. data/lib/marvin/exception_tracker.rb +19 -0
  23. data/lib/marvin/exceptions.rb +11 -0
  24. data/lib/marvin/irc.rb +7 -0
  25. data/lib/marvin/irc/client.rb +168 -0
  26. data/lib/marvin/irc/event.rb +39 -0
  27. data/lib/marvin/irc/replies.rb +154 -0
  28. data/lib/marvin/logging_handler.rb +76 -0
  29. data/lib/marvin/middle_man.rb +103 -0
  30. data/lib/marvin/parsers.rb +9 -0
  31. data/lib/marvin/parsers/command.rb +107 -0
  32. data/lib/marvin/parsers/prefixes.rb +8 -0
  33. data/lib/marvin/parsers/prefixes/host_mask.rb +35 -0
  34. data/lib/marvin/parsers/prefixes/server.rb +24 -0
  35. data/lib/marvin/parsers/ragel_parser.rb +720 -0
  36. data/lib/marvin/parsers/ragel_parser.rl +143 -0
  37. data/lib/marvin/parsers/simple_parser.rb +35 -0
  38. data/lib/marvin/settings.rb +31 -0
  39. data/lib/marvin/test_client.rb +58 -0
  40. data/lib/marvin/util.rb +54 -0
  41. data/templates/boot.erb +3 -0
  42. data/templates/connections.yml.erb +10 -0
  43. data/templates/debug_handler.erb +5 -0
  44. data/templates/hello_world.erb +10 -0
  45. data/templates/rakefile.erb +15 -0
  46. data/templates/settings.yml.erb +8 -0
  47. data/templates/setup.erb +31 -0
  48. data/templates/test_helper.erb +17 -0
  49. data/test/abstract_client_test.rb +63 -0
  50. data/test/parser_comparison.rb +62 -0
  51. data/test/parser_test.rb +266 -0
  52. data/test/test_helper.rb +62 -0
  53. data/test/util_test.rb +57 -0
  54. metadata +136 -0
data/bin/marvin ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ require File.join(File.dirname(__FILE__), "..", "lib", "marvin")
4
+
5
+ Marvin::Application.processing(ARGV) do |a|
6
+
7
+ a.banner = "Marvin v#{Marvin::VERSION} - An IRC Library for Ruby"
8
+
9
+ a.generator!
10
+
11
+ a.option :development, "Runs the app in development mode (handler reloading)", :shortcut => "D"
12
+ a.controller! :client, "Starts the actual Marvin client instance"
13
+ a.controller! :console, "Opens a friendly IRB prompt with Marvin pre-loaded"
14
+ a.controller! :distributed_client, "Starts a distributed client instance"
15
+
16
+ a.option :force, "force the creation of the application"
17
+ a.add "create PATH", "Creates a marvin application at the given location" do |path, options|
18
+ path = File.expand_path(path)
19
+ if File.exists?(path) && !options[:force]
20
+ die! "The path you tried to use, #{path}, already exists. Please try another or use the --force option"
21
+ end
22
+ setup_generator(path)
23
+ folders 'tmp', 'config', 'lib', 'handlers', 'test'
24
+ template 'boot.erb', 'config/boot.rb'
25
+ template 'setup.erb', 'config/setup.rb'
26
+ template 'settings.yml.erb', 'config/settings.yml'
27
+ template 'connections.yml.erb', 'config/connections.yml'
28
+ template 'debug_handler.erb', 'handlers/debug_handler.rb'
29
+ template 'hello_world.erb', 'handlers/hello_world.rb'
30
+ template 'rakefile.erb', 'Rakefile'
31
+ end
32
+
33
+ end
@@ -0,0 +1,5 @@
1
+ # Use this class to debug stuff as you
2
+ # go along - e.g. dump events etc.
3
+ class DebugHandler < Marvin::CommandHandler
4
+
5
+ end
@@ -0,0 +1,9 @@
1
+ class HelloWorld < Marvin::CommandHandler
2
+
3
+ exposes :hello
4
+
5
+ def hello(data)
6
+ reply "Hola from process with pid #{Process.pid}!"
7
+ end
8
+
9
+ end
@@ -0,0 +1,21 @@
1
+ class KeikiThwopper < Marvin::Base
2
+
3
+ FROM_REGEXP = /^(sutto)/i
4
+ THWOP_REGEXP = /(t+h+w+o+m*p+|stab|kill)/i
5
+
6
+ MESSAGES = [
7
+ "mwahahahaha",
8
+ "you totally deserved it",
9
+ "oi! leave 'em alone!",
10
+ "say hello to my little friend",
11
+ "you know, they could have liked that?"
12
+ ]
13
+
14
+ on_event :incoming_action, :thwop_back
15
+
16
+ def thwop_back
17
+ return if !from || from !~ FROM_REGEXP || options.message !~ THWOP_REGEXP
18
+ action "#{$1}s #{from} (#{MESSAGES[rand(MESSAGES.length)]})"
19
+ end
20
+
21
+ end
@@ -0,0 +1,24 @@
1
+ # A reference logger example
2
+ class SimpleLogger < Marvin::LoggingHandler
3
+
4
+ def setup_logging
5
+ logger.warn "Setting up the client"
6
+ end
7
+
8
+ def teardown_logging
9
+ logger.warn "Tearing down the logger"
10
+ end
11
+
12
+ def log_incoming(server, nick, target, message)
13
+ logger.fatal "[INCOMING] #{server} (#{target}) #{nick}: #{message}"
14
+ end
15
+
16
+ def log_outgoing(server, nick, target, message)
17
+ logger.fatal "[OUTGOING] #{server} (#{target}) #{nick}: #{message}"
18
+ end
19
+
20
+ def log_message(server, nick, target, message)
21
+ logger.fatal "[MESSAGE] #{server} (#{target}) #{nick}: #{message}"
22
+ end
23
+
24
+ end
@@ -0,0 +1,19 @@
1
+ # Not Yet Complete: Twitter Client in Channel.
2
+ class TweetTweet < Marvin::Base
3
+
4
+ on_event :client_connected do
5
+ start_tweeting
6
+ end
7
+
8
+ def start_tweeting
9
+ client.periodically 180, :check_tweets
10
+ end
11
+
12
+ def handle_check_tweets
13
+ logger.debug ">> Check Tweets"
14
+ end
15
+
16
+ def show_tweet(tweet)
17
+ end
18
+
19
+ end
data/lib/marvin.rb ADDED
@@ -0,0 +1,56 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ require 'rubygems'
3
+ require 'perennial'
4
+
5
+ module Marvin
6
+ include Perennial
7
+
8
+ VERSION = [0, 8, 0, 0]
9
+
10
+ # Misc.
11
+ #autoload :Util, 'marvin/util'
12
+ # Client
13
+ #autoload :AbstractClient, 'marvin/abstract_client'
14
+ #autoload :IRC, 'marvin/irc'
15
+ autoload :TestClient, 'marvin/test_client'
16
+ # Console of DOOM.
17
+ autoload :Console, 'marvin/console'
18
+ # Distributed
19
+ autoload :Distributed, 'marvin/distributed'
20
+ autoload :Status, 'marvin/status'
21
+ # Handler
22
+ autoload :Base, 'marvin/base'
23
+ autoload :CommandHandler, 'marvin/command_handler'
24
+ autoload :LoggingHandler, 'marvin/logging_handler'
25
+ autoload :CoreCommands, 'marvin/core_commands'
26
+ autoload :MiddleMan, 'marvin/middle_man'
27
+ # These should be namespaced under IRC
28
+ #autoload :AbstractParser, 'marvin/abstract_parser'
29
+ autoload :Parsers, 'marvin/parsers'
30
+
31
+
32
+ manifest do |m, l|
33
+ Settings.root = File.dirname(File.dirname(__FILE__))
34
+ l.register_controller :client, 'Marvin::Settings.client'
35
+ l.register_controller :console, 'Marvin::Console'
36
+ l.register_controller :distributed_client, 'Marvin::Distributed::Client'
37
+ # Core Commands handily makes available a set
38
+ # of information about what is running etc.
39
+
40
+ l.before_run do
41
+ if l.distributed_client?
42
+ Marvin::Settings.client = Marvin::Distributed::Client
43
+ end
44
+ end
45
+
46
+ end
47
+
48
+ def self.version(include_minor = false)
49
+ VERSION[0, (include_minor ? 4 : 3)].join(".")
50
+ end
51
+
52
+ has_library :util, :abstract_client, :abstract_parser, :irc, :exception_tracker
53
+
54
+ extends_library :settings
55
+
56
+ end
@@ -0,0 +1,146 @@
1
+ require "marvin/irc/event"
2
+
3
+ module Marvin
4
+ class AbstractClient
5
+
6
+ is :dispatchable, :loggable
7
+
8
+ def initialize(opts)
9
+ opts = opts.to_nash if opts.is_a?(Hash)
10
+ @connection_config = opts.dup # Copy the options so we can use them to reconnect.
11
+ @server = opts.server
12
+ @port = opts.port
13
+ @default_channels = opts.channels
14
+ @nicks = opts.nicks || []
15
+ @pass = opts.pass
16
+ end
17
+
18
+ cattr_accessor :events, :configuration, :is_setup, :connections, :development
19
+ attr_accessor :channels, :nickname, :server, :port, :nicks, :pass,
20
+ :disconnect_expected, :connection_config
21
+
22
+ # Set the default values for the variables
23
+ @@events = []
24
+ @@configuration = Marvin::Nash.new
25
+ @@connections = []
26
+ @@development = false
27
+
28
+ # Initializes the instance variables used for the
29
+ # current connection, dispatching a :client_connected event
30
+ # once it has finished. During this process, it will
31
+ # call #client= on each handler if they respond to it.
32
+ def process_connect
33
+ self.class.setup
34
+ logger.info "Initializing the current instance"
35
+ @channels = []
36
+ connections << self
37
+ logger.info "Setting the client for each handler"
38
+ setup_handlers
39
+ logger.info "Dispatching the default :client_connected event"
40
+ dispatch :client_connected
41
+ end
42
+
43
+ def process_disconnect
44
+ logger.info "Handling disconnect for #{host_with_port}"
45
+ connections.delete(self)
46
+ dispatch :client_disconnected
47
+ unless @disconnect_expected
48
+ logger.warn "Unexpectly lost connection to server; adding reconnect"
49
+ self.class.add_reconnect @connection_config
50
+ else
51
+ Marvin::Loader.stop! if connections.blank?
52
+ end
53
+ end
54
+
55
+ def setup_handlers
56
+ handlers.each { |h| h.client = self if h.respond_to?(:client=) }
57
+ end
58
+
59
+ def process_development
60
+ if @@development
61
+ Marvin::Reloading.reload!
62
+ setup_handlers
63
+ end
64
+ end
65
+
66
+ def dispatch(*args)
67
+ process_development
68
+ super
69
+ end
70
+
71
+ # Sets the current class-wide settings of this IRC Client
72
+ # to either an OpenStruct or the results of #to_hash on
73
+ # any other value that is passed in.
74
+ def self.configuration=(config)
75
+ config = Marvin::Nash.new(config.to_hash) unless config.is_a?(Marvin::Nash)
76
+ @@configuration = config.normalized
77
+ end
78
+
79
+ def self.setup?
80
+ @setup ||= false
81
+ end
82
+
83
+ def self.setup
84
+ return if setup?
85
+ configure
86
+ end
87
+
88
+ def self.configure
89
+ config = Marvin::Nash.new
90
+ config.merge! Marvin::Settings.configuration
91
+ if block_given?
92
+ yield(nash = Marvin::Nash.new)
93
+ config.merge! nash
94
+ end
95
+ @@configuration = config
96
+ # Help is only currently available on an instance running
97
+ # distributed handler.
98
+ Marvin::CoreCommands.register! unless Marvin::Distributed::Handler.registered?
99
+ @setup = true
100
+ end
101
+
102
+ ## Handling all of the the actual client stuff.
103
+
104
+ def receive_line(line)
105
+ dispatch :incoming_line, :line => line
106
+ event = Marvin::Settings.parser.parse(line)
107
+ dispatch(event.to_incoming_event_name, event.to_hash) unless event.nil?
108
+ end
109
+
110
+ def default_channels
111
+ @default_channels ||= []
112
+ end
113
+
114
+ def default_channels=(channels)
115
+ @default_channels = channels.to_a.map { |c| c.to_s }
116
+ end
117
+
118
+ def host_with_port
119
+ @host_with_port ||= "#{server}:#{port}"
120
+ end
121
+
122
+ def nicks
123
+ if @nicks.blank? && !@nicks_loaded
124
+ logger.info "Setting default nick list"
125
+ @nicks = []
126
+ @nicks << configuration.nick if configuration.nick?
127
+ @nicks += configuration.nicks.to_a if configuration.nicks?
128
+ @nicks.compact!
129
+ raise "No initial nicks for #{host_with_port}" if @nicks.blank?
130
+ @nicks_loaded = true
131
+ end
132
+ return @nicks
133
+ end
134
+
135
+ # Break it down into a couple of different files.
136
+ require 'marvin/client/default_handlers'
137
+ require 'marvin/client/actions'
138
+
139
+ protected
140
+
141
+ def util
142
+ Marvin::Util
143
+ end
144
+
145
+ end
146
+ end
@@ -0,0 +1,29 @@
1
+ module Marvin
2
+ class AbstractParser
3
+
4
+ attr_accessor :line, :command, :event
5
+
6
+ # Instantiates a parser instance, attempts to
7
+ # parse it for it's command and it's event.
8
+ def initialize(line)
9
+ @line = line
10
+ @command = self.class.parse!(line)
11
+ @event = @command.to_event unless @command.blank?
12
+ end
13
+
14
+ def to_event
15
+ @event
16
+ end
17
+
18
+ def self.parse(line)
19
+ new(line.strip).to_event
20
+ end
21
+
22
+ protected
23
+
24
+ def self.parse!(line)
25
+ raise NotImplementedError, "Must be implemented in a subclass"
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,195 @@
1
+ module Marvin
2
+
3
+ def self.handler_parent_classes
4
+ @@handler_parent_classes ||= Hash.new { |h,k| h[k] = Set.new }
5
+ end
6
+
7
+ class Base
8
+ is :loggable
9
+
10
+ @@handlers = Hash.new do |h,k|
11
+ h[k] = Hash.new { |h2, k2| h2[k2] = [] }
12
+ end
13
+
14
+ attr_accessor :client, :target, :from, :options
15
+
16
+ class << self
17
+
18
+ def registered?
19
+ @registered ||= false
20
+ end
21
+
22
+ def registered=(value)
23
+ @registered = !!value
24
+ end
25
+
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
35
+ end
36
+ items
37
+ end
38
+
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)
43
+ def on_event(name, method_name = nil, &blk)
44
+ blk = proc { self.send(method_name) } if method_name.present?
45
+ @@handlers[self][name] << blk
46
+ end
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.
51
+ def on_numeric(value, method_name = nil, &blk)
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?
54
+ end
55
+
56
+ # Register this specific handler on the IRC handler.
57
+ def register!(parent = Marvin::Settings.client)
58
+ return if self == Marvin::Base # Only do it for sub-classes.
59
+ parent.register_handler self.new unless parent.handlers.any? { |h| h.class == self }
60
+ Marvin.handler_parent_classes[self.name] << parent
61
+ end
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
71
+ end
72
+ end
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
+
84
+ end
85
+
86
+ def handle(message, options)
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
104
+ end
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.
109
+ def handle_incoming_numeric(opts)
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)
118
+ end
119
+
120
+ alias say msg
121
+
122
+ def action(message, target = self.target)
123
+ client.action(target, message)
124
+ end
125
+
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)
130
+ end
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.
135
+ def reply(message)
136
+ if from_channel?
137
+ say("#{from}: #{message}")
138
+ else
139
+ say(message, from)
140
+ end
141
+ end
142
+
143
+ def ctcp(message)
144
+ say("\01#{message}\01", from) if !from_channel?
145
+ end
146
+
147
+ # Request information
148
+
149
+ # reflects whether or not the current message / previous message came
150
+ # from a user via pm.
151
+ def from_user?
152
+ !from_channel?
153
+ end
154
+
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 =~ /^[\&\#]/
159
+ end
160
+
161
+ def addressed?
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?
178
+ end
179
+
180
+ def reset_details
181
+ @options = nil
182
+ @target = nil
183
+ @from = nil
184
+ end
185
+
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.
190
+ def halt!
191
+ raise Marvin::HaltHandlerProcessing
192
+ end
193
+
194
+ end
195
+ end