Sutto-marvin 0.1.0.20081014

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.
@@ -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,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,4 @@
1
+ module Marvin::IRC
2
+ class AbstractServer
3
+ end
4
+ end
@@ -0,0 +1,107 @@
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
+ super
61
+ client.process_connect
62
+ end
63
+
64
+ def unbind
65
+ super
66
+ client.process_disconnect
67
+ end
68
+
69
+ def receive_line(line)
70
+ self.client.receive_line(line)
71
+ end
72
+
73
+ end
74
+
75
+ def send_line(*args)
76
+ em_connection.send_data *args
77
+ end
78
+
79
+ ## Client specific details
80
+
81
+ # Starts the EventMachine loop and hence starts up the actual
82
+ # networking portion of the IRC Client.
83
+ def self.run
84
+ self.setup # So we have options etc
85
+ EventMachine::run do
86
+ logger.debug "Connecting to #{self.configuration.server}:#{self.configuration.port}"
87
+ EventMachine::connect self.configuration.server, self.configuration.port, Marvin::IRC::Client::EMConnection
88
+ end
89
+ end
90
+
91
+ def self.stop
92
+ logger.debug "Telling all connections to quit"
93
+ self.connections.dup.each { |connection| connection.quit }
94
+ logger.debug "Telling Event Machine to Stop"
95
+ EventMachine::stop_event_loop
96
+ logger.debug "Stopped."
97
+ end
98
+
99
+ # Registers a callback handle that will be periodically run.
100
+ def periodically(timing, event_callback)
101
+ callback = proc { self.dispatch_event event_callback.to_sym }
102
+ EventMachine::add_periodic_timer(timing, &callback)
103
+ end
104
+
105
+ end
106
+
107
+ end
@@ -0,0 +1,29 @@
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
+ self.keys.each do |key|
15
+ results[key] = values.shift
16
+ end
17
+ return results
18
+ end
19
+
20
+ def inspect
21
+ "#<Marvin::IRC::Event name=#{self.name} attributes=[#{keys * ","}] >"
22
+ end
23
+
24
+ def to_incoming_event_name
25
+ :"incoming_#{self.name}"
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,63 @@
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 event_callback.to_sym }
60
+ end
61
+
62
+ end
63
+ end
data/lib/marvin/irc.rb ADDED
@@ -0,0 +1,8 @@
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
+ end
8
+ end
@@ -0,0 +1,55 @@
1
+ module Marvin
2
+ class Loader
3
+
4
+ cattr_accessor :setup_block
5
+
6
+ def self.before_connecting(&blk)
7
+ self.setup_block = blk
8
+ end
9
+
10
+ def setup_defaults
11
+ Marvin::Logger.setup
12
+ end
13
+
14
+ def load_handlers
15
+ handlers = Dir[Marvin::Settings.root / "handlers/**/*.rb"].map { |h| h[0..-4] }
16
+ handlers.each do |handler|
17
+ require handler
18
+ end
19
+ end
20
+
21
+ def load_settings
22
+ Marvin::Settings.setup
23
+ Marvin::Settings.default_client.configuration = Marvin::Settings.to_hash
24
+ Marvin::Settings.default_client.setup
25
+ end
26
+
27
+ def pre_connect_setup
28
+ Marvin::DataStore.load!
29
+ require(Marvin::Settings.root / "config/setup")
30
+ self.setup_block.call unless self.setup_block.blank?
31
+ end
32
+
33
+ def run!
34
+ self.setup_defaults
35
+ self.load_settings
36
+ self.load_handlers
37
+ self.pre_connect_setup
38
+ Marvin::Settings.default_client.run
39
+ end
40
+
41
+ def stop!
42
+ Marvin::Settings.default_client.stop
43
+ Marvin::DataStore.dump!
44
+ end
45
+
46
+ def self.run!
47
+ self.new.run!
48
+ end
49
+
50
+ def self.stop!
51
+ self.new.stop!
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,22 @@
1
+ require 'logger'
2
+
3
+ module Marvin
4
+ class Logger
5
+
6
+ cattr_accessor :logger
7
+
8
+ class << self
9
+
10
+ def setup
11
+ self.logger ||= ::Logger.new(STDOUT)
12
+ end
13
+
14
+ def method_missing(name, *args, &blk)
15
+ self.setup # Ensure the logger is setup
16
+ self.logger.send(name, *args, &blk)
17
+ end
18
+
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,105 @@
1
+ module Marvin
2
+ # The middle man is a class you can use to register
3
+ # other handlers on. e.g. it acts as a way to 'filter'
4
+ # incoming and outgoing messages. Akin to Rack / WSGI
5
+ # middleware.
6
+ class MiddleMan
7
+
8
+ # Set the logger cattr to the default marvin logger.
9
+ cattr_accessor :logger
10
+ self.logger ||= Marvin::Logger
11
+
12
+ # By default, we are *not* setup.
13
+ @@setup = false
14
+
15
+ # Our list of subhandlers. We make sure
16
+ # the list is unique to our subclass / class.
17
+ class_inheritable_accessor :subhandlers
18
+ self.subhandlers = []
19
+
20
+ # Finally, the client.
21
+ attr_reader :client
22
+
23
+ # When we're told to set the client,
24
+ # not only do we set out own instance
25
+ # but we also echo the command down
26
+ # to all of our sub-clients.
27
+ def client=(new_client)
28
+ @client = new_client
29
+ setup_subhandler_clients
30
+ end
31
+
32
+ def process_event(message, options)
33
+ return message, options
34
+ end
35
+
36
+ # Filter incoming events.
37
+ def handle(message, options)
38
+ # Process the current event.
39
+ message, options = process_event(message, options)
40
+ full_handler_name = "handle_#{message}"
41
+ self.send(full_handler_name, opts) if respond_to?(full_handler_name)
42
+ self.subhandlers.each do |sh|
43
+ forward_message_to_handler(sh, message, options, full_handler_name)
44
+ end
45
+ rescue HaltHandlerProcessing
46
+ logger.info "Asked to halt the filter processing chain inside a middleman."
47
+ rescue Exception => e
48
+ logger.fatal "Exception processing handle #{message}"
49
+ Marvin::ExceptionTracker.log(e)
50
+ end
51
+
52
+ class << self
53
+
54
+ def setup?
55
+ @@setup
56
+ end
57
+
58
+ # Forcefully do the setup routine.
59
+ def setup!
60
+ # Register ourselves as a new handler.
61
+ Marvin::Settings.default_client.register_handler self.new
62
+ @@setup = true
63
+ end
64
+
65
+ # Setup iff setup hasn't been done.
66
+ def setup
67
+ return if self.setup?
68
+ self.setup!
69
+ end
70
+
71
+ # Register a single subhandler.
72
+ def register_handler(handler, run_setup = true)
73
+ self.setup if run_setup
74
+ self.subhandlers << handler unless handler.blank?
75
+ end
76
+
77
+ # Registers a group of subhandlers.
78
+ def register_handlers(*args)
79
+ self.setup
80
+ args.each { |h| self.register_handler(h, false) }
81
+ end
82
+
83
+ end
84
+
85
+ private
86
+
87
+ def setup_subhandler_clients
88
+ self.subhandlers.each do |sh|
89
+ sh.client = self.client if sh.respond_to?(:client=)
90
+ end
91
+ end
92
+
93
+ # This should probably be extracted into some sort of Util's library as
94
+ # it's shared across a couple of classes but I really can't be bothered
95
+ # at the moment - I just want to test the concept.
96
+ def forward_message_to_handler(handler, message, options, full_handler_name)
97
+ if handler.respond_to?(full_handler_name)
98
+ handler.send(full_handler_name, options)
99
+ elsif handler.respond_to?(:handle)
100
+ handler.handle message, options
101
+ end
102
+ end
103
+
104
+ end
105
+ end
@@ -0,0 +1,94 @@
1
+ module Marvin
2
+ module Parsers
3
+ class RegexpParser < Marvin::AbstractParser
4
+
5
+ cattr_accessor :regexp_matchers, :events
6
+ # Since we cbf implemented an ordered hash, just use regexp => event at the same
7
+ # index.
8
+ self.regexp_matchers = []
9
+ self.events = []
10
+
11
+ attr_accessor :current_line
12
+
13
+ # Appends an event to the end of the the events callback
14
+ # chain. It will be search in order of first-registered
15
+ # when used to match a URL (hence, order matters).
16
+ def self.register_event(*args)
17
+ matcher = args.delete_at(1) # Extract regexp.
18
+ if args.first.is_a?(Marvin::IRC::Event)
19
+ event = args.first
20
+ else
21
+ event = Marvin::IRC::Event.new(*args)
22
+ end
23
+ self.regexp_matchers << matcher
24
+ self.events << event
25
+ end
26
+
27
+
28
+ # Initialize a new RegexpParser from the given line.
29
+ def initialize(line)
30
+ self.current_line = line
31
+ end
32
+
33
+ def to_event
34
+ self.regexp_matchers.each_with_index do |matcher, offset|
35
+ if (match_data = matcher.match(self.current_line))
36
+ event = self.events[offset].dup
37
+ event.raw_arguments = match_data.to_a[1..-1]
38
+ return event
39
+ end
40
+ end
41
+ # otherwise, return nil
42
+ return nil
43
+ end
44
+
45
+ ## The Default IRC Events
46
+
47
+ # Note that some of these Regexp's are from Net::YAIL,
48
+ # which apparantly sources them itself from the IRCSocket
49
+ # library.
50
+
51
+ register_event :invite, /^\:(.+)\!\~?(.+)\@(.+) INVITE (\S+) :?(.+?)$/i,
52
+ :nick, :ident, :host, :target, :channel
53
+
54
+ register_event :action, /^\:(.+)\!\~?(.+)\@(.+) PRIVMSG (\S+) :?\001ACTION (.+?)\001$/i,
55
+ :nick, :ident, :host, :target, :message
56
+
57
+ register_event :ctcp, /^\:(.+)\!\~?(.+)\@(.+) PRIVMSG (\S+) :?\001(.+?)\001$/i,
58
+ :nick, :ident, :host, :target, :message
59
+
60
+ register_event :message, /^\:(.+)\!\~?(.+)\@(.+) PRIVMSG (\S+) :?(.+?)$/i,
61
+ :nick, :ident, :host, :target, :message
62
+
63
+ register_event :join, /^\:(.+)\!\~?(.+)\@(.+) JOIN (\S+)/i,
64
+ :nick, :ident, :host, :target
65
+
66
+ register_event :part, /^\:(.+)\!\~?(.+)\@(.+) PART (\S+)\s?:?(.+?)$/i,
67
+ :nick, :ident, :host, :target, :message
68
+
69
+ register_event :mode, /^\:(.+)\!\~?(.+)\@(.+) MODE (\S+) :?(.+?)$/i,
70
+ :nick, :ident, :host, :target, :mode
71
+
72
+ register_event :kick, /^\:(.+)\!\~?(.+)\@(.+) KICK (\S+) (\S+)\s?:?(.+?)$/i,
73
+ :nick, :ident, :host, :target, :channel, :reason
74
+
75
+ register_event :topic, /^\:(.+)\!\~?(.+)\@(.+) TOPIC (\S+) :?(.+?)$/i,
76
+ :nick, :ident, :host, :target, :topic
77
+
78
+ register_event :nick, /^\:(.+)\!\~?(.+)\@(.+) NICK :?(.+?)$/i,
79
+ :nick, :ident, :host, :new_nick
80
+
81
+ register_event :quit, /^\:(.+)\!\~?(.+)\@(.+) QUIT :?(.+?)$/i,
82
+ :nick, :ident, :host, :message
83
+
84
+ register_event :nick_taken, /^\:(\S+) 433 \* (\w+) :(.+)$/,
85
+ :server, :target, :message
86
+
87
+ register_event :ping, /^\:(.+)\!\~?(.+)\@(.+) PING (.*)$/,
88
+ :nick, :ident, :host, :data
89
+
90
+ register_event :numeric, /^\:(\S+) ([0-9]+) (.*)$/,
91
+ :host, :code, :data
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,7 @@
1
+ module Marvin
2
+ module Parsers
3
+ # Default Parsers
4
+ autoload :RegexpParser, 'marvin/parsers/regexp_parser'
5
+
6
+ end
7
+ end
@@ -0,0 +1,73 @@
1
+ require 'yaml'
2
+
3
+ module Marvin
4
+ class Settings
5
+
6
+ cattr_accessor :environment, :configuration, :is_setup, :default_client, :handler_folder, :default_parser
7
+
8
+ class << self
9
+
10
+ def root
11
+ defined?(MARVIN_ROOT) ? MARVIN_ROOT : File.dirname(__FILE__) / "../.."
12
+ end
13
+
14
+ def setup(options = {})
15
+ return if self.is_setup
16
+ self.setup!(options)
17
+ end
18
+
19
+ def setup!(options = {})
20
+ self.environment ||= "development"
21
+ self.configuration = {}
22
+ self.default_client ||= begin
23
+ require 'eventmachine'
24
+ Marvin::IRC::Client
25
+ rescue LoadError
26
+ Marvin::IRC::SocketClient
27
+ end
28
+ self.default_parser ||= Marvin::Parsers::RegexpParser
29
+ loaded_yaml = YAML.load_file(root / "config/settings.yml")
30
+ loaded_options = loaded_yaml["default"].
31
+ merge(loaded_yaml[self.environment]).
32
+ merge(options)
33
+ self.configuration.merge!(loaded_options)
34
+ self.configuration.symbolize_keys!
35
+ mod = Module.new do
36
+ Settings.configuration.keys.each do |k|
37
+ define_method(k) do
38
+ return Settings.configuration[k]
39
+ end
40
+
41
+ define_method("#{k}=") do |val|
42
+ Settings.configuration[k] = val
43
+ end
44
+ end
45
+ end
46
+
47
+ # Extend and include.
48
+
49
+ extend mod
50
+ include mod
51
+
52
+ self.is_setup = true
53
+ end
54
+
55
+ def [](key)
56
+ self.setup
57
+ return self.configuration[key.to_sym]
58
+ end
59
+
60
+ def []=(key, value)
61
+ self.setup
62
+ self.configuration[key.to_sym] = value
63
+ return value
64
+ end
65
+
66
+ def to_hash
67
+ self.configuration
68
+ end
69
+
70
+ end
71
+
72
+ end
73
+ end