jeffrafter-marvin 0.1.20081115

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 (43) hide show
  1. data/README.textile +110 -0
  2. data/VERSION.yml +4 -0
  3. data/bin/marvin +67 -0
  4. data/config/settings.yml.sample +13 -0
  5. data/config/setup.rb +14 -0
  6. data/handlers/hello_world.rb +9 -0
  7. data/handlers/logging_handler.rb +87 -0
  8. data/handlers/tweet_tweet.rb +21 -0
  9. data/lib/marvin/abstract_client.rb +210 -0
  10. data/lib/marvin/abstract_parser.rb +19 -0
  11. data/lib/marvin/base.rb +121 -0
  12. data/lib/marvin/command_handler.rb +62 -0
  13. data/lib/marvin/core_ext.rb +11 -0
  14. data/lib/marvin/data_store.rb +73 -0
  15. data/lib/marvin/dispatchable.rb +94 -0
  16. data/lib/marvin/drb_handler.rb +7 -0
  17. data/lib/marvin/exception_tracker.rb +16 -0
  18. data/lib/marvin/exceptions.rb +8 -0
  19. data/lib/marvin/handler.rb +12 -0
  20. data/lib/marvin/irc/abstract_server.rb +4 -0
  21. data/lib/marvin/irc/base_server.rb +11 -0
  22. data/lib/marvin/irc/client.rb +105 -0
  23. data/lib/marvin/irc/event.rb +30 -0
  24. data/lib/marvin/irc/socket_client.rb +69 -0
  25. data/lib/marvin/irc.rb +9 -0
  26. data/lib/marvin/loader.rb +68 -0
  27. data/lib/marvin/logger.rb +23 -0
  28. data/lib/marvin/middle_man.rb +103 -0
  29. data/lib/marvin/parsers/regexp_parser.rb +96 -0
  30. data/lib/marvin/parsers/simple_parser/default_events.rb +37 -0
  31. data/lib/marvin/parsers/simple_parser/event_extensions.rb +14 -0
  32. data/lib/marvin/parsers/simple_parser/prefixes.rb +34 -0
  33. data/lib/marvin/parsers/simple_parser.rb +101 -0
  34. data/lib/marvin/parsers.rb +7 -0
  35. data/lib/marvin/settings.rb +77 -0
  36. data/lib/marvin/test_client.rb +60 -0
  37. data/lib/marvin/util.rb +30 -0
  38. data/lib/marvin.rb +44 -0
  39. data/script/daemon-runner +12 -0
  40. data/script/run +25 -0
  41. data/spec/marvin/abstract_client_test.rb +38 -0
  42. data/spec/spec_helper.rb +14 -0
  43. metadata +99 -0
@@ -0,0 +1,62 @@
1
+ module Marvin
2
+
3
+ # A Simple Marvin handler based on processing
4
+ # commands, similar in design to MatzBot.
5
+ class CommandHandler < Base
6
+
7
+ class_inheritable_accessor :exposed_methods, :command_prefix
8
+
9
+ self.command_prefix = ""
10
+ self.exposed_methods = []
11
+
12
+ class << self
13
+
14
+ def exposes(*args)
15
+ self.exposed_methods ||= []
16
+ self.exposed_methods += args.map { |a| a.to_sym }.flatten
17
+ end
18
+
19
+ end
20
+
21
+ on_event :incoming_message do
22
+ logger.debug "Incoming message"
23
+ check_for_commands
24
+ end
25
+
26
+ def check_for_commands
27
+ data, command = nil, nil
28
+ if self.from_channel?
29
+ logger.debug "Processing command in channel"
30
+ split_message = options.message.split(" ", 3)
31
+ prefix = split_message.shift
32
+ # Return if in channel and it isn't address to the user.
33
+ return unless prefix == "#{self.client.nickname}:"
34
+ command, data = split_message # Set remaining.
35
+ else
36
+ command, data = options.message.split(" ", 2)
37
+ end
38
+ # Double check for sanity
39
+ return if command.blank?
40
+ command_name = extract_command_name(command)
41
+ unless command_name.nil?
42
+ logger.debug "Command Exists - processing"
43
+ # Dispatch the command.
44
+ self.send(command_name, data.to_a) if self.respond_to?(command_name)
45
+ end
46
+ end
47
+
48
+ def extract_command_name(command)
49
+ prefix_length = self.command_prefix.to_s.length
50
+ has_prefix = command[0...prefix_length] == self.command_prefix.to_s
51
+ logger.debug "Debugging, prefix is #{prefix_length} characters, has prefix? = #{has_prefix}"
52
+ if has_prefix
53
+ # Normalize the method name
54
+ method_name = command[prefix_length..-1].to_s.underscore.to_sym
55
+ logger.debug "Computed method name is #{method_name.inspect}"
56
+ return method_name if self.exposed_methods.to_a.include?(method_name)
57
+ end
58
+ end
59
+
60
+ end
61
+
62
+ end
@@ -0,0 +1,11 @@
1
+ class File
2
+ def self.present_dir
3
+ File.dirname(__FILE__)
4
+ end
5
+ end
6
+
7
+ class String
8
+ def /(*args)
9
+ File.join(self, *args)
10
+ end
11
+ end
@@ -0,0 +1,73 @@
1
+ require 'json'
2
+
3
+ module Marvin
4
+ # Implements a simple datastore interface, designed to make
5
+ # it easy to develop handlers which have persistent data.
6
+ class DataStore
7
+
8
+ cattr_accessor :logger, :registered_stores
9
+ self.logger = Marvin::Logger.logger
10
+ self.registered_stores = {}
11
+
12
+ # Returns the path to the data store relative to this file.
13
+ # Used when loading / dumping the data.
14
+ def self.datastore_location
15
+ path = Marvin::Settings[:datastore_location] ? Marvin::Settings.datastore_location : "tmp/datastore.json"
16
+ return Marvin::Settings.root / path
17
+ end
18
+
19
+ # Dump the current data store contents to file.
20
+ def self.dump!
21
+ File.open(self.datastore_location, "w+") do |f|
22
+ f.write self.registered_stores.to_json
23
+ end
24
+ end
25
+
26
+ # Load the current data store contents from file.
27
+ def self.load!
28
+ results = {}
29
+ if File.exists?(self.datastore_location)
30
+ begin
31
+ json = JSON.load(File.read(self.datastore_location))
32
+ results = json if json.is_a?(Hash)
33
+ rescue JSON::ParserError
34
+ end
35
+ end
36
+ self.registered_stores = results
37
+ end
38
+
39
+
40
+ # For each individual datastore.
41
+
42
+ attr_accessor :name
43
+
44
+ def initialize(name)
45
+ self.name = name.to_s
46
+ self.registered_stores ||= {}
47
+ self.registered_stores[self.name] ||= {}
48
+ end
49
+
50
+ def [](key)
51
+ ((self.registered_stores||={})[self.name]||={})[key.to_s]
52
+ end
53
+
54
+ def []=(key,value)
55
+ self.registered_stores[self.name][key.to_s] = value
56
+ end
57
+
58
+ def method_missing(name, *args, &blk)
59
+ if name.to_s =~ /^(.*)=$/i
60
+ self[$1.to_s] = args.first
61
+ elsif self.registered_stores[self.name].has_key?(name.to_s)
62
+ return self.registered_stores[self.name][name.to_s]
63
+ else
64
+ super(name, *args, &blk)
65
+ end
66
+ end
67
+
68
+ def to_hash
69
+ self.registered_stores[self.name]
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,94 @@
1
+ module Marvin
2
+ # = Marvin::Dispatchable
3
+ # A Generic mixin which lets you define an object
4
+ # Which accepts handlers which can have arbitrary
5
+ # events dispatched.
6
+ # == Usage
7
+ #
8
+ # class X
9
+ # include Marvin::Dispatchable
10
+ # self.handlers << SomeHandler.new
11
+ # end
12
+ # X.new.dispatch(:name, {:args => "Values"})
13
+ #
14
+ # Will first check if SomeHandler#handle_name exists,
15
+ # calling handle_name({:args => "Values"}) if it does,
16
+ # otherwise calling SomeHandler#handle(:name, {:args => "Values"})
17
+ module Dispatchable
18
+
19
+ def self.included(parent)
20
+ parent.class_eval do
21
+ include InstanceMethods
22
+ extend ClassMethods
23
+ end
24
+ end
25
+
26
+ module InstanceMethods
27
+
28
+ # Returns the handlers registered on this class,
29
+ # used inside +dispatch+.
30
+ def handlers
31
+ self.class.handlers
32
+ end
33
+
34
+ # Dispatch an 'event' with a given name to the handlers
35
+ # registered on the current class. Used as a nicer way of defining
36
+ # behaviours that should occur under a given set of circumstances.
37
+ # == Params
38
+ # +name+: The name of the current event
39
+ # +opts+: an optional hash of options to pass
40
+ def dispatch(name, opts = {})
41
+ # The full handler name is the method we call given it exists.
42
+ full_handler_name = :"handle_#{name.to_s.underscore}"
43
+ # First, dispatch locally if the method is defined.
44
+ if self.respond_to?(full_handler_name)
45
+ self.send(full_handler_name, opts)
46
+ end
47
+ # Iterate through all of the registered handlers,
48
+ # If there is a method named handle_<event_name>
49
+ # defined we sent that otherwise we call the handle
50
+ # method on the handler. Note that the handle method
51
+ # is the only required aspect of a handler. An improved
52
+ # version of this would likely cache the respond_to?
53
+ # call.
54
+ self.handlers.each do |handler|
55
+ if handler.respond_to?(full_handler_name)
56
+ handler.sent(full_handler_name, opts)
57
+ else
58
+ handler.handle name, opts
59
+ end
60
+ end
61
+ # If we get the HaltHandlerProcessing exception, we
62
+ # catch it and continue on our way. In essence, we
63
+ # stop the dispatch of events to the next set of the
64
+ # handlers.
65
+ rescue HaltHandlerProcessing
66
+ end
67
+
68
+ end
69
+
70
+ module ClassMethods
71
+
72
+ # Return an array of all registered handlers, stored in the
73
+ # class variable @@handlers. Used inside the #handlers instance
74
+ # method as well as inside things such as register_handler.
75
+ def handlers
76
+ @@handlers ||= []
77
+ end
78
+
79
+ # Assigns a new array of handlers and assigns each.
80
+ def handlers=(new_value)
81
+ @@handlers = []
82
+ new_value.to_a.each { |h| register_handler h }
83
+ end
84
+
85
+ # Appends a handler to the list of handlers for this object.
86
+ # Handlers are called in the order they are registered.
87
+ def register_handler(handler)
88
+ self.handlers << handler unless handler.nil? || !handler.respond_to?(:handle)
89
+ end
90
+
91
+ end
92
+
93
+ end
94
+ end
@@ -0,0 +1,7 @@
1
+ module Marvin
2
+
3
+ def handle(message, options)
4
+ end
5
+
6
+ end
7
+ end
@@ -0,0 +1,16 @@
1
+ module Marvin
2
+ class ExceptionTracker
3
+
4
+ cattr_accessor :logger
5
+ self.logger = Marvin::Logger.logger
6
+
7
+ def self.log(e)
8
+ logger.fatal "Exception raised inside Marvin Instance."
9
+ logger.fatal "#{e} - #{e.message}"
10
+ e.backtrace.each do |line|
11
+ logger.fatal line
12
+ end
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,8 @@
1
+ module Marvin
2
+
3
+ class Error < StandardError; end
4
+
5
+ # Used to stop the flow of handler chains.
6
+ class HaltHandlerProcessing < Error; end
7
+
8
+ end
@@ -0,0 +1,12 @@
1
+ module Marvin
2
+ module Handler
3
+
4
+ # Received a given +message+ with a set of default
5
+ # +opts+ (defaulting back to an empty hash), which
6
+ # will be used to perform some sort of action.
7
+ def handle(message, opts = {})
8
+ Marvin::Logger.debug "NOP handle - got message #{message.inspect}"
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,4 @@
1
+ module Marvin::IRC
2
+ class AbstractServer
3
+ end
4
+ end
@@ -0,0 +1,11 @@
1
+ require 'webrick'
2
+
3
+ module Marvin::IRC
4
+ class BaseServer < WEBrick::GenericServer
5
+
6
+ def run(sock)
7
+ File.open("x", "w+") { |f| f.puts sock.inspect }
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,105 @@
1
+ require 'eventmachine'
2
+
3
+ module Marvin::IRC
4
+
5
+ # == Marvin::IRC::Client
6
+ # An EventMachine protocol implementation built to
7
+ # serve as a basic, single server IRC client.
8
+ #
9
+ # Operates on the principal of Events as well
10
+ # as handlers.
11
+ #
12
+ # === Events
13
+ # Events are things that can happen (e.g. an
14
+ # incoming message). All outgoing events are
15
+ # automatically handled from within the client
16
+ # class. Incoming events are currently based
17
+ # on regular expression based matches of
18
+ # incoming messages. the Client#register_event
19
+ # method takes either an instance of Marvin::IRC::Event
20
+ # or a set of arguments which will then be used
21
+ # in the constructor of a new Marvin::IRC::Event
22
+ # instance (see, for example, the source code for
23
+ # this class for examples).
24
+ #
25
+ # === Handlers
26
+ # Handlers on the other hand do as the name suggests
27
+ # - they listen for dispatched events and act accordingly.
28
+ # Handlers are simply objects which follow a certain
29
+ # set of guidelines. Typically, a handler will at
30
+ # minimum respond to #handle(event_name, details)
31
+ # where event_name is a symbol for the current
32
+ # event (e.g. :incoming_event) whilst details is a
33
+ # a hash of details about the current event (e.g.
34
+ # message target and the message itself).
35
+ #
36
+ # ==== Getting the current client instance
37
+ # If the object responds to client=, The client will
38
+ # call it with the current instance of itself
39
+ # enabling the handler to do things such as respond.
40
+ # Also, if a method handle_[message_name] exists,
41
+ # it will be called instead of handle.
42
+ #
43
+ # ==== Adding handlers
44
+ # To add an object as a handler, you simply call
45
+ # the class method, register_handler with the
46
+ # handler as the only argument.
47
+ class Client < Marvin::AbstractClient
48
+ attr_accessor :em_connection
49
+
50
+ class EMConnection < EventMachine::Protocols::LineAndTextProtocol
51
+ attr_accessor :client
52
+
53
+ def initialize
54
+ super
55
+ self.client = Marvin::IRC::Client.new
56
+ self.client.em_connection = self
57
+ end
58
+
59
+ def post_init
60
+ client.process_connect
61
+ end
62
+
63
+ def unbind
64
+ client.process_disconnect
65
+ end
66
+
67
+ def receive_line(line)
68
+ self.client.receive_line(line)
69
+ end
70
+
71
+ end
72
+
73
+ def send_line(*args)
74
+ em_connection.send_data *args
75
+ end
76
+
77
+ ## Client specific details
78
+
79
+ # Starts the EventMachine loop and hence starts up the actual
80
+ # networking portion of the IRC Client.
81
+ def self.run
82
+ self.setup # So we have options etc
83
+ EventMachine::run do
84
+ logger.debug "Connecting to #{self.configuration.server}:#{self.configuration.port}"
85
+ EventMachine::connect self.configuration.server, self.configuration.port, Marvin::IRC::Client::EMConnection
86
+ end
87
+ end
88
+
89
+ def self.stop
90
+ logger.debug "Telling all connections to quit"
91
+ self.connections.dup.each { |connection| connection.quit }
92
+ logger.debug "Telling Event Machine to Stop"
93
+ EventMachine::stop_event_loop
94
+ logger.debug "Stopped."
95
+ end
96
+
97
+ # Registers a callback handle that will be periodically run.
98
+ def periodically(timing, event_callback)
99
+ callback = proc { self.dispatch event_callback.to_sym }
100
+ EventMachine::add_periodic_timer(timing, &callback)
101
+ end
102
+
103
+ end
104
+
105
+ end
@@ -0,0 +1,30 @@
1
+ module Marvin::IRC
2
+ class Event
3
+ attr_accessor :keys, :name, :raw_arguments
4
+
5
+ def initialize(name, *args)
6
+ self.name = name.to_sym
7
+ self.keys = args.flatten.map { |k| k.to_sym }
8
+ end
9
+
10
+ def to_hash
11
+ return {} unless self.raw_arguments
12
+ results = {}
13
+ values = self.raw_arguments.to_a
14
+ last_index = self.keys.size - 1
15
+ self.keys.each_with_index do |key, i|
16
+ results[key] = (i == last_index ? values.join(" ").strip : values.shift)
17
+ end
18
+ return results
19
+ end
20
+
21
+ def inspect
22
+ "#<Marvin::IRC::Event name=#{self.name} attributes=[#{keys * ","}] >"
23
+ end
24
+
25
+ def to_incoming_event_name
26
+ :"incoming_#{self.name}"
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,69 @@
1
+ require 'socket'
2
+
3
+ module Marvin::IRC
4
+ class SocketClient < Marvin::AbstractClient
5
+ attr_accessor :socket
6
+
7
+ def run
8
+ @socket = TCPSocket.new(self.configuration.server, self.configuration.port)
9
+ self.process_connect
10
+ self.enter_loop
11
+ end
12
+
13
+ def send_line(*args)
14
+ args.each { |l| @socket.write l } if !@socket.closed?
15
+ end
16
+
17
+ def disconnect_processed?
18
+ @disconnect_processed
19
+ end
20
+
21
+ def enter_loop
22
+ until @socket.closed?
23
+ line = @socket.readline.strip
24
+ receive_line line
25
+ end
26
+ self.process_disconnect unless self.disconnect_processed?
27
+ @disconnect_processed = true
28
+ rescue SystemExit
29
+ self.process_disconnect unless self.disconnect_processed?
30
+ @disconnect_processed = true
31
+ rescue Exception => e
32
+ Marvin::ExceptionTracker.log(e)
33
+ end
34
+
35
+ def quit(*args)
36
+ super(*args)
37
+ end
38
+
39
+ ## Client specific details
40
+
41
+ def self.run
42
+ self.setup # So we have options etc
43
+ logger.debug "Connecting to #{self.configuration.server}:#{self.configuration.port}"
44
+ self.new.run
45
+ end
46
+
47
+ def self.stop
48
+ logger.debug "Telling all connections to quit"
49
+ self.connections.each do |connection|
50
+ connection.quit
51
+ logger.debug "Preparing to close socket"
52
+ connection.socket.close
53
+ end
54
+ logger.debug "Stopped."
55
+ end
56
+
57
+ # Registers a callback handle that will be periodically run.
58
+ def periodically(timing, event_callback)
59
+ callback = proc { self.dispatch event_callback.to_sym }
60
+ Thread.new do
61
+ while true
62
+ callback.call
63
+ sleep timing
64
+ end
65
+ end
66
+ end
67
+
68
+ end
69
+ end
data/lib/marvin/irc.rb ADDED
@@ -0,0 +1,9 @@
1
+ module Marvin
2
+ module IRC
3
+ autoload :Client, 'marvin/irc/client'
4
+ autoload :Event, 'marvin/irc/event'
5
+ autoload :SocketClient, 'marvin/irc/socket_client'
6
+ autoload :AbstractServer, 'marvin/irc/abstract_server'
7
+ autoload :BaseServer, 'marvin/irc/base_server'
8
+ end
9
+ end
@@ -0,0 +1,68 @@
1
+ module Marvin
2
+ class Loader
3
+
4
+ cattr_accessor :setup_block
5
+
6
+ cattr_accessor :start_hooks, :stop_hooks
7
+ self.stop_hooks, self.start_hooks = [], []
8
+
9
+ def self.before_connecting(&blk)
10
+ self.setup_block = blk
11
+ end
12
+
13
+ def setup_defaults
14
+ Marvin::Logger.setup
15
+ end
16
+
17
+ def self.before_run(&blk)
18
+ self.start_hooks << blk unless blk.blank?
19
+ end
20
+
21
+ def self.after_stop(&blk)
22
+ self.stop_hooks << blk unless blk.blank?
23
+ end
24
+
25
+ def load_handlers
26
+ handlers = Dir[Marvin::Settings.root / "handlers/**/*.rb"].map { |h| h[0..-4] }
27
+ handlers.each do |handler|
28
+ require handler
29
+ end
30
+ end
31
+
32
+ def load_settings
33
+ Marvin::Settings.setup
34
+ Marvin::Settings.default_client.configuration = Marvin::Settings.to_hash
35
+ Marvin::Settings.default_client.setup
36
+ end
37
+
38
+ def pre_connect_setup
39
+ Marvin::DataStore.load!
40
+ require(Marvin::Settings.root / "config/setup")
41
+ self.setup_block.call unless self.setup_block.blank?
42
+ end
43
+
44
+ def run!
45
+ self.setup_defaults
46
+ self.load_settings
47
+ self.load_handlers
48
+ self.pre_connect_setup
49
+ self.start_hooks.each { |h| h.call }
50
+ Marvin::Settings.default_client.run
51
+ end
52
+
53
+ def stop!
54
+ Marvin::Settings.default_client.stop
55
+ self.stop_hooks.each { |h| h.call }
56
+ Marvin::DataStore.dump!
57
+ end
58
+
59
+ def self.run!
60
+ self.new.run!
61
+ end
62
+
63
+ def self.stop!
64
+ self.new.stop!
65
+ end
66
+
67
+ end
68
+ end
@@ -0,0 +1,23 @@
1
+ require 'logger'
2
+
3
+ module Marvin
4
+ class Logger
5
+
6
+ cattr_accessor :logger
7
+
8
+ class << self
9
+
10
+ def setup
11
+ log_path = Marvin::Settings.root / "log/#{Marvin::Settings.environment}.log"
12
+ self.logger ||= ::Logger.new(Marvin::Settings.daemon? ? log_path : STDOUT)
13
+ end
14
+
15
+ def method_missing(name, *args, &blk)
16
+ self.setup # Ensure the logger is setup
17
+ self.logger.send(name, *args, &blk)
18
+ end
19
+
20
+ end
21
+
22
+ end
23
+ end