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.
- data/README.textile +110 -0
- data/VERSION.yml +4 -0
- data/bin/marvin +67 -0
- data/config/settings.yml.sample +13 -0
- data/config/setup.rb +14 -0
- data/handlers/hello_world.rb +9 -0
- data/handlers/logging_handler.rb +87 -0
- data/handlers/tweet_tweet.rb +21 -0
- data/lib/marvin/abstract_client.rb +210 -0
- data/lib/marvin/abstract_parser.rb +19 -0
- data/lib/marvin/base.rb +121 -0
- data/lib/marvin/command_handler.rb +62 -0
- data/lib/marvin/core_ext.rb +11 -0
- data/lib/marvin/data_store.rb +73 -0
- data/lib/marvin/dispatchable.rb +94 -0
- data/lib/marvin/drb_handler.rb +7 -0
- data/lib/marvin/exception_tracker.rb +16 -0
- data/lib/marvin/exceptions.rb +8 -0
- data/lib/marvin/handler.rb +12 -0
- data/lib/marvin/irc/abstract_server.rb +4 -0
- data/lib/marvin/irc/base_server.rb +11 -0
- data/lib/marvin/irc/client.rb +105 -0
- data/lib/marvin/irc/event.rb +30 -0
- data/lib/marvin/irc/socket_client.rb +69 -0
- data/lib/marvin/irc.rb +9 -0
- data/lib/marvin/loader.rb +68 -0
- data/lib/marvin/logger.rb +23 -0
- data/lib/marvin/middle_man.rb +103 -0
- data/lib/marvin/parsers/regexp_parser.rb +96 -0
- data/lib/marvin/parsers/simple_parser/default_events.rb +37 -0
- data/lib/marvin/parsers/simple_parser/event_extensions.rb +14 -0
- data/lib/marvin/parsers/simple_parser/prefixes.rb +34 -0
- data/lib/marvin/parsers/simple_parser.rb +101 -0
- data/lib/marvin/parsers.rb +7 -0
- data/lib/marvin/settings.rb +77 -0
- data/lib/marvin/test_client.rb +60 -0
- data/lib/marvin/util.rb +30 -0
- data/lib/marvin.rb +44 -0
- data/script/daemon-runner +12 -0
- data/script/run +25 -0
- data/spec/marvin/abstract_client_test.rb +38 -0
- data/spec/spec_helper.rb +14 -0
- metadata +99 -0
@@ -0,0 +1,103 @@
|
|
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 { |sh| sh.client = self.client if sh.respond_to?(:client=) }
|
89
|
+
end
|
90
|
+
|
91
|
+
# This should probably be extracted into some sort of Util's library as
|
92
|
+
# it's shared across a couple of classes but I really can't be bothered
|
93
|
+
# at the moment - I just want to test the concept.
|
94
|
+
def forward_message_to_handler(handler, message, options, full_handler_name)
|
95
|
+
if handler.respond_to?(full_handler_name)
|
96
|
+
handler.send(full_handler_name, options)
|
97
|
+
elsif handler.respond_to?(:handle)
|
98
|
+
handler.handle message, options
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,96 @@
|
|
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 :ping, /PING (.*)$/, :data
|
91
|
+
|
92
|
+
register_event :numeric, /^\:(\S+) ([0-9]+) (.*)$/,
|
93
|
+
:server, :code, :data
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class Marvin::Parsers::SimpleParser < Marvin::AbstractParser
|
2
|
+
module DefaultEvents
|
3
|
+
|
4
|
+
def self.included(parent)
|
5
|
+
parent.class_eval do
|
6
|
+
extend ClassMethods
|
7
|
+
# Register the default set of events with commands
|
8
|
+
register_event :nick, :NICK, :new_nick
|
9
|
+
register_event :quit, :QUIT, :message
|
10
|
+
register_event :ping, :PING, :data
|
11
|
+
register_event :join, :JOIN, :target
|
12
|
+
register_event :invite, :INVITE, :target, :channel
|
13
|
+
register_event :message, :PRIVMSG, :target, :message
|
14
|
+
register_event :part, :PART, :target, :message
|
15
|
+
register_event :mode, :MODE, :target, :mode
|
16
|
+
register_event :kick, :KICK, :target, :channel, :reason
|
17
|
+
register_event :topic, :TOPIC, :target, :topic
|
18
|
+
# Add the default numeric event
|
19
|
+
register_event :numeric, :numeric, :code, :data
|
20
|
+
# And a few others reserved for special purposed
|
21
|
+
register_event :action, :action, :message
|
22
|
+
register_event :ctcp, :ctcp, :message
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module ClassMethods
|
27
|
+
|
28
|
+
# Register an event from a given name,
|
29
|
+
# command as well as a set of arguments.
|
30
|
+
def register_event(name, command, *args)
|
31
|
+
event = Marvin::Parsers::SimpleParser::EventWithPrefix.new(name, *args)
|
32
|
+
self.events[command] = event
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# An extension to Marvin::IRC::Event which
|
2
|
+
# lets a user specify a prefix to use.
|
3
|
+
class Marvin::Parsers::SimpleParser < Marvin::AbstractParser
|
4
|
+
|
5
|
+
class EventWithPrefix < Marvin::IRC::Event
|
6
|
+
attr_accessor :prefix
|
7
|
+
|
8
|
+
def to_hash
|
9
|
+
super.merge(prefix.blank? ? {} : prefix.to_hash)
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# A Set of prefixes for a given IRC line
|
2
|
+
# as parsed from an incoming line.
|
3
|
+
class Marvin::Parsers::SimpleParser < Marvin::AbstractParser
|
4
|
+
|
5
|
+
class Prefix; end
|
6
|
+
|
7
|
+
class ServerNamePrefix < Prefix
|
8
|
+
attr_accessor :server_name
|
9
|
+
|
10
|
+
def initialize(name)
|
11
|
+
self.server_name = name
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_hash
|
15
|
+
{:server => self.server_name}
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
class UserPrefix < Prefix
|
21
|
+
attr_accessor :nick, :ident, :host
|
22
|
+
|
23
|
+
def initialize(nick, ident = nil, host = nil)
|
24
|
+
self.nick = nick
|
25
|
+
self.ident = ident
|
26
|
+
self.host = host
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_hash
|
30
|
+
{:host => self.host, :nick => self.nick, :ident => self.ident}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require File.dirname(__FILE__) / "simple_parser/prefixes"
|
2
|
+
require File.dirname(__FILE__) / "simple_parser/event_extensions"
|
3
|
+
require File.dirname(__FILE__) / "simple_parser/default_events"
|
4
|
+
|
5
|
+
module Marvin
|
6
|
+
module Parsers
|
7
|
+
class SimpleParser < Marvin::AbstractParser
|
8
|
+
|
9
|
+
cattr_accessor :events
|
10
|
+
self.events ||= {}
|
11
|
+
|
12
|
+
attr_accessor :arguments, :prefix, :current_line, :parts, :event
|
13
|
+
|
14
|
+
def initialize(line)
|
15
|
+
self.current_line = line
|
16
|
+
parse!
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_event
|
20
|
+
if self.event.blank?
|
21
|
+
parse!
|
22
|
+
return nil
|
23
|
+
else
|
24
|
+
return self.event
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def parse!
|
31
|
+
# Split the message
|
32
|
+
line = self.current_line
|
33
|
+
if line[0] == ?:
|
34
|
+
prefix_text, line = line.split(" ", 2)
|
35
|
+
else
|
36
|
+
prefix_text = nil
|
37
|
+
end
|
38
|
+
extract_prefix! prefix_text
|
39
|
+
|
40
|
+
head, tail = line.split(":", 2)
|
41
|
+
self.parts = head.split(" ")
|
42
|
+
self.parts << tail
|
43
|
+
command = self.parts.shift.upcase.to_sym
|
44
|
+
if command.to_s =~ /^[0-9]{3}$/
|
45
|
+
# Other Command
|
46
|
+
self.parts.unshift(command.to_s) # Reappend the command
|
47
|
+
process_event self.events[:numeric]
|
48
|
+
elsif self.events.has_key? command
|
49
|
+
# Registered Command
|
50
|
+
process_event self.events[command]
|
51
|
+
else
|
52
|
+
# Unknown Command
|
53
|
+
self.event = nil
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def process_event(prototype, skip_mutation = false)
|
58
|
+
self.event = prototype.dup
|
59
|
+
self.event.prefix = self.prefix
|
60
|
+
self.event.raw_arguments = self.parts
|
61
|
+
mutate_event! unless skip_mutation
|
62
|
+
end
|
63
|
+
|
64
|
+
def mutate_event!
|
65
|
+
# Do nothing by default
|
66
|
+
name, contents = self.event.name, self.event.raw_arguments.last
|
67
|
+
# mutate for ctcp and actions
|
68
|
+
if name == :message && contents[0..0] == "\001" && contents[-1..-1] == "\001"
|
69
|
+
if message.index("ACTION: ") == 1
|
70
|
+
message = message[9..-2]
|
71
|
+
new_event = :action
|
72
|
+
else
|
73
|
+
message = message[1..-2]
|
74
|
+
new_event = :ctcp
|
75
|
+
end
|
76
|
+
self.parts = [message]
|
77
|
+
process_event self.events[new_event], true
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def extract_prefix!(text)
|
82
|
+
return if text.blank?
|
83
|
+
full_prefix = text[1..-1]
|
84
|
+
prefix = full_prefix
|
85
|
+
# Ugly regexp for nick!ident@host format
|
86
|
+
# Officially this should be less-terse, but hey
|
87
|
+
# it's a simple parser.
|
88
|
+
if full_prefix =~ /^([^@!]+)\!\~?([^@]+)@(.*)$/
|
89
|
+
prefix = UserPrefix.new($1, $2, $3)
|
90
|
+
else
|
91
|
+
# TODO: Validate the hostname here.
|
92
|
+
prefix = ServerNamePrefix.new(prefix.strip)
|
93
|
+
end
|
94
|
+
self.prefix = prefix
|
95
|
+
return prefix
|
96
|
+
end
|
97
|
+
|
98
|
+
include DefaultEvents
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,77 @@
|
|
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 daemon?
|
20
|
+
defined?(IS_DAEMON) && IS_DAEMON
|
21
|
+
end
|
22
|
+
|
23
|
+
def setup!(options = {})
|
24
|
+
self.environment ||= "development"
|
25
|
+
self.configuration = {}
|
26
|
+
self.default_client ||= begin
|
27
|
+
require 'eventmachine'
|
28
|
+
Marvin::IRC::Client
|
29
|
+
rescue LoadError
|
30
|
+
Marvin::IRC::SocketClient
|
31
|
+
end
|
32
|
+
self.default_parser ||= Marvin::Parsers::RegexpParser
|
33
|
+
loaded_yaml = YAML.load_file(root / "config/settings.yml")
|
34
|
+
loaded_options = loaded_yaml["default"].
|
35
|
+
merge(loaded_yaml[self.environment]).
|
36
|
+
merge(options)
|
37
|
+
self.configuration.merge!(loaded_options)
|
38
|
+
self.configuration.symbolize_keys!
|
39
|
+
mod = Module.new do
|
40
|
+
Settings.configuration.keys.each do |k|
|
41
|
+
define_method(k) do
|
42
|
+
return Settings.configuration[k]
|
43
|
+
end
|
44
|
+
|
45
|
+
define_method("#{k}=") do |val|
|
46
|
+
Settings.configuration[k] = val
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Extend and include.
|
52
|
+
|
53
|
+
extend mod
|
54
|
+
include mod
|
55
|
+
|
56
|
+
self.is_setup = true
|
57
|
+
end
|
58
|
+
|
59
|
+
def [](key)
|
60
|
+
self.setup
|
61
|
+
return self.configuration[key.to_sym]
|
62
|
+
end
|
63
|
+
|
64
|
+
def []=(key, value)
|
65
|
+
self.setup
|
66
|
+
self.configuration[key.to_sym] = value
|
67
|
+
return value
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_hash
|
71
|
+
self.configuration
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'active_support'
|
3
|
+
|
4
|
+
module Marvin
|
5
|
+
# Marvin::TestClient is a simple client used for testing
|
6
|
+
# Marvin::Base derivatives in a non-network-reliant setting.
|
7
|
+
class TestClient < AbstractClient
|
8
|
+
attr_accessor :incoming_commands, :outgoing_commands, :last_sent, :dispatched_events, :connection_open
|
9
|
+
|
10
|
+
cattr_accessor :instances
|
11
|
+
self.instances = []
|
12
|
+
|
13
|
+
DispatchedEvents = Struct.new(:name, :options)
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
super
|
17
|
+
self.incoming_commands = []
|
18
|
+
self.outgoing_commands = []
|
19
|
+
self.dispatched_events = []
|
20
|
+
self.connection_open = false
|
21
|
+
self.instances << self
|
22
|
+
end
|
23
|
+
|
24
|
+
def connection_open?
|
25
|
+
self.connection_open
|
26
|
+
end
|
27
|
+
|
28
|
+
def send_line(*args)
|
29
|
+
self.outgoing_commands += args
|
30
|
+
self.last_sent = args.last
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_command(name, *args)
|
34
|
+
options = args.extract_options!
|
35
|
+
host_mask = options.delete(:host_mask) || ":WiZ!jto@tolsun.oulu.fi"
|
36
|
+
name = name.to_s.upcase
|
37
|
+
args = args.flatten.compact
|
38
|
+
irc_command = "#{host_mask} #{name} #{args.join(" ").strip}"
|
39
|
+
self.receive_line irc_command
|
40
|
+
end
|
41
|
+
|
42
|
+
def dispatch(name, opts = {})
|
43
|
+
self.dispatched_events << [name, opts]
|
44
|
+
super(name, opts)
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.run
|
48
|
+
self.instances.each do |i|
|
49
|
+
i.connection_open = true
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.stop
|
54
|
+
self.instances.each do |i|
|
55
|
+
i.connection_open = false
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
data/lib/marvin/util.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
module Marvin
|
2
|
+
module Util
|
3
|
+
|
4
|
+
# Return the channel-name version of a string by
|
5
|
+
# appending "#" to the front if it doesn't already
|
6
|
+
# start with it.
|
7
|
+
def channel_name(name)
|
8
|
+
return name.to_s[0..0] == "#" ? name.to_s : "##{name}"
|
9
|
+
end
|
10
|
+
alias chan channel_name
|
11
|
+
|
12
|
+
def arguments(input)
|
13
|
+
prefix, *ending = input.split(":")
|
14
|
+
prefix = prefix.split(" ")
|
15
|
+
prefix << ending.join(":").strip
|
16
|
+
return prefix
|
17
|
+
end
|
18
|
+
|
19
|
+
# Specifies the last parameter of a response, used to
|
20
|
+
# specify parameters which have spaces etc (for example,
|
21
|
+
# the actual message part of a response).
|
22
|
+
def last_param(section)
|
23
|
+
section && ":#{section.to_s.strip} "
|
24
|
+
end
|
25
|
+
alias lp last_param
|
26
|
+
|
27
|
+
extend self
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
data/lib/marvin.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__) # Append the current working dir to the front of the line.
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'active_support'
|
5
|
+
require 'marvin/core_ext'
|
6
|
+
|
7
|
+
# Make all exceptions available
|
8
|
+
require 'marvin/exceptions'
|
9
|
+
|
10
|
+
module Marvin
|
11
|
+
autoload :Util, 'marvin/util'
|
12
|
+
autoload :Dispatchable, 'marvin/dispatchable'
|
13
|
+
autoload :AbstractClient, 'marvin/abstract_client'
|
14
|
+
autoload :Base, 'marvin/base'
|
15
|
+
autoload :ClientMixin, 'marvin/client_mixin'
|
16
|
+
autoload :Settings, 'marvin/settings'
|
17
|
+
autoload :Logger, 'marvin/logger'
|
18
|
+
autoload :IRC, 'marvin/irc'
|
19
|
+
autoload :TestClient, 'marvin/test_client'
|
20
|
+
autoload :Loader, 'marvin/loader'
|
21
|
+
autoload :MiddleMan, 'marvin/middle_man'
|
22
|
+
autoload :DRBHandler, 'marvin/drb_handler'
|
23
|
+
autoload :DataStore, 'marvin/data_store'
|
24
|
+
autoload :ExceptionTracker, 'marvin/exception_tracker'
|
25
|
+
# Parsers
|
26
|
+
autoload :AbstractParser, 'marvin/abstract_parser'
|
27
|
+
autoload :Parsers, 'marvin/parsers.rb'
|
28
|
+
|
29
|
+
# Default Handlers
|
30
|
+
autoload :CommandHandler, 'marvin/command_handler'
|
31
|
+
|
32
|
+
Settings.setup # Load Settings etc.
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
def p(text)
|
37
|
+
res = Marvin::Parsers::SimpleParser.parse(text)
|
38
|
+
if res.blank?
|
39
|
+
puts "Unrecognized Result"
|
40
|
+
else
|
41
|
+
STDOUT.puts "Event: #{res.to_incoming_event_name}"
|
42
|
+
STDOUT.puts "Args: #{res.to_hash.inspect}"
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rubygems'
|
3
|
+
require 'daemons'
|
4
|
+
|
5
|
+
DIR = File.join(File.dirname(__FILE__), "..")
|
6
|
+
|
7
|
+
Daemons.run(File.join(DIR, "script/run --is-daemon"),
|
8
|
+
{:mode => :exec,
|
9
|
+
:dir => DIR,
|
10
|
+
:dir_mode => :normal,
|
11
|
+
:log_output => true,
|
12
|
+
:app_name => 'marvin'})
|
data/script/run
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rubygems'
|
3
|
+
|
4
|
+
if File.exist?(File.dirname(__FILE__) + "/../lib/marvin.rb")
|
5
|
+
$:.unshift(File.dirname(__FILE__) + "/../lib/")
|
6
|
+
end
|
7
|
+
|
8
|
+
MARVIN_ROOT = File.join(File.dirname(__FILE__), "..")
|
9
|
+
IS_DAEMON = ARGV.include?("--is-daemon")
|
10
|
+
|
11
|
+
# And Require Marvin.
|
12
|
+
require 'marvin'
|
13
|
+
|
14
|
+
["INT", "TERM"].each do |sig|
|
15
|
+
|
16
|
+
# Trap a given signal and run all
|
17
|
+
# of our callbacks etc,
|
18
|
+
trap sig do
|
19
|
+
Marvin::Loader.stop!
|
20
|
+
exit
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
Marvin::Loader.run!
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/../spec_helper"
|
2
|
+
|
3
|
+
# Tests the behaviour of the AbstractClient functionality
|
4
|
+
# via a thin wrapper of the class via Marvin::TestClient.
|
5
|
+
|
6
|
+
describe "the base Marvin::TestClient functionality" do
|
7
|
+
|
8
|
+
it "should dispatch :client_connected as the first event on process_connect" do
|
9
|
+
client(true).dispatched_events.should == []
|
10
|
+
client.process_connect
|
11
|
+
client.dispatched_events.first.should == [:client_connected, {}]
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should dispatch :client_connected as the first event on process_connect" do
|
15
|
+
client(true).dispatched_events.should == []
|
16
|
+
client.process_connect
|
17
|
+
Marvin::Logger.info client.outgoing_commands.inspect
|
18
|
+
client.dispatched_events[-2].first.should == :outgoing_nick
|
19
|
+
client.dispatched_events[-1].first.should == :outgoing_join
|
20
|
+
client.outgoing_commands.length.should == 3
|
21
|
+
client.outgoing_commands[0].should =~ /^USER \w+ 0 \* :\w+ \r\n$/
|
22
|
+
client.outgoing_commands[1].should =~ /^NICK \w+ \r\n$/
|
23
|
+
client.outgoing_commands[2].should =~ /^JOIN \#[A-Za-z0-9\-\_]+ \r\n$/
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should dispatch :client_disconnect on process_disconnect" do
|
27
|
+
client(true).dispatched_events.should == []
|
28
|
+
client.process_disconnect
|
29
|
+
client.dispatched_events.last.should == [:client_disconnected, {}]
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should add an :incoming_line event for each incoming line" do
|
33
|
+
client(true).dispatched_events.should == []
|
34
|
+
client.receive_line "SOME RANDOM LINE THAT HAS ZERO ACTUAL USE"
|
35
|
+
client.dispatched_events.first.should == [:incoming_line, {:line => "SOME RANDOM LINE THAT HAS ZERO ACTUAL USE"}]
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|