marvin 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 (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