Sutto-marvin 0.1.0.20081014

Sign up to get free protection for your applications and to get access to all the features.
@@ -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